chore: migrate to pure Go SQLite build

This commit is contained in:
Andy Dixon
2026-06-24 10:22:41 +01:00
parent 614f87b8f2
commit 4fc3b19441
24 changed files with 94 additions and 4156 deletions
+4 -7
View File
@@ -76,27 +76,24 @@ jobs:
sudo apt-get update
sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.0-dev
- name: Install Windows cross-compiler
- name: Install Windows packaging tools
if: matrix.os == 'windows' && matrix.cross
run: |
sudo apt-get update
sudo apt-get install -y gcc-mingw-w64-x86-64 gcc-mingw-w64-aarch64 zip
sudo apt-get install -y zip
- name: Build (non-Windows runners)
if: matrix.runner != 'windows-latest'
env:
CGO_ENABLED: "1"
CGO_ENABLED: "0"
run: |
if [ "${{ matrix.os }}" = "windows" ] && [ "${{ matrix.cross }}" = "true" ]; then
export CC=aarch64-w64-mingw32-gcc
fi
wails build -platform "${{ matrix.platform }}" -ldflags "-s -w"
- name: Build (Windows runner)
if: matrix.runner == 'windows-latest'
shell: pwsh
env:
CGO_ENABLED: "1"
CGO_ENABLED: "0"
run: |
wails build -platform "${{ matrix.platform }}" -ldflags "-s -w"
+2 -2
View File
@@ -37,11 +37,11 @@ dev: deps
# Production build
build: deps
CGO_CFLAGS="$${CGO_CFLAGS:--O2 -g0}" CGO_CXXFLAGS="$${CGO_CXXFLAGS:--O2 -g0}" $(WAILS) build -trimpath -ldflags "-s -w -buildid= -X github.com/andydixon/nexus/internal/api.appVersion=$(VERSION) $(LD_UPDATE_URL)"
CGO_ENABLED=0 $(WAILS) build -trimpath -ldflags "-s -w -buildid= -X github.com/andydixon/nexus/internal/api.appVersion=$(VERSION) $(LD_UPDATE_URL)"
# Production build with UPX compression
build-compressed: deps upx-check
CGO_CFLAGS="$${CGO_CFLAGS:--O2 -g0}" CGO_CXXFLAGS="$${CGO_CXXFLAGS:--O2 -g0}" $(WAILS) build -trimpath -upx $(if $(UPX_FLAGS),-upxflags "$(UPX_FLAGS)",) -ldflags "-s -w -buildid= -X github.com/andydixon/nexus/internal/api.appVersion=$(VERSION) $(LD_UPDATE_URL)"
CGO_ENABLED=0 $(WAILS) build -trimpath -upx $(if $(UPX_FLAGS),-upxflags "$(UPX_FLAGS)",) -ldflags "-s -w -buildid= -X github.com/andydixon/nexus/internal/api.appVersion=$(VERSION) $(LD_UPDATE_URL)"
# Run tests
test:
+2 -42
View File
@@ -179,31 +179,6 @@ host_arch() {
esac
}
pick_cc() {
local target_os="$1"
local target_arch="$2"
if [ "$target_os" = "windows" ]; then
if [ "$target_arch" = "amd64" ]; then
echo "x86_64-w64-mingw32-gcc"
else
echo "aarch64-w64-mingw32-gcc"
fi
return
fi
if [ "$target_os" = "linux" ]; then
if [ "$target_arch" = "amd64" ]; then
echo "x86_64-linux-gnu-gcc"
else
echo "aarch64-linux-gnu-gcc"
fi
return
fi
echo ""
}
build_target() {
local target_os="$1"
local target_arch="$2"
@@ -211,7 +186,7 @@ build_target() {
local host_arch_value="$4"
local version="$5"
local platform="${target_os}/${target_arch}"
nexus_configure_release_cgo
nexus_configure_pure_go_build
local ldflags
ldflags="$(build_ldflags "${version}")"
local tags
@@ -235,29 +210,14 @@ build_target() {
fi
build_args+=("-ldflags" "$ldflags")
local cc=""
if [ "$target_os" = "linux" ] && ! linux_deps_ok; then
warn "Skipping ${platform} (missing Linux GTK/WebKit build dependencies)"
((SKIPPED_TARGETS += 1))
return 0
fi
if [ "$target_os" != "$host" ] || [ "$target_arch" != "$host_arch_value" ]; then
cc="$(pick_cc "$target_os" "$target_arch")"
if [ -n "$cc" ] && ! command -v "$cc" >/dev/null 2>&1; then
warn "Skipping ${platform} (missing C compiler: ${cc})"
((SKIPPED_TARGETS += 1))
return 0
fi
fi
log "Building ${platform}…"
if [ -n "$cc" ]; then
CC="$cc" CGO_ENABLED=1 wails_cmd build "${build_args[@]}"
else
CGO_ENABLED=1 wails_cmd build "${build_args[@]}"
fi
CGO_ENABLED=0 wails_cmd build "${build_args[@]}"
((BUILT_TARGETS += 1))
}
+4 -8
View File
@@ -35,10 +35,6 @@ function Check-Deps {
Write-Info "Node: $(& node --version)"
Write-Info "Wails: $(& $wailsPath version | Select-Object -First 1)"
if (-not (Get-Command gcc -ErrorAction SilentlyContinue)) {
Write-Warn 'gcc not found. SQLite requires cgo + a C compiler (e.g. MSYS2 mingw-w64).'
}
return $wailsPath
}
@@ -91,9 +87,9 @@ function Build {
$version = Get-BuildVersion
$ldflags = Get-Ldflags $version
$env:CGO_ENABLED = '1'
if (-not $env:CGO_CFLAGS) { $env:CGO_CFLAGS = '-O2 -g0' }
if (-not $env:CGO_CXXFLAGS) { $env:CGO_CXXFLAGS = '-O2 -g0' }
$env:CGO_ENABLED = '0'
Remove-Item Env:CGO_CFLAGS -ErrorAction SilentlyContinue
Remove-Item Env:CGO_CXXFLAGS -ErrorAction SilentlyContinue
$buildArgs = @('build', '-trimpath')
if (Test-BoolEnabled $env:NEXUS_COMPRESS) {
@@ -107,7 +103,7 @@ function Build {
}
$buildArgs += @('-ldflags', $ldflags)
Write-Info 'Building production binary (CGO_ENABLED=1)...'
Write-Info 'Building production binary (CGO_ENABLED=0)...'
& $wailsPath @buildArgs
Write-Info 'Build complete: build\bin\'
}
+1 -1
View File
@@ -200,7 +200,7 @@ build() {
version="$(build_version)"
local tags
tags="$(build_tags)"
nexus_configure_release_cgo
nexus_configure_pure_go_build
local build_args=("-trimpath")
if nexus_bool_enabled "${NEXUS_COMPRESS:-0}"; then
if nexus_skips_upx_for_target "$(host_os)"; then
+4 -13
View File
@@ -53,17 +53,9 @@ Build a packaged release for the current host platform:
By default, `./release.sh` now builds only the current host OS/architecture. On this Ubuntu 24.04 `amd64` host that means `linux/amd64`.
## Optional cross-compilers
## Optional cross-target builds
If you want to add extra Linux/Windows targets from Ubuntu, install the relevant cross-compiler packages first:
```bash
sudo apt-get install -y \
gcc-aarch64-linux-gnu \
gcc-mingw-w64-x86-64
```
Then request explicit targets:
The release scripts disable CGO and use a pure-Go SQLite driver, so Linux and Windows cross-targets no longer need C cross-compilers for the application backend. Request explicit targets with:
```bash
NEXUS_RELEASE_TARGETS=linux/amd64,windows/amd64 ./release.sh
@@ -72,6 +64,5 @@ NEXUS_RELEASE_TARGETS=linux/amd64,windows/amd64 ./release.sh
## Platform caveats
- macOS targets cannot be cross-built from Linux with Wails. Build those on macOS.
- `windows/arm64` is not covered by the default Ubuntu toolchain here.
- `linux/arm64` needs more than `gcc-aarch64-linux-gnu`; it also needs an `arm64` sysroot and target GTK/WebKit development libraries for CGO to link cleanly.
- Tested on Ubuntu 24.04 `amd64`: `linux/amd64` and `windows/amd64` build successfully; `linux/arm64` does not in the default host setup above.
- Cross-target support still depends on Wails and frontend packaging support for the requested platform.
- Tested on Ubuntu 24.04 `amd64`: `linux/amd64` and `windows/amd64` build successfully.
+7 -1
View File
@@ -9,7 +9,6 @@ require (
github.com/hajimehoshi/go-mp3 v0.3.4
github.com/klauspost/compress v1.17.11
github.com/lrstanley/girc v1.1.1
github.com/mattn/go-sqlite3 v1.14.44
github.com/pkg/sftp v1.13.10
github.com/pquerna/otp v1.5.0
github.com/ulikunitz/xz v0.5.15
@@ -18,6 +17,7 @@ require (
golang.org/x/crypto v0.52.0
golang.org/x/net v0.55.0
golang.org/x/text v0.37.0
modernc.org/sqlite v1.53.0
)
require (
@@ -25,6 +25,7 @@ require (
github.com/bep/debounce v1.2.1 // indirect
github.com/boombuler/barcode v1.1.0 // indirect
github.com/danieljoos/wincred v1.2.3 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/go-ole/go-ole v1.3.0 // indirect
github.com/godbus/dbus/v5 v5.2.2 // indirect
github.com/jchv/go-winloader v0.0.0-20250406163304-c1995be93bd1 // indirect
@@ -37,8 +38,10 @@ require (
github.com/leaanthony/u v1.1.1 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.22 // indirect
github.com/ncruces/go-strftime v1.0.0 // indirect
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/samber/lo v1.53.0 // indirect
github.com/tkrajina/go-reflector v0.5.8 // indirect
@@ -47,4 +50,7 @@ require (
github.com/wailsapp/go-webview2 v1.0.23 // indirect
github.com/wailsapp/mimetype v1.4.1 // indirect
golang.org/x/sys v0.45.0 // indirect
modernc.org/libc v1.73.4 // indirect
modernc.org/mathutil v1.7.1 // indirect
modernc.org/memory v1.11.0 // indirect
)
+44 -2
View File
@@ -12,10 +12,14 @@ github.com/danieljoos/wincred v1.2.3/go.mod h1:6qqX0WNrS4RzPZ1tnroDzq9kY3fu1KwE7
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
github.com/godbus/dbus/v5 v5.2.2 h1:TUR3TgtSVDmjiXOgAAyaZbYmIeP3DPkld3jgKGV8mXQ=
github.com/godbus/dbus/v5 v5.2.2/go.mod h1:3AAv2+hPq5rdnr5txxxRwiGjPXamgoIHgz9FPBfOp3c=
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs=
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
@@ -23,6 +27,8 @@ github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/ad
github.com/hajimehoshi/go-mp3 v0.3.4 h1:NUP7pBYH8OguP4diaTZ9wJbUbk3tC0KlfzsEpWmYj68=
github.com/hajimehoshi/go-mp3 v0.3.4/go.mod h1:fRtZraRFcWb0pu7ok0LqyFhCUrPeMsGRSVop0eemFmo=
github.com/hajimehoshi/oto/v2 v2.3.1/go.mod h1:seWLbgHH7AyUMYKfKYT9pg7PhUu9/SisyJvNTT+ASQo=
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
github.com/jchv/go-winloader v0.0.0-20250406163304-c1995be93bd1 h1:njuLRcjAuMKr7kI3D85AXWkw6/+v9PwtV6M6o11sWHQ=
github.com/jchv/go-winloader v0.0.0-20250406163304-c1995be93bd1/go.mod h1:alcuEEnZsY1WQsagKhZDsoPCRoOijYqhZvPwLG0kzVs=
github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc=
@@ -52,8 +58,8 @@ github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHP
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
github.com/mattn/go-isatty v0.0.22 h1:j8l17JJ9i6VGPUFUYoTUKPSgKe/83EYU2zBC7YNKMw4=
github.com/mattn/go-isatty v0.0.22/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4=
github.com/mattn/go-sqlite3 v1.14.44 h1:3VSe+xafpbzsLbdr2AWlAZk9yRHiBhTBakioXaCKTF8=
github.com/mattn/go-sqlite3 v1.14.44/go.mod h1:pjEuOr8IwzLJP2MfGeTb0A35jauH+C2kbHKBr7yXKVQ=
github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w=
github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
@@ -64,6 +70,8 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pquerna/otp v1.5.0 h1:NMMR+WrmaqXU4EzdGJEE1aUUI0AMRzsp96fFFWNPwxs=
github.com/pquerna/otp v1.5.0/go.mod h1:dkJfzwRKNiegxyNb54X/3fLwhCynbMspSyWKnvi1AEg=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
@@ -93,9 +101,13 @@ github.com/zalando/go-keyring v0.2.8 h1:6sD/Ucpl7jNq10rM2pgqTs0sZ9V3qMrqfIIy5YPc
github.com/zalando/go-keyring v0.2.8/go.mod h1:tsMo+VpRq5NGyKfxoBVjCuMrG47yj8cmakZDO5QGii0=
golang.org/x/crypto v0.52.0 h1:RMs7fP2rXdep0CftQlK8Uf+kibLm7qkCcradZWYz988=
golang.org/x/crypto v0.52.0/go.mod h1:1QgfPxDqh0T2M/elOJtp9RvuR95kVjir0e6/BvEmGbc=
golang.org/x/mod v0.36.0 h1:JJjpVx6myfUsUdAzZuOSTTmRE0PfZeNWzzvKrP7amb4=
golang.org/x/mod v0.36.0/go.mod h1:moc6ELqsWcOw5Ef3xVprK5ul/MvtVvkIXLziUOICjUQ=
golang.org/x/net v0.0.0-20210505024714-0287a6fb4125/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8=
golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww=
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sys v0.0.0-20200810151505-1b9f1253b3ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -110,5 +122,35 @@ golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc=
golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.45.0 h1:18qN3FAooORvApf5XjCXgsuayZOEtXf6JK18I3+ONa8=
golang.org/x/tools v0.45.0/go.mod h1:LuUGqqaXcXMEFEruIVJVm5mgDD8vww/z/SR1gQ4uE/0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
modernc.org/cc/v4 v4.28.4 h1:Hd/4Es+MBj+/7hSdZaisNyu6bv3V0Dp2MdllyfqaH+c=
modernc.org/cc/v4 v4.28.4/go.mod h1:OnovgIhbbMXMu1aISnJ0wvVD1KnW+cAUJkIrAWh+kVI=
modernc.org/ccgo/v4 v4.34.4 h1:OVnSOWQjVKOYkFxoHYB+qQmSHK5gqMqARM+K9DpR/Ws=
modernc.org/ccgo/v4 v4.34.4/go.mod h1:qdKqE8FNIYyysougB1RX9MxCzp5oJOcQXSobANJ4TuE=
modernc.org/fileutil v1.4.0 h1:j6ZzNTftVS054gi281TyLjHPp6CPHr2KCxEXjEbD6SM=
modernc.org/fileutil v1.4.0/go.mod h1:EqdKFDxiByqxLk8ozOxObDSfcVOv/54xDs/DUHdvCUU=
modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=
modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
modernc.org/gc/v3 v3.1.3 h1:6QAplYyVO+KdPW3pGnqmJDUxtkec8ooEWvks/hhU3lc=
modernc.org/gc/v3 v3.1.3/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY=
modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks=
modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI=
modernc.org/libc v1.73.4 h1:+ra4Ui8ngyt8HDcO1FTDPWlkAh6yOdaO2yAoh8MddQA=
modernc.org/libc v1.73.4/go.mod h1:DXZ3eO8qMCNn2SnmTNCiC71nJ9Rcq3PsnpU6Vc4rWK8=
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw=
modernc.org/opt v0.2.0 h1:tGyef5ApycA7FSEOMraay9SaTk5zmbx7Tu+cJs4QKZg=
modernc.org/opt v0.2.0/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
modernc.org/sqlite v1.53.0 h1:20WG8N9q4ji/dEqGk4uiI0c6OPjSeLTNYGFCc3+7c1M=
modernc.org/sqlite v1.53.0/go.mod h1:xoEpOIpGrgT48H5iiyt/YXPCZPEzlfmfFwtk8Lklw8s=
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
+18 -2
View File
@@ -9,7 +9,7 @@ import (
"strings"
"time"
_ "github.com/mattn/go-sqlite3"
_ "modernc.org/sqlite"
"github.com/andydixon/nexus/internal/models"
"github.com/andydixon/nexus/internal/storage/migrations"
@@ -25,13 +25,17 @@ type Store struct {
// NewStore opens (or creates) the SQLite database at dbPath, applies any
// pending migrations, and returns an initialised Store.
func NewStore(dbPath string) (*Store, error) {
db, err := sql.Open("sqlite3", dbPath+"?_journal_mode=WAL&_busy_timeout=5000")
db, err := sql.Open("sqlite", dbPath)
if err != nil {
return nil, fmt.Errorf("storage: failed to open database: %w", err)
}
if err := db.Ping(); err != nil {
return nil, fmt.Errorf("storage: database not reachable: %w", err)
}
if err := configureSQLite(db); err != nil {
_ = db.Close()
return nil, err
}
s := &Store{db: db}
if err := s.migrate(); err != nil {
@@ -40,6 +44,18 @@ func NewStore(dbPath string) (*Store, error) {
return s, nil
}
func configureSQLite(db *sql.DB) error {
db.SetMaxOpenConns(1)
if _, err := db.Exec("PRAGMA busy_timeout = 5000"); err != nil {
return fmt.Errorf("storage: failed to set SQLite busy timeout: %w", err)
}
if _, err := db.Exec("PRAGMA journal_mode = WAL"); err != nil {
return fmt.Errorf("storage: failed to enable SQLite WAL mode: %w", err)
}
return nil
}
// Close shuts down the database connection.
func (s *Store) Close() error {
return s.db.Close()
+4 -70
View File
@@ -132,39 +132,6 @@ ensure_upx() {
exit 1
}
pick_cc() {
local target_os="$1"
local target_arch="$2"
if [ "$target_os" = "windows" ]; then
if command -v zig >/dev/null 2>&1; then
echo "zig:${target_arch}"
return
fi
if [ "$target_arch" = "amd64" ]; then
echo "x86_64-w64-mingw32-gcc"
else
echo "aarch64-w64-mingw32-gcc"
fi
return
fi
if [ "$target_os" = "linux" ]; then
if command -v zig >/dev/null 2>&1; then
echo "zig:${target_arch}"
return
fi
if [ "$target_arch" = "amd64" ]; then
echo "x86_64-linux-gnu-gcc"
else
echo "aarch64-linux-gnu-gcc"
fi
return
fi
echo ""
}
build_target() {
local target_os="$1"
local target_arch="$2"
@@ -172,24 +139,6 @@ build_target() {
local out_dir="$4"
local platform="${target_os}/${target_arch}"
local cc
cc="$(pick_cc "$target_os" "$target_arch")"
local zig_target=""
if [[ "$cc" == zig:* ]]; then
zig_target="${cc#zig:}"
if [ "$zig_target" = "amd64" ]; then
zig_target="x86_64"
elif [ "$zig_target" = "arm64" ]; then
zig_target="aarch64"
fi
cc="zig"
fi
if [ -n "$cc" ] && ! command -v "$cc" >/dev/null 2>&1; then
warn "Skipping ${platform} (missing C compiler: ${cc})"
return 0
fi
local output_base="NexusTerminal-${target_os}-${target_arch}"
local output_path
@@ -202,7 +151,7 @@ build_target() {
fi
log "Building ${platform}..."
nexus_configure_release_cgo
nexus_configure_pure_go_build
local ldflags
ldflags="$(build_ldflags "$version")"
local tags
@@ -220,24 +169,9 @@ build_target() {
build_args+=("-tags" "$tags")
fi
build_args+=("-ldflags" "$ldflags" "-o" "$output_path")
if [ -n "$cc" ]; then
local cc_cmd="$cc"
if [ -n "$zig_target" ]; then
if [ "$target_os" = "linux" ]; then
cc_cmd="zig cc -target ${zig_target}-linux-gnu"
elif [ "$target_os" = "windows" ]; then
cc_cmd="zig cc -target ${zig_target}-windows-gnu"
fi
fi
if ! CC="$cc_cmd" CGO_ENABLED=1 wails build "${build_args[@]}"; then
warn "Build failed for ${platform}"
return 1
fi
else
if ! CGO_ENABLED=1 wails build "${build_args[@]}"; then
warn "Build failed for ${platform}"
return 1
fi
if ! CGO_ENABLED=0 wails build "${build_args[@]}"; then
warn "Build failed for ${platform}"
return 1
fi
local expected_path=""
+4 -3
View File
@@ -29,9 +29,10 @@ nexus_skips_upx_for_target() {
! nexus_bool_enabled "${NEXUS_FORCE_MACOS_UPX:-0}"
}
nexus_configure_release_cgo() {
export CGO_CFLAGS="${CGO_CFLAGS:--O2 -g0}"
export CGO_CXXFLAGS="${CGO_CXXFLAGS:--O2 -g0}"
nexus_configure_pure_go_build() {
export CGO_ENABLED=0
unset CGO_CFLAGS
unset CGO_CXXFLAGS
}
nexus_build_tags() {
-58
View File
@@ -1,58 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
VSCODE_DIR="$ROOT_DIR/vscode"
OUT_DIR="$VSCODE_DIR/dist"
cd "$ROOT_DIR"
echo "[vscode] Building frontend assets"
if [ ! -d "$ROOT_DIR/frontend/node_modules" ]; then
npm --prefix "$ROOT_DIR/frontend" install
fi
npm --prefix "$ROOT_DIR/frontend" run build
echo "[vscode] Building Go bridge"
mkdir -p "$VSCODE_DIR/bin"
BRIDGE_NAME="nexus-vscode-bridge"
if [ "$(go env GOOS)" = "windows" ]; then
BRIDGE_NAME="nexus-vscode-bridge.exe"
fi
go build -trimpath -o "$VSCODE_DIR/bin/$BRIDGE_NAME" ./cmd/vscode-bridge
echo "[vscode] Staging extension assets"
for EXT in nexus leviathan; do
mkdir -p "$VSCODE_DIR/$EXT/bin"
cp "$VSCODE_DIR/bin/$BRIDGE_NAME" "$VSCODE_DIR/$EXT/bin/$BRIDGE_NAME"
done
cp "$ROOT_DIR/frontend/dist/src/js/lib/xterm.min.js" "$VSCODE_DIR/nexus/media/xterm.min.js"
cp "$ROOT_DIR/frontend/dist/src/js/lib/xterm-addon-fit.min.js" "$VSCODE_DIR/nexus/media/xterm-addon-fit.min.js"
cp "$ROOT_DIR/frontend/dist/src/js/lib/xterm-addon-unicode11.min.js" "$VSCODE_DIR/nexus/media/xterm-addon-unicode11.min.js"
cp "$ROOT_DIR/frontend/dist/src/css/lib/xterm.css" "$VSCODE_DIR/nexus/media/xterm.css"
cp "$ROOT_DIR/frontend/dist/src/js/lib/novnc.min.js" "$VSCODE_DIR/nexus/media/novnc.min.js"
echo "[vscode] Packaging VSIX-compatible archives"
rm -rf "$OUT_DIR"
mkdir -p "$OUT_DIR"
package_extension() {
local ext="$1"
local out="$2"
if command -v vsce >/dev/null 2>&1; then
vsce package --no-dependencies --allow-missing-repository -o "$out"
else
npx --yes @vscode/vsce package --no-dependencies --allow-missing-repository -o "$out"
fi
}
for EXT in nexus leviathan; do
(
cd "$VSCODE_DIR/$EXT"
package_extension "$EXT" "$OUT_DIR/$EXT-0.1.0.vsix"
)
echo "[vscode] Wrote $OUT_DIR/$EXT-0.1.0.vsix"
done
echo "[vscode] Build complete"
-969
View File
@@ -1,969 +0,0 @@
'use strict';
const vscode = require('vscode');
const cp = require('child_process');
const crypto = require('crypto');
const fs = require('fs');
const http = require('http');
const path = require('path');
function createCSPNonce() {
return crypto.randomBytes(16).toString('base64');
}
const TOOLS = [
['Editor', 'hex', 'Hex Editor', 'Browse and patch bytes in the working copy.'],
['Triage', 'overview', 'Overview', 'Fast first-pass summary across hashes, strings, entropy and IOCs.'],
['Triage', 'hashes', 'Hashes', 'CRC32, MD5, SHA-1, SHA-256 and SHA-512.'],
['Triage', 'entropy', 'Entropy', 'Windowed entropy for packed or encrypted regions.'],
['Triage', 'strings', 'Strings', 'ASCII and UTF-16 string extraction.'],
['Triage', 'iocs', 'IOCs', 'URLs, domains, hashes, paths and indicators.'],
['Find', 'search', 'Search', 'Text, hex or regular-expression search.'],
['Find', 'multi-search', 'Multi-search', 'Batch text, hex and regex searches.'],
['Structure', 'parse', 'Structures', 'PE, ELF, Mach-O, ZIP, images, RIFF, ASN.1 and certificates.'],
['Structure', 'disasm', 'Disasm', 'Heuristic disassembly for common CPU families.'],
['Structure', 'pdf', 'PDF', 'Objects, streams, xrefs and suspicious PDF surface.'],
['Structure', 'squashfs', 'SquashFS', 'Superblock and directory metadata.'],
['Audio', 'spectrogram-encode', 'Spectrogram Encode', 'Convert images into WAV spectrogram payloads.'],
['Audio', 'spectrogram-view', 'Spectrogram View', 'Render audio files as spectrogram images.'],
['Compare', 'diff', 'Diff', 'Changed byte regions against a compare file.'],
['Recover', 'carve', 'Carve', 'Embedded file carving candidates.'],
['Recover', 'keys', 'Keys', 'PEM, DER, OpenSSH and PKCS#12 artefact discovery.'],
['Recover', 'timestamps', 'Timestamps', 'UNIX, FILETIME, WebKit and OLE timestamp heuristics.'],
['Recover', 'stego', 'Stego', 'Appended payloads and embedded trailer formats.'],
['Recover', 'metadata', 'Metadata', 'PNG, JPEG and GIF textual metadata extraction.'],
['Advanced', 'binsnoop', 'Binsnoop', 'Deep signature scanning through Binsnoop.'],
['Advanced', 'rules', 'Rules', 'YARA-like rule scans over the current document.'],
['Edit', 'patch-plan', 'Patch Plan', 'Validate patch scripts without modifying bytes.'],
['Edit', 'patch-apply', 'Patch Apply', 'Apply a patch script to the working copy.'],
['Edit', 'transform', 'Transforms', 'Hex, Base64, URL, XOR, inflate and cipher pipelines.'],
['Case', 'case', 'Case Export', 'Build a case archive payload from the working copy.']
].map(([group, id, name, desc]) => ({ group, id, name, desc }));
const DEFAULTS = {
search: { mode: 'text', query: 'MZ', max: 1000 },
'multi-search': { spec: 'text MZ\nhex 7f454c46\nregex https?://', max: 1000 },
parse: { kind: 'auto' },
disasm: { arch: 'x86_64', start: 0, length: 4096, max: 1000 },
entropy: { window: 4096, step: 4096 },
strings: { min: 4, max: 5000 },
iocs: { min: 5 },
carve: { max: 256 },
keys: { max: 200 },
timestamps: { max: 1000 },
rules: { source: 'rule mz { strings: $a = "MZ" condition: $a }', max: 1000 },
'patch-plan': { source: 'set 0x00 90 90' },
'patch-apply': { source: 'set 0x00 90 90' },
transform: { pipeline: 'hex.encode', start: 0, length: 1024 },
'spectrogram-view': { fftSize: 2048, height: 1024, maxWidth: 1400, aspectRatio: 1, minHz: 0, maxHz: 0, brightness: 1, contrast: 1 },
hex: { start: 0, bytes: 2048, width: 16 }
};
class Bridge {
constructor(context) {
this.context = context;
this.proc = null;
this.addr = null;
this.token = null;
}
async start() {
if (this.addr) return;
const exe = process.platform === 'win32' ? 'nexus-vscode-bridge.exe' : 'nexus-vscode-bridge';
const bridgePath = path.join(this.context.extensionPath, 'bin', exe);
if (!fs.existsSync(bridgePath)) {
throw new Error(`Bridge executable not found: ${bridgePath}. Run vscode/build.sh first.`);
}
// VSIX extraction does not always preserve the executable bit, so ensure it.
if (process.platform !== 'win32') {
try { fs.chmodSync(bridgePath, 0o755); } catch (_) { /* best effort */ }
}
this.proc = cp.spawn(bridgePath, ['-listen', '127.0.0.1:0'], {
stdio: ['ignore', 'pipe', 'pipe'],
windowsHide: true
});
this.context.subscriptions.push({ dispose: () => this.stop() });
const firstLine = await new Promise((resolve, reject) => {
let buf = '';
const timer = setTimeout(() => reject(new Error('Timed out waiting for bridge startup')), 10000);
this.proc.stdout.on('data', chunk => {
buf += chunk.toString('utf8');
const idx = buf.indexOf('\n');
if (idx >= 0) {
clearTimeout(timer);
resolve(buf.slice(0, idx));
}
});
this.proc.once('error', reject);
this.proc.once('exit', code => reject(new Error(`Bridge exited during startup (${code})`)));
});
const meta = JSON.parse(firstLine);
this.addr = meta.addr;
this.token = meta.token;
}
stop() {
if (this.proc && !this.proc.killed) this.proc.kill();
this.proc = null;
}
async rpc(method, args = []) {
await this.start();
const payload = Buffer.from(JSON.stringify({ method, args }));
return new Promise((resolve, reject) => {
const req = http.request({
hostname: this.addr.split(':')[0],
port: Number(this.addr.split(':')[1]),
path: '/rpc',
method: 'POST',
headers: {
Authorization: `Bearer ${this.token}`,
'Content-Type': 'application/json',
'Content-Length': payload.length
}
}, res => {
let data = '';
res.on('data', chunk => data += chunk);
res.on('end', () => {
if (res.statusCode < 200 || res.statusCode >= 300) {
reject(new Error(`Bridge HTTP ${res.statusCode}: ${data}`));
return;
}
const decoded = JSON.parse(data || '{}');
if (decoded.error) reject(new Error(decoded.error));
else resolve(decoded.result);
});
});
req.on('error', reject);
req.write(payload);
req.end();
});
}
}
class ToolsView {
constructor(context, bridge) {
this.context = context;
this.bridge = bridge;
this.view = null;
this.state = null;
}
resolveWebviewView(view) {
this.view = view;
view.webview.options = { enableScripts: true };
view.webview.html = this.html(view.webview);
view.webview.onDidReceiveMessage(async msg => {
try {
if (msg.type === 'load') await loadFile(this.bridge, false, this);
if (msg.type === 'compare') await loadFile(this.bridge, true, this);
if (msg.type === 'save') await saveFile(this.bridge, this);
if (msg.type === 'refresh') await this.refreshState();
if (msg.type === 'tool') await openTool(this.context, this.bridge, this, msg.tool);
} catch (err) {
this.postError(err);
}
});
this.refreshState();
}
async refreshState() {
try {
this.state = await this.bridge.rpc('LeviathanState');
} catch (_) {
this.state = null;
}
this.postState();
}
postState() {
if (this.view) this.view.webview.postMessage({ type: 'state', state: this.state });
}
postError(err) {
if (this.view) this.view.webview.postMessage({ type: 'error', error: err.message || String(err) });
else vscode.window.showErrorMessage(err.message || String(err));
}
html(webview) {
const nonce = createCSPNonce();
const toolsJson = JSON.stringify(TOOLS);
return `<!doctype html>
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; style-src 'unsafe-inline'; script-src 'nonce-${nonce}';">
<style>
body {
margin: 0;
color: var(--vscode-foreground);
background: var(--vscode-sideBar-background);
font: var(--vscode-font-size) var(--vscode-font-family);
}
.shell { display: grid; gap: 10px; padding: 10px; }
.actions { display: grid; grid-template-columns: 1fr 1fr; gap: 6px; }
button {
border: 1px solid var(--vscode-button-border, transparent);
background: var(--vscode-button-secondaryBackground);
color: var(--vscode-button-secondaryForeground);
padding: 5px 8px;
min-height: 26px;
font: inherit;
cursor: pointer;
}
button:hover { filter: brightness(1.08); }
button.primary { background: var(--vscode-button-background); color: var(--vscode-button-foreground); }
.state {
border: 1px solid var(--vscode-panel-border);
background: var(--vscode-editorWidget-background);
padding: 8px;
overflow-wrap: anywhere;
}
.state strong { display: block; line-height: 18px; }
.state span { color: var(--vscode-descriptionForeground); font-size: 11px; }
.search {
width: 100%;
box-sizing: border-box;
border: 1px solid var(--vscode-input-border, transparent);
background: var(--vscode-input-background);
color: var(--vscode-input-foreground);
padding: 6px 8px;
outline: none;
}
.search:focus { border-color: var(--vscode-focusBorder); }
.segments { display: flex; flex-wrap: wrap; gap: 5px; }
.seg {
min-height: 23px;
padding: 3px 7px;
background: transparent;
color: var(--vscode-descriptionForeground);
border: 1px solid var(--vscode-panel-border);
font-size: 11px;
}
.seg.active {
background: var(--vscode-button-background);
color: var(--vscode-button-foreground);
border-color: transparent;
}
.group {
display: flex;
justify-content: space-between;
margin: 10px 0 5px;
color: var(--vscode-descriptionForeground);
font-size: 11px;
text-transform: uppercase;
letter-spacing: .04em;
}
.tool {
width: 100%;
display: grid;
grid-template-columns: auto 1fr;
gap: 8px;
text-align: left;
border: 1px solid var(--vscode-panel-border);
background: var(--vscode-editor-background);
color: var(--vscode-foreground);
padding: 8px;
margin-bottom: 7px;
}
.tool:hover { border-color: var(--vscode-focusBorder); background: var(--vscode-list-hoverBackground); }
.mark {
width: 30px;
height: 28px;
display: grid;
place-items: center;
background: var(--vscode-badge-background);
color: var(--vscode-badge-foreground);
border: 1px solid var(--vscode-panel-border);
font-size: 10px;
font-weight: 700;
}
.name { font-weight: 600; line-height: 17px; }
.desc { color: var(--vscode-descriptionForeground); font-size: 11px; line-height: 15px; }
.empty {
border: 1px dashed var(--vscode-panel-border);
color: var(--vscode-descriptionForeground);
text-align: center;
padding: 16px 10px;
}
.status { color: var(--vscode-descriptionForeground); font-size: 11px; min-height: 15px; }
</style>
</head>
<body>
<div class="shell">
<div class="actions">
<button class="primary" id="load">Load File</button>
<button id="compare">Compare</button>
<button id="save">Save Copy</button>
<button id="refresh">Refresh</button>
</div>
<div class="state" id="state"><strong>No file loaded</strong><span>Load a binary or right-click a file in Explorer.</span></div>
<input id="search" class="search" placeholder="Search tools">
<div id="segments" class="segments"></div>
<div id="tools"></div>
<div id="status" class="status"></div>
</div>
<script nonce="${nonce}">
const vscode = acquireVsCodeApi();
const tools = ${toolsJson};
let state = null;
let filter = 'all';
const groups = ['all', ...Array.from(new Set(tools.map(t => t.group)))];
const search = document.getElementById('search');
document.getElementById('load').onclick = () => vscode.postMessage({ type: 'load' });
document.getElementById('compare').onclick = () => vscode.postMessage({ type: 'compare' });
document.getElementById('save').onclick = () => vscode.postMessage({ type: 'save' });
document.getElementById('refresh').onclick = () => vscode.postMessage({ type: 'refresh' });
function esc(value) {
return String(value == null ? '' : value).replace(/[&<>"']/g, ch => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[ch]));
}
function mark(tool) {
return tool.name.split(/\\s+/).map(part => part[0]).join('').slice(0, 3).toUpperCase();
}
function renderState() {
const el = document.getElementById('state');
if (!state || !state.name) {
el.innerHTML = '<strong>No file loaded</strong><span>Load a binary or right-click a file in Explorer.</span>';
return;
}
el.innerHTML = '<strong>' + esc(state.name) + '</strong><span>' + Number(state.size || 0).toLocaleString() + ' bytes' + (state.dirty ? ' • modified' : '') + (state.compare ? ' • compare loaded' : '') + '</span>';
}
function renderSegments() {
const root = document.getElementById('segments');
root.textContent = '';
for (const group of groups) {
const count = group === 'all' ? tools.length : tools.filter(t => t.group === group).length;
const btn = document.createElement('button');
btn.className = 'seg' + (filter === group ? ' active' : '');
btn.textContent = group === 'all' ? 'All ' + count : group + ' ' + count;
btn.onclick = () => { filter = group; renderTools(); renderSegments(); };
root.appendChild(btn);
}
}
function visibleTools() {
const q = search.value.trim().toLowerCase();
return tools.filter(tool => {
if (filter !== 'all' && tool.group !== filter) return false;
if (!q) return true;
return (tool.name + ' ' + tool.group + ' ' + tool.desc).toLowerCase().includes(q);
});
}
function renderTools() {
const root = document.getElementById('tools');
const visible = visibleTools();
if (!visible.length) {
root.innerHTML = '<div class="empty">No matching tools</div>';
return;
}
const byGroup = new Map();
for (const tool of visible) {
if (!byGroup.has(tool.group)) byGroup.set(tool.group, []);
byGroup.get(tool.group).push(tool);
}
root.innerHTML = [...byGroup.entries()].map(([group, items]) =>
'<div class="group"><span>' + esc(group) + '</span><span>' + items.length + '</span></div>' +
items.map(tool => '<button class="tool" data-tool="' + esc(tool.id) + '"><span class="mark">' + esc(mark(tool)) + '</span><span><span class="name">' + esc(tool.name) + '</span><br><span class="desc">' + esc(tool.desc) + '</span></span></button>').join('')
).join('');
}
document.getElementById('tools').addEventListener('click', event => {
const btn = event.target.closest('[data-tool]');
if (btn) vscode.postMessage({ type: 'tool', tool: btn.dataset.tool });
});
search.addEventListener('input', renderTools);
renderSegments();
renderTools();
window.addEventListener('message', event => {
const msg = event.data || {};
if (msg.type === 'state') {
state = msg.state;
renderState();
}
if (msg.type === 'error') document.getElementById('status').textContent = msg.error || '';
});
vscode.postMessage({ type: 'refresh' });
</script>
</body>
</html>`;
}
}
class ToolPanel {
constructor(context, bridge, tool, options = {}) {
this.context = context;
this.bridge = bridge;
this.tool = tool;
this.fileState = options.fileState || null;
const meta = TOOLS.find(t => t.id === tool) || { name: tool };
const title = this.fileState?.name ? `${meta.name}: ${this.fileState.name}` : meta.name;
this.panel = vscode.window.createWebviewPanel('leviathanTool', title, vscode.ViewColumn.Active, { enableScripts: true, retainContextWhenHidden: true });
this.panel.webview.html = this.tool === 'hex' ? this.hexHtml(meta) : this.html(meta);
this.panel.webview.onDidReceiveMessage(msg => {
if (msg.type === 'run') this.run(msg.params);
if (msg.type === 'hexLoad') this.loadHex(msg);
if (msg.type === 'hexSet') this.setHexByte(msg);
if (msg.type === 'hexSearch') this.searchHex(msg);
if (msg.type === 'undo') this.stateAction('LeviathanUndo', msg);
if (msg.type === 'redo') this.stateAction('LeviathanRedo', msg);
if (msg.type === 'revert') this.stateAction('LeviathanRevert', msg);
});
this.panel.webview.postMessage({ type: 'fileState', state: this.fileState });
this.refreshState();
if (options.autoRun) {
if (this.tool === 'hex') {
setTimeout(() => this.loadHex(DEFAULTS.hex), 150);
} else {
setTimeout(() => this.run(JSON.stringify(DEFAULTS[this.tool] || {})), 150);
}
}
}
async refreshState() {
try {
const state = await this.bridge.rpc('LeviathanState');
this.fileState = state;
this.panel.webview.postMessage({ type: 'fileState', state });
} catch (err) {
this.error(err);
}
}
async run(paramsText) {
try {
const params = JSON.parse(paramsText || '{}');
const result = await this.bridge.rpc('LeviathanRunTool', [this.tool, params]);
this.panel.webview.postMessage({ type: 'result', result });
} catch (err) {
this.error(err);
}
}
error(err) {
this.panel.webview.postMessage({ type: 'error', error: err.message || String(err) });
}
async loadHex(params = {}) {
try {
const start = Number(params.start || 0);
const bytes = Number(params.bytes || 2048);
const width = Number(params.width || 16);
const result = await this.bridge.rpc('LeviathanRunTool', ['hex', { start, bytes, width }]);
this.panel.webview.postMessage({ type: 'hexRows', rows: result.rows || [], start, bytes, width });
await this.refreshState();
} catch (err) {
this.error(err);
}
}
async setHexByte(msg) {
try {
const offset = Number(msg.offset);
const value = String(msg.value || '').trim();
const payload = await this.bridge.rpc('LeviathanSetByte', [offset, value]);
if (payload && payload.state) {
this.fileState = payload.state;
this.panel.webview.postMessage({ type: 'fileState', state: payload.state });
}
await this.loadHex({ start: msg.start, bytes: msg.bytes, width: msg.width });
} catch (err) {
this.panel.webview.postMessage({ type: 'hexEditError', offset: msg.offset, error: err.message || String(err) });
}
}
async searchHex(msg) {
try {
const mode = msg.mode === 'hex' ? 'hex' : 'text';
const query = String(msg.query || '');
const result = await this.bridge.rpc('LeviathanRunTool', ['search', { mode, query, max: 2000 }]);
this.panel.webview.postMessage({ type: 'hexSearchResults', hits: result.hits || [], mode, query });
} catch (err) {
this.error(err);
}
}
async stateAction(method, msg = {}) {
try {
const state = await this.bridge.rpc(method);
this.panel.webview.postMessage({ type: 'state', state });
this.panel.webview.postMessage({ type: 'fileState', state });
if (this.tool === 'hex') {
await this.loadHex(msg.start === undefined ? DEFAULTS.hex : msg);
}
} catch (err) {
this.error(err);
}
}
hexHtml(meta) {
const nonce = createCSPNonce();
const initialFileState = JSON.stringify(this.fileState || null);
const name = escapeHtml(meta.name);
return `<!doctype html>
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; style-src 'unsafe-inline'; script-src 'nonce-${nonce}';">
<style>
body {
margin: 0;
color: var(--vscode-foreground);
background: var(--vscode-editor-background);
font: var(--vscode-font-size) var(--vscode-font-family);
overflow: hidden;
}
header {
display: grid;
grid-template-columns: 1fr auto;
gap: 8px;
padding: 9px 10px;
border-bottom: 1px solid var(--vscode-panel-border);
}
h1 { margin: 0; font-size: 15px; line-height: 19px; }
.file-state { color: var(--vscode-descriptionForeground); font-size: 12px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.toolbar {
display: grid;
grid-template-columns: auto auto auto auto 1fr auto auto;
gap: 7px;
align-items: center;
padding: 8px 10px;
border-bottom: 1px solid var(--vscode-panel-border);
background: var(--vscode-sideBar-background);
}
.searchbar {
display: grid;
grid-template-columns: auto 1fr auto auto auto;
gap: 7px;
align-items: center;
padding: 8px 10px;
border-bottom: 1px solid var(--vscode-panel-border);
background: var(--vscode-sideBar-background);
}
input, select {
min-width: 0;
color: var(--vscode-input-foreground);
background: var(--vscode-input-background);
border: 1px solid var(--vscode-input-border, transparent);
padding: 5px 7px;
font: 12px Menlo, Monaco, Consolas, "Courier New", monospace;
box-sizing: border-box;
}
input:focus, select:focus { outline: 1px solid var(--vscode-focusBorder); }
button {
border: 1px solid var(--vscode-button-border, transparent);
background: var(--vscode-button-secondaryBackground);
color: var(--vscode-button-secondaryForeground);
padding: 5px 9px;
min-height: 26px;
font: inherit;
cursor: pointer;
}
button.primary { background: var(--vscode-button-background); color: var(--vscode-button-foreground); }
.seg { display: inline-flex; border: 1px solid var(--vscode-panel-border); }
.seg button { border: 0; border-right: 1px solid var(--vscode-panel-border); background: transparent; }
.seg button:last-child { border-right: 0; }
.seg button.active { background: var(--vscode-button-background); color: var(--vscode-button-foreground); }
main { height: calc(100vh - 132px); overflow: auto; }
table { width: 100%; border-collapse: collapse; font: 12px Menlo, Monaco, Consolas, "Courier New", monospace; }
th, td { border-bottom: 1px solid var(--vscode-panel-border); padding: 3px 6px; vertical-align: top; }
th { position: sticky; top: 0; background: var(--vscode-editor-background); z-index: 1; text-align: left; color: var(--vscode-descriptionForeground); }
.offset { width: 88px; color: var(--vscode-descriptionForeground); user-select: text; }
.hex-cell {
width: 2.35ch;
margin-right: .2ch;
padding: 1px 2px;
border: 1px solid transparent;
background: transparent;
color: var(--vscode-foreground);
text-align: center;
text-transform: uppercase;
font: 12px Menlo, Monaco, Consolas, "Courier New", monospace;
}
.hex-cell:focus { border-color: var(--vscode-focusBorder); background: var(--vscode-input-background); outline: none; }
.hex-cell.dirty { color: var(--vscode-terminal-ansiYellow); }
.hex-cell.hit, .ascii-char.hit { background: var(--vscode-editor-findMatchHighlightBackground); }
.ascii {
white-space: pre;
color: var(--vscode-descriptionForeground);
letter-spacing: .08em;
min-width: 34ch;
}
.ascii-char { display: inline-block; min-width: 1ch; }
.status { color: var(--vscode-descriptionForeground); font-size: 12px; }
</style>
</head>
<body>
<header>
<div><h1>${name}</h1><div id="fileState" class="file-state">Checking loaded file...</div></div>
<div><button id="undo">Undo</button><button id="redo">Redo</button><button id="revert">Revert</button></div>
</header>
<section class="toolbar">
<label>Offset <input id="start" value="0" size="10"></label>
<label>Bytes <input id="bytes" value="2048" size="8"></label>
<span class="seg"><button id="width16" class="active">16</button><button id="width32">32</button></span>
<button id="prev">Prev</button>
<button id="next">Next</button>
<button id="load" class="primary">Load</button>
<span id="status" class="status"></span>
</section>
<section class="searchbar">
<select id="searchMode"><option value="hex">Hex</option><option value="ascii">ASCII</option></select>
<input id="searchQuery" placeholder="Search hex bytes or ASCII text">
<button id="search">Search</button>
<button id="searchPrev">Prev Hit</button>
<button id="searchNext">Next Hit</button>
</section>
<main>
<table>
<thead><tr><th class="offset">Offset</th><th>Hex</th><th>ASCII</th></tr></thead>
<tbody id="rows"></tbody>
</table>
</main>
<script nonce="${nonce}">
const vscode = acquireVsCodeApi();
const initialFileState = ${initialFileState};
let width = 16;
let rows = [];
let hits = [];
let hitIndex = -1;
const changed = new Map();
function esc(value) {
return String(value == null ? '' : value).replace(/[&<>"']/g, ch => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[ch]));
}
function parseNum(value, fallback) {
const text = String(value || '').trim();
if (!text) return fallback;
const n = Number(text.startsWith('0x') || text.startsWith('0X') ? parseInt(text, 16) : parseInt(text, 10));
return Number.isFinite(n) ? n : fallback;
}
function printable(hex) {
const n = parseInt(hex, 16);
return n >= 32 && n < 127 ? String.fromCharCode(n) : '.';
}
function currentParams() {
return {
start: parseNum(document.getElementById('start').value, 0),
bytes: parseNum(document.getElementById('bytes').value, 2048),
width
};
}
function renderFileState(s) {
document.getElementById('fileState').textContent = s && s.name ? s.name + ' • ' + Number(s.size || 0).toLocaleString() + ' bytes' + (s.dirty ? ' • modified' : '') : 'No file loaded';
}
function hitOffsets() {
const set = new Set();
for (const hit of hits) {
const start = Number(hit.offset || hit.start || 0);
const length = Math.max(1, Number(hit.length || 1));
for (let i = 0; i < length; i++) set.add(start + i);
}
return set;
}
function renderRows() {
const hitSet = hitOffsets();
const body = document.getElementById('rows');
body.textContent = '';
for (const row of rows) {
const tr = document.createElement('tr');
const off = Number(row.offset || 0);
const bytes = Array.isArray(row.bytes) ? row.bytes : String(row.hex || '').split(/\\s+/).filter(Boolean);
const hexTd = document.createElement('td');
const asciiTd = document.createElement('td');
asciiTd.className = 'ascii';
bytes.forEach((byte, idx) => {
const absolute = off + idx;
const input = document.createElement('input');
input.className = 'hex-cell' + (changed.has(absolute) ? ' dirty' : '') + (hitSet.has(absolute) ? ' hit' : '');
input.maxLength = 2;
input.value = String(changed.get(absolute) || byte).toUpperCase();
input.dataset.offset = String(absolute);
input.addEventListener('input', () => {
input.value = input.value.replace(/[^0-9a-fA-F]/g, '').slice(0, 2).toUpperCase();
updateAsciiFromInputs(tr);
});
input.addEventListener('change', () => commitByte(input));
input.addEventListener('keydown', event => {
if (event.key === 'Enter') { event.preventDefault(); input.blur(); }
});
hexTd.appendChild(input);
});
tr.innerHTML = '<td class="offset">0x' + off.toString(16).padStart(8, '0') + '</td>';
tr.appendChild(hexTd);
tr.appendChild(asciiTd);
body.appendChild(tr);
updateAsciiFromInputs(tr);
}
}
function updateAsciiFromInputs(tr) {
const asciiTd = tr.querySelector('.ascii');
asciiTd.textContent = '';
for (const input of tr.querySelectorAll('.hex-cell')) {
const span = document.createElement('span');
span.className = 'ascii-char' + (input.classList.contains('hit') ? ' hit' : '');
span.textContent = /^[0-9A-Fa-f]{2}$/.test(input.value) ? printable(input.value) : '.';
asciiTd.appendChild(span);
}
}
function commitByte(input) {
if (!/^[0-9A-Fa-f]{2}$/.test(input.value)) {
document.getElementById('status').textContent = 'Byte must be two hex digits';
return;
}
const offset = Number(input.dataset.offset);
changed.set(offset, input.value.toUpperCase());
input.classList.add('dirty');
vscode.postMessage({ type: 'hexSet', offset, value: input.value, ...currentParams() });
}
function load() {
document.getElementById('status').textContent = 'Loading...';
vscode.postMessage({ type: 'hexLoad', ...currentParams() });
}
function gotoHit(delta) {
if (!hits.length) return;
hitIndex = (hitIndex + delta + hits.length) % hits.length;
const offset = Number(hits[hitIndex].offset || hits[hitIndex].start || 0);
const aligned = Math.max(0, Math.floor(offset / width) * width);
document.getElementById('start').value = String(aligned);
load();
}
document.getElementById('width16').onclick = () => { width = 16; document.getElementById('width16').classList.add('active'); document.getElementById('width32').classList.remove('active'); load(); };
document.getElementById('width32').onclick = () => { width = 32; document.getElementById('width32').classList.add('active'); document.getElementById('width16').classList.remove('active'); load(); };
document.getElementById('load').onclick = load;
document.getElementById('prev').onclick = () => { const p = currentParams(); document.getElementById('start').value = String(Math.max(0, p.start - p.bytes)); load(); };
document.getElementById('next').onclick = () => { const p = currentParams(); document.getElementById('start').value = String(p.start + p.bytes); load(); };
document.getElementById('search').onclick = () => {
const mode = document.getElementById('searchMode').value === 'hex' ? 'hex' : 'ascii';
vscode.postMessage({ type: 'hexSearch', mode, query: document.getElementById('searchQuery').value });
};
document.getElementById('searchPrev').onclick = () => gotoHit(-1);
document.getElementById('searchNext').onclick = () => gotoHit(1);
document.getElementById('undo').onclick = () => vscode.postMessage({ type: 'undo', ...currentParams() });
document.getElementById('redo').onclick = () => vscode.postMessage({ type: 'redo', ...currentParams() });
document.getElementById('revert').onclick = () => vscode.postMessage({ type: 'revert', ...currentParams() });
window.addEventListener('message', event => {
const msg = event.data || {};
if (msg.type === 'fileState') renderFileState(msg.state);
if (msg.type === 'hexRows') {
rows = msg.rows || [];
width = Number(msg.width || width);
renderRows();
document.getElementById('status').textContent = rows.length ? rows.length + ' rows' : 'No bytes in range';
}
if (msg.type === 'hexSearchResults') {
hits = msg.hits || [];
hitIndex = hits.length ? 0 : -1;
document.getElementById('status').textContent = hits.length + ' hit' + (hits.length === 1 ? '' : 's');
if (hits.length) {
const offset = Number(hits[0].offset || hits[0].start || 0);
document.getElementById('start').value = String(Math.max(0, Math.floor(offset / width) * width));
load();
} else {
renderRows();
}
}
if (msg.type === 'hexEditError') document.getElementById('status').textContent = msg.error || 'Edit failed';
if (msg.type === 'state') renderFileState(msg.state);
if (msg.type === 'error') document.getElementById('status').textContent = msg.error || '';
});
renderFileState(initialFileState);
</script>
</body>
</html>`;
}
html(meta) {
const nonce = createCSPNonce();
const defaults = JSON.stringify(DEFAULTS[this.tool] || {});
const initialFileState = JSON.stringify(this.fileState || null);
const name = escapeHtml(meta.name);
return `<!doctype html>
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; style-src 'unsafe-inline'; script-src 'nonce-${nonce}';">
<style>
body { margin: 0; color: var(--vscode-foreground); background: var(--vscode-editor-background); font: var(--vscode-font-size) var(--vscode-font-family); }
header { display: flex; align-items: center; justify-content: space-between; gap: 8px; padding: 10px 12px; border-bottom: 1px solid var(--vscode-panel-border); }
h1 { margin: 0; font-size: 15px; }
.title-wrap { min-width: 0; }
.file-state { color: var(--vscode-descriptionForeground); font-size: 12px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
header button, .actions button { border: 1px solid var(--vscode-button-border, transparent); background: var(--vscode-button-secondaryBackground); color: var(--vscode-button-secondaryForeground); }
header button:hover, .actions button:hover { background: var(--vscode-button-secondaryHoverBackground, var(--vscode-button-secondaryBackground)); }
main { display: grid; grid-template-columns: minmax(260px, 360px) 1fr; height: calc(100vh - 58px); }
.left { border-right: 1px solid var(--vscode-panel-border); display: flex; flex-direction: column; min-height: 0; min-width: 0; }
.params-head { padding: 12px 12px 6px; font-size: 11px; text-transform: uppercase; letter-spacing: .04em; color: var(--vscode-descriptionForeground); }
textarea { flex: 1; margin: 0 12px; resize: none; color: var(--vscode-input-foreground); background: var(--vscode-input-background); border: 1px solid var(--vscode-input-border); padding: 8px; box-sizing: border-box; font-family: var(--vscode-editor-font-family, monospace); font-size: 12px; }
textarea:focus { outline: none; border-color: var(--vscode-focusBorder); }
.actions { display: flex; gap: 6px; padding: 10px 12px; }
button { padding: 5px 10px; cursor: pointer; }
button.primary { background: var(--vscode-button-background); color: var(--vscode-button-foreground); border-color: var(--vscode-button-border, transparent); }
button.primary:hover { background: var(--vscode-button-hoverBackground, var(--vscode-button-background)); }
#out { margin: 0; padding: 12px; overflow: auto; height: calc(100% - 24px); box-sizing: border-box; }
.out-empty { color: var(--vscode-descriptionForeground); }
table { border-collapse: collapse; width: 100%; margin: 0 0 12px; }
th, td { border: 1px solid var(--vscode-panel-border); padding: 4px 6px; text-align: left; vertical-align: top; font-family: var(--vscode-editor-font-family); font-size: 12px; }
th { background: var(--vscode-editorWidget-background); position: sticky; top: 0; }
pre { white-space: pre-wrap; word-break: break-word; }
img { max-width: 100%; height: auto; border: 1px solid var(--vscode-panel-border); }
h2 { font-size: 14px; margin: 12px 0 6px; }
.hex-table td, .hex-table th { font-family: Menlo, Monaco, Consolas, "Courier New", monospace; }
.hex-cell { display: inline-block; min-width: 2.2ch; margin-right: .35ch; }
.ascii { color: var(--vscode-descriptionForeground); }
</style>
</head>
<body>
<header>
<div class="title-wrap"><h1>${name}</h1><div id="fileState" class="file-state">Checking loaded file...</div></div>
<div><button id="undo">Undo</button><button id="redo">Redo</button><button id="revert">Revert</button></div>
</header>
<main>
<section class="left">
<div class="params-head">Parameters (JSON)</div>
<textarea id="params" spellcheck="false">${escapeHtml(JSON.stringify(JSON.parse(defaults), null, 2))}</textarea>
<div class="actions"><button id="run" class="primary">Run</button></div>
</section>
<section id="out"><span class="out-empty">Adjust the parameters and select Run to see results.</span></section>
</main>
<script nonce="${nonce}">
const vscode = acquireVsCodeApi();
const initialFileState = ${initialFileState};
const runTool = () => vscode.postMessage({ type: 'run', params: document.getElementById('params').value });
document.getElementById('run').onclick = runTool;
document.getElementById('params').addEventListener('keydown', event => {
if ((event.ctrlKey || event.metaKey) && event.key === 'Enter') { event.preventDefault(); runTool(); }
});
document.getElementById('undo').onclick = () => vscode.postMessage({ type: 'undo' });
document.getElementById('redo').onclick = () => vscode.postMessage({ type: 'redo' });
document.getElementById('revert').onclick = () => vscode.postMessage({ type: 'revert' });
function esc(value) {
return String(value == null ? '' : value).replace(/[&<>"']/g, ch => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[ch]));
}
function table(rows) {
if (!Array.isArray(rows) || rows.length === 0 || typeof rows[0] !== 'object') return '<pre>' + esc(JSON.stringify(rows, null, 2)) + '</pre>';
const keys = Array.from(rows.reduce((set, row) => {
Object.keys(row || {}).slice(0, 12).forEach(key => set.add(key));
return set;
}, new Set()));
return '<table><thead><tr>' + keys.map(k => '<th>' + esc(k) + '</th>').join('') + '</tr></thead><tbody>' +
rows.slice(0, 1000).map(row => '<tr>' + keys.map(k => '<td>' + esc(typeof row[k] === 'object' ? JSON.stringify(row[k]) : row[k]) + '</td>').join('') + '</tr>').join('') +
'</tbody></table>' + (rows.length > 1000 ? '<p>Showing first 1000 rows.</p>' : '');
}
function renderHexRows(rows) {
return '<table class="hex-table"><thead><tr><th>Offset</th><th>Hex</th><th>ASCII</th></tr></thead><tbody>' +
rows.map(row => {
const cells = Array.isArray(row.bytes) ? row.bytes.map(cell => '<span class="hex-cell">' + esc(cell && cell.hex ? cell.hex : cell) + '</span>').join('') : esc(row.hex || '');
return '<tr><td>' + esc(row.offset) + '</td><td>' + cells + '</td><td class="ascii">' + esc(row.ascii || '') + '</td></tr>';
}).join('') + '</tbody></table>';
}
function render(value) {
if (value && value.mime === 'image/png' && value.pngBase64) {
const summary = value.summary ? '<h2>Summary</h2>' + render(value.summary) : '';
return '<img src="data:image/png;base64,' + value.pngBase64 + '">' + summary;
}
if (Array.isArray(value)) return table(value);
if (!value || typeof value !== 'object') return '<pre>' + esc(value) + '</pre>';
if (Array.isArray(value.rows) && value.rows.length && Object.prototype.hasOwnProperty.call(value.rows[0], 'ascii')) {
return renderHexRows(value.rows);
}
const preferred = ['rows', 'items', 'hits', 'results', 'entries', 'plan'];
let html = '';
for (const key of preferred) {
if (Array.isArray(value[key])) html += '<h2>' + esc(key) + '</h2>' + table(value[key]);
}
for (const [key, val] of Object.entries(value)) {
if (preferred.includes(key) && Array.isArray(val)) continue;
html += '<h2>' + esc(key) + '</h2>' + (Array.isArray(val) ? table(val) : '<pre>' + esc(JSON.stringify(val, null, 2)) + '</pre>');
}
return html || '<pre>' + esc(JSON.stringify(value, null, 2)) + '</pre>';
}
function renderFileState(s) {
document.getElementById('fileState').textContent = s && s.name ? s.name + ' • ' + Number(s.size || 0).toLocaleString() + ' bytes' + (s.dirty ? ' • modified' : '') : 'No file loaded';
}
window.addEventListener('message', event => {
const msg = event.data || {};
if (msg.type === 'result') document.getElementById('out').innerHTML = render(msg.result);
if (msg.type === 'state') document.getElementById('out').innerHTML = render(msg.state);
if (msg.type === 'fileState') renderFileState(msg.state);
if (msg.type === 'error') document.getElementById('out').textContent = msg.error;
});
renderFileState(initialFileState);
</script>
</body>
</html>`;
}
}
async function loadFile(bridge, compare, view) {
const picked = await vscode.window.showOpenDialog({ canSelectFiles: true, canSelectFolders: false, canSelectMany: false });
if (!picked || !picked.length) return null;
await bridge.rpc('LeviathanLoadFile', [picked[0].fsPath, compare]);
if (view) await view.refreshState();
return bridge.rpc('LeviathanState');
}
async function saveFile(bridge, view) {
const state = await bridge.rpc('LeviathanState');
const defaultName = state && state.name ? `${state.name}.patched` : 'working-copy.bin';
const picked = await vscode.window.showSaveDialog({ defaultUri: vscode.Uri.file(defaultName) });
if (!picked) return;
await bridge.rpc('LeviathanSaveWorkingToPath', [picked.fsPath]);
await view.refreshState();
}
async function ensureLoadedFile(bridge, toolsView) {
try {
const state = await bridge.rpc('LeviathanState');
if (state && state.name) return state;
} catch (_) {
// Fall through to picker.
}
const loaded = await loadFile(bridge, false, toolsView);
if (!loaded || !loaded.name) {
throw new Error('Load a file before opening a Leviathan tool.');
}
return loaded;
}
async function openTool(context, bridge, toolsView, tool) {
const state = await ensureLoadedFile(bridge, toolsView);
const autoRun = !['case', 'patch-apply', 'spectrogram-encode'].includes(tool);
new ToolPanel(context, bridge, tool, { autoRun, fileState: state });
}
async function openSelectedTool(context, bridge, toolsView, tool, resource, selectedResources) {
const target = Array.isArray(selectedResources) && selectedResources.length > 0
? selectedResources[0]
: resource;
if (!target || target.scheme !== 'file') {
vscode.window.showErrorMessage('Select a local file before opening it in Leviathan.');
return;
}
await bridge.rpc('LeviathanLoadFile', [target.fsPath, false]);
await toolsView.refreshState();
const state = await bridge.rpc('LeviathanState');
const autoRun = !['case', 'patch-apply', 'spectrogram-encode'].includes(tool);
new ToolPanel(context, bridge, tool, { autoRun, fileState: state });
}
function escapeHtml(value) {
return String(value).replace(/[&<>"']/g, ch => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[ch]));
}
async function activate(context) {
const bridge = new Bridge(context);
await bridge.start();
const toolsView = new ToolsView(context, bridge);
context.subscriptions.push(vscode.window.registerWebviewViewProvider('leviathan.tools', toolsView));
context.subscriptions.push(vscode.commands.registerCommand('leviathan.loadFile', () => loadFile(bridge, false, toolsView)));
context.subscriptions.push(vscode.commands.registerCommand('leviathan.loadCompare', () => loadFile(bridge, true, toolsView)));
context.subscriptions.push(vscode.commands.registerCommand('leviathan.saveFile', () => saveFile(bridge, toolsView)));
context.subscriptions.push(vscode.commands.registerCommand('leviathan.openTool', tool => openTool(context, bridge, toolsView, tool)));
for (const tool of TOOLS) {
context.subscriptions.push(vscode.commands.registerCommand(
`leviathan.openSelected.${tool.id}`,
(resource, selectedResources) => openSelectedTool(context, bridge, toolsView, tool.id, resource, selectedResources)
));
}
}
function deactivate() {}
module.exports = { activate, deactivate };
-8
View File
@@ -1,8 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round">
<path d="M13.4 3.5H5a1.5 1.5 0 0 0-1.5 1.5v14A1.5 1.5 0 0 0 5 20.5h5.2"/>
<path d="M6.6 7.3h7"/>
<path d="M6.6 10.4h4.4"/>
<path d="M6.6 13.5h2.2"/>
<circle cx="15.3" cy="14.1" r="3.7"/>
<path d="m18 16.8 2.8 2.8"/>
</svg>

Before

Width:  |  Height:  |  Size: 396 B

-312
View File
@@ -1,312 +0,0 @@
{
"name": "leviathan",
"displayName": "Leviathan",
"description": "Leviathan binary analysis tools inside Visual Studio Code.",
"version": "0.1.0",
"publisher": "andydixon",
"engines": {
"vscode": "^1.90.0"
},
"categories": [
"Other"
],
"files": [
"extension.js",
"package.json",
"media/**",
"bin/**"
],
"activationEvents": [
"onView:leviathan.tools",
"onCommand:leviathan.openSelected.hex",
"onCommand:leviathan.openSelected.overview",
"onCommand:leviathan.openSelected.hashes",
"onCommand:leviathan.openSelected.entropy",
"onCommand:leviathan.openSelected.strings",
"onCommand:leviathan.openSelected.iocs",
"onCommand:leviathan.openSelected.search",
"onCommand:leviathan.openSelected.multi-search",
"onCommand:leviathan.openSelected.parse",
"onCommand:leviathan.openSelected.disasm",
"onCommand:leviathan.openSelected.pdf",
"onCommand:leviathan.openSelected.squashfs",
"onCommand:leviathan.openSelected.spectrogram-encode",
"onCommand:leviathan.openSelected.spectrogram-view",
"onCommand:leviathan.openSelected.diff",
"onCommand:leviathan.openSelected.carve",
"onCommand:leviathan.openSelected.keys",
"onCommand:leviathan.openSelected.timestamps",
"onCommand:leviathan.openSelected.stego",
"onCommand:leviathan.openSelected.metadata",
"onCommand:leviathan.openSelected.binsnoop",
"onCommand:leviathan.openSelected.rules",
"onCommand:leviathan.openSelected.patch-plan",
"onCommand:leviathan.openSelected.patch-apply",
"onCommand:leviathan.openSelected.transform",
"onCommand:leviathan.openSelected.case"
],
"main": "./extension.js",
"contributes": {
"commands": [
{
"command": "leviathan.loadFile",
"title": "Load Binary"
},
{
"command": "leviathan.loadCompare",
"title": "Load Compare Binary"
},
{
"command": "leviathan.saveFile",
"title": "Save Working Copy"
},
{
"command": "leviathan.openTool",
"title": "Open Leviathan Tool"
},
{
"command": "leviathan.openSelected.hex",
"title": "Hex Editor"
},
{
"command": "leviathan.openSelected.overview",
"title": "Overview"
},
{
"command": "leviathan.openSelected.hashes",
"title": "Hashes"
},
{
"command": "leviathan.openSelected.entropy",
"title": "Entropy"
},
{
"command": "leviathan.openSelected.strings",
"title": "Strings"
},
{
"command": "leviathan.openSelected.iocs",
"title": "IOCs"
},
{
"command": "leviathan.openSelected.search",
"title": "Search"
},
{
"command": "leviathan.openSelected.multi-search",
"title": "Multi-search"
},
{
"command": "leviathan.openSelected.parse",
"title": "Structures"
},
{
"command": "leviathan.openSelected.disasm",
"title": "Disasm"
},
{
"command": "leviathan.openSelected.pdf",
"title": "PDF"
},
{
"command": "leviathan.openSelected.squashfs",
"title": "SquashFS"
},
{
"command": "leviathan.openSelected.spectrogram-encode",
"title": "Spectrogram Encode"
},
{
"command": "leviathan.openSelected.spectrogram-view",
"title": "Spectrogram View"
},
{
"command": "leviathan.openSelected.diff",
"title": "Diff"
},
{
"command": "leviathan.openSelected.carve",
"title": "Carve"
},
{
"command": "leviathan.openSelected.keys",
"title": "Keys"
},
{
"command": "leviathan.openSelected.timestamps",
"title": "Timestamps"
},
{
"command": "leviathan.openSelected.stego",
"title": "Stego"
},
{
"command": "leviathan.openSelected.metadata",
"title": "Metadata"
},
{
"command": "leviathan.openSelected.binsnoop",
"title": "Binsnoop"
},
{
"command": "leviathan.openSelected.rules",
"title": "Rules"
},
{
"command": "leviathan.openSelected.patch-plan",
"title": "Patch Plan"
},
{
"command": "leviathan.openSelected.patch-apply",
"title": "Patch Apply"
},
{
"command": "leviathan.openSelected.transform",
"title": "Transforms"
},
{
"command": "leviathan.openSelected.case",
"title": "Case Export"
}
],
"submenus": [
{
"id": "leviathan.contextTools",
"label": "Leviathan"
}
],
"viewsContainers": {
"activitybar": [
{
"id": "leviathan",
"title": "Leviathan",
"icon": "media/leviathan.svg"
}
]
},
"views": {
"leviathan": [
{
"id": "leviathan.tools",
"name": "Tools",
"type": "webview"
}
]
},
"menus": {
"explorer/context": [
{
"submenu": "leviathan.contextTools",
"when": "resourceScheme == file && explorerResourceIsFolder == false",
"group": "navigation"
}
],
"leviathan.contextTools": [
{
"command": "leviathan.openSelected.hex",
"group": "01_editor"
},
{
"command": "leviathan.openSelected.overview",
"group": "02_triage@1"
},
{
"command": "leviathan.openSelected.hashes",
"group": "02_triage@2"
},
{
"command": "leviathan.openSelected.entropy",
"group": "02_triage@3"
},
{
"command": "leviathan.openSelected.strings",
"group": "02_triage@4"
},
{
"command": "leviathan.openSelected.iocs",
"group": "02_triage@5"
},
{
"command": "leviathan.openSelected.search",
"group": "03_find@1"
},
{
"command": "leviathan.openSelected.multi-search",
"group": "03_find@2"
},
{
"command": "leviathan.openSelected.parse",
"group": "04_structure@1"
},
{
"command": "leviathan.openSelected.disasm",
"group": "04_structure@2"
},
{
"command": "leviathan.openSelected.pdf",
"group": "04_structure@3"
},
{
"command": "leviathan.openSelected.squashfs",
"group": "04_structure@4"
},
{
"command": "leviathan.openSelected.spectrogram-encode",
"group": "05_audio@1"
},
{
"command": "leviathan.openSelected.spectrogram-view",
"group": "05_audio@2"
},
{
"command": "leviathan.openSelected.diff",
"group": "06_compare"
},
{
"command": "leviathan.openSelected.carve",
"group": "07_recover@1"
},
{
"command": "leviathan.openSelected.keys",
"group": "07_recover@2"
},
{
"command": "leviathan.openSelected.timestamps",
"group": "07_recover@3"
},
{
"command": "leviathan.openSelected.stego",
"group": "07_recover@4"
},
{
"command": "leviathan.openSelected.metadata",
"group": "07_recover@5"
},
{
"command": "leviathan.openSelected.binsnoop",
"group": "08_advanced@1"
},
{
"command": "leviathan.openSelected.rules",
"group": "08_advanced@2"
},
{
"command": "leviathan.openSelected.patch-plan",
"group": "09_edit@1"
},
{
"command": "leviathan.openSelected.patch-apply",
"group": "09_edit@2"
},
{
"command": "leviathan.openSelected.transform",
"group": "09_edit@3"
},
{
"command": "leviathan.openSelected.case",
"group": "10_case"
}
]
}
}
}
File diff suppressed because it is too large Load Diff
-5
View File
@@ -1,5 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round">
<path d="M12 2.5 20.5 7.25v9.5L12 21.5 3.5 16.75v-9.5L12 2.5Z"/>
<path d="m8.5 9.7 2.8 2.3-2.8 2.3"/>
<path d="M12.6 15h3.1"/>
</svg>

Before

Width:  |  Height:  |  Size: 301 B

File diff suppressed because one or more lines are too long
-1
View File
@@ -1 +0,0 @@
!function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t():"function"==typeof define&&define.amd?define([],t):"object"==typeof exports?exports.FitAddon=t():e.FitAddon=t()}(globalThis,(()=>(()=>{"use strict";var e={};return(()=>{var t=e;Object.defineProperty(t,"__esModule",{value:!0}),t.FitAddon=void 0,t.FitAddon=class{activate(e){this._terminal=e}dispose(){}fit(){const e=this.proposeDimensions();if(!e||!this._terminal||isNaN(e.cols)||isNaN(e.rows))return;const t=this._terminal._core;this._terminal.rows===e.rows&&this._terminal.cols===e.cols||(t._renderService.clear(),this._terminal.resize(e.cols,e.rows))}proposeDimensions(){if(!this._terminal)return;if(!this._terminal.element||!this._terminal.element.parentElement)return;const e=this._terminal._core._renderService.dimensions;if(0===e.css.cell.width||0===e.css.cell.height)return;const t=0===this._terminal.options.scrollback?0:this._terminal.options.overviewRuler?.width||14,r=window.getComputedStyle(this._terminal.element.parentElement),i=parseInt(r.getPropertyValue("height")),o=Math.max(0,parseInt(r.getPropertyValue("width"))),s=window.getComputedStyle(this._terminal.element),n=i-(parseInt(s.getPropertyValue("padding-top"))+parseInt(s.getPropertyValue("padding-bottom"))),l=o-(parseInt(s.getPropertyValue("padding-right"))+parseInt(s.getPropertyValue("padding-left")))-t;return{cols:Math.max(2,Math.floor(l/e.css.cell.width)),rows:Math.max(1,Math.floor(n/e.css.cell.height))}}}})(),e})()));
File diff suppressed because one or more lines are too long
-285
View File
@@ -1,285 +0,0 @@
/**
* Copyright (c) 2014 The xterm.js authors. All rights reserved.
* Copyright (c) 2012-2013, Christopher Jeffrey (MIT License)
* https://github.com/chjj/term.js
* @license MIT
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*
* Originally forked from (with the author's permission):
* Fabrice Bellard's javascript vt100 for jslinux:
* http://bellard.org/jslinux/
* Copyright (c) 2011 Fabrice Bellard
* The original design remains. The terminal itself
* has been extended to include xterm CSI codes, among
* other features.
*/
/**
* Default styles for xterm.js
*/
.xterm {
cursor: text;
position: relative;
user-select: none;
-ms-user-select: none;
-webkit-user-select: none;
}
.xterm.focus,
.xterm:focus {
outline: none;
}
.xterm .xterm-helpers {
position: absolute;
top: 0;
/**
* The z-index of the helpers must be higher than the canvases in order for
* IMEs to appear on top.
*/
z-index: 5;
}
.xterm .xterm-helper-textarea {
padding: 0;
border: 0;
margin: 0;
/* Move textarea out of the screen to the far left, so that the cursor is not visible */
position: absolute;
opacity: 0;
left: -9999em;
top: 0;
width: 0;
height: 0;
z-index: -5;
/** Prevent wrapping so the IME appears against the textarea at the correct position */
white-space: nowrap;
overflow: hidden;
resize: none;
}
.xterm .composition-view {
/* TODO: Composition position got messed up somewhere */
background: #000;
color: #FFF;
display: none;
position: absolute;
white-space: nowrap;
z-index: 1;
}
.xterm .composition-view.active {
display: block;
}
.xterm .xterm-viewport {
/* On OS X this is required in order for the scroll bar to appear fully opaque */
background-color: #000;
overflow-y: scroll;
cursor: default;
position: absolute;
right: 0;
left: 0;
top: 0;
bottom: 0;
}
.xterm .xterm-screen {
position: relative;
}
.xterm .xterm-screen canvas {
position: absolute;
left: 0;
top: 0;
}
.xterm-char-measure-element {
display: inline-block;
visibility: hidden;
position: absolute;
top: 0;
left: -9999em;
line-height: normal;
}
.xterm.enable-mouse-events {
/* When mouse events are enabled (eg. tmux), revert to the standard pointer cursor */
cursor: default;
}
.xterm.xterm-cursor-pointer,
.xterm .xterm-cursor-pointer {
cursor: pointer;
}
.xterm.column-select.focus {
/* Column selection mode */
cursor: crosshair;
}
.xterm .xterm-accessibility:not(.debug),
.xterm .xterm-message {
position: absolute;
left: 0;
top: 0;
bottom: 0;
right: 0;
z-index: 10;
color: transparent;
pointer-events: none;
}
.xterm .xterm-accessibility-tree:not(.debug) *::selection {
color: transparent;
}
.xterm .xterm-accessibility-tree {
font-family: monospace;
user-select: text;
white-space: pre;
}
.xterm .xterm-accessibility-tree > div {
transform-origin: left;
width: fit-content;
}
.xterm .live-region {
position: absolute;
left: -9999px;
width: 1px;
height: 1px;
overflow: hidden;
}
.xterm-dim {
/* Dim should not apply to background, so the opacity of the foreground color is applied
* explicitly in the generated class and reset to 1 here */
opacity: 1 !important;
}
.xterm-underline-1 { text-decoration: underline; }
.xterm-underline-2 { text-decoration: double underline; }
.xterm-underline-3 { text-decoration: wavy underline; }
.xterm-underline-4 { text-decoration: dotted underline; }
.xterm-underline-5 { text-decoration: dashed underline; }
.xterm-overline {
text-decoration: overline;
}
.xterm-overline.xterm-underline-1 { text-decoration: overline underline; }
.xterm-overline.xterm-underline-2 { text-decoration: overline double underline; }
.xterm-overline.xterm-underline-3 { text-decoration: overline wavy underline; }
.xterm-overline.xterm-underline-4 { text-decoration: overline dotted underline; }
.xterm-overline.xterm-underline-5 { text-decoration: overline dashed underline; }
.xterm-strikethrough {
text-decoration: line-through;
}
.xterm-screen .xterm-decoration-container .xterm-decoration {
z-index: 6;
position: absolute;
}
.xterm-screen .xterm-decoration-container .xterm-decoration.xterm-decoration-top-layer {
z-index: 7;
}
.xterm-decoration-overview-ruler {
z-index: 8;
position: absolute;
top: 0;
right: 0;
pointer-events: none;
}
.xterm-decoration-top {
z-index: 2;
position: relative;
}
/* Derived from vs/base/browser/ui/scrollbar/media/scrollbar.css */
/* xterm.js customization: Override xterm's cursor style */
.xterm .xterm-scrollable-element > .scrollbar {
cursor: default;
}
/* Arrows */
.xterm .xterm-scrollable-element > .scrollbar > .scra {
cursor: pointer;
font-size: 11px !important;
}
.xterm .xterm-scrollable-element > .visible {
opacity: 1;
/* Background rule added for IE9 - to allow clicks on dom node */
background:rgba(0,0,0,0);
transition: opacity 100ms linear;
/* In front of peek view */
z-index: 11;
}
.xterm .xterm-scrollable-element > .invisible {
opacity: 0;
pointer-events: none;
}
.xterm .xterm-scrollable-element > .invisible.fade {
transition: opacity 800ms linear;
}
/* Scrollable Content Inset Shadow */
.xterm .xterm-scrollable-element > .shadow {
position: absolute;
display: none;
}
.xterm .xterm-scrollable-element > .shadow.top {
display: block;
top: 0;
left: 3px;
height: 3px;
width: 100%;
box-shadow: var(--vscode-scrollbar-shadow, #000) 0 6px 6px -6px inset;
}
.xterm .xterm-scrollable-element > .shadow.left {
display: block;
top: 3px;
left: 0;
height: 100%;
width: 3px;
box-shadow: var(--vscode-scrollbar-shadow, #000) 6px 0 6px -6px inset;
}
.xterm .xterm-scrollable-element > .shadow.top-left-corner {
display: block;
top: 0;
left: 0;
height: 3px;
width: 3px;
}
.xterm .xterm-scrollable-element > .shadow.top.left {
box-shadow: var(--vscode-scrollbar-shadow, #000) 6px 0 6px -6px inset;
}
File diff suppressed because one or more lines are too long
-112
View File
@@ -1,112 +0,0 @@
{
"name": "nexus",
"displayName": "Nexus",
"description": "Nexus terminal connections inside Visual Studio Code.",
"version": "0.1.0",
"publisher": "andydixon",
"engines": {
"vscode": "^1.90.0"
},
"categories": [
"Other"
],
"files": [
"extension.js",
"package.json",
"media/**",
"bin/**"
],
"activationEvents": [
"onView:nexus.connections",
"onCommand:nexus.newProfile",
"onCommand:nexus.openSettings"
],
"main": "./extension.js",
"contributes": {
"commands": [
{
"command": "nexus.refreshConnections",
"title": "Refresh Connections"
},
{
"command": "nexus.connectLocal",
"title": "Connect Local Terminal"
},
{
"command": "nexus.openConnection",
"title": "Open Nexus Connection"
},
{
"command": "nexus.newProfile",
"title": "New Nexus Profile"
},
{
"command": "nexus.editProfile",
"title": "Edit Nexus Profile"
},
{
"command": "nexus.duplicateProfile",
"title": "Duplicate Nexus Profile"
},
{
"command": "nexus.deleteProfile",
"title": "Delete Nexus Profile"
},
{
"command": "nexus.openSettings",
"title": "Nexus Settings"
},
{
"command": "nexus.openSftp",
"title": "Open SFTP Browser"
}
],
"viewsContainers": {
"activitybar": [
{
"id": "nexus",
"title": "Nexus",
"icon": "media/nexus.svg"
}
]
},
"views": {
"nexus": [
{
"id": "nexus.connections",
"name": "Connections",
"type": "webview"
}
]
},
"menus": {
"view/item/context": [
{
"command": "nexus.openConnection",
"when": "view == nexus.connections && viewItem == profile",
"group": "inline"
},
{
"command": "nexus.editProfile",
"when": "view == nexus.connections && viewItem == profile",
"group": "navigation@1"
},
{
"command": "nexus.duplicateProfile",
"when": "view == nexus.connections && viewItem == profile",
"group": "navigation@2"
},
{
"command": "nexus.deleteProfile",
"when": "view == nexus.connections && viewItem == profile",
"group": "navigation@3"
},
{
"command": "nexus.openSftp",
"when": "view == nexus.connections && viewItem == profile",
"group": "navigation@4"
}
]
}
}
}
-191
View File
@@ -1,191 +0,0 @@
#!/usr/bin/env bash
#
# Build platform-specific VSIX packages for the Nexus and Leviathan VSCode
# extensions, covering Windows, macOS and Linux on both x64 and arm64.
#
# Each extension embeds a Go "bridge" binary that is OS/arch specific, so we
# produce one VSIX per target using VSCode's platform-specific extension
# mechanism (`vsce package --target <target>`). VSCode then installs the VSIX
# matching the user's platform automatically (e.g. from the Marketplace, or via
# `code --install-extension <file>.vsix`).
#
# The bridge is pure Go (CGO_ENABLED=0), so every target cross-compiles from any
# host with just the Go toolchain — no C compilers or per-OS toolchains needed.
#
# Usage:
# vscode/package-all.sh # build every target
# vscode/package-all.sh linux-x64 ... # build only the named vsce target(s)
#
# Optional macOS code signing + notarization (darwin targets only):
# Runs only on a macOS host with Xcode command line tools, and only when the
# relevant env vars are set — otherwise it is skipped with a warning and the
# darwin VSIXs are produced unsigned (fine for local use, blocked by Gatekeeper
# when distributed to other machines).
#
# MACOS_SIGN_IDENTITY "Developer ID Application: Name (TEAMID)" — enables signing.
#
# Then, to also notarize, provide EITHER a stored notarytool profile:
# MACOS_NOTARY_PROFILE profile name from `xcrun notarytool store-credentials`
# OR an Apple ID + app-specific password:
# MACOS_NOTARY_APPLE_ID Apple ID email
# MACOS_NOTARY_TEAM_ID Apple Developer team id
# MACOS_NOTARY_PASSWORD app-specific password
#
# Example:
# MACOS_SIGN_IDENTITY="Developer ID Application: Andy Dixon (AB12CD34EF)" \
# MACOS_NOTARY_PROFILE="nexus-notary" \
# vscode/package-all.sh darwin-x64 darwin-arm64
#
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
VSCODE_DIR="$ROOT_DIR/vscode"
OUT_DIR="$VSCODE_DIR/dist"
EXTENSIONS=(nexus leviathan)
# vsce target -> GOOS/GOARCH
ALL_TARGETS=(
"win32-x64:windows/amd64"
"win32-arm64:windows/arm64"
"darwin-x64:darwin/amd64"
"darwin-arm64:darwin/arm64"
"linux-x64:linux/amd64"
"linux-arm64:linux/arm64"
)
# Optional positional filter: only build the vsce targets passed as arguments.
SELECTED=("$@")
target_selected() {
[ ${#SELECTED[@]} -eq 0 ] && return 0
local want="$1"
for t in "${SELECTED[@]}"; do [ "$t" = "$want" ] && return 0; done
return 1
}
run_vsce() {
if command -v vsce >/dev/null 2>&1; then
vsce "$@"
else
npx --yes @vscode/vsce "$@"
fi
}
# Codesign (hardened runtime) and optionally notarize a single darwin bridge
# binary. Opt-in via env vars and skipped with a warning when it can't run, so
# the cross-platform build still succeeds on non-macOS hosts. Signing the staged
# binary before it is copied into each extension means both VSIXs inherit the
# signature (and notarization, which is keyed to the binary's hash).
macos_sign() {
local bin="$1"
if [ -z "${MACOS_SIGN_IDENTITY:-}" ]; then
echo "[vscode] (skip signing: MACOS_SIGN_IDENTITY not set — shipping unsigned)"
return 0
fi
if [ "$(uname -s)" != "Darwin" ] || ! command -v codesign >/dev/null 2>&1; then
echo "[vscode] WARNING: cannot sign darwin bridge here (needs macOS + Xcode tools); shipping unsigned"
return 0
fi
echo "[vscode] codesign ($MACOS_SIGN_IDENTITY)"
codesign --force --options runtime --timestamp --sign "$MACOS_SIGN_IDENTITY" "$bin"
codesign --verify --strict --verbose=2 "$bin"
# Notarize only when credentials are provided.
local notary_args=()
if [ -n "${MACOS_NOTARY_PROFILE:-}" ]; then
notary_args=(--keychain-profile "$MACOS_NOTARY_PROFILE")
elif [ -n "${MACOS_NOTARY_APPLE_ID:-}" ] && [ -n "${MACOS_NOTARY_TEAM_ID:-}" ] && [ -n "${MACOS_NOTARY_PASSWORD:-}" ]; then
notary_args=(--apple-id "$MACOS_NOTARY_APPLE_ID" --team-id "$MACOS_NOTARY_TEAM_ID" --password "$MACOS_NOTARY_PASSWORD")
else
echo "[vscode] (skip notarization: no notary credentials set)"
return 0
fi
if ! command -v xcrun >/dev/null 2>&1; then
echo "[vscode] WARNING: xcrun not found; cannot notarize"
return 0
fi
echo "[vscode] notarize (submitting to Apple, waiting for result…)"
local zip="${bin}.notarize.zip"
# notarytool requires an archive; a bare Mach-O binary cannot be submitted directly.
ditto -c -k --keepParent "$bin" "$zip"
xcrun notarytool submit "$zip" "${notary_args[@]}" --wait
rm -f "$zip"
# A standalone executable cannot be stapled (stapler only supports app/dmg/pkg);
# Gatekeeper verifies the notarization online via the now-registered hash, which
# matches the exact bytes embedded in every VSIX.
echo "[vscode] notarized (online check; standalone binaries are not stapleable)"
}
echo "[vscode] Building frontend assets (platform independent)"
if [ ! -d "$ROOT_DIR/frontend/node_modules" ]; then
npm --prefix "$ROOT_DIR/frontend" install
fi
npm --prefix "$ROOT_DIR/frontend" run build
echo "[vscode] Staging shared media assets"
cp "$ROOT_DIR/frontend/dist/src/js/lib/xterm.min.js" "$VSCODE_DIR/nexus/media/xterm.min.js"
cp "$ROOT_DIR/frontend/dist/src/js/lib/xterm-addon-fit.min.js" "$VSCODE_DIR/nexus/media/xterm-addon-fit.min.js"
cp "$ROOT_DIR/frontend/dist/src/js/lib/xterm-addon-unicode11.min.js" "$VSCODE_DIR/nexus/media/xterm-addon-unicode11.min.js"
cp "$ROOT_DIR/frontend/dist/src/css/lib/xterm.css" "$VSCODE_DIR/nexus/media/xterm.css"
cp "$ROOT_DIR/frontend/dist/src/js/lib/novnc.min.js" "$VSCODE_DIR/nexus/media/novnc.min.js"
rm -rf "$OUT_DIR"
mkdir -p "$OUT_DIR"
cd "$ROOT_DIR"
VERSION="$(node -p "require('$VSCODE_DIR/nexus/package.json').version")"
for entry in "${ALL_TARGETS[@]}"; do
VSCE_TARGET="${entry%%:*}"
GOPLATFORM="${entry##*:}"
GOOS="${GOPLATFORM%%/*}"
GOARCH="${GOPLATFORM##*/}"
if ! target_selected "$VSCE_TARGET"; then
continue
fi
BIN_NAME="nexus-vscode-bridge"
[ "$GOOS" = "windows" ] && BIN_NAME="nexus-vscode-bridge.exe"
echo "[vscode] Building bridge for $VSCE_TARGET ($GOOS/$GOARCH)"
STAGE_BIN="$VSCODE_DIR/bin/$VSCE_TARGET/$BIN_NAME"
mkdir -p "$(dirname "$STAGE_BIN")"
GOOS="$GOOS" GOARCH="$GOARCH" CGO_ENABLED=0 \
go build -trimpath -o "$STAGE_BIN" ./cmd/vscode-bridge
# Sign + notarize macOS bridges before they are copied into the extensions.
if [ "$GOOS" = "darwin" ]; then
macos_sign "$STAGE_BIN"
fi
for EXT in "${EXTENSIONS[@]}"; do
EXT_BIN_DIR="$VSCODE_DIR/$EXT/bin"
# Ship exactly one binary per VSIX so the package can't carry a wrong-arch
# (or wrong-OS) bridge alongside the right one.
rm -rf "$EXT_BIN_DIR"
mkdir -p "$EXT_BIN_DIR"
cp "$STAGE_BIN" "$EXT_BIN_DIR/$BIN_NAME"
chmod 0755 "$EXT_BIN_DIR/$BIN_NAME"
OUT_VSIX="$OUT_DIR/$EXT-$VERSION-$VSCE_TARGET.vsix"
( cd "$VSCODE_DIR/$EXT" && run_vsce package --target "$VSCE_TARGET" \
--no-dependencies --allow-missing-repository -o "$OUT_VSIX" >/dev/null )
echo "[vscode] wrote $(basename "$OUT_VSIX")"
done
done
# Leave the working tree with a host-native bridge staged so a plain
# `code --install-extension` of an unpacked extension still works locally.
HOST_OS="$(go env GOOS)"; HOST_ARCH="$(go env GOARCH)"
HOST_BIN="nexus-vscode-bridge"; [ "$HOST_OS" = "windows" ] && HOST_BIN="nexus-vscode-bridge.exe"
for EXT in "${EXTENSIONS[@]}"; do
rm -rf "$VSCODE_DIR/$EXT/bin"; mkdir -p "$VSCODE_DIR/$EXT/bin"
GOOS="$HOST_OS" GOARCH="$HOST_ARCH" CGO_ENABLED=0 \
go build -trimpath -o "$VSCODE_DIR/$EXT/bin/$HOST_BIN" ./cmd/vscode-bridge
done
echo "[vscode] Done. Packages in $OUT_DIR:"
ls -1 "$OUT_DIR"