forgedock 1.32.1 → 1.34.0

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.
Files changed (77) hide show
  1. package/bin/engine/outcome-codes.mjs +7 -3
  2. package/bin/engine/phases.mjs +3 -3
  3. package/bin/engine-cli.mjs +56 -11
  4. package/bin/forgedock.mjs +21 -1
  5. package/bin/lean/basesync.mjs +98 -0
  6. package/bin/lean/basesync.test.mjs +151 -0
  7. package/bin/lean/batch.test.mjs +103 -2
  8. package/bin/lean/budget.mjs +7 -1
  9. package/bin/lean/budget.test.mjs +9 -1
  10. package/bin/lean/conformance.mjs +46 -10
  11. package/bin/lean/conformance.test.mjs +50 -2
  12. package/bin/lean/depcache.mjs +23 -3
  13. package/bin/lean/depcache.test.mjs +42 -1
  14. package/bin/lean/effects.mjs +1 -1
  15. package/bin/lean/execution.mjs +102 -12
  16. package/bin/lean/execution.test.mjs +101 -1
  17. package/bin/lean/hostkill.mjs +8 -0
  18. package/bin/lean/lean.mjs +49 -21
  19. package/bin/lean/lean.test.mjs +133 -7
  20. package/bin/lean/lightlane.mjs +38 -13
  21. package/bin/lean/lightlane.test.mjs +35 -1
  22. package/bin/lean/orchestrate.mjs +63 -17
  23. package/bin/lean/prompts.mjs +19 -9
  24. package/bin/lean/recovery.test.mjs +26 -0
  25. package/bin/lean/sandbox-provider.contract.test.mjs +74 -1
  26. package/bin/lean/sandbox-remote.mjs +40 -7
  27. package/bin/lean/seqlease.mjs +49 -2
  28. package/bin/lean/seqlease.test.mjs +14 -0
  29. package/bin/lean/sequence.mjs +54 -6
  30. package/bin/lean/sequence.test.mjs +83 -1
  31. package/bin/pool/agent.mjs +435 -0
  32. package/bin/pool/cli.mjs +110 -0
  33. package/bin/pool/docker.mjs +56 -0
  34. package/bin/pool/http.mjs +55 -0
  35. package/bin/pool/router.mjs +299 -0
  36. package/bin/pool/store.mjs +25 -0
  37. package/bin/pool/version.mjs +44 -0
  38. package/bin/review-staging.mjs +14 -2
  39. package/bin/runner.mjs +1 -0
  40. package/bin/stats/aggregate.mjs +6 -0
  41. package/bin/stats/efficiency.mjs +103 -17
  42. package/bin/stats/efficiency.test.mjs +103 -4
  43. package/bin/stats/index.mjs +40 -6
  44. package/bin/stats/local-source.mjs +1 -1
  45. package/bin/stats/pool.mjs +51 -0
  46. package/bin/stats/pool.test.mjs +39 -0
  47. package/bin/stats/query.mjs +2 -2
  48. package/bin/stats/stats.test.mjs +15 -1
  49. package/commands/orchestrate.md +3 -3
  50. package/commands/review-pr-agents/protocols.md +1 -1
  51. package/commands/review-pr-staging.md +1 -1
  52. package/docs/CONFIG.md +23 -9
  53. package/docs/OUTCOME-CODES.md +5 -3
  54. package/docs/STATS.md +36 -1
  55. package/package.json +2 -1
  56. package/pool/README.md +13 -0
  57. package/pool/image/Dockerfile +46 -0
  58. package/pool/image/claude-shim +8 -0
  59. package/pool/image/cs-cred +12 -0
  60. package/pool/image/entrypoint.sh +61 -0
  61. package/pool/image/gh-shim +6 -0
  62. package/pool/install/install-agent-linux.sh +53 -0
  63. package/pool/install/install-agent.ps1 +225 -0
  64. package/pool/install/install-agent.sh +101 -0
  65. package/pool/install/install-router.sh +67 -0
  66. package/pool/install/keepalive.ps1 +36 -0
  67. package/pool/install/uninstall-agent.ps1 +20 -0
  68. package/scripts/hotspots.sh +33 -21
  69. package/scripts/hotspots.test.sh +22 -3
  70. package/scripts/review-finalize.sh +17 -1
  71. package/scripts/review-finalize.test.sh +41 -0
  72. package/scripts/review-pr-domain-map.test.sh +2 -2
  73. package/scripts/review-pr-staging-scope.test.sh +4 -2
  74. package/scripts/review-prepare.sh +62 -27
  75. package/scripts/review-prepare.test.sh +53 -0
  76. package/scripts/review-staging-pre.sh +29 -9
  77. package/scripts/review-staging-pre.test.sh +18 -0
