Additional Build Stuff

Co-authored-by: Copilot <copilot@github.com>
This commit is contained in:
Andy Dixon
2026-04-28 10:30:28 +01:00
co-authored by Copilot
parent 190bf59085
commit d0bfb894f0
11 changed files with 588 additions and 255 deletions
+5
View File
@@ -58,3 +58,8 @@ vault.json
*.pem *.pem
*.key *.key
!build/appicon.png !build/appicon.png
# Builds
build/releases/v*
build/releases/manifest*
View File
+5 -1
View File
@@ -269,7 +269,11 @@
<span id="status-protocol"></span> <span id="status-protocol"></span>
<span id="status-session-count">0 sessions</span> <span id="status-session-count">0 sessions</span>
<span class="status-spacer"></span> <span class="status-spacer"></span>
<button class="status-update is-hidden" id="status-update" type="button"> <button
class="status-update is-hidden"
id="status-update"
type="button"
>
Update available Update available
</button> </button>
<span id="status-vault">🔒 Locked</span> <span id="status-vault">🔒 Locked</span>
+16 -5
View File
@@ -133,7 +133,10 @@
padding: 2px 8px; padding: 2px 8px;
border-radius: 999px; border-radius: 999px;
cursor: pointer; cursor: pointer;
transition: background 150ms ease, color 150ms ease, border-color 150ms ease; transition:
background 150ms ease,
color 150ms ease,
border-color 150ms ease;
} }
.status-update:hover { .status-update:hover {
@@ -153,10 +156,18 @@
border-radius: 50%; border-radius: 50%;
display: inline-block; display: inline-block;
} }
.status-dot.is-connected { background: var(--success); } .status-dot.is-connected {
.status-dot.is-connecting { background: var(--warning); } background: var(--success);
.status-dot.is-error { background: var(--error); } }
.status-dot.is-disconnected { background: var(--text-tertiary); } .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 (between sidebar and workspace)
+3 -1
View File
@@ -513,7 +513,9 @@
const updateBtn = document.getElementById("status-update"); const updateBtn = document.getElementById("status-update");
if (updateBtn) { if (updateBtn) {
updateBtn.addEventListener("mousedown", (e) => e.preventDefault()); 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"); const searchInput = document.getElementById("profile-search");
+18 -99
View File
@@ -36,8 +36,7 @@ export namespace models {
} }
export class AuditEntry { export class AuditEntry {
id: string; id: string;
// Go type: time timestamp: string;
timestamp: any;
action: string; action: string;
profileId?: string; profileId?: string;
sessionId?: string; sessionId?: string;
@@ -50,30 +49,12 @@ export namespace models {
constructor(source: any = {}) { constructor(source: any = {}) {
if ('string' === typeof source) source = JSON.parse(source); if ('string' === typeof source) source = JSON.parse(source);
this.id = source["id"]; this.id = source["id"];
this.timestamp = this.convertValues(source["timestamp"], null); this.timestamp = source["timestamp"];
this.action = source["action"]; this.action = source["action"];
this.profileId = source["profileId"]; this.profileId = source["profileId"];
this.sessionId = source["sessionId"]; this.sessionId = source["sessionId"];
this.details = source["details"]; 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 { export class GitHubConfig {
baseURL: string; baseURL: string;
@@ -160,10 +141,8 @@ export namespace models {
keyType: string; keyType: string;
fingerprint: string; fingerprint: string;
publicKey: string; publicKey: string;
// Go type: time firstSeen: string;
firstSeen: any; lastSeen: string;
// Go type: time
lastSeen: any;
trusted: boolean; trusted: boolean;
static createFrom(source: any = {}) { static createFrom(source: any = {}) {
@@ -178,28 +157,10 @@ export namespace models {
this.keyType = source["keyType"]; this.keyType = source["keyType"];
this.fingerprint = source["fingerprint"]; this.fingerprint = source["fingerprint"];
this.publicKey = source["publicKey"]; this.publicKey = source["publicKey"];
this.firstSeen = this.convertValues(source["firstSeen"], null); this.firstSeen = source["firstSeen"];
this.lastSeen = this.convertValues(source["lastSeen"], null); this.lastSeen = source["lastSeen"];
this.trusted = source["trusted"]; 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 { export class LoginScriptStep {
type: string; type: string;
@@ -436,14 +397,11 @@ export namespace models {
environment: string; environment: string;
notes: string; notes: string;
favourite: boolean; favourite: boolean;
// Go type: time lastConnected?: string;
lastConnected?: any;
connectTimeout: number; connectTimeout: number;
readTimeout: number; readTimeout: number;
// Go type: time createdAt: string;
createdAt: any; updatedAt: string;
// Go type: time
updatedAt: any;
terminalFontFamily: string; terminalFontFamily: string;
terminalFontSize: number; terminalFontSize: number;
terminalScrollback: number; terminalScrollback: number;
@@ -471,11 +429,11 @@ export namespace models {
this.environment = source["environment"]; this.environment = source["environment"];
this.notes = source["notes"]; this.notes = source["notes"];
this.favourite = source["favourite"]; this.favourite = source["favourite"];
this.lastConnected = this.convertValues(source["lastConnected"], null); this.lastConnected = source["lastConnected"];
this.connectTimeout = source["connectTimeout"]; this.connectTimeout = source["connectTimeout"];
this.readTimeout = source["readTimeout"]; this.readTimeout = source["readTimeout"];
this.createdAt = this.convertValues(source["createdAt"], null); this.createdAt = source["createdAt"];
this.updatedAt = this.convertValues(source["updatedAt"], null); this.updatedAt = source["updatedAt"];
this.terminalFontFamily = source["terminalFontFamily"]; this.terminalFontFamily = source["terminalFontFamily"];
this.terminalFontSize = source["terminalFontSize"]; this.terminalFontSize = source["terminalFontSize"];
this.terminalScrollback = source["terminalScrollback"]; this.terminalScrollback = source["terminalScrollback"];
@@ -516,8 +474,7 @@ export namespace models {
state: string; state: string;
tabName: string; tabName: string;
pinned: boolean; pinned: boolean;
// Go type: time connectedAt: string;
connectedAt: any;
error?: string; error?: string;
static createFrom(source: any = {}) { static createFrom(source: any = {}) {
@@ -535,27 +492,9 @@ export namespace models {
this.state = source["state"]; this.state = source["state"];
this.tabName = source["tabName"]; this.tabName = source["tabName"];
this.pinned = source["pinned"]; this.pinned = source["pinned"];
this.connectedAt = this.convertValues(source["connectedAt"], null); this.connectedAt = source["connectedAt"];
this.error = source["error"]; 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 { export class Snippet {
id: string; id: string;
@@ -564,10 +503,8 @@ export namespace models {
description: string; description: string;
folder: string; folder: string;
tags: string[]; tags: string[];
// Go type: time createdAt: string;
createdAt: any; updatedAt: string;
// Go type: time
updatedAt: any;
static createFrom(source: any = {}) { static createFrom(source: any = {}) {
return new Snippet(source); return new Snippet(source);
@@ -581,26 +518,8 @@ export namespace models {
this.description = source["description"]; this.description = source["description"];
this.folder = source["folder"]; this.folder = source["folder"];
this.tags = source["tags"]; this.tags = source["tags"];
this.createdAt = this.convertValues(source["createdAt"], null); this.createdAt = source["createdAt"];
this.updatedAt = this.convertValues(source["updatedAt"], null); 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;
} }
} }
+4 -1
View File
@@ -1306,7 +1306,10 @@ func (s *Service) CheckForUpdates() (map[string]interface{}, error) {
var err error var err error
if appUpdateURL != "" { 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) info, err = checker.Check(ctx)
} else { } else {
checker := updater.NewChecker(appVersion) checker := updater.NewChecker(appVersion)
+9 -9
View File
@@ -57,11 +57,11 @@ type Profile struct {
Environment string `json:"environment"` Environment string `json:"environment"`
Notes string `json:"notes"` Notes string `json:"notes"`
Favourite bool `json:"favourite"` Favourite bool `json:"favourite"`
LastConnected *time.Time `json:"lastConnected"` LastConnected *time.Time `json:"lastConnected" ts_type:"string"`
ConnectTimeout int `json:"connectTimeout"` // seconds ConnectTimeout int `json:"connectTimeout"` // seconds
ReadTimeout int `json:"readTimeout"` // seconds ReadTimeout int `json:"readTimeout"` // seconds
CreatedAt time.Time `json:"createdAt"` CreatedAt time.Time `json:"createdAt" ts_type:"string"`
UpdatedAt time.Time `json:"updatedAt"` UpdatedAt time.Time `json:"updatedAt" ts_type:"string"`
// Terminal preferences // Terminal preferences
TerminalFontFamily string `json:"terminalFontFamily"` TerminalFontFamily string `json:"terminalFontFamily"`
@@ -233,7 +233,7 @@ type SessionInfo struct {
State SessionState `json:"state"` State SessionState `json:"state"`
TabName string `json:"tabName"` TabName string `json:"tabName"`
Pinned bool `json:"pinned"` Pinned bool `json:"pinned"`
ConnectedAt time.Time `json:"connectedAt"` ConnectedAt time.Time `json:"connectedAt" ts_type:"string"`
Error string `json:"error,omitempty"` Error string `json:"error,omitempty"`
} }
@@ -245,8 +245,8 @@ type KnownHost struct {
KeyType string `json:"keyType"` KeyType string `json:"keyType"`
Fingerprint string `json:"fingerprint"` Fingerprint string `json:"fingerprint"`
PublicKey string `json:"publicKey"` PublicKey string `json:"publicKey"`
FirstSeen time.Time `json:"firstSeen"` FirstSeen time.Time `json:"firstSeen" ts_type:"string"`
LastSeen time.Time `json:"lastSeen"` LastSeen time.Time `json:"lastSeen" ts_type:"string"`
Trusted bool `json:"trusted"` Trusted bool `json:"trusted"`
} }
@@ -269,7 +269,7 @@ type AppPreferences struct {
// AuditEntry records an auditable action within the application. // AuditEntry records an auditable action within the application.
type AuditEntry struct { type AuditEntry struct {
ID string `json:"id"` ID string `json:"id"`
Timestamp time.Time `json:"timestamp"` Timestamp time.Time `json:"timestamp" ts_type:"string"`
Action string `json:"action"` Action string `json:"action"`
ProfileID string `json:"profileId,omitempty"` ProfileID string `json:"profileId,omitempty"`
SessionID string `json:"sessionId,omitempty"` SessionID string `json:"sessionId,omitempty"`
@@ -284,6 +284,6 @@ type Snippet struct {
Description string `json:"description"` Description string `json:"description"`
Folder string `json:"folder"` Folder string `json:"folder"`
Tags []string `json:"tags"` Tags []string `json:"tags"`
CreatedAt time.Time `json:"createdAt"` CreatedAt time.Time `json:"createdAt" ts_type:"string"`
UpdatedAt time.Time `json:"updatedAt"` UpdatedAt time.Time `json:"updatedAt" ts_type:"string"`
} }
+1 -1
View File
@@ -14,8 +14,8 @@ import (
"github.com/wailsapp/wails/v2/pkg/options/mac" "github.com/wailsapp/wails/v2/pkg/options/mac"
"github.com/wailsapp/wails/v2/pkg/options/windows" "github.com/wailsapp/wails/v2/pkg/options/windows"
application "github.com/andydixon/nexus/internal/app"
"github.com/andydixon/nexus/frontend" "github.com/andydixon/nexus/frontend"
application "github.com/andydixon/nexus/internal/app"
) )
func buildAppMenu(app *application.App) *menu.Menu { func buildAppMenu(app *application.App) *menu.Menu {
Regular → Executable
+120 -39
View File
@@ -11,6 +11,9 @@ YELLOW='\033[1;33m'
RED='\033[0;31m' RED='\033[0;31m'
NC='\033[0m' 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"; } log() { echo -e "${GREEN}[nexus]${NC} $1"; }
warn() { echo -e "${YELLOW}[nexus]${NC} $1"; } warn() { echo -e "${YELLOW}[nexus]${NC} $1"; }
err() { echo -e "${RED}[nexus]${NC} $1" >&2; } err() { echo -e "${RED}[nexus]${NC} $1" >&2; }
@@ -62,6 +65,10 @@ pick_cc() {
local target_arch="$2" local target_arch="$2"
if [ "$target_os" = "windows" ]; then 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 if [ "$target_arch" = "amd64" ]; then
echo "x86_64-w64-mingw32-gcc" echo "x86_64-w64-mingw32-gcc"
else else
@@ -71,6 +78,10 @@ pick_cc() {
fi fi
if [ "$target_os" = "linux" ]; then 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 if [ "$target_arch" = "amd64" ]; then
echo "x86_64-linux-gnu-gcc" echo "x86_64-linux-gnu-gcc"
else else
@@ -92,6 +103,17 @@ build_target() {
local cc local cc
cc="$(pick_cc "$target_os" "$target_arch")" 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 if [ -n "$cc" ] && ! command -v "$cc" >/dev/null 2>&1; then
warn "Skipping ${platform} (missing C compiler: ${cc})" warn "Skipping ${platform} (missing C compiler: ${cc})"
return 0 return 0
@@ -111,18 +133,75 @@ build_target() {
local ldflags local ldflags
ldflags="$(build_ldflags "$version")" ldflags="$(build_ldflags "$version")"
if [ -n "$cc" ]; then 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 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 fi
local zip_name="${out_dir}/${output_base}.zip" local expected_path=""
local build_bin="build/bin"
if [ "$target_os" = "darwin" ]; then 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 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 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 fi
echo "$target_os|$target_arch|$output_base.zip" >> "${out_dir}/.release-files" echo "$target_os|$target_arch|$output_base.zip" >> "${out_dir}/.release-files"
@@ -131,13 +210,26 @@ build_target() {
write_manifest() { write_manifest() {
local version="$1" local version="$1"
local out_dir="$2" local out_dir="$2"
local manifest_path="$3" local release_root="$3"
local version_path="$4"
local base_url="${NEXUS_RELEASE_BASE_URL:-}" local base_url="${NEXUS_RELEASE_BASE_URL:-}"
local published_at local published_at
published_at="$(date -u +"%Y-%m-%dT%H:%M:%SZ")" published_at="$(date -u +"%Y-%m-%dT%H:%M:%SZ")"
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
url="${base_url}/${filename}"
fi
echo "$version" > "$version_path" echo "$version" > "$version_path"
{ {
@@ -150,30 +242,12 @@ write_manifest() {
echo " \"releaseURL\": \"\"," echo " \"releaseURL\": \"\","
fi fi
echo " \"files\": [" 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=""
if [ -n "$base_url" ]; then
url="${base_url}/${filename}"
fi
if [ $first -eq 0 ]; then
echo ","
fi
first=0
printf " {\"platform\":\"%s\",\"arch\":\"%s\",\"filename\":\"%s\",\"url\":\"%s\",\"sha256\":\"%s\",\"size\":%s}" "$os" "$arch" "$filename" "$url" "$sha" "$size" 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 " ]" echo " ]"
echo "}" echo "}"
} > "$manifest_path" } > "$manifest_path"
done < "${out_dir}/.release-files"
rm -f "${out_dir}/.release-files" rm -f "${out_dir}/.release-files"
} }
@@ -186,7 +260,10 @@ main() {
version="$(build_version)" version="$(build_version)"
if [ -z "${NEXUS_UPDATE_URL:-}" ]; then 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 fi
local release_root="build/releases" local release_root="build/releases"
@@ -196,20 +273,24 @@ main() {
log "Building release ${version}..." log "Building release ${version}..."
: > "${out_dir}/.release-files" : > "${out_dir}/.release-files"
build_target "darwin" "amd64" "$version" "$out_dir" local errors=0
build_target "darwin" "arm64" "$version" "$out_dir" if ! build_target "darwin" "amd64" "$version" "$out_dir"; then errors=1; fi
build_target "linux" "amd64" "$version" "$out_dir" if ! build_target "darwin" "arm64" "$version" "$out_dir"; then errors=1; fi
build_target "linux" "arm64" "$version" "$out_dir" if ! build_target "linux" "amd64" "$version" "$out_dir"; then errors=1; fi
build_target "windows" "amd64" "$version" "$out_dir" if ! build_target "linux" "arm64" "$version" "$out_dir"; then errors=1; fi
build_target "windows" "arm64" "$version" "$out_dir" 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" write_manifest "$version" "$out_dir" "$release_root"
local version_path="${release_root}/version.txt"
write_manifest "$version" "$out_dir" "$manifest_path" "$version_path"
log "Release artefacts written to ${out_dir}" log "Release artefacts written to ${out_dir}"
log "Manifest: ${manifest_path}" log "Manifests: ${release_root}/manifest-<platform>-<arch>.json"
log "Version: ${version_path}" log "Versions: ${release_root}/version-<platform>-<arch>.txt"
if [ $errors -ne 0 ]; then
err "One or more targets failed. Manifest and version were still generated."
exit 12
fi
} }
main "$@" main "$@"
+308
View File
@@ -0,0 +1,308 @@
<?php
$platform = isset($_GET['platform']) ? $_GET['platform'] : '';
$arch = isset($_GET['arch']) ? $_GET['arch'] : '';
$platform = preg_replace('/[^a-z0-9_-]/i', '', $platform);
$arch = preg_replace('/[^a-z0-9_-]/i', '', $arch);
$manifestPath = __DIR__ . '/manifest.json';
$versionPath = __DIR__ . '/version.txt';
$manifestFiles = glob(__DIR__ . '/manifest-*-*.json') ?: [];
if ($platform && $arch) {
$manifestPath = __DIR__ . '/manifest-' . $platform . '-' . $arch . '.json';
$versionPath = __DIR__ . '/version-' . $platform . '-' . $arch . '.txt';
}
$accept = isset($_SERVER['HTTP_ACCEPT']) ? $_SERVER['HTTP_ACCEPT'] : '';
$format = isset($_GET['format']) ? strtolower($_GET['format']) : '';
$wantsJson = $format === 'json' || strpos($accept, 'application/json') !== false;
$wantsVersion = $format === 'version';
$manifest = null;
if (is_readable($manifestPath)) {
$raw = file_get_contents($manifestPath);
$decoded = json_decode($raw, true);
if (json_last_error() === JSON_ERROR_NONE && is_array($decoded)) {
$manifest = $decoded;
}
}
$allManifests = [];
foreach ($manifestFiles as $path) {
$raw = file_get_contents($path);
$decoded = json_decode($raw, true);
if (json_last_error() === JSON_ERROR_NONE && is_array($decoded)) {
$allManifests[] = $decoded;
}
}
if ($wantsJson) {
header('Content-Type: application/json; charset=utf-8');
if ($manifest) {
echo json_encode($manifest, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
} else {
http_response_code(503);
echo json_encode(['error' => '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);
});
}
?>
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Nexus Terminal Downloads</title>
<style>
:root {
--bg: #111318;
--panel: #1b1e24;
--text: #e7e9ee;
--muted: #9aa2b2;
--accent: #4ecdc4;
}
body {
margin: 0;
font-family: ui-sans-serif, system-ui, -apple-system, Segoe UI, sans-serif;
background: var(--bg);
color: var(--text);
}
.wrap {
max-width: 960px;
margin: 0 auto;
padding: 32px 20px 60px;
}
h1 {
margin: 0 0 6px;
font-size: 28px;
}
.meta {
color: var(--muted);
font-size: 14px;
margin-bottom: 24px;
}
.card {
background: var(--panel);
border: 1px solid #2a2f3a;
border-radius: 12px;
padding: 20px;
}
table {
width: 100%;
border-collapse: collapse;
margin-top: 12px;
}
th,
td {
text-align: left;
padding: 10px 8px;
border-bottom: 1px solid #2a2f3a;
font-size: 14px;
}
th {
color: var(--muted);
font-weight: 600;
}
a {
color: var(--accent);
text-decoration: none;
}
a:hover {
text-decoration: underline;
}
.badge {
display: inline-block;
padding: 2px 8px;
border-radius: 999px;
background: rgba(78, 205, 196, 0.12);
color: var(--accent);
font-size: 12px;
}
.empty {
color: var(--muted);
}
</style>
</head>
<body>
<div class="wrap">
<h1>Nexus Terminal Downloads</h1>
<div class="meta">Latest version: <span class="badge"><?php echo h($latest); ?></span><?php if ($platformLabel) {
echo ' • ' . h($platformLabel);
} ?><?php if ($published) {
echo ' • Published ' . h($published);
} ?></div>
<div class="card">
<h2 style="margin:0 0 10px;font-size:18px;">Release files</h2>
<?php if (!$platform && !$arch) { ?>
<?php if (empty($rows)) { ?>
<div class="empty">No release files available.</div>
<?php } else { ?>
<table>
<thead>
<tr>
<th>Platform</th>
<th>CPU</th>
<th>Version</th>
<th>Size</th>
<th>Download</th>
</tr>
</thead>
<tbody>
<?php foreach ($rows as $row) { ?>
<?php
$downloadLink = '';
if ($row['platform'] && $row['arch']) {
$downloadLink = '?platform=' . urlencode($row['platform']) . '&arch=' . urlencode($row['arch']) . '&format=download';
}
?>
<tr>
<td><?php echo h($row['platform']); ?></td>
<td><?php echo h($row['arch']); ?></td>
<td><?php echo h($row['version']); ?></td>
<td><?php echo h(formatSize($row['size'])); ?></td>
<td>
<?php if ($downloadLink) { ?>
<a href="<?php echo h($downloadLink); ?>">Download</a>
<?php } else { ?>
<?php echo h($row['filename']); ?>
<?php } ?>
</td>
</tr>
<?php } ?>
</tbody>
</table>
<?php } ?>
<?php } else { ?>
<?php if (empty($files)) { ?>
<div class="empty">No release files available.</div>
<?php } else { ?>
<table>
<thead>
<tr>
<th>Platform</th>
<th>Arch</th>
<th>File</th>
<th>Size</th>
<th>SHA-256</th>
</tr>
</thead>
<tbody>
<?php foreach ($files as $file) {
$platform = isset($file['platform']) ? $file['platform'] : '';
$arch = isset($file['arch']) ? $file['arch'] : '';
$filename = isset($file['filename']) ? $file['filename'] : '';
$url = isset($file['url']) ? $file['url'] : '';
$size = isset($file['size']) ? $file['size'] : '';
$sha = isset($file['sha256']) ? $file['sha256'] : '';
?>
<tr>
<td><?php echo h($platform); ?></td>
<td><?php echo h($arch); ?></td>
<td>
<?php if ($url) { ?>
<a href="<?php echo h($url); ?>"><?php echo h($filename); ?></a>
<?php } else { ?>
<?php echo h($filename); ?>
<?php } ?>
</td>
<td><?php echo h(formatSize($size)); ?></td>
<td style="font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 12px; color: var(--muted);">
<?php echo h($sha); ?>
</td>
</tr>
<?php } ?>
</tbody>
</table>
<?php } ?>
<?php } ?>
</div>
</div>
</body>
</html>