diff --git a/Makefile b/Makefile index 5f319f1..f5f2e0d 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,9 @@ -.PHONY: all build dev clean test test-cover lint install deps frontend-deps run package help +.PHONY: all build dev clean test test-cover lint install deps frontend-deps run package release help # Resolve the Wails binary. Prefer wails on PATH; fall back to GOPATH/bin. WAILS := $(or $(shell which wails 2>/dev/null),$(shell go env GOPATH)/bin/wails) +VERSION := $(shell git describe --tags --abbrev=0 2>/dev/null || echo dev)-$(shell git rev-parse --short HEAD 2>/dev/null || echo unknown) +LD_UPDATE_URL := $(if $(NEXUS_UPDATE_URL),-X github.com/andydixon/nexus/internal/api.appUpdateURL=$(NEXUS_UPDATE_URL),) # Default target all: build @@ -20,7 +22,7 @@ dev: deps # Production build build: deps - $(WAILS) build -ldflags "-s -w" + $(WAILS) build -ldflags "-s -w -X github.com/andydixon/nexus/internal/api.appVersion=$(VERSION) $(LD_UPDATE_URL)" # Run tests test: @@ -47,6 +49,10 @@ lint: package: build @echo "Binary built at build/bin/" +# Build and package release artefacts for all platforms +release: + ./release.sh + # Help help: @echo "Nexus Terminal — Build Targets" @@ -59,4 +65,5 @@ help: @echo " make clean — Clean build artefacts" @echo " make lint — Run linter (requires golangci-lint)" @echo " make package — Build and package for distribution" + @echo " make release — Build multi-platform release artefacts" @echo " make help — Show this help" diff --git a/build-all.sh b/build-all.sh index 5a639b9..e52d7b0 100755 --- a/build-all.sh +++ b/build-all.sh @@ -16,6 +16,23 @@ log() { echo -e "${GREEN}[nexus]${NC} $1"; } warn() { echo -e "${YELLOW}[nexus]${NC} $1"; } err() { echo -e "${RED}[nexus]${NC} $1" >&2; } +build_version() { + local tag + local commit + tag="$(git describe --tags --abbrev=0 2>/dev/null || echo "dev")" + commit="$(git rev-parse --short HEAD 2>/dev/null || echo "unknown")" + echo "${tag}-${commit}" +} + +build_ldflags() { + local version="$1" + local flags="-s -w -X github.com/andydixon/nexus/internal/api.appVersion=${version}" + if [ -n "${NEXUS_UPDATE_URL:-}" ]; then + flags="${flags} -X github.com/andydixon/nexus/internal/api.appUpdateURL=${NEXUS_UPDATE_URL}" + fi + echo "${flags}" +} + check_deps() { command -v go >/dev/null 2>&1 || { err "Go is not installed"; exit 1; } command -v node >/dev/null 2>&1 || { err "Node.js is not installed"; exit 1; } @@ -80,6 +97,9 @@ build_target() { local target_os="$1" local target_arch="$2" local host="$3" + local version="$4" + local ldflags + ldflags="$(build_ldflags "${version}")" local platform="${target_os}/${target_arch}" local cc="" @@ -94,9 +114,9 @@ build_target() { log "Building ${platform}…" if [ -n "$cc" ]; then - CC="$cc" CGO_ENABLED=1 wails build -platform "$platform" -ldflags "-s -w" + CC="$cc" CGO_ENABLED=1 wails build -platform "$platform" -ldflags "$ldflags" else - CGO_ENABLED=1 wails build -platform "$platform" -ldflags "-s -w" + CGO_ENABLED=1 wails build -platform "$platform" -ldflags "$ldflags" fi } @@ -104,6 +124,9 @@ main() { check_deps install_deps + local version + version="$(build_version)" + local host host="$(host_os)" if [ "$host" = "unknown" ]; then @@ -112,15 +135,15 @@ main() { fi # Build host targets first - build_target "$host" "amd64" "$host" - build_target "$host" "arm64" "$host" + build_target "$host" "amd64" "$host" "$version" + build_target "$host" "arm64" "$host" "$version" # Cross-build Linux and Windows targets where toolchains are available if [ "$host" = "darwin" ] || [ "$host" = "linux" ]; then - build_target "linux" "amd64" "$host" - build_target "linux" "arm64" "$host" - build_target "windows" "amd64" "$host" - build_target "windows" "arm64" "$host" + build_target "linux" "amd64" "$host" "$version" + build_target "linux" "arm64" "$host" "$version" + build_target "windows" "amd64" "$host" "$version" + build_target "windows" "arm64" "$host" "$version" fi log "Builds complete. Output: build/bin/" diff --git a/build.ps1 b/build.ps1 index 509493e..5363e06 100644 --- a/build.ps1 +++ b/build.ps1 @@ -48,6 +48,32 @@ function Install-Deps { Pop-Location } +function Get-BuildVersion { + try { + $tag = & git describe --tags --abbrev=0 2>$null + if (-not $tag) { $tag = 'dev' } + } catch { + $tag = 'dev' + } + + try { + $commit = & git rev-parse --short HEAD 2>$null + if (-not $commit) { $commit = 'unknown' } + } catch { + $commit = 'unknown' + } + + return "$tag-$commit" +} + +function Get-Ldflags([string]$Version) { + $flags = "-s -w -X github.com/andydixon/nexus/internal/api.appVersion=$Version" + if ($env:NEXUS_UPDATE_URL) { + $flags += " -X github.com/andydixon/nexus/internal/api.appUpdateURL=$($env:NEXUS_UPDATE_URL)" + } + return $flags +} + function Dev { $wailsPath = Check-Deps Install-Deps @@ -58,10 +84,12 @@ function Dev { function Build { $wailsPath = Check-Deps Install-Deps + $version = Get-BuildVersion + $ldflags = Get-Ldflags $version $env:CGO_ENABLED = '1' Write-Info 'Building production binary (CGO_ENABLED=1)...' - & $wailsPath build -ldflags "-s -w" + & $wailsPath build -ldflags $ldflags Write-Info 'Build complete: build\bin\' } diff --git a/build.sh b/build.sh index a8ab195..c394099 100755 --- a/build.sh +++ b/build.sh @@ -17,6 +17,23 @@ log() { echo -e "${GREEN}[nexus]${NC} $1"; } warn() { echo -e "${YELLOW}[nexus]${NC} $1"; } err() { echo -e "${RED}[nexus]${NC} $1" >&2; } +build_version() { + local tag + local commit + tag="$(git describe --tags --abbrev=0 2>/dev/null || echo "dev")" + commit="$(git rev-parse --short HEAD 2>/dev/null || echo "unknown")" + echo "${tag}-${commit}" +} + +build_ldflags() { + local version="$1" + local flags="-s -w -X github.com/andydixon/nexus/internal/api.appVersion=${version}" + if [ -n "${NEXUS_UPDATE_URL:-}" ]; then + flags="${flags} -X github.com/andydixon/nexus/internal/api.appUpdateURL=${NEXUS_UPDATE_URL}" + fi + echo "${flags}" +} + # Check prerequisites check_deps() { command -v go >/dev/null 2>&1 || { err "Go is not installed"; exit 1; } @@ -62,7 +79,9 @@ build() { check_deps install_deps log "Building production binary…" - wails build -ldflags "-s -w" + local version + version="$(build_version)" + wails build -ldflags "$(build_ldflags "${version}")" log "Build complete: build/bin/" } diff --git a/frontend/index.html b/frontend/index.html index 3adf795..652c4dc 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -269,6 +269,9 @@ 0 sessions + 🔒 Locked diff --git a/frontend/src/css/layout.css b/frontend/src/css/layout.css index 504aa62..fabca0f 100644 --- a/frontend/src/css/layout.css +++ b/frontend/src/css/layout.css @@ -125,6 +125,27 @@ flex: 1; } +.status-update { + border: 1px solid var(--accent); + background: rgba(78, 205, 196, 0.12); + color: var(--accent); + font-size: var(--text-xs); + padding: 2px 8px; + border-radius: 999px; + cursor: pointer; + transition: background 150ms ease, color 150ms ease, border-color 150ms ease; +} + +.status-update:hover { + background: rgba(78, 205, 196, 0.2); + color: var(--text-primary); + border-color: var(--accent-strong, var(--accent)); +} + +.status-update.is-hidden { + display: none; +} + /* Status bar indicator dots */ .status-bar .status-dot { width: 6px; diff --git a/frontend/src/js/app.js b/frontend/src/js/app.js index f3ec615..fec4e76 100644 --- a/frontend/src/js/app.js +++ b/frontend/src/js/app.js @@ -40,6 +40,7 @@ this.exportImport = new ExportImportUI(this); this.sftp = new SFTPBrowser(this); this.snippets = new SnippetLibrary(this); + this.updateInfo = null; } /** @@ -61,6 +62,7 @@ await this.loadPreferences(); await this.checkSecretStore(); this.updateStatusBar(); + this.checkForUpdates(true); } /** @@ -254,6 +256,11 @@ ); }); + // App-wide commands from the native menu + window.runtime.EventsOn("app:open-about", () => { + this.settings.openTab("about"); + }); + // Port-forwarding events window.runtime.EventsOn("session:forward:started", (payload) => { const fwd = payload.forward || {}; @@ -503,6 +510,12 @@ this._bindClick("btn-empty-connect", () => this.profileEditor.openNew()); this._bindClick("btn-empty-quick", () => this.quickConnect()); + const updateBtn = document.getElementById("status-update"); + if (updateBtn) { + updateBtn.addEventListener("mousedown", (e) => e.preventDefault()); + updateBtn.addEventListener("click", () => this.settings.openTab("about")); + } + const searchInput = document.getElementById("profile-search"); if (searchInput) { searchInput.addEventListener("input", (e) => @@ -851,6 +864,7 @@ const connEl = document.getElementById("status-connection"); const countEl = document.getElementById("status-session-count"); const vaultEl = document.getElementById("status-vault"); + const updateEl = document.getElementById("status-update"); if (!connEl) return; const sessions = this.state.getAllSessions(); @@ -874,6 +888,44 @@ ? "🔓 Unlocked" : "🔒 Locked"; } + + if (updateEl) { + if (this.updateInfo && this.updateInfo.available) { + const version = this.updateInfo.latestVersion + ? ` (${this.updateInfo.latestVersion})` + : ""; + updateEl.textContent = `Update available${version}`; + updateEl.classList.remove("is-hidden"); + } else { + updateEl.textContent = "Update available"; + updateEl.classList.add("is-hidden"); + } + } + } + + async checkForUpdates(silent) { + if (!window.go || !window.go.api || !window.go.api.Service) { + return; + } + + try { + const info = await window.go.api.Service.CheckForUpdates(); + this.setUpdateInfo(info); + if (!silent && info && info.available) { + this.notifications.info( + `Update available: ${info.latestVersion || "new version"}`, + ); + } + } catch (err) { + if (!silent) { + this.notifications.error("Update check failed: " + err); + } + } + } + + setUpdateInfo(info) { + this.updateInfo = info || null; + this.updateStatusBar(); } // ── Internal helpers ───────────────────────────────────────────── diff --git a/frontend/src/js/settings.js b/frontend/src/js/settings.js index ba8e846..fc328a6 100644 --- a/frontend/src/js/settings.js +++ b/frontend/src/js/settings.js @@ -848,7 +848,7 @@ body.innerHTML = `
Version 1.0.0
+Version …
Developed by Andy Dixon - www.dixon.cx/nexus
AES-256-GCM encrypted credential vault
@@ -864,6 +864,22 @@