upd scripts and readme

This commit is contained in:
Aslan Dukaev 2026-02-03 08:01:55 +03:00
commit 2b06350f02
10 changed files with 292 additions and 202 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 MiB

After

Width:  |  Height:  |  Size: 2 MiB

Before After
Before After

View file

@ -24,7 +24,6 @@ jobs:
id: static-analysis
with:
token: ${{ secrets.GITHUB_TOKEN }}
# This check name is defined as the github action job name (in .github/workflows/validations.yaml)
checkName: "Static analysis"
ref: ${{ github.event.pull_request.head.sha || github.sha }}
@ -33,55 +32,21 @@ jobs:
id: unit
with:
token: ${{ secrets.GITHUB_TOKEN }}
# This check name is defined as the github action job name (in .github/workflows/validations.yaml)
checkName: "Unit tests (ubuntu-latest)"
ref: ${{ github.event.pull_request.head.sha || github.sha }}
- name: Check acceptance test results (linux)
uses: fountainhead/action-wait-for-check@v1.2.0
id: acceptance-linux
with:
token: ${{ secrets.GITHUB_TOKEN }}
# This check name is defined as the github action job name (in .github/workflows/validations.yaml)
checkName: "Acceptance tests (Linux)"
ref: ${{ github.event.pull_request.head.sha || github.sha }}
- name: Check acceptance test results (mac)
uses: fountainhead/action-wait-for-check@v1.2.0
id: acceptance-mac
with:
token: ${{ secrets.GITHUB_TOKEN }}
# This check name is defined as the github action job name (in .github/workflows/validations.yaml)
checkName: "Acceptance tests (Mac)"
ref: ${{ github.event.pull_request.head.sha || github.sha }}
- name: Check acceptance test results (windows)
uses: fountainhead/action-wait-for-check@v1.2.0
id: acceptance-windows
with:
token: ${{ secrets.GITHUB_TOKEN }}
# This check name is defined as the github action job name (in .github/workflows/validations.yaml)
checkName: "Acceptance tests (Windows)"
ref: ${{ github.event.pull_request.head.sha || github.sha }}
- name: Quality gate
if: steps.static-analysis.outputs.conclusion != 'success' || steps.unit.outputs.conclusion != 'success' || steps.acceptance-linux.outputs.conclusion != 'success' || steps.acceptance-mac.outputs.conclusion != 'success' || steps.acceptance-windows.outputs.conclusion != 'success'
if: steps.static-analysis.outputs.conclusion != 'success' || steps.unit.outputs.conclusion != 'success'
run: |
echo "Static Analysis Status: ${{ steps.static-analysis.conclusion }}"
echo "Unit Test Status: ${{ steps.unit.outputs.conclusion }}"
echo "Acceptance Test (Linux) Status: ${{ steps.acceptance-linux.outputs.conclusion }}"
echo "Acceptance Test (Mac) Status: ${{ steps.acceptance-mac.outputs.conclusion }}"
echo "Acceptance Test (Windows) Status: ${{ steps.acceptance-windows.outputs.conclusion }}"
false
release:
needs: [quality-gate]
runs-on: ubuntu-latest
permissions:
# for tagging
contents: write
# for pushing container images
packages: write
steps:
- uses: actions/checkout@v4
@ -91,15 +56,8 @@ jobs:
- name: Bootstrap environment
uses: ./.github/actions/bootstrap
- name: Login to Docker Hub
uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 #v3.4.0
with:
registry: docker.io
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Login to GitHub Container Registry
uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 #v3.4.0
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
@ -121,10 +79,27 @@ jobs:
- name: Build & publish release artifacts
run: make ci-release
env:
# for creating the release (requires write access to content)
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# for updating brew formula in wagoodman/homebrew-dive
TAP_GITHUB_TOKEN: ${{ secrets.TAP_GITHUB_TOKEN }}
- name: Smoke test published image
run: make ci-test-docker-image
npm-publish:
needs: [release]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
registry-url: 'https://registry.npmjs.org'
- name: Update package.json version
run: |
VERSION="${{ github.event.inputs.version }}"
VERSION="${VERSION#v}"
npm version $VERSION --no-git-tag-version
- name: Publish to npm
run: npm publish --access public
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