@@ -0,0 +1,53 @@
1
+ #!/usr/bin/env bash
2
+ # SPDX-FileCopyrightText: Copyright (c) RapierCraft Studios
3
+ # SPDX-License-Identifier: AGPL-3.0-or-later
4
+ # Native Linux (systemd) agent install: the Windows installer without the VM layer. Resolves host settings, then runs install-agent.sh,
5
+ # which installs docker, node, enrolls the agent, builds the lane image and installs the computeshare-agent systemd unit.
6
+ # Usage: install-agent-linux.sh --router URL --token CODE [--name N] [--port P] [--cpus N] [--mem GB] [--bundle FILE] [--dry-run]
7
+ # Env equivalents: ROUTER_URL ENROLL_TOKEN AGENT_NAME AGENT_PORT ALLOW_CPUS ALLOW_MEM_GB BUNDLE AGENT_URL
8
+ set -euo pipefail
9
+ DRY=0
10
+ while [ $# -gt 0 ]; do
11
+ case "$1" in
12
+ --router) ROUTER_URL="${2:?}"; shift 2 ;;
13
+ --token) ENROLL_TOKEN="${2:?}"; shift 2 ;;
14
+ --name) AGENT_NAME="${2:?}"; shift 2 ;;
15
+ --port) AGENT_PORT="${2:?}"; shift 2 ;;
16
+ --cpus) ALLOW_CPUS="${2:?}"; shift 2 ;;
17
+ --mem) ALLOW_MEM_GB="${2:?}"; shift 2 ;;
18
+ --bundle) BUNDLE="${2:?}"; shift 2 ;;
19
+ --dry-run) DRY=1; shift ;;
20
+ *) echo "unknown argument: $1" >&2; exit 2 ;;
21
+ esac
22
+ done
23
+ : "${ROUTER_URL:?--router or ROUTER_URL required}" "${ENROLL_TOKEN:?--token or ENROLL_TOKEN required}"
24
+ HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
25
+ PORT="${AGENT_PORT:-7711}"
26
+ AGENT_NAME="${AGENT_NAME:-$(hostname | tr 'A-Z' 'a-z')}"
27
+ # Same defaults as install-agent.ps1: logical CPUs minus 4 (min 2); half of physical RAM (min 4 GB).
28
+ CPUS="${ALLOW_CPUS:-0}"
29
+ if [ "$CPUS" -le 0 ]; then CPUS=$(( $(nproc) - 4 )); [ "$CPUS" -ge 2 ] || CPUS=2; fi
30
+ MEM="${ALLOW_MEM_GB:-0}"
31
+ if [ "$MEM" -le 0 ]; then MEM=$(( $(awk '/MemTotal/ {print int($2/1024/1024)}' /proc/meminfo) / 2 )); [ "$MEM" -ge 4 ] || MEM=4; fi
32
+ if [ -z "${AGENT_URL:-}" ]; then
33
+ if [ "$DRY" = 1 ] && ! command -v tailscale >/dev/null; then TS_IP="<tailscale-ip>"; else
34
+ TS_IP="$(tailscale ip -4 | head -1)"
35
+ [[ "$TS_IP" == 100.* ]] || { echo "no Tailscale IPv4 found (got '$TS_IP')" >&2; exit 1; }
36
+ fi
37
+ AGENT_URL="http://${TS_IP}:${PORT}"
38
+ fi
39
+
40
+ if [ "$DRY" = 1 ]; then
41
+ echo "router=$ROUTER_URL name=$AGENT_NAME url=$AGENT_URL port=$PORT cpus=$CPUS mem_gb=$MEM bundle=${BUNDLE:-<download>} token=<redacted>"
42
+ exit 0
43
+ fi
44
+ [ "$(id -u)" = 0 ] || { echo "run as root" >&2; exit 1; }
45
+
46
+ if [ -z "${BUNDLE:-}" ]; then
47
+ BUNDLE="$(mktemp /tmp/computeshare-bundle.XXXXXX.tar.gz)"
48
+ trap 'rm -f "$BUNDLE"' EXIT
49
+ curl -fsS "${ROUTER_URL}/bootstrap/bundle.tar.gz?token=${ENROLL_TOKEN}" -o "$BUNDLE"
50
+ fi
51
+ export ROUTER_URL ENROLL_TOKEN AGENT_NAME AGENT_URL BUNDLE
52
+ export AGENT_PORT="$PORT" ALLOW_CPUS="$CPUS" ALLOW_MEM_GB="$MEM"
53
+ bash "$HERE/install-agent.sh"
@@ -0,0 +1,225 @@
1
+ # SPDX-FileCopyrightText: Copyright (c) RapierCraft Studios
2
+ # SPDX-License-Identifier: AGPL-3.0-or-later
3
+ <#
4
+ .SYNOPSIS
5
+ Installs a computeshare agent on a Windows host: a dedicated WSL2 distro ("computeshare") running Docker and the agent,
6
+ reachable on this machine's Tailscale IP, with a CPU/RAM ceiling enforced by the WSL VM itself.
7
+ .NOTES
8
+ Run in an elevated PowerShell. Normally started by the router's bootstrap one-liner (computeshare install-command).
9
+ #>
10
+ [CmdletBinding()]
11
+ param(
12
+ [Parameter(Mandatory)] [string] $RouterUrl,
13
+ [Parameter(Mandatory)] [string] $EnrollToken,
14
+ [Parameter(Mandatory)] [string] $BundlePath,
15
+ [string] $AgentName = $env:COMPUTERNAME.ToLower(),
16
+ [int] $Port = 7711,
17
+ [int] $AllowCpus = 0, # 0 = logical CPUs minus 4 (min 2)
18
+ [int] $AllowMemGb = 0, # 0 = half of physical RAM (min 4)
19
+ [string] $Distro = 'computeshare',
20
+ [switch] $Force # kept for compatibility; the install never prompts
21
+ )
22
+ $ErrorActionPreference = 'Stop'
23
+ $env:WSL_UTF8 = '1'
24
+ function Say($m) { Write-Host "[computeshare] $m" -ForegroundColor Cyan }
25
+ function Fail($m) { Write-Host "[computeshare] $m" -ForegroundColor Red; exit 1 }
26
+
27
+ $principal = [Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()
28
+ if (-not $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) { Fail 'Run this from an elevated (Administrator) PowerShell.' }
29
+
30
+ $installDir = Join-Path $env:ProgramData 'computeshare'
31
+ New-Item -ItemType Directory -Force -Path $installDir | Out-Null
32
+
33
+ # --- Tailscale ---------------------------------------------------------------------------------------------------
34
+ $ts = (Get-Command tailscale.exe -ErrorAction SilentlyContinue).Source
35
+ if (-not $ts) { $ts = Join-Path $env:ProgramFiles 'Tailscale\tailscale.exe' }
36
+ if (-not (Test-Path $ts)) { Fail 'Tailscale is not installed.' }
37
+ $tsIp = (& $ts ip -4 | Select-Object -First 1).Trim()
38
+ if ($tsIp -notmatch '^100\.') { Fail "Could not read this machine's Tailscale IPv4 (got '$tsIp')." }
39
+ Say "Tailscale IP $tsIp"
40
+
41
+ # --- Resource ceiling --------------------------------------------------------------------------------------------
42
+ $threads = [Environment]::ProcessorCount
43
+ Add-Type -AssemblyName Microsoft.VisualBasic
44
+ $ramGb = [math]::Floor((New-Object Microsoft.VisualBasic.Devices.ComputerInfo).TotalPhysicalMemory / 1GB)
45
+ if ($AllowCpus -le 0) { $AllowCpus = [math]::Max(2, $threads - 4) }
46
+ if ($AllowMemGb -le 0) { $AllowMemGb = [math]::Max(4, [math]::Floor($ramGb / 2)) }
47
+ $vmMemGb = $AllowMemGb + 2 # docker + agent overhead inside the VM
48
+ Say "Host: $threads threads, $ramGb GB RAM. Lanes get $AllowCpus CPUs / $AllowMemGb GB (WSL VM capped at $AllowCpus CPUs / $vmMemGb GB)."
49
+
50
+ # --- WSL ---------------------------------------------------------------------------------------------------------
51
+ # wsl.exe can hang when the WSL service is wedged. Every call runs in a job with a timeout; on a hang the service is
52
+ # restarted once and the call retried, so the install never needs a manual fix.
53
+ function Reset-WslService {
54
+ Say 'WSL service is not responding: restarting it'
55
+ Get-Process wsl, wslhost, wslrelay, wslservice -ErrorAction SilentlyContinue | Stop-Process -Force -ErrorAction SilentlyContinue
56
+ Start-Sleep -Seconds 2
57
+ Start-Service WSLService -ErrorAction SilentlyContinue
58
+ Start-Sleep -Seconds 3
59
+ }
60
+ function Invoke-Wsl([string[]] $WslArgs, [int] $TimeoutSec = 120, [switch] $Optional) {
61
+ for ($try = 1; $try -le 2; $try++) {
62
+ $job = Start-Job -ScriptBlock { param($a) $env:WSL_UTF8 = '1'; $o = & wsl.exe @a 2>&1 | Out-String; [pscustomobject]@{ rc = $LASTEXITCODE; out = $o } } -ArgumentList (, $WslArgs)
63
+ if (Wait-Job $job -Timeout $TimeoutSec) { $r = Receive-Job $job; Remove-Job $job -Force; return $r }
64
+ Stop-Job $job -ErrorAction SilentlyContinue; Remove-Job $job -Force -ErrorAction SilentlyContinue
65
+ if ($Optional) { return $null }
66
+ if ($try -eq 1) { Reset-WslService }
67
+ }
68
+ Fail "wsl $($WslArgs -join ' ') still hangs after restarting the WSL service. Reboot this PC, then run a fresh install command."
69
+ }
70
+ Say 'Checking WSL'
71
+ $v = Invoke-Wsl @('--version') 60
72
+ if ($v.rc -ne 0) {
73
+ Say 'WSL is missing or the old inbox version: installing it. Reboot when it finishes, then run a fresh install command.'
74
+ $r = Invoke-Wsl @('--install', '--no-distribution', '--web-download') 1200
75
+ Write-Host $r.out
76
+ exit 3010
77
+ }
78
+ $wslVer = [version]'0.0'
79
+ if ($v.out -match 'WSL[^:\r\n]*:\s*([0-9]+(\.[0-9]+)+)') { $wslVer = [version]$Matches[1] }
80
+ Say "WSL $wslVer"
81
+ if ($wslVer -lt [version]'2.0') {
82
+ # mirrored networking and the current --import behaviour need WSL 2.0+; only then is an update worth the wait
83
+ Say 'Updating WSL (up to 10 min; skipped if it stalls)'
84
+ $u = Invoke-Wsl @('--update', '--web-download') 600 -Optional
85
+ if ($null -eq $u) { Say 'wsl --update stalled; continuing with the installed version.' }
86
+ }
87
+
88
+ # WSL 2 needs the Virtual Machine Platform feature and CPU virtualisation enabled in firmware (wsl.exe alone is not enough).
89
+ $cpu = Get-CimInstance Win32_Processor -ErrorAction SilentlyContinue | Select-Object -First 1
90
+ $hvPresent = [bool](Get-CimInstance Win32_ComputerSystem -ErrorAction SilentlyContinue).HypervisorPresent
91
+ if (-not $hvPresent -and $cpu -and $cpu.VirtualizationFirmwareEnabled -eq $false) {
92
+ Fail 'CPU virtualisation is disabled in the BIOS/UEFI (Intel VT-x / AMD SVM). Enable it in the firmware setup, boot Windows, then run a fresh install command.'
93
+ }
94
+ $needReboot = $false
95
+ foreach ($f in 'VirtualMachinePlatform', 'Microsoft-Windows-Subsystem-Linux') {
96
+ $st = (Get-WindowsOptionalFeature -Online -FeatureName $f -ErrorAction SilentlyContinue).State
97
+ if ($st -and $st -ne 'Enabled') {
98
+ Say "Enabling Windows feature $f"
99
+ $r = Enable-WindowsOptionalFeature -Online -FeatureName $f -All -NoRestart -ErrorAction Stop
100
+ if ($r.RestartNeeded) { $needReboot = $true }
101
+ }
102
+ }
103
+ if ($needReboot) {
104
+ Say 'Windows features for WSL 2 were enabled. Reboot this PC, then run a fresh install command (computeshare install-command).'
105
+ exit 3010
106
+ }
107
+
108
+ $build = [int](Get-ItemProperty 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion').CurrentBuildNumber
109
+ $mirrored = $build -ge 22621
110
+ Say ("Networking: " + ($(if ($mirrored) { 'mirrored (agent listens on the Tailscale IP directly)' } else { 'NAT + portproxy (Windows 10 / older 11)' })))
111
+
112
+ # .wslconfig is per Windows user and applies to every distro: merge our keys, keep everything else.
113
+ $wslConfig = Join-Path $env:USERPROFILE '.wslconfig'
114
+ $lines = @(); if (Test-Path $wslConfig) { $lines = Get-Content $wslConfig }
115
+ $want = [ordered]@{ processors = "$AllowCpus"; memory = "${vmMemGb}GB"; vmIdleTimeout = '-1' }
116
+ if ($mirrored) { $want['networkingMode'] = 'mirrored' }
117
+ $out = New-Object System.Collections.Generic.List[string]; $inWsl2 = $false; $seen = @{}
118
+ foreach ($l in $lines) {
119
+ if ($l -match '^\s*\[(.+)\]\s*$') {
120
+ if ($inWsl2) { foreach ($k in $want.Keys) { if (-not $seen[$k]) { $out.Add("$k=$($want[$k])"); $seen[$k] = $true } } }
121
+ $inWsl2 = ($Matches[1].Trim().ToLower() -eq 'wsl2')
122
+ $out.Add($l); continue
123
+ }
124
+ if ($inWsl2 -and $l -match '^\s*([A-Za-z]+)\s*=') {
125
+ $k = $want.Keys | Where-Object { $_ -ieq $Matches[1] } | Select-Object -First 1
126
+ if ($k) { $out.Add("$k=$($want[$k])"); $seen[$k] = $true; continue }
127
+ }
128
+ $out.Add($l)
129
+ }
130
+ if ($inWsl2) { foreach ($k in $want.Keys) { if (-not $seen[$k]) { $out.Add("$k=$($want[$k])"); $seen[$k] = $true } } }
131
+ if (-not ($out | Where-Object { $_ -match '^\s*\[wsl2\]\s*$' })) { $out.Add('[wsl2]'); foreach ($k in $want.Keys) { $out.Add("$k=$($want[$k])") } }
132
+ $changed = (($lines -join "`n") -ne ($out -join "`n"))
133
+ if ($changed) {
134
+ if (Test-Path $wslConfig) { Copy-Item $wslConfig "$wslConfig.computeshare-backup" -Force }
135
+ Set-Content -Path $wslConfig -Value $out -Encoding ascii
136
+ Say "Updated $wslConfig (backup: .wslconfig.computeshare-backup). NOTE: these limits apply to all your WSL distros."
137
+ }
138
+
139
+ # --- Dedicated distro -------------------------------------------------------------------------------------------
140
+ $distros = (Invoke-Wsl @('-l', '-q') 60).out -split "`r?`n" | ForEach-Object { $_.Trim() } | Where-Object { $_ }
141
+ if ($distros -notcontains $Distro) {
142
+ $base = 'https://cloud-images.ubuntu.com/wsl/releases/24.04/current'
143
+ $file = 'ubuntu-noble-wsl-amd64-wsl.rootfs.tar.gz'
144
+ $tar = Join-Path $installDir $file
145
+ # SHA256SUMS is served without a content type, so PowerShell 5.1 would hand back bytes: save it and read it as text.
146
+ $sumsFile = Join-Path $installDir 'SHA256SUMS'
147
+ Invoke-WebRequest -UseBasicParsing "$base/SHA256SUMS" -OutFile $sumsFile
148
+ $expected = ((Get-Content $sumsFile | Where-Object { $_ -match [regex]::Escape($file) } | Select-Object -First 1) -split '\s+')[0]
149
+ if (-not $expected) { Fail "no checksum for $file in SHA256SUMS" }
150
+ $actual = if (Test-Path $tar) { (Get-FileHash -Algorithm SHA256 $tar).Hash.ToLower() } else { '' }
151
+ if ($actual -ne $expected.ToLower()) {
152
+ Say "Downloading Ubuntu 24.04 WSL rootfs"
153
+ Invoke-WebRequest -UseBasicParsing "$base/$file" -OutFile $tar
154
+ $actual = (Get-FileHash -Algorithm SHA256 $tar).Hash.ToLower()
155
+ if ($actual -ne $expected.ToLower()) { Remove-Item $tar -Force; Fail "rootfs checksum mismatch ($actual vs $expected)" }
156
+ } else { Say 'Reusing the already downloaded rootfs (checksum verified)' }
157
+ Say "Importing WSL distro '$Distro'"
158
+ $r = Invoke-Wsl @('--import', $Distro, (Join-Path $installDir 'wsl'), $tar, '--version', '2') 900
159
+ if ($r.rc -ne 0) {
160
+ if ($r.out -match 'HCS_E_HYPERV_NOT_INSTALLED|Virtual Machine Platform') { Fail "WSL 2 cannot start a VM here: enable virtualisation (Intel VT-x / AMD SVM) in the BIOS/UEFI, reboot, and run a fresh install command. Details: $($r.out.Trim())" }
161
+ Fail "wsl --import failed: $($r.out)"
162
+ }
163
+ Remove-Item $tar -Force
164
+ }
165
+
166
+ # systemd for docker + the agent; no Windows PATH or Windows binaries inside the lane distro.
167
+ $wslConf = "[boot]\nsystemd=true\n\n[network]\nhostname=$AgentName-cs\n\n[interop]\nappendWindowsPath=false\n"
168
+ $before = (Invoke-Wsl @('-d', $Distro, '-u', 'root', '--', 'cat', '/etc/wsl.conf') 120).out
169
+ [void](Invoke-Wsl @('-d', $Distro, '-u', 'root', '--', 'sh', '-c', "printf '$wslConf' > /etc/wsl.conf") 120)
170
+ $after = (Invoke-Wsl @('-d', $Distro, '-u', 'root', '--', 'cat', '/etc/wsl.conf') 120).out
171
+ $confChanged = ($before -ne $after)
172
+
173
+ if ($changed -or $confChanged -or -not ((Invoke-Wsl @('-d', $Distro, '-u', 'root', '--', 'ps', '-p', '1', '-o', 'comm=') 120).out -match 'systemd')) {
174
+ Say 'Restarting WSL to apply the settings (this briefly stops every running WSL distro)'
175
+ [void](Invoke-Wsl @('--shutdown') 120)
176
+ Start-Sleep -Seconds 3
177
+ $p1 = (Invoke-Wsl @('-d', $Distro, '-u', 'root', '--', 'ps', '-p', '1', '-o', 'comm=') 180).out
178
+ if ($p1 -notmatch 'systemd') { Fail "systemd did not start in the '$Distro' distro (PID 1: $($p1.Trim()))." }
179
+ }
180
+
181
+ # --- Firewall ------------------------------------------------------------------------------------------------------
182
+ # Idempotent: re-runs replace the rules (a leftover rule must never stop the install).
183
+ Remove-NetFirewallRule -Name 'computeshare-agent' -ErrorAction SilentlyContinue
184
+ New-NetFirewallRule -Name 'computeshare-agent' -DisplayName 'computeshare agent (Tailscale only)' -Direction Inbound -Protocol TCP -LocalPort $Port -InterfaceAlias 'Tailscale' -Action Allow | Out-Null
185
+ if ($mirrored -and (Get-Command New-NetFirewallHyperVRule -ErrorAction SilentlyContinue)) {
186
+ $wslCreator = '{40E0AC32-46A5-438A-A0B2-2B479E8F2E90}'
187
+ # Hyper-V rules are not always addressable by the -Name they were created with: match ours by display name or port.
188
+ Get-NetFirewallHyperVRule -ErrorAction SilentlyContinue |
189
+ Where-Object { $_.DisplayName -like 'computeshare*' -or $_.Name -like '*computeshare*' -or ("$($_.LocalPorts)" -eq "$Port" -and "$($_.VMCreatorId)" -eq $wslCreator) } |
190
+ ForEach-Object { Remove-NetFirewallHyperVRule -Name $_.Name -ErrorAction SilentlyContinue }
191
+ try {
192
+ New-NetFirewallHyperVRule -Name "computeshare-agent-$Port" -DisplayName 'computeshare agent' -Direction Inbound -VMCreatorId $wslCreator -Protocol TCP -LocalPorts $Port -Action Allow -ErrorAction Stop | Out-Null
193
+ } catch {
194
+ Say "Hyper-V firewall rule not created ($($_.Exception.Message.Trim())); continuing, reachability is checked at the end."
195
+ }
196
+ }
197
+
198
+ # --- Agent inside the distro --------------------------------------------------------------------------------------
199
+ $bundleWsl = (Invoke-Wsl @('-d', $Distro, '-u', 'root', '--', 'wslpath', '-a', ($BundlePath -replace '\\', '/')) 60).out.Trim()
200
+ $r = Invoke-Wsl @('-d', $Distro, '-u', 'root', '--', 'sh', '-c', "tar -xzf '$bundleWsl' -C /tmp pool/install/install-agent.sh && cp '$bundleWsl' /tmp/computeshare-bundle.tar.gz") 120
201
+ if ($r.rc -ne 0) { Fail "could not stage the bundle in the distro: $($r.out)" }
202
+ $envs = "ROUTER_URL='$RouterUrl' ENROLL_TOKEN='$EnrollToken' AGENT_NAME='$AgentName' AGENT_URL='http://${tsIp}:$Port' AGENT_PORT='$Port' ALLOW_CPUS='$AllowCpus' ALLOW_MEM_GB='$AllowMemGb' BUNDLE=/tmp/computeshare-bundle.tar.gz"
203
+ Say 'Installing Docker, Node and the agent inside the distro (first run builds the lane image; this takes a while)'
204
+ & wsl.exe -d $Distro -u root -- bash -c "$envs bash /tmp/pool/install/install-agent.sh"
205
+ if ($LASTEXITCODE -ne 0) { Fail 'Linux-side install failed (see output above).' }
206
+
207
+ # --- Keep-alive task: keeps the distro up, holds off sleep while lanes run, refreshes portproxy on NAT -----------------
208
+ $keep = Join-Path $installDir 'keepalive.ps1'
209
+ Copy-Item (Join-Path $PSScriptRoot 'keepalive.ps1') $keep -Force
210
+ $taskArgs = "-NoProfile -WindowStyle Hidden -ExecutionPolicy Bypass -File `"$keep`" -Distro $Distro -Port $Port -TailscaleIp $tsIp" + $(if ($mirrored) { ' -Mirrored' } else { '' })
211
+ $action = New-ScheduledTaskAction -Execute 'powershell.exe' -Argument $taskArgs
212
+ $trigger = New-ScheduledTaskTrigger -AtLogOn -User "$env:USERDOMAIN\$env:USERNAME"
213
+ $settings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries -ExecutionTimeLimit ([TimeSpan]::Zero) -RestartCount 999 -RestartInterval (New-TimeSpan -Minutes 1)
214
+ Register-ScheduledTask -TaskName 'computeshare-keepalive' -Action $action -Trigger $trigger -Settings $settings -Description 'computeshare agent keep-alive' -Force | Out-Null
215
+ Start-ScheduledTask -TaskName 'computeshare-keepalive'
216
+
217
+ Start-Sleep -Seconds 5
218
+ try {
219
+ # Windows cannot reach its own Tailscale IP through the mirrored WSL network (no hairpin), so check via loopback;
220
+ # the router reaches the agent on the Tailscale IP.
221
+ $h = Invoke-RestMethod -Uri "http://127.0.0.1:$Port/health" -TimeoutSec 10
222
+ Say "Agent '$AgentName' is up on http://${tsIp}:$Port : $($h.cpus_total) CPUs / $($h.mem_total_gb) GB reserved for lanes."
223
+ } catch {
224
+ Say "Installed, but http://${tsIp}:$Port/health is not answering yet. Check: wsl -d $Distro -u root -- journalctl -u computeshare-agent -n 50"
225
+ }
@@ -0,0 +1,101 @@
1
+ #!/usr/bin/env bash
2
+ # SPDX-FileCopyrightText: Copyright (c) RapierCraft Studios
3
+ # SPDX-License-Identifier: AGPL-3.0-or-later
4
+ # Linux side of the agent install. Runs as root inside the dedicated `computeshare` WSL distro (or any systemd Linux host).
5
+ # Required env: ROUTER_URL ENROLL_TOKEN AGENT_NAME AGENT_URL ALLOW_CPUS ALLOW_MEM_GB BUNDLE
6
+ set -euo pipefail
7
+ : "${ROUTER_URL:?}" "${ENROLL_TOKEN:?}" "${AGENT_NAME:?}" "${AGENT_URL:?}" "${ALLOW_CPUS:?}" "${ALLOW_MEM_GB:?}" "${BUNDLE:?}"
8
+ # Never resolve tools through the Windows PATH that WSL appends (e.g. Docker Desktop's docker CLI).
9
+ export PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
10
+ NODE_VERSION=22.12.0
11
+ PORT="${AGENT_PORT:-7711}"
12
+ log() { echo "[computeshare] $*"; }
13
+
14
+ [ "$(id -u)" = 0 ] || { echo "run as root" >&2; exit 1; }
15
+ pidof systemd >/dev/null || [ "$(ps -p 1 -o comm=)" = systemd ] || { echo "systemd is not PID 1; enable it in /etc/wsl.conf and restart the distro" >&2; exit 1; }
16
+
17
+ log "base packages"
18
+ export DEBIAN_FRONTEND=noninteractive
19
+ apt-get update -qq
20
+ apt-get install -y -qq ca-certificates curl git jq xz-utils >/dev/null
21
+
22
+ if [ ! -x /usr/bin/dockerd ]; then
23
+ log "docker engine"
24
+ install -d -m 0755 /etc/apt/keyrings
25
+ . /etc/os-release
26
+ DOCKER_DISTRO=ubuntu; [ "${ID:-}" = debian ] && DOCKER_DISTRO=debian
27
+ curl -fsSL "https://download.docker.com/linux/${DOCKER_DISTRO}/gpg" -o /etc/apt/keyrings/docker.asc
28
+ echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/${DOCKER_DISTRO} ${VERSION_CODENAME} stable" > /etc/apt/sources.list.d/docker.list
29
+ apt-get update -qq
30
+ apt-get install -y -qq docker-ce docker-ce-cli containerd.io >/dev/null
31
+ fi
32
+ # WSL mirrored networking usually has no IPv6 route; dockerd would still dial registries over IPv6 and fail.
33
+ if grep -qi microsoft /proc/sys/kernel/osrelease && [ -z "$(ip -6 route show default 2>/dev/null)" ]; then
34
+ printf 'net.ipv6.conf.all.disable_ipv6=1\nnet.ipv6.conf.default.disable_ipv6=1\n' > /etc/sysctl.d/90-computeshare-noipv6.conf
35
+ sysctl -q -p /etc/sysctl.d/90-computeshare-noipv6.conf
36
+ systemctl restart docker 2>/dev/null || true
37
+ fi
38
+ systemctl enable --now docker >/dev/null
39
+
40
+ if [ ! -x /opt/node/bin/node ] || [ "$(/opt/node/bin/node --version)" != "v${NODE_VERSION}" ]; then
41
+ log "node ${NODE_VERSION}"
42
+ rm -rf /opt/node
43
+ case "$(uname -m)" in aarch64|arm64) NODE_ARCH=arm64 ;; *) NODE_ARCH=x64 ;; esac
44
+ curl -fsSL "https://nodejs.org/dist/v${NODE_VERSION}/node-v${NODE_VERSION}-linux-${NODE_ARCH}.tar.xz" | tar -xJ -C /opt
45
+ mv "/opt/node-v${NODE_VERSION}-linux-${NODE_ARCH}" /opt/node
46
+ fi
47
+
48
+ log "agent code"
49
+ rm -rf /opt/computeshare.new && mkdir -p /opt/computeshare.new
50
+ tar -xzf "$BUNDLE" -C /opt/computeshare.new
51
+ rm -rf /opt/computeshare && mv /opt/computeshare.new /opt/computeshare
52
+
53
+ install -d -m 0700 /etc/computeshare /var/lib/computeshare
54
+ if [ ! -s /etc/computeshare/agent.token ]; then
55
+ log "enrolling ${AGENT_NAME} (${AGENT_URL}) with ${ROUTER_URL}"
56
+ resp=$(curl -fsS -X POST "${ROUTER_URL}/enroll" -H 'content-type: application/json' \
57
+ -d "$(jq -n --arg t "$ENROLL_TOKEN" --arg n "$AGENT_NAME" --arg u "$AGENT_URL" '{token:$t,name:$n,url:$u}')")
58
+ umask 077
59
+ echo "$resp" | jq -r .agent_token > /etc/computeshare/agent.token
60
+ [ -s /etc/computeshare/agent.token ] && [ "$(cat /etc/computeshare/agent.token)" != null ] || { echo "enrollment failed: $resp" >&2; rm -f /etc/computeshare/agent.token; exit 1; }
61
+ fi
62
+
63
+ jq -n --arg name "$AGENT_NAME" --arg router "$ROUTER_URL" --argjson port "$PORT" --argjson cpus "$ALLOW_CPUS" --argjson mem "$ALLOW_MEM_GB" '{
64
+ name: $name, listen: {host: "0.0.0.0", port: $port}, token_file: "/etc/computeshare/agent.token",
65
+ router_url: $router, code_root: "/opt/computeshare",
66
+ state_dir: "/var/lib/computeshare",
67
+ allow_cpus: $cpus, allow_mem_gb: $mem, lease_ttl_s: 600, max_lifetime_s: 21600, keep_exited_s: 7200, reap_interval_s: 30
68
+ }' > /etc/computeshare/agent.json
69
+ chmod 0600 /etc/computeshare/agent.json
70
+
71
+ log "building lane image (first run takes several minutes)"
72
+ FD_VERSION="$(jq -r .version /opt/computeshare/package.json)"
73
+ docker build -q -t "forgedock-lane:${FD_VERSION}" --build-arg "FORGEDOCK_VERSION=${FD_VERSION}" /opt/computeshare/pool/image >/dev/null
74
+
75
+ cat > /etc/systemd/system/computeshare-agent.service <<EOF
76
+ [Unit]
77
+ Description=computeshare agent
78
+ After=docker.service network-online.target
79
+ Requires=docker.service
80
+
81
+ [Service]
82
+ ExecStart=/opt/node/bin/node /opt/computeshare/bin/pool/cli.mjs agent --config /etc/computeshare/agent.json
83
+ Restart=always
84
+ RestartSec=5
85
+
86
+ [Install]
87
+ WantedBy=multi-user.target
88
+ EOF
89
+ systemctl daemon-reload
90
+ systemctl enable computeshare-agent >/dev/null
91
+ systemctl restart computeshare-agent
92
+
93
+ for i in $(seq 1 20); do
94
+ if curl -fsS "http://127.0.0.1:${PORT}/health" >/dev/null 2>&1; then
95
+ log "agent healthy: $(curl -fsS "http://127.0.0.1:${PORT}/health")"
96
+ exit 0
97
+ fi
98
+ sleep 1
99
+ done
100
+ echo "agent did not become healthy; see: journalctl -u computeshare-agent" >&2
101
+ exit 1
@@ -0,0 +1,67 @@
1
+ #!/usr/bin/env bash
2
+ # SPDX-FileCopyrightText: Copyright (c) RapierCraft Studios
3
+ # SPDX-License-Identifier: AGPL-3.0-or-later
4
+ # Installs the computeshare router as a systemd --user service on the host that runs ForgeDock.
5
+ # It listens only on this machine's Tailscale IP. Re-running keeps existing tokens and agent enrollments.
6
+ set -euo pipefail
7
+ REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
8
+ CONF_DIR="${XDG_CONFIG_HOME:-$HOME/.config}/computeshare"
9
+ PORT="${ROUTER_PORT:-7710}"
10
+ TS_IP="$(tailscale ip -4 | head -1)"
11
+ [[ "$TS_IP" == 100.* ]] || { echo "no Tailscale IPv4 found" >&2; exit 1; }
12
+ NODE="$(command -v node)"
13
+
14
+ mkdir -p "$CONF_DIR" && chmod 700 "$CONF_DIR"
15
+ umask 077
16
+ for t in forgedock enroll; do
17
+ [ -s "$CONF_DIR/$t.token" ] || openssl rand -hex 32 > "$CONF_DIR/$t.token"
18
+ done
19
+
20
+ # repos: slug -> gitignored forge.yaml that lanes need (passed to the sandbox, never committed). Extend as needed.
21
+ # An existing router.json is kept (re-running the installer must not drop repos or settings added later).
22
+ [ -s "$CONF_DIR/router.json" ] || cat > "$CONF_DIR/router.json" <<EOF
23
+ {
24
+ "listen": { "host": "$TS_IP", "port": $PORT },
25
+ "public_url": "http://$TS_IP:$PORT",
26
+ "state_dir": "$CONF_DIR/state",
27
+ "lane": { "cpus": ${LANE_CPUS:-2}, "mem_gb": ${LANE_MEM_GB:-3} },
28
+ "lease_interval_s": 60,
29
+ "abandon_after_s": 1800,
30
+ "retry_after_s": 60,
31
+ "repos": {}
32
+ }
33
+ EOF
34
+
35
+ mkdir -p "$HOME/.config/systemd/user"
36
+ cat > "$HOME/.config/systemd/user/computeshare-router.service" <<EOF
37
+ [Unit]
38
+ Description=computeshare router
39
+ After=network-online.target tailscaled.service
40
+
41
+ [Service]
42
+ ExecStart=$NODE $REPO_ROOT/bin/pool/cli.mjs router --config $CONF_DIR/router.json
43
+ Restart=always
44
+ RestartSec=5
45
+
46
+ [Install]
47
+ WantedBy=default.target
48
+ EOF
49
+ systemctl --user daemon-reload
50
+ systemctl --user enable --now computeshare-router >/dev/null
51
+ systemctl --user restart computeshare-router
52
+ loginctl enable-linger "$USER" 2>/dev/null || true
53
+
54
+ # ForgeDock reads execution.token_env from the env, then the OS keychain (service forgedock, account = the name).
55
+ if command -v secret-tool >/dev/null; then
56
+ secret-tool store --label "forgedock COMPUTESHARE_TOKEN" service forgedock account COMPUTESHARE_TOKEN < "$CONF_DIR/forgedock.token" \
57
+ && echo "[computeshare] stored the ForgeDock token in the keychain as COMPUTESHARE_TOKEN"
58
+ fi
59
+
60
+ for i in $(seq 1 15); do
61
+ curl -fsS "http://$TS_IP:$PORT/health" >/dev/null 2>&1 && break
62
+ sleep 1
63
+ done
64
+ echo "[computeshare] router: $(curl -fsS "http://$TS_IP:$PORT/health")"
65
+ echo
66
+ echo "Install an agent: run this in an elevated PowerShell on the Windows host:"
67
+ "$NODE" "$REPO_ROOT/bin/pool/cli.mjs" install-command --config "$CONF_DIR/router.json"
@@ -0,0 +1,36 @@
1
+ # SPDX-FileCopyrightText: Copyright (c) RapierCraft Studios
2
+ # SPDX-License-Identifier: AGPL-3.0-or-later
3
+ # Runs at logon (scheduled task). Keeps the computeshare distro running, keeps Windows awake while lanes are active,
4
+ # and on NAT networking keeps a portproxy from the Tailscale IP to the distro's current address.
5
+ param([string] $Distro = 'computeshare', [int] $Port = 7711, [string] $TailscaleIp, [switch] $Mirrored)
6
+ $ErrorActionPreference = 'Continue'
7
+ $env:WSL_UTF8 = '1'
8
+
9
+ Add-Type -Namespace CS -Name Power -MemberDefinition '[DllImport("kernel32.dll")] public static extern uint SetThreadExecutionState(uint flags);'
10
+ $ES_CONTINUOUS = [uint32]'0x80000000'; $ES_SYSTEM_REQUIRED = [uint32]'0x00000001'
11
+
12
+ $holder = $null
13
+ while ($true) {
14
+ if (-not $holder -or $holder.HasExited) {
15
+ # A long-lived process keeps the distro (and its systemd services) from being stopped by WSL.
16
+ $holder = Start-Process -FilePath 'wsl.exe' -ArgumentList @('-d', $Distro, '-u', 'root', '--', 'sleep', 'infinity') -WindowStyle Hidden -PassThru
17
+ Start-Sleep -Seconds 5
18
+ }
19
+ if (-not $Mirrored -and $TailscaleIp) {
20
+ $wslIp = ((& wsl.exe -d $Distro -u root -- hostname -I) -split '\s+' | Where-Object { $_ -match '^\d+\.' } | Select-Object -First 1)
21
+ if ($wslIp) {
22
+ $cur = (netsh interface portproxy show v4tov4) -match [regex]::Escape("$TailscaleIp") -match "$Port"
23
+ if (-not ($cur -match [regex]::Escape($wslIp))) {
24
+ netsh interface portproxy delete v4tov4 listenaddress=$TailscaleIp listenport=$Port | Out-Null
25
+ netsh interface portproxy add v4tov4 listenaddress=$TailscaleIp listenport=$Port connectaddress=$wslIp connectport=$Port | Out-Null
26
+ }
27
+ }
28
+ }
29
+ $active = 0
30
+ try { $active = [int](Invoke-RestMethod -Uri "http://127.0.0.1:$Port/health" -TimeoutSec 5).active } catch {
31
+ if ($TailscaleIp) { try { $active = [int](Invoke-RestMethod -Uri "http://${TailscaleIp}:$Port/health" -TimeoutSec 5).active } catch {} }
32
+ }
33
+ if ($active -gt 0) { [void][CS.Power]::SetThreadExecutionState($ES_CONTINUOUS -bor $ES_SYSTEM_REQUIRED) }
34
+ else { [void][CS.Power]::SetThreadExecutionState($ES_CONTINUOUS) }
35
+ Start-Sleep -Seconds 60
36
+ }
@@ -0,0 +1,20 @@
1
+ # SPDX-FileCopyrightText: Copyright (c) RapierCraft Studios
2
+ # SPDX-License-Identifier: AGPL-3.0-or-later
3
+ <#
4
+ .SYNOPSIS
5
+ Removes the computeshare agent from this Windows host: keep-alive task, firewall rules, portproxy, the dedicated WSL
6
+ distro (with every container, image and cache in it) and the .wslconfig changes (restored from the backup).
7
+ #>
8
+ param([string] $Distro = 'computeshare', [int] $Port = 7711)
9
+ $ErrorActionPreference = 'Continue'
10
+ $env:WSL_UTF8 = '1'
11
+ Unregister-ScheduledTask -TaskName 'computeshare-keepalive' -Confirm:$false -ErrorAction SilentlyContinue
12
+ Get-Process powershell -ErrorAction SilentlyContinue | Where-Object { $_.CommandLine -match 'computeshare\\keepalive.ps1' } | Stop-Process -Force
13
+ Get-NetFirewallRule -Name 'computeshare-agent' -ErrorAction SilentlyContinue | Remove-NetFirewallRule
14
+ if (Get-Command Get-NetFirewallHyperVRule -ErrorAction SilentlyContinue) { Get-NetFirewallHyperVRule -ErrorAction SilentlyContinue | Where-Object { $_.DisplayName -like 'computeshare*' -or $_.Name -like '*computeshare*' } | ForEach-Object { Remove-NetFirewallHyperVRule -Name $_.Name -ErrorAction SilentlyContinue } }
15
+ (netsh interface portproxy show v4tov4) -match " $Port " | ForEach-Object { $a = ($_ -split '\s+')[0]; if ($a) { netsh interface portproxy delete v4tov4 listenaddress=$a listenport=$Port | Out-Null } }
16
+ & wsl.exe --unregister $Distro
17
+ $cfg = Join-Path $env:USERPROFILE '.wslconfig'
18
+ if (Test-Path "$cfg.computeshare-backup") { Move-Item "$cfg.computeshare-backup" $cfg -Force; Write-Host 'Restored .wslconfig from backup (takes effect after wsl --shutdown).' }
19
+ Remove-Item -Recurse -Force (Join-Path $env:ProgramData 'computeshare') -ErrorAction SilentlyContinue
20
+ Write-Host '[computeshare] agent removed. Remove it from the router with: computeshare status (then delete it from router-state.json).'
@@ -7,14 +7,17 @@
7
7
  #
8
8
  # Usage: hotspots.sh [--base <ref>] [--] <path>...
9
9
  # Config: forge.yaml maintenance.hotspot {enabled, file_lines, growth_lines, exclude_globs}; defaults true/2000/150/[]
10
- # (also when forge.yaml or yq is missing).
10
+ # (also when forge.yaml or yq is missing); maintenance.generated_paths (forge#4519): shell globs of generated,
11
+ # bot-owned files, default [].
11
12
  # Output (stdout, one JSON object):
12
- # {"enabled","file_lines","growth_lines","files":[{path,lines,commits_90d,hotspot,over,excluded,base_lines,growth}]}
13
+ # {"enabled","file_lines","growth_lines","generated_paths",
14
+ # "files":[{path,lines,commits_90d,hotspot,over,generated,excluded,base_lines,growth}]}
13
15
  # lines = `wc -l` of the working-tree file (0 when missing); commits_90d = `git rev-list --count --since=90.days HEAD`;
14
- # hotspot = lines × commits_90d; over = lines > file_lines; excluded = linguist-generated / linguist-vendored set by
15
- # .gitattributes, or a match of an exclude_glob (shell pattern, `*` crosses `/`). Without --base no blob is read and
16
+ # hotspot = lines × commits_90d; over = lines > file_lines; generated = linguist-generated set by .gitattributes, a
17
+ # match of a generated_paths glob, or `@generated` / `DO NOT EDIT` in the first 5 lines of the working-tree file;
18
+ # excluded = generated, linguist-vendored, or a match of an exclude_glob (shell pattern, `*` crosses `/`). Without --base no blob is read and
16
19
  # base_lines/growth are null; with --base they are the line count at git merge-base(<ref>, HEAD) (0 when absent there)
17
- # and lines - base_lines. enabled:false prints files:[] and does no per-file work.
20
+ # and lines - base_lines. enabled:false prints files:[] (generated_paths still set) and does no per-file work.
18
21
  # Exit: 0 ok | 2 usage or bad ref. bash 3.2 portable.
19
22
  set -uo pipefail
20
23
 
@@ -31,19 +34,21 @@ while [ $# -gt 0 ]; do
31
34
  done
32
35
 
33
36
  # ---- config: maintenance.hotspot as JSON, {} when unreadable ----
34
- CFG='{}'
37
+ CFG='{}'; GEN_CFG='[]'
35
38
  if [ -f forge.yaml ] && command -v yq >/dev/null 2>&1; then
36
39
  c=$(yq -o=json '.maintenance.hotspot // {}' forge.yaml 2>/dev/null) || c=""
37
40
  printf '%s' "$c" | jq -e 'type == "object"' >/dev/null 2>&1 && CFG="$c"
41
+ g=$(yq -o=json '.maintenance.generated_paths // []' forge.yaml 2>/dev/null) || g=""
42
+ printf '%s' "$g" | jq -e 'type == "array"' >/dev/null 2>&1 && GEN_CFG="$g"
38
43
  fi
44
+ GEN_CFG=$(printf '%s' "$GEN_CFG" | jq -c '[.[] | strings | select(length > 0)]')
39
45
  CFG=$(printf '%s' "$CFG" | jq -c '
40
46
  def pos($d): if type == "number" and . > 0 then floor else $d end;
41
47
  { enabled: (.enabled != false), file_lines: (.file_lines | pos(2000)), growth_lines: (.growth_lines | pos(150)),
42
48
  exclude_globs: ([(.exclude_globs // [])[]? | strings]) }')
43
49
  ENABLED=$(printf '%s' "$CFG" | jq -r '.enabled')
44
50
  FILE_LINES=$(printf '%s' "$CFG" | jq -r '.file_lines')
45
- GROWTH_LINES=$(printf '%s' "$CFG" | jq -r '.growth_lines')
46
- head_json() { printf '%s' "$CFG" | jq -c --argjson files "$1" '{enabled, file_lines, growth_lines, files: $files}'; }
51
+ head_json() { printf '%s' "$CFG" | jq -c --argjson files "$1" --argjson gen "$GEN_CFG" '{enabled, file_lines, growth_lines, generated_paths: $gen, files: $files}'; }
47
52
  [ "$ENABLED" = true ] || { head_json '[]'; exit 0; }
48
53
 
49
54
  MB=""
@@ -53,6 +58,19 @@ if [ -n "$BASE" ]; then
53
58
  fi
54
59
  HAS_HEAD=0; git rev-parse --verify -q "HEAD^{commit}" >/dev/null && HAS_HEAD=1
55
60
  GLOBS=$(printf '%s' "$CFG" | jq -r '.exclude_globs[]')
61
+ GEN_GLOBS=$(printf '%s' "$GEN_CFG" | jq -r '.[]')
62
+ # 0 when $1 matches a newline-separated list of shell patterns in $2 (`*` crosses `/`)
63
+ glob_hit() {
64
+ [ -n "$2" ] || return 1
65
+ while IFS= read -r g; do
66
+ [ -n "$g" ] || continue
67
+ # shellcheck disable=SC2254
68
+ case "$1" in $g) return 0 ;; esac
69
+ done <<EOF
70
+ $2
71
+ EOF
72
+ return 1
73
+ }
56
74
 
57
75
  T=$(mktemp -d "${TMPDIR:-/tmp}/hotspots.XXXXXX") || die "mktemp failed"
58
76
  trap 'rm -rf "$T"' EXIT
@@ -72,29 +90,23 @@ for p in "$@"; do
72
90
  [ -n "$p" ] || continue
73
91
  lines=0; [ -f "$p" ] && lines=$(wc -l < "$p" | tr -d ' ')
74
92
  commits=$(cat "$T/c.$i" 2>/dev/null); case "$commits" in ''|*[!0-9]*) commits=0 ;; esac
75
- excluded=false
76
- if git check-attr linguist-generated linguist-vendored -- "$p" 2>/dev/null | grep -qE ': (set|true)$'; then excluded=true; fi
77
- if [ "$excluded" = false ] && [ -n "$GLOBS" ]; then
78
- while IFS= read -r g; do
79
- [ -n "$g" ] || continue
80
- # shellcheck disable=SC2254
81
- case "$p" in $g) excluded=true; break ;; esac
82
- done <<EOF
83
- $GLOBS
84
- EOF
85
- fi
93
+ generated=false; excluded=false
94
+ if git check-attr linguist-generated -- "$p" 2>/dev/null | grep -qE ': (set|true)$' || glob_hit "$p" "$GEN_GLOBS" \
95
+ || { [ -f "$p" ] && head -n 5 "$p" 2>/dev/null | grep -qE '@generated|DO NOT EDIT'; }; then generated=true; excluded=true; fi
96
+ if [ "$excluded" = false ] && git check-attr linguist-vendored -- "$p" 2>/dev/null | grep -qE ': (set|true)$'; then excluded=true; fi
97
+ if [ "$excluded" = false ] && glob_hit "$p" "$GLOBS"; then excluded=true; fi
86
98
  base_lines=null; growth=null
87
99
  if [ -n "$MB" ]; then
88
100
  base_lines=0
89
101
  git cat-file -e "$MB:$p" 2>/dev/null && base_lines=$(git show "$MB:$p" | wc -l | tr -d ' ')
90
102
  growth=$((lines - base_lines))
91
103
  fi
92
- printf '%s\t%s\t%s\t%s\t%s\t%s\n' "$p" "$lines" "$commits" "$excluded" "$base_lines" "$growth" >> "$T/rows"
104
+ printf '%s\t%s\t%s\t%s\t%s\t%s\t%s\n' "$p" "$lines" "$commits" "$excluded" "$base_lines" "$growth" "$generated" >> "$T/rows"
93
105
  done
94
106
 
95
107
  FILES=$(jq -R -s -c --argjson fl "$FILE_LINES" '
96
108
  [split("\n")[] | select(length > 0) | split("\t") |
97
109
  { path: .[0], lines: (.[1] | tonumber), commits_90d: (.[2] | tonumber) } as $r |
98
- $r + { hotspot: ($r.lines * $r.commits_90d), over: ($r.lines > $fl), excluded: (.[3] == "true"),
110
+ $r + { hotspot: ($r.lines * $r.commits_90d), over: ($r.lines > $fl), generated: (.[6] == "true"), excluded: (.[3] == "true"),
99
111
  base_lines: (.[4] | fromjson), growth: (.[5] | fromjson) }]' < "$T/rows")
100
112
  head_json "$FILES"