From d0bfb894f04157645e98d8c337eeac33cd226ef9 Mon Sep 17 00:00:00 2001 From: Andy Dixon Date: Tue, 28 Apr 2026 10:30:28 +0100 Subject: [PATCH] Additional Build Stuff Co-authored-by: Copilot --- .gitignore | 5 + build/releases/.gitkeep | 0 frontend/index.html | 6 +- frontend/src/css/layout.css | 197 ++++++++++++---------- frontend/src/js/app.js | 4 +- frontend/wailsjs/go/models.ts | 117 ++----------- internal/api/api.go | 5 +- internal/models/models.go | 18 +- main.go | 2 +- release.sh | 181 ++++++++++++++------ updates/index.php | 308 ++++++++++++++++++++++++++++++++++ 11 files changed, 588 insertions(+), 255 deletions(-) create mode 100644 build/releases/.gitkeep mode change 100644 => 100755 release.sh create mode 100644 updates/index.php diff --git a/.gitignore b/.gitignore index 7162c14..9dfa06f 100644 --- a/.gitignore +++ b/.gitignore @@ -58,3 +58,8 @@ vault.json *.pem *.key !build/appicon.png + + +# Builds +build/releases/v* +build/releases/manifest* diff --git a/build/releases/.gitkeep b/build/releases/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/frontend/index.html b/frontend/index.html index 652c4dc..a8ad843 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -269,7 +269,11 @@ 0 sessions - 🔒 Locked diff --git a/frontend/src/css/layout.css b/frontend/src/css/layout.css index fabca0f..444156c 100644 --- a/frontend/src/css/layout.css +++ b/frontend/src/css/layout.css @@ -7,170 +7,181 @@ Main application grid ----------------------------------------------------------------------- */ .app { - display: grid; - grid-template-columns: var(--sidebar-width) 1fr; - grid-template-rows: var(--tab-bar-height) 1fr var(--status-bar-height); - height: 100vh; - overflow: hidden; - background: var(--bg-base); + display: grid; + grid-template-columns: var(--sidebar-width) 1fr; + grid-template-rows: var(--tab-bar-height) 1fr var(--status-bar-height); + height: 100vh; + overflow: hidden; + background: var(--bg-base); } /* Collapsed sidebar variant */ .app.sidebar-collapsed { - grid-template-columns: var(--sidebar-width-collapsed) 1fr; + grid-template-columns: var(--sidebar-width-collapsed) 1fr; } /* ----------------------------------------------------------------------- Sidebar ----------------------------------------------------------------------- */ .sidebar { - grid-row: 1 / -1; - grid-column: 1; - background: var(--bg-surface); - border-right: 1px solid var(--border-subtle); - display: flex; - flex-direction: column; - overflow: hidden; - z-index: var(--z-sticky); + grid-row: 1 / -1; + grid-column: 1; + background: var(--bg-surface); + border-right: 1px solid var(--border-subtle); + display: flex; + flex-direction: column; + overflow: hidden; + z-index: var(--z-sticky); } /* ----------------------------------------------------------------------- Tab bar ----------------------------------------------------------------------- */ .tab-bar { - grid-column: 2; - grid-row: 1; - background: var(--bg-surface); - border-bottom: 1px solid var(--border-subtle); - display: flex; - align-items: center; - min-height: var(--tab-bar-height); - overflow: hidden; + grid-column: 2; + grid-row: 1; + background: var(--bg-surface); + border-bottom: 1px solid var(--border-subtle); + display: flex; + align-items: center; + min-height: var(--tab-bar-height); + overflow: hidden; } .tab-list { - display: flex; - align-items: center; - flex: 1; - min-width: 0; - overflow-x: auto; - overflow-y: hidden; - scrollbar-width: none; /* Firefox */ + display: flex; + align-items: center; + flex: 1; + min-width: 0; + overflow-x: auto; + overflow-y: hidden; + scrollbar-width: none; /* Firefox */ } .tab-list::-webkit-scrollbar { - display: none; + display: none; } /* ----------------------------------------------------------------------- Workspace ----------------------------------------------------------------------- */ .workspace { - grid-column: 2; - grid-row: 2; - position: relative; - overflow: hidden; - background: var(--bg-base); - display: flex; - flex-direction: column; + grid-column: 2; + grid-row: 2; + position: relative; + overflow: hidden; + background: var(--bg-base); + display: flex; + flex-direction: column; } /* Panel container — fills workspace, holds session panels */ .panel-container { - flex: 1; - position: relative; - overflow: hidden; + flex: 1; + position: relative; + overflow: hidden; } /* Individual session panel — one per tab, fills the panel container */ .session-panel { - position: absolute; - inset: 0; - display: flex; - flex-direction: column; - overflow: hidden; + position: absolute; + inset: 0; + display: flex; + flex-direction: column; + overflow: hidden; } /* Terminal container — fills workspace */ .terminal-container { - position: absolute; - inset: 0; - padding: 0; + position: absolute; + inset: 0; + padding: 0; } /* ----------------------------------------------------------------------- Status bar ----------------------------------------------------------------------- */ .status-bar { - grid-column: 2; - grid-row: 3; - background: var(--bg-surface); - border-top: 1px solid var(--border-subtle); - display: flex; - align-items: center; - padding: 0 var(--space-3); - font-size: var(--text-xs); - color: var(--text-tertiary); - gap: var(--space-4); - white-space: nowrap; - overflow: hidden; + grid-column: 2; + grid-row: 3; + background: var(--bg-surface); + border-top: 1px solid var(--border-subtle); + display: flex; + align-items: center; + padding: 0 var(--space-3); + font-size: var(--text-xs); + color: var(--text-tertiary); + gap: var(--space-4); + white-space: nowrap; + overflow: hidden; } .status-bar span { - display: inline-flex; - align-items: center; - gap: var(--space-1); + display: inline-flex; + align-items: center; + gap: var(--space-1); } .status-spacer { - flex: 1; + 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; + 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)); + background: rgba(78, 205, 196, 0.2); + color: var(--text-primary); + border-color: var(--accent-strong, var(--accent)); } .status-update.is-hidden { - display: none; + display: none; } /* Status bar indicator dots */ .status-bar .status-dot { - width: 6px; - height: 6px; - border-radius: 50%; - display: inline-block; + width: 6px; + height: 6px; + border-radius: 50%; + display: inline-block; +} +.status-dot.is-connected { + background: var(--success); +} +.status-dot.is-connecting { + background: var(--warning); +} +.status-dot.is-error { + background: var(--error); +} +.status-dot.is-disconnected { + background: var(--text-tertiary); } -.status-dot.is-connected { background: var(--success); } -.status-dot.is-connecting { background: var(--warning); } -.status-dot.is-error { background: var(--error); } -.status-dot.is-disconnected { background: var(--text-tertiary); } /* ----------------------------------------------------------------------- Resize handle (between sidebar and workspace) ----------------------------------------------------------------------- */ .resize-handle { - position: absolute; - top: 0; - right: -3px; - width: 6px; - height: 100%; - cursor: col-resize; - z-index: var(--z-sticky); + position: absolute; + top: 0; + right: -3px; + width: 6px; + height: 100%; + cursor: col-resize; + z-index: var(--z-sticky); } .resize-handle:hover, .resize-handle.is-dragging { - background: var(--accent-muted); + background: var(--accent-muted); } diff --git a/frontend/src/js/app.js b/frontend/src/js/app.js index fec4e76..c2c9760 100644 --- a/frontend/src/js/app.js +++ b/frontend/src/js/app.js @@ -513,7 +513,9 @@ const updateBtn = document.getElementById("status-update"); if (updateBtn) { updateBtn.addEventListener("mousedown", (e) => e.preventDefault()); - updateBtn.addEventListener("click", () => this.settings.openTab("about")); + updateBtn.addEventListener("click", () => + this.settings.openTab("about"), + ); } const searchInput = document.getElementById("profile-search"); diff --git a/frontend/wailsjs/go/models.ts b/frontend/wailsjs/go/models.ts index 6ba138c..68b7707 100755 --- a/frontend/wailsjs/go/models.ts +++ b/frontend/wailsjs/go/models.ts @@ -36,8 +36,7 @@ export namespace models { } export class AuditEntry { id: string; - // Go type: time - timestamp: any; + timestamp: string; action: string; profileId?: string; sessionId?: string; @@ -50,30 +49,12 @@ export namespace models { constructor(source: any = {}) { if ('string' === typeof source) source = JSON.parse(source); this.id = source["id"]; - this.timestamp = this.convertValues(source["timestamp"], null); + this.timestamp = source["timestamp"]; this.action = source["action"]; this.profileId = source["profileId"]; this.sessionId = source["sessionId"]; this.details = source["details"]; } - - convertValues(a: any, classs: any, asMap: boolean = false): any { - if (!a) { - return a; - } - if (a.slice && a.map) { - return (a as any[]).map(elem => this.convertValues(elem, classs)); - } else if ("object" === typeof a) { - if (asMap) { - for (const key of Object.keys(a)) { - a[key] = new classs(a[key]); - } - return a; - } - return new classs(a); - } - return a; - } } export class GitHubConfig { baseURL: string; @@ -160,10 +141,8 @@ export namespace models { keyType: string; fingerprint: string; publicKey: string; - // Go type: time - firstSeen: any; - // Go type: time - lastSeen: any; + firstSeen: string; + lastSeen: string; trusted: boolean; static createFrom(source: any = {}) { @@ -178,28 +157,10 @@ export namespace models { this.keyType = source["keyType"]; this.fingerprint = source["fingerprint"]; this.publicKey = source["publicKey"]; - this.firstSeen = this.convertValues(source["firstSeen"], null); - this.lastSeen = this.convertValues(source["lastSeen"], null); + this.firstSeen = source["firstSeen"]; + this.lastSeen = source["lastSeen"]; this.trusted = source["trusted"]; } - - convertValues(a: any, classs: any, asMap: boolean = false): any { - if (!a) { - return a; - } - if (a.slice && a.map) { - return (a as any[]).map(elem => this.convertValues(elem, classs)); - } else if ("object" === typeof a) { - if (asMap) { - for (const key of Object.keys(a)) { - a[key] = new classs(a[key]); - } - return a; - } - return new classs(a); - } - return a; - } } export class LoginScriptStep { type: string; @@ -436,14 +397,11 @@ export namespace models { environment: string; notes: string; favourite: boolean; - // Go type: time - lastConnected?: any; + lastConnected?: string; connectTimeout: number; readTimeout: number; - // Go type: time - createdAt: any; - // Go type: time - updatedAt: any; + createdAt: string; + updatedAt: string; terminalFontFamily: string; terminalFontSize: number; terminalScrollback: number; @@ -471,11 +429,11 @@ export namespace models { this.environment = source["environment"]; this.notes = source["notes"]; this.favourite = source["favourite"]; - this.lastConnected = this.convertValues(source["lastConnected"], null); + this.lastConnected = source["lastConnected"]; this.connectTimeout = source["connectTimeout"]; this.readTimeout = source["readTimeout"]; - this.createdAt = this.convertValues(source["createdAt"], null); - this.updatedAt = this.convertValues(source["updatedAt"], null); + this.createdAt = source["createdAt"]; + this.updatedAt = source["updatedAt"]; this.terminalFontFamily = source["terminalFontFamily"]; this.terminalFontSize = source["terminalFontSize"]; this.terminalScrollback = source["terminalScrollback"]; @@ -516,8 +474,7 @@ export namespace models { state: string; tabName: string; pinned: boolean; - // Go type: time - connectedAt: any; + connectedAt: string; error?: string; static createFrom(source: any = {}) { @@ -535,27 +492,9 @@ export namespace models { this.state = source["state"]; this.tabName = source["tabName"]; this.pinned = source["pinned"]; - this.connectedAt = this.convertValues(source["connectedAt"], null); + this.connectedAt = source["connectedAt"]; this.error = source["error"]; } - - convertValues(a: any, classs: any, asMap: boolean = false): any { - if (!a) { - return a; - } - if (a.slice && a.map) { - return (a as any[]).map(elem => this.convertValues(elem, classs)); - } else if ("object" === typeof a) { - if (asMap) { - for (const key of Object.keys(a)) { - a[key] = new classs(a[key]); - } - return a; - } - return new classs(a); - } - return a; - } } export class Snippet { id: string; @@ -564,10 +503,8 @@ export namespace models { description: string; folder: string; tags: string[]; - // Go type: time - createdAt: any; - // Go type: time - updatedAt: any; + createdAt: string; + updatedAt: string; static createFrom(source: any = {}) { return new Snippet(source); @@ -581,27 +518,9 @@ export namespace models { this.description = source["description"]; this.folder = source["folder"]; this.tags = source["tags"]; - this.createdAt = this.convertValues(source["createdAt"], null); - this.updatedAt = this.convertValues(source["updatedAt"], null); + this.createdAt = source["createdAt"]; + this.updatedAt = source["updatedAt"]; } - - convertValues(a: any, classs: any, asMap: boolean = false): any { - if (!a) { - return a; - } - if (a.slice && a.map) { - return (a as any[]).map(elem => this.convertValues(elem, classs)); - } else if ("object" === typeof a) { - if (asMap) { - for (const key of Object.keys(a)) { - a[key] = new classs(a[key]); - } - return a; - } - return new classs(a); - } - return a; - } } } diff --git a/internal/api/api.go b/internal/api/api.go index ebf5c5f..a7e4e22 100644 --- a/internal/api/api.go +++ b/internal/api/api.go @@ -1306,7 +1306,10 @@ func (s *Service) CheckForUpdates() (map[string]interface{}, error) { var err error if appUpdateURL != "" { - checker := updater.NewURLChecker(appUpdateURL, appVersion) + url := strings.ReplaceAll(appUpdateURL, "{platform}", runtime.GOOS) + url = strings.ReplaceAll(url, "{os}", runtime.GOOS) + url = strings.ReplaceAll(url, "{arch}", runtime.GOARCH) + checker := updater.NewURLChecker(url, appVersion) info, err = checker.Check(ctx) } else { checker := updater.NewChecker(appVersion) diff --git a/internal/models/models.go b/internal/models/models.go index b641927..de12403 100644 --- a/internal/models/models.go +++ b/internal/models/models.go @@ -57,11 +57,11 @@ type Profile struct { Environment string `json:"environment"` Notes string `json:"notes"` Favourite bool `json:"favourite"` - LastConnected *time.Time `json:"lastConnected"` + LastConnected *time.Time `json:"lastConnected" ts_type:"string"` ConnectTimeout int `json:"connectTimeout"` // seconds ReadTimeout int `json:"readTimeout"` // seconds - CreatedAt time.Time `json:"createdAt"` - UpdatedAt time.Time `json:"updatedAt"` + CreatedAt time.Time `json:"createdAt" ts_type:"string"` + UpdatedAt time.Time `json:"updatedAt" ts_type:"string"` // Terminal preferences TerminalFontFamily string `json:"terminalFontFamily"` @@ -233,7 +233,7 @@ type SessionInfo struct { State SessionState `json:"state"` TabName string `json:"tabName"` Pinned bool `json:"pinned"` - ConnectedAt time.Time `json:"connectedAt"` + ConnectedAt time.Time `json:"connectedAt" ts_type:"string"` Error string `json:"error,omitempty"` } @@ -245,8 +245,8 @@ type KnownHost struct { KeyType string `json:"keyType"` Fingerprint string `json:"fingerprint"` PublicKey string `json:"publicKey"` - FirstSeen time.Time `json:"firstSeen"` - LastSeen time.Time `json:"lastSeen"` + FirstSeen time.Time `json:"firstSeen" ts_type:"string"` + LastSeen time.Time `json:"lastSeen" ts_type:"string"` Trusted bool `json:"trusted"` } @@ -269,7 +269,7 @@ type AppPreferences struct { // AuditEntry records an auditable action within the application. type AuditEntry struct { ID string `json:"id"` - Timestamp time.Time `json:"timestamp"` + Timestamp time.Time `json:"timestamp" ts_type:"string"` Action string `json:"action"` ProfileID string `json:"profileId,omitempty"` SessionID string `json:"sessionId,omitempty"` @@ -284,6 +284,6 @@ type Snippet struct { Description string `json:"description"` Folder string `json:"folder"` Tags []string `json:"tags"` - CreatedAt time.Time `json:"createdAt"` - UpdatedAt time.Time `json:"updatedAt"` + CreatedAt time.Time `json:"createdAt" ts_type:"string"` + UpdatedAt time.Time `json:"updatedAt" ts_type:"string"` } diff --git a/main.go b/main.go index 7a33b06..79d69bf 100644 --- a/main.go +++ b/main.go @@ -14,8 +14,8 @@ import ( "github.com/wailsapp/wails/v2/pkg/options/mac" "github.com/wailsapp/wails/v2/pkg/options/windows" - application "github.com/andydixon/nexus/internal/app" "github.com/andydixon/nexus/frontend" + application "github.com/andydixon/nexus/internal/app" ) func buildAppMenu(app *application.App) *menu.Menu { diff --git a/release.sh b/release.sh old mode 100644 new mode 100755 index 2d6af59..98612c7 --- a/release.sh +++ b/release.sh @@ -11,6 +11,9 @@ YELLOW='\033[1;33m' RED='\033[0;31m' NC='\033[0m' +DEFAULT_UPDATE_URL="https://dixon.cx/downloads/nexus/manifest-{platform}-{arch}.json" +DEFAULT_RELEASE_BASE_URL="https://dixon.cx/downloads/nexus" + log() { echo -e "${GREEN}[nexus]${NC} $1"; } warn() { echo -e "${YELLOW}[nexus]${NC} $1"; } err() { echo -e "${RED}[nexus]${NC} $1" >&2; } @@ -62,6 +65,10 @@ pick_cc() { 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 @@ -71,6 +78,10 @@ pick_cc() { 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 @@ -92,6 +103,17 @@ build_target() { 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 @@ -111,18 +133,75 @@ build_target() { local ldflags ldflags="$(build_ldflags "$version")" if [ -n "$cc" ]; then - CC="$cc" CGO_ENABLED=1 wails build -platform "$platform" -ldflags "$ldflags" -o "$output_path" + 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 -platform "$platform" -ldflags "$ldflags" -o "$output_path"; then + warn "Build failed for ${platform}" + return 1 + fi else - CGO_ENABLED=1 wails build -platform "$platform" -ldflags "$ldflags" -o "$output_path" + if ! CGO_ENABLED=1 wails build -platform "$platform" -ldflags "$ldflags" -o "$output_path"; then + warn "Build failed for ${platform}" + return 1 + fi fi - local zip_name="${out_dir}/${output_base}.zip" + local expected_path="" + local build_bin="build/bin" if [ "$target_os" = "darwin" ]; then - (cd "$out_dir" && zip -r "${output_base}.zip" "${output_base}.app" >/dev/null) + expected_path="${out_dir}/${output_base}.app" + if [ ! -d "$expected_path" ]; then + local source_app + source_app="$(find "$build_bin" -type d -name "*.app" -print0 | xargs -0 ls -td 2>/dev/null | head -1)" + if [ -z "$source_app" ]; then + warn "Built .app not found for ${platform}" + return 1 + fi + log "Using app bundle: ${source_app}" + cp -R "$source_app" "$expected_path" + fi + if ! (cd "$out_dir" && zip -r "${output_base}.zip" "${output_base}.app" >/dev/null); then + warn "Zip failed for ${platform}" + return 1 + fi elif [ "$target_os" = "windows" ]; then - (cd "$out_dir" && zip -j "${output_base}.zip" "${output_base}.exe" >/dev/null) + expected_path="${out_dir}/${output_base}.exe" + if [ ! -f "$expected_path" ]; then + local source_exe + source_exe="$(find "$build_bin" -type f -name "${output_base}.exe" -print0 | xargs -0 ls -t 2>/dev/null | head -1)" + if [ -z "$source_exe" ]; then + warn "Built .exe not found for ${platform}" + return 1 + fi + log "Using executable: ${source_exe}" + cp "$source_exe" "$expected_path" + fi + if ! (cd "$out_dir" && zip -j "${output_base}.zip" "${output_base}.exe" >/dev/null); then + warn "Zip failed for ${platform}" + return 1 + fi else - (cd "$out_dir" && zip -j "${output_base}.zip" "${output_base}" >/dev/null) + expected_path="${out_dir}/${output_base}" + if [ ! -f "$expected_path" ]; then + local source_bin + source_bin="$(find "$build_bin" -type f -name "${output_base}" -print0 | xargs -0 ls -t 2>/dev/null | head -1)" + if [ -z "$source_bin" ] || [ ! -f "$source_bin" ]; then + warn "Built binary not found for ${platform}" + return 1 + fi + log "Using binary: ${source_bin}" + cp "$source_bin" "$expected_path" + fi + if ! (cd "$out_dir" && zip -j "${output_base}.zip" "${output_base}" >/dev/null); then + warn "Zip failed for ${platform}" + return 1 + fi fi echo "$target_os|$target_arch|$output_base.zip" >> "${out_dir}/.release-files" @@ -131,49 +210,44 @@ build_target() { write_manifest() { local version="$1" local out_dir="$2" - local manifest_path="$3" - local version_path="$4" + local release_root="$3" local base_url="${NEXUS_RELEASE_BASE_URL:-}" local published_at published_at="$(date -u +"%Y-%m-%dT%H:%M:%SZ")" - echo "$version" > "$version_path" - - { - echo "{" - echo " \"latestVersion\": \"${version}\"," - echo " \"publishedAt\": \"${published_at}\"," + while IFS='|' read -r os arch filename; do + [ -z "$os" ] && continue + local manifest_path="${release_root}/manifest-${os}-${arch}.json" + local version_path="${release_root}/version-${os}-${arch}.txt" + local file_path="${out_dir}/${filename}" + local sha + sha="$(shasum -a 256 "$file_path" | awk '{print $1}')" + local size + size="$(wc -c < "$file_path" | tr -d ' ')" + local url="" if [ -n "$base_url" ]; then - echo " \"releaseURL\": \"${base_url}/\"," - else - echo " \"releaseURL\": \"\"," + url="${base_url}/${filename}" fi - echo " \"files\": [" - local first=1 - while IFS='|' read -r os arch filename; do - [ -z "$os" ] && continue - local file_path="${out_dir}/${filename}" - local sha - sha="$(shasum -a 256 "$file_path" | awk '{print $1}')" - local size - size="$(wc -c < "$file_path" | tr -d ' ')" - local url="" + echo "$version" > "$version_path" + + { + echo "{" + echo " \"latestVersion\": \"${version}\"," + echo " \"publishedAt\": \"${published_at}\"," if [ -n "$base_url" ]; then - url="${base_url}/${filename}" + echo " \"releaseURL\": \"${base_url}/\"," + else + echo " \"releaseURL\": \"\"," fi - - if [ $first -eq 0 ]; then - echo "," - fi - first=0 + echo " \"files\": [" printf " {\"platform\":\"%s\",\"arch\":\"%s\",\"filename\":\"%s\",\"url\":\"%s\",\"sha256\":\"%s\",\"size\":%s}" "$os" "$arch" "$filename" "$url" "$sha" "$size" - done < "${out_dir}/.release-files" - echo - echo " ]" - echo "}" - } > "$manifest_path" + echo + echo " ]" + echo "}" + } > "$manifest_path" + done < "${out_dir}/.release-files" rm -f "${out_dir}/.release-files" } @@ -186,7 +260,10 @@ main() { version="$(build_version)" if [ -z "${NEXUS_UPDATE_URL:-}" ]; then - warn "NEXUS_UPDATE_URL is not set; updates will default to GitHub in this build" + NEXUS_UPDATE_URL="${DEFAULT_UPDATE_URL}" + fi + if [ -z "${NEXUS_RELEASE_BASE_URL:-}" ]; then + NEXUS_RELEASE_BASE_URL="${DEFAULT_RELEASE_BASE_URL}" fi local release_root="build/releases" @@ -196,20 +273,24 @@ main() { log "Building release ${version}..." : > "${out_dir}/.release-files" - build_target "darwin" "amd64" "$version" "$out_dir" - build_target "darwin" "arm64" "$version" "$out_dir" - build_target "linux" "amd64" "$version" "$out_dir" - build_target "linux" "arm64" "$version" "$out_dir" - build_target "windows" "amd64" "$version" "$out_dir" - build_target "windows" "arm64" "$version" "$out_dir" + local errors=0 + if ! build_target "darwin" "amd64" "$version" "$out_dir"; then errors=1; fi + if ! build_target "darwin" "arm64" "$version" "$out_dir"; then errors=1; fi + if ! build_target "linux" "amd64" "$version" "$out_dir"; then errors=1; fi + if ! build_target "linux" "arm64" "$version" "$out_dir"; then errors=1; fi + if ! build_target "windows" "amd64" "$version" "$out_dir"; then errors=1; fi + if ! build_target "windows" "arm64" "$version" "$out_dir"; then errors=1; fi - local manifest_path="${release_root}/manifest.json" - local version_path="${release_root}/version.txt" - write_manifest "$version" "$out_dir" "$manifest_path" "$version_path" + write_manifest "$version" "$out_dir" "$release_root" log "Release artefacts written to ${out_dir}" - log "Manifest: ${manifest_path}" - log "Version: ${version_path}" + log "Manifests: ${release_root}/manifest--.json" + log "Versions: ${release_root}/version--.txt" + + if [ $errors -ne 0 ]; then + err "One or more targets failed. Manifest and version were still generated." + exit 12 + fi } main "$@" diff --git a/updates/index.php b/updates/index.php new file mode 100644 index 0000000..1fce0b3 --- /dev/null +++ b/updates/index.php @@ -0,0 +1,308 @@ + 'manifest unavailable'], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES); + } + exit; +} + +if ($wantsVersion) { + header('Content-Type: text/plain; charset=utf-8'); + if ($manifest && !empty($manifest['latestVersion'])) { + echo $manifest['latestVersion']; + } elseif (is_readable($versionPath)) { + echo trim(file_get_contents($versionPath)); + } else { + http_response_code(503); + echo 'unknown'; + } + exit; +} + +if ($platform && $arch && $format === 'download') { + if ($manifest && !empty($manifest['files'][0]['url'])) { + header('Location: ' . $manifest['files'][0]['url'], true, 302); + } else { + http_response_code(404); + echo 'download not available'; + } + exit; +} + +function h($value) +{ + return htmlspecialchars((string)$value, ENT_QUOTES, 'UTF-8'); +} + +function formatSize($bytes) +{ + if (!is_numeric($bytes)) { + return ''; + } + $bytes = (float)$bytes; + $units = ['B', 'KB', 'MB', 'GB']; + $i = 0; + while ($bytes >= 1024 && $i < count($units) - 1) { + $bytes /= 1024; + $i++; + } + return number_format($bytes, $i === 0 ? 0 : 2) . ' ' . $units[$i]; +} + +$latest = $manifest && isset($manifest['latestVersion']) ? $manifest['latestVersion'] : 'unknown'; +$published = $manifest && isset($manifest['publishedAt']) ? $manifest['publishedAt'] : ''; +$platformLabel = $platform && $arch ? strtoupper($platform) . ' / ' . $arch : ''; +$files = $manifest && isset($manifest['files']) && is_array($manifest['files']) ? $manifest['files'] : []; + +$rows = []; +if (!$platform && !$arch) { + foreach ($allManifests as $entry) { + $file = isset($entry['files'][0]) ? $entry['files'][0] : null; + if (!$file) { + continue; + } + $rows[] = [ + 'platform' => isset($file['platform']) ? $file['platform'] : '', + 'arch' => isset($file['arch']) ? $file['arch'] : '', + 'version' => isset($entry['latestVersion']) ? $entry['latestVersion'] : 'unknown', + 'size' => isset($file['size']) ? $file['size'] : '', + 'url' => isset($file['url']) ? $file['url'] : '', + 'filename' => isset($file['filename']) ? $file['filename'] : '', + ]; + } + usort($rows, function ($a, $b) { + $ka = $a['platform'] . '/' . $a['arch'] . '/' . $a['version']; + $kb = $b['platform'] . '/' . $b['arch'] . '/' . $b['version']; + return strcmp($ka, $kb); + }); +} +?> + + + + + + + Nexus Terminal Downloads + + + + +
+

Nexus Terminal Downloads

+
Latest version:
+ +
+

Release files

+ + +
No release files available.
+ + + + + + + + + + + + + + + + + + + + + + + +
PlatformCPUVersionSizeDownload
+ + Download + + + +
+ + + +
No release files available.
+ + + + + + + + + + + + + + + + + + + + + + +
PlatformArchFileSizeSHA-256
+ + + + + + + +
+ + +
+
+ + + \ No newline at end of file