View file

@ -8,7 +8,6 @@ on:
jobs:
Static-Analysis:
# Note: changing this job name requires making the same update in the .github/workflows/release.yaml pipeline
name: "Static analysis"
runs-on: ubuntu-latest
steps:
@ -21,15 +20,11 @@ jobs:
run: make static-analysis
Unit-Test:
# Note: changing this job name requires making the same update in the .github/workflows/release.yaml pipeline
name: "Unit tests"
strategy:
matrix:
platform:
- ubuntu-latest
# - macos-latest # todo: mac runners are expensive minute-wise
# - windows-latest # todo: support windows
runs-on: ${{ matrix.platform }}
steps:
- uses: actions/checkout@v4
@ -58,25 +53,16 @@ jobs:
- name: Build snapshot artifacts
run: make snapshot
- run: docker images wagoodman/dive
# todo: compare against known json output in shared volume
- name: Test production image
run: make ci-test-docker-image
# why not use actions/upload-artifact? It is very slow (3 minutes to upload ~600MB of data, vs 10 seconds with this approach).
# see https://github.com/actions/upload-artifact/issues/199 for more info
- name: Upload snapshot artifacts
uses: actions/cache/save@v4
with:
path: snapshot
key: snapshot-build-${{ github.run_id }}
# ... however the cache trick doesn't work on windows :(
- uses: actions/upload-artifact@v4
with:
name: windows-artifacts
path: snapshot/dive_windows_amd64_v1/dive.exe
path: snapshot/superdive_windows_amd64_v1/superdive.exe
Acceptance-Linux:
name: "Acceptance tests (Linux)"
@ -94,12 +80,6 @@ jobs:
- name: Test linux run
run: make ci-test-linux-run
- name: Test DEB package installation
run: make ci-test-deb-package-install
- name: Test RPM package installation
run: make ci-test-rpm-package-install
Acceptance-Mac:
name: "Acceptance tests (Mac)"
needs: [Build-Snapshot-Artifacts]

View file

