@@ -58,3 +58,8 @@ vault.json
|
|||||||
*.pem
|
*.pem
|
||||||
*.key
|
*.key
|
||||||
!build/appicon.png
|
!build/appicon.png
|
||||||
|
|
||||||
|
|
||||||
|
# Builds
|
||||||
|
build/releases/v*
|
||||||
|
build/releases/manifest*
|
||||||
|
|||||||
+5
-1
@@ -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>
|
||||||
|
|||||||
+104
-93
@@ -7,170 +7,181 @@
|
|||||||
Main application grid
|
Main application grid
|
||||||
----------------------------------------------------------------------- */
|
----------------------------------------------------------------------- */
|
||||||
.app {
|
.app {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: var(--sidebar-width) 1fr;
|
grid-template-columns: var(--sidebar-width) 1fr;
|
||||||
grid-template-rows: var(--tab-bar-height) 1fr var(--status-bar-height);
|
grid-template-rows: var(--tab-bar-height) 1fr var(--status-bar-height);
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
background: var(--bg-base);
|
background: var(--bg-base);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Collapsed sidebar variant */
|
/* Collapsed sidebar variant */
|
||||||
.app.sidebar-collapsed {
|
.app.sidebar-collapsed {
|
||||||
grid-template-columns: var(--sidebar-width-collapsed) 1fr;
|
grid-template-columns: var(--sidebar-width-collapsed) 1fr;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* -----------------------------------------------------------------------
|
/* -----------------------------------------------------------------------
|
||||||
Sidebar
|
Sidebar
|
||||||
----------------------------------------------------------------------- */
|
----------------------------------------------------------------------- */
|
||||||
.sidebar {
|
.sidebar {
|
||||||
grid-row: 1 / -1;
|
grid-row: 1 / -1;
|
||||||
grid-column: 1;
|
grid-column: 1;
|
||||||
background: var(--bg-surface);
|
background: var(--bg-surface);
|
||||||
border-right: 1px solid var(--border-subtle);
|
border-right: 1px solid var(--border-subtle);
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
z-index: var(--z-sticky);
|
z-index: var(--z-sticky);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* -----------------------------------------------------------------------
|
/* -----------------------------------------------------------------------
|
||||||
Tab bar
|
Tab bar
|
||||||
----------------------------------------------------------------------- */
|
----------------------------------------------------------------------- */
|
||||||
.tab-bar {
|
.tab-bar {
|
||||||
grid-column: 2;
|
grid-column: 2;
|
||||||
grid-row: 1;
|
grid-row: 1;
|
||||||
background: var(--bg-surface);
|
background: var(--bg-surface);
|
||||||
border-bottom: 1px solid var(--border-subtle);
|
border-bottom: 1px solid var(--border-subtle);
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
min-height: var(--tab-bar-height);
|
min-height: var(--tab-bar-height);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tab-list {
|
.tab-list {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
overflow-y: hidden;
|
overflow-y: hidden;
|
||||||
scrollbar-width: none; /* Firefox */
|
scrollbar-width: none; /* Firefox */
|
||||||
}
|
}
|
||||||
.tab-list::-webkit-scrollbar {
|
.tab-list::-webkit-scrollbar {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* -----------------------------------------------------------------------
|
/* -----------------------------------------------------------------------
|
||||||
Workspace
|
Workspace
|
||||||
----------------------------------------------------------------------- */
|
----------------------------------------------------------------------- */
|
||||||
.workspace {
|
.workspace {
|
||||||
grid-column: 2;
|
grid-column: 2;
|
||||||
grid-row: 2;
|
grid-row: 2;
|
||||||
position: relative;
|
position: relative;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
background: var(--bg-base);
|
background: var(--bg-base);
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Panel container — fills workspace, holds session panels */
|
/* Panel container — fills workspace, holds session panels */
|
||||||
.panel-container {
|
.panel-container {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
position: relative;
|
position: relative;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Individual session panel — one per tab, fills the panel container */
|
/* Individual session panel — one per tab, fills the panel container */
|
||||||
.session-panel {
|
.session-panel {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
inset: 0;
|
inset: 0;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Terminal container — fills workspace */
|
/* Terminal container — fills workspace */
|
||||||
.terminal-container {
|
.terminal-container {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
inset: 0;
|
inset: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* -----------------------------------------------------------------------
|
/* -----------------------------------------------------------------------
|
||||||
Status bar
|
Status bar
|
||||||
----------------------------------------------------------------------- */
|
----------------------------------------------------------------------- */
|
||||||
.status-bar {
|
.status-bar {
|
||||||
grid-column: 2;
|
grid-column: 2;
|
||||||
grid-row: 3;
|
grid-row: 3;
|
||||||
background: var(--bg-surface);
|
background: var(--bg-surface);
|
||||||
border-top: 1px solid var(--border-subtle);
|
border-top: 1px solid var(--border-subtle);
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 0 var(--space-3);
|
padding: 0 var(--space-3);
|
||||||
font-size: var(--text-xs);
|
font-size: var(--text-xs);
|
||||||
color: var(--text-tertiary);
|
color: var(--text-tertiary);
|
||||||
gap: var(--space-4);
|
gap: var(--space-4);
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.status-bar span {
|
.status-bar span {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: var(--space-1);
|
gap: var(--space-1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.status-spacer {
|
.status-spacer {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.status-update {
|
.status-update {
|
||||||
border: 1px solid var(--accent);
|
border: 1px solid var(--accent);
|
||||||
background: rgba(78, 205, 196, 0.12);
|
background: rgba(78, 205, 196, 0.12);
|
||||||
color: var(--accent);
|
color: var(--accent);
|
||||||
font-size: var(--text-xs);
|
font-size: var(--text-xs);
|
||||||
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 {
|
||||||
background: rgba(78, 205, 196, 0.2);
|
background: rgba(78, 205, 196, 0.2);
|
||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
border-color: var(--accent-strong, var(--accent));
|
border-color: var(--accent-strong, var(--accent));
|
||||||
}
|
}
|
||||||
|
|
||||||
.status-update.is-hidden {
|
.status-update.is-hidden {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Status bar indicator dots */
|
/* Status bar indicator dots */
|
||||||
.status-bar .status-dot {
|
.status-bar .status-dot {
|
||||||
width: 6px;
|
width: 6px;
|
||||||
height: 6px;
|
height: 6px;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
display: inline-block;
|
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 (between sidebar and workspace)
|
||||||
----------------------------------------------------------------------- */
|
----------------------------------------------------------------------- */
|
||||||
.resize-handle {
|
.resize-handle {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
right: -3px;
|
right: -3px;
|
||||||
width: 6px;
|
width: 6px;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
cursor: col-resize;
|
cursor: col-resize;
|
||||||
z-index: var(--z-sticky);
|
z-index: var(--z-sticky);
|
||||||
}
|
}
|
||||||
.resize-handle:hover,
|
.resize-handle:hover,
|
||||||
.resize-handle.is-dragging {
|
.resize-handle.is-dragging {
|
||||||
background: var(--accent-muted);
|
background: var(--accent-muted);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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");
|
||||||
|
|||||||
@@ -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,27 +518,9 @@ 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
@@ -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)
|
||||||
|
|||||||
@@ -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"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
+131
-50
@@ -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,49 +210,44 @@ 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")"
|
||||||
|
|
||||||
echo "$version" > "$version_path"
|
while IFS='|' read -r os arch filename; do
|
||||||
|
[ -z "$os" ] && continue
|
||||||
{
|
local manifest_path="${release_root}/manifest-${os}-${arch}.json"
|
||||||
echo "{"
|
local version_path="${release_root}/version-${os}-${arch}.txt"
|
||||||
echo " \"latestVersion\": \"${version}\","
|
local file_path="${out_dir}/${filename}"
|
||||||
echo " \"publishedAt\": \"${published_at}\","
|
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
|
if [ -n "$base_url" ]; then
|
||||||
echo " \"releaseURL\": \"${base_url}/\","
|
url="${base_url}/${filename}"
|
||||||
else
|
|
||||||
echo " \"releaseURL\": \"\","
|
|
||||||
fi
|
fi
|
||||||
echo " \"files\": ["
|
|
||||||
|
|
||||||
local first=1
|
echo "$version" > "$version_path"
|
||||||
while IFS='|' read -r os arch filename; do
|
|
||||||
[ -z "$os" ] && continue
|
{
|
||||||
local file_path="${out_dir}/${filename}"
|
echo "{"
|
||||||
local sha
|
echo " \"latestVersion\": \"${version}\","
|
||||||
sha="$(shasum -a 256 "$file_path" | awk '{print $1}')"
|
echo " \"publishedAt\": \"${published_at}\","
|
||||||
local size
|
|
||||||
size="$(wc -c < "$file_path" | tr -d ' ')"
|
|
||||||
local url=""
|
|
||||||
if [ -n "$base_url" ]; then
|
if [ -n "$base_url" ]; then
|
||||||
url="${base_url}/${filename}"
|
echo " \"releaseURL\": \"${base_url}/\","
|
||||||
|
else
|
||||||
|
echo " \"releaseURL\": \"\","
|
||||||
fi
|
fi
|
||||||
|
echo " \"files\": ["
|
||||||
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 "$@"
|
||||||
|
|||||||
@@ -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>
|
||||||
Reference in New Issue
Block a user