@ -14,7 +14,8 @@ env:
- CGO_ENABLED=0
builds:
- binary: dive
- id: superdive
binary: superdive
dir: ./cmd/dive
env:
- CGO_ENABLED=0
@ -25,7 +26,6 @@ builds:
goarch:
- amd64
- arm64
- ppc64le
ldflags:
-w
-s
@ -35,40 +35,35 @@ builds:
-X main.buildDate={{.Date}}
-X main.gitDescription={{.Summary}}
brews:
- repository:
owner: wagoodman
name: homebrew-dive
token: "{{.Env.TAP_GITHUB_TOKEN}}"
homepage: &project_url "https://github.com/wagoodman/dive/"
description: &description "A tool for exploring layers in a docker image"
archives:
- format: tar.gz
- id: superdive-archives
builds:
- superdive
name_template: "superdive_{{ .Version }}_{{ .Os }}_{{ .Arch }}"
format: tar.gz
format_overrides:
- goos: windows
format: zip
nfpms:
- license: MIT
maintainer: Alex Goodman
homepage: *project_url
description: *description
maintainer: Aslan Dukaev
homepage: "https://github.com/dukaev/superdive"
description: "A tool for exploring layers in a docker image"
formats:
- rpm
- deb
dockers:
# docker.io amd64
- &dockerhub_amd64
- &docker_amd64
id: docker-amd64
ids:
- dive
- superdive
use: buildx
goarch: amd64
image_templates:
- docker.io/wagoodman/dive:latest
- docker.io/wagoodman/dive:v{{.Version}}-amd64
- ghcr.io/dukaev/superdive:latest
- ghcr.io/dukaev/superdive:v{{.Version}}-amd64
build_flag_templates:
- "--build-arg=DOCKER_CLI_VERSION={{.Env.DOCKER_CLI_VERSION}}"
- "--platform=linux/amd64"
@ -80,17 +75,15 @@ dockers:
- "--label=org.opencontainers.image.version={{.Version}}"
- "--label=org.opencontainers.image.revision={{.FullCommit}}"
- "--label=org.opencontainers.image.licenses=MIT"
- "--label=org.opencontainers.image.authors=Alex Goodman <@wagoodman>"
# docker.io arm64
- &dockerhub_arm64
- &docker_arm64
id: docker-arm64
ids:
- dive
- superdive
use: buildx
goarch: arm64
image_templates:
- docker.io/wagoodman/dive:v{{.Version}}-arm64
- ghcr.io/dukaev/superdive:v{{.Version}}-arm64
build_flag_templates:
- "--build-arg=DOCKER_CLI_VERSION={{.Env.DOCKER_CLI_VERSION}}"
- "--platform=linux/arm64/v8"
@ -102,47 +95,14 @@ dockers:
- "--label=org.opencontainers.image.version={{.Version}}"
- "--label=org.opencontainers.image.revision={{.FullCommit}}"
- "--label=org.opencontainers.image.licenses=MIT"
- "--label=org.opencontainers.image.authors=Alex Goodman <@wagoodman>"
# ghcr.io amd64
- id: ghcr-amd64
<<: *dockerhub_amd64
image_templates:
- ghcr.io/wagoodman/dive:v{{.Version}}-amd64
# ghcr.io arm64
- id: ghcr-arm64
<<: *dockerhub_arm64
image_templates:
- ghcr.io/wagoodman/dive:v{{.Version}}-arm64
docker_manifests:
# docker.io manifests
- name_template: docker.io/wagoodman/dive:latest
image_templates: &dockerhub_images
- docker.io/wagoodman/dive:v{{.Version}}-amd64
- docker.io/wagoodman/dive:v{{.Version}}-arm64
- name_template: ghcr.io/dukaev/superdive:latest
image_templates:
- ghcr.io/dukaev/superdive:v{{.Version}}-amd64
- ghcr.io/dukaev/superdive:v{{.Version}}-arm64
- name_template: docker.io/wagoodman/dive:v{{.Major}}
image_templates: *dockerhub_images
- name_template: docker.io/wagoodman/dive:v{{.Major}}.{{.Minor}}
image_templates: *dockerhub_images
- name_template: docker.io/wagoodman/dive:v{{.Version}}
image_templates: *dockerhub_images
# ghcr.io manifests
- name_template: ghcr.io/wagoodman/dive:latest
image_templates: &ghcr_images
- ghcr.io/wagoodman/dive:v{{.Version}}-amd64
- ghcr.io/wagoodman/dive:v{{.Version}}-arm64
- name_template: ghcr.io/wagoodman/dive:v{{.Major}}
image_templates: *ghcr_images
- name_template: ghcr.io/wagoodman/dive:v{{.Major}}.{{.Minor}}
image_templates: *ghcr_images
- name_template: ghcr.io/wagoodman/dive:v{{.Version}}
image_templates: *ghcr_images
- name_template: ghcr.io/dukaev/superdive:v{{.Version}}
image_templates:
- ghcr.io/dukaev/superdive:v{{.Version}}-amd64
- ghcr.io/dukaev/superdive:v{{.Version}}-arm64

38
.npmignore Normal file
View file

@ -0,0 +1,38 @@
# Source files
cmd/
dive/
internal/
*.go
go.mod
go.sum
# Build files
Makefile
Taskfile.yaml
Dockerfile
.goreleaser.yaml
.golangci.yaml
coverage.out
snapshot/
# Git/GitHub
.git/
.github/
.gitignore
# Config files
.binny.yaml
.bouncer.yaml
.dockerignore
.dive-ci
# Documentation (keep README)
RELEASE.md
AGENTS.md
# Data
.data/
# Test files
*_test.go
testdata/

View file

@ -1,5 +1,5 @@
OWNER = wagoodman
PROJECT = dive
OWNER = dukaev
PROJECT = superdive
TOOL_DIR = .tool
BINNY = $(TOOL_DIR)/binny
@ -41,7 +41,7 @@ $(TASKS): $(TASK)
## actual targets
ci-test-windows-run:
dive.exe --source docker-archive .data/test-docker-image.tar --ci --ci-config .data/.dive-ci
superdive.exe --source docker-archive .data/test-docker-image.tar --ci --ci-config .data/.dive-ci
help: $(TASK)
@$(TASK) -l

View file

@ -2,47 +2,41 @@
**A tool for exploring a Docker image, layer contents, and discovering ways to shrink the size of your Docker/OCI image.**
![Image](.data/demo.gif)
To analyze a Docker image simply run dive with an image tag/id/digest:
```bash
dive <your-image-tag>
```
## Installation
or you can dive with Docker directly:
```
alias dive="docker run -ti --rm -v /var/run/docker.sock:/var/run/docker.sock docker.io/wagoodman/dive"
dive <your-image-tag>
# for example
dive nginx:latest
```
or if you want to build your image then jump straight into analyzing it:
```bash
dive build -t <some-tag> .
```
Building on macOS (supporting only the Docker container engine):
### bun/npm (recommended)
```bash
docker run --rm -it \
-v /var/run/docker.sock:/var/run/docker.sock \
-v "$(pwd)":"$(pwd)" \
-w "$(pwd)" \
-v "$HOME/.dive.yaml":"$HOME/.dive.yaml" \
docker.io/wagoodman/dive:latest build -t <some-tag> .
bun install -g superdive
npm install -g superdive
```
### From source
```bash
git clone https://github.com/dukaev/superdive.git
cd superdive
go build -o superdive ./cmd/dive
```
## Usage
To analyze a Docker image simply run superdive with an image tag/id/digest:
```bash
superdive <your-image-tag>
```
Or if you want to build your image then jump straight into analyzing it:
```bash
superdive build -t <some-tag> .
```
Additionally you can run this in your CI pipeline to ensure you're keeping wasted space to a minimum (this skips the UI):
```bash
CI=true superdive <your-image>
```
CI=true dive <your-image>
```
![Image](.data/demo-ci.png)
**This is beta quality!** *Feel free to submit an issue if you want a new feature or find a bug :)*
## Basic Features
@ -61,24 +55,25 @@ The lower left pane shows basic layer info and an experimental metric that will
**Quick build/analysis cycles**
You can build a Docker image and do an immediate analysis with one command:
`dive build -t some-tag .`
```bash
superdive build -t some-tag .
```
You only need to replace your `docker build` command with the same `dive build`
command.
You only need to replace your `docker build` command with the same `superdive build` command.
**CI Integration**
Analyze an image and get a pass/fail result based on the image efficiency and wasted space. Simply set `CI=true` in the environment when invoking any valid dive command.
Analyze an image and get a pass/fail result based on the image efficiency and wasted space. Simply set `CI=true` in the environment when invoking any valid superdive command.
**Multiple Image Sources and Container Engines Supported**
With the `--source` option, you can select where to fetch the container image from:
```bash
dive <your-image> --source <source>
superdive <your-image> --source <source>
```
or
```bash
dive <source>://<your-image>
superdive <source>://<your-image>
```
With valid `source` options as such:
@ -86,11 +81,10 @@ With valid `source` options as such:
- `docker-archive`: A Docker Tar Archive from disk
- `podman`: Podman engine (linux only)
## CI Integration
When running dive with the environment variable `CI=true` then the dive UI will be bypassed and will instead analyze your docker image, giving it a pass/fail indication via return code. Currently there are three metrics supported via a `.dive-ci` file that you can put at the root of your repo:
```
When running superdive with the environment variable `CI=true` then the UI will be bypassed and will instead analyze your docker image, giving it a pass/fail indication via return code. Currently there are three metrics supported via a `.dive-ci` file that you can put at the root of your repo:
```yaml
rules:
# If the efficiency is measured below X%, mark as failed.
# Expressed as a ratio between 0-1.
@ -106,3 +100,7 @@ rules:
highestUserWastedPercent: 0.20
```
You can override the CI config path with the `--ci-config` option.
## License
MIT

View file

@ -1,8 +1,8 @@
version: "3"
vars:
OWNER: wagoodman
PROJECT: dive
OWNER: dukaev
PROJECT: superdive
# static file dirs
TOOL_DIR: .tool
@ -202,23 +202,9 @@ tasks:
ci-test-linux:
cmds:
- task: ci-test-linux-run
- task: ci-test-docker-image
- task: ci-test-deb-package-install
- task: ci-test-rpm-package-install
ci-test-docker-image:
desc: Test using the docker image
cmds:
- |
docker run \
--rm \
-t \
--env CLICOLOR_FORCE=true \
-v /var/run/docker.sock:/var/run/docker.sock \
'docker.io/wagoodman/dive:latest' \
'{{ .TEST_IMAGE }}' \
--ci
ci-test-deb-package-install:
desc: Test debian package installation
cmds:
@ -237,9 +223,9 @@ tasks:
tar -vxzf - docker/docker --strip-component=1 && \
mv docker /usr/local/bin/ &&\
docker version && \
apt install ./snapshot/dive_*_linux_amd64.deb -y && \
dive --version && \
dive '{{ .TEST_IMAGE }}' --ci \
apt install ./snapshot/superdive_*_linux_amd64.deb -y && \
superdive --version && \
superdive '{{ .TEST_IMAGE }}' --ci \
"
ci-test-rpm-package-install:
@ -258,9 +244,9 @@ tasks:
tar -vxzf - docker/docker --strip-component=1 && \
mv docker /usr/local/bin/ &&\
docker version && \
dnf install ./snapshot/dive_*_linux_amd64.rpm -y && \
dive --version && \
dive '{{ .TEST_IMAGE }}' --ci \
dnf install ./snapshot/superdive_*_linux_amd64.rpm -y && \
superdive --version && \
superdive '{{ .TEST_IMAGE }}' --ci \
"
generate-compressed-test-images:
@ -271,7 +257,7 @@ tasks:
for exporter in docker image; do \
docker buildx build \
-f .data/Dockerfile.minimal \
--tag test-dive-${exporter}:${alg} \
--tag test-superdive-${exporter}:${alg} \
--output type=${exporter},force-compression=true,compression=${alg} . ; \
done ; \
done && \
@ -298,16 +284,16 @@ tasks:
cmds:
- |
ls -la {{ .SNAPSHOT_DIR }}
ls -la {{ .SNAPSHOT_DIR }}/dive_linux_amd64_v1
chmod 755 {{ .SNAPSHOT_DIR }}/dive_linux_amd64_v1/dive && \
{{ .SNAPSHOT_DIR }}/dive_linux_amd64_v1/dive '{{ .TEST_IMAGE }}' --ci && \
{{ .SNAPSHOT_DIR }}/dive_linux_amd64_v1/dive --source docker-archive .data/test-kaniko-image.tar --ci --ci-config .data/.dive-ci
ls -la {{ .SNAPSHOT_DIR }}/superdive_linux_amd64_v1
chmod 755 {{ .SNAPSHOT_DIR }}/superdive_linux_amd64_v1/superdive && \
{{ .SNAPSHOT_DIR }}/superdive_linux_amd64_v1/superdive '{{ .TEST_IMAGE }}' --ci && \
{{ .SNAPSHOT_DIR }}/superdive_linux_amd64_v1/superdive --source docker-archive .data/test-kaniko-image.tar --ci --ci-config .data/.dive-ci
- |
for alg in uncompressed gzip estargz zstd; do \
for exporter in docker image; do \
{{ .SNAPSHOT_DIR }}/dive_linux_amd64_v1/dive "test-dive-${exporter}:${alg}" --ci ; \
{{ .SNAPSHOT_DIR }}/superdive_linux_amd64_v1/superdive "test-superdive-${exporter}:${alg}" --ci ; \
done && \
{{ .SNAPSHOT_DIR }}/dive_linux_amd64_v1/dive --source docker-archive .data/test-oci-${alg}-image.tar --ci --ci-config .data/.dive-ci; \
{{ .SNAPSHOT_DIR }}/superdive_linux_amd64_v1/superdive --source docker-archive .data/test-oci-${alg}-image.tar --ci --ci-config .data/.dive-ci; \
done
ci-test-mac-run:
@ -315,8 +301,8 @@ tasks:
deps: [ci-check]
cmds:
- |
chmod 755 {{ .SNAPSHOT_DIR }}/dive_darwin_amd64_v1/dive && \
{{ .SNAPSHOT_DIR }}/dive_darwin_amd64_v1/dive --source docker-archive .data/test-docker-image.tar --ci --ci-config .data/.dive-ci
chmod 755 {{ .SNAPSHOT_DIR }}/superdive_darwin_amd64_v1/superdive && \
{{ .SNAPSHOT_DIR }}/superdive_darwin_amd64_v1/superdive --source docker-archive .data/test-docker-image.tar --ci --ci-config .data/.dive-ci
## Build-related targets #################################

38
package.json Normal file
View file

@ -0,0 +1,38 @@
{
"name": "superdive",
"version": "0.14.0",
"description": "A tool for exploring a Docker image, layer contents, and discovering ways to shrink the size of your Docker/OCI image.",
"bin": {
"superdive": "bin/superdive.js"
},
"scripts": {
"postinstall": "node scripts/install.js",
"test": "echo \"Error: no test specified\" && exit 1"
},
"repository": {
"type": "git",
"url": "git+https://github.com/dukaev/superdive.git"
},
"keywords": [
"docker",
"container",
"image",
"oci",
"layer",
"dive",
"cli"
],
"author": "Aslan Dukaev",
"license": "MIT",
"bugs": {
"url": "https://github.com/dukaev/superdive/issues"
},
"homepage": "https://github.com/dukaev/superdive#readme",
"engines": {
"node": ">=14"
},
"files": [
"bin",
"scripts"
]
}

115
scripts/install.js Executable file
View file

@ -0,0 +1,115 @@
#!/usr/bin/env node
const https = require('https');
const fs = require('fs');
const path = require('path');
const os = require('os');
const { execSync } = require('child_process');
const VERSION = require('../package.json').version;
const REPO = 'dukaev/superdive';
function getPlatform() {
const platform = os.platform();
switch (platform) {
case 'darwin': return 'darwin';
case 'linux': return 'linux';
case 'win32': return 'windows';
default: throw new Error(`Unsupported platform: ${platform}`);
}
}
function getArch() {
const arch = os.arch();
switch (arch) {
case 'x64': return 'amd64';
case 'arm64': return 'arm64';
case 'ia32': return '386';
default: throw new Error(`Unsupported architecture: ${arch}`);
}
}
function getDownloadUrl() {
const platform = getPlatform();
const arch = getArch();
const ext = platform === 'windows' ? '.zip' : '.tar.gz';
return `https://github.com/${REPO}/releases/download/v${VERSION}/superdive_${VERSION}_${platform}_${arch}${ext}`;
}
function download(url, dest) {
return new Promise((resolve, reject) => {
const file = fs.createWriteStream(dest);
const request = (url) => {
https.get(url, (response) => {
if (response.statusCode === 302 || response.statusCode === 301) {
request(response.headers.location);
return;
}
if (response.statusCode !== 200) {
reject(new Error(`Failed to download: ${response.statusCode}`));
return;
}
response.pipe(file);
file.on('finish', () => {
file.close();
resolve();
});
}).on('error', (err) => {
fs.unlink(dest, () => {});
reject(err);
});
};
request(url);
});
}
async function install() {
const platform = getPlatform();
const binDir = path.join(__dirname, '..', 'bin');
const binaryName = platform === 'windows' ? 'superdive.exe' : 'superdive';
const binaryPath = path.join(binDir, binaryName);
// Skip if binary already exists
if (fs.existsSync(binaryPath)) {
console.log('superdive binary already exists');
return;
}
const url = getDownloadUrl();
const ext = platform === 'windows' ? '.zip' : '.tar.gz';
const archivePath = path.join(binDir, `superdive${ext}`);
console.log(`Downloading superdive from ${url}...`);
try {
await download(url, archivePath);
console.log('Extracting...');
if (platform === 'windows') {
execSync(`powershell -command "Expand-Archive -Path '${archivePath}' -DestinationPath '${binDir}'"`, { stdio: 'inherit' });
} else {
execSync(`tar -xzf "${archivePath}" -C "${binDir}"`, { stdio: 'inherit' });
}
// Make binary executable
if (platform !== 'windows') {
fs.chmodSync(binaryPath, 0o755);
}
// Clean up archive
fs.unlinkSync(archivePath);
console.log('superdive installed successfully!');
} catch (err) {
console.error(`Failed to install superdive: ${err.message}`);
console.error('Please install manually from https://github.com/dukaev/superdive/releases');
process.exit(1);
}
}
install();