From d80d5c23dc428b6a7ec828aac8276c9992dd37fd Mon Sep 17 00:00:00 2001 From: Jokin Date: Mon, 13 Jul 2026 23:55:39 +0200 Subject: [PATCH] Sublime-style Build-n versioning + About box Build n = commit count, baked at build time into a gitignored version.gen.ts (postinstall + all build scripts regenerate it). About lives in the macOS app menu and as a footer row in the options popover (the cross-platform path), showing build, commit, date and engines. Co-Authored-By: Claude Fable 5 --- .gitignore | 2 ++ package.json | 16 ++++++++---- scripts/gen-version.ts | 46 +++++++++++++++++++++++++++++++++++ src/bun/index.ts | 19 +++++++++++++++ src/bun/menu.ts | 3 +++ src/bun/rpc-handlers.ts | 7 ++++++ src/shared/branding.ts | 8 +++++- src/shared/rpc.ts | 2 ++ ui/src/app/tab-bar/tab-bar.ts | 15 ++++++++++++ 9 files changed, 112 insertions(+), 6 deletions(-) create mode 100644 scripts/gen-version.ts diff --git a/.gitignore b/.gitignore index 9295a73..a46f740 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,5 @@ artifacts/ .DS_Store ui/dist/ ui/.angular/ +src/shared/version.gen.ts +release/ diff --git a/package.json b/package.json index 51be5a7..0ed5567 100644 --- a/package.json +++ b/package.json @@ -3,13 +3,17 @@ "version": "0.1.0", "private": true, "type": "module", - "workspaces": ["ui"], + "workspaces": [ + "ui" + ], "scripts": { - "dev": "bun scripts/dev.ts", + "dev": "bun scripts/gen-version.ts && bun scripts/dev.ts", "dev:ui": "bun run --cwd ui start", "build:ui": "bun run --cwd ui build", - "build": "bun run build:ui && bunx electrobun build", - "start": "bun run build:ui && bunx electrobun dev" + "build": "bun scripts/gen-version.ts && bun run build:ui && bunx electrobun build", + "start": "bun scripts/gen-version.ts && bun run build:ui && bunx electrobun dev", + "postinstall": "bun scripts/gen-version.ts", + "release": "bun scripts/release.ts" }, "dependencies": { "electrobun": "1.18.1", @@ -22,5 +26,7 @@ "@types/ssh2": "^1.15.5", "typescript": "~5.9.3" }, - "trustedDependencies": ["@parcel/watcher"] + "trustedDependencies": [ + "@parcel/watcher" + ] } diff --git a/scripts/gen-version.ts b/scripts/gen-version.ts new file mode 100644 index 0000000..d1e18ce --- /dev/null +++ b/scripts/gen-version.ts @@ -0,0 +1,46 @@ +// Writes src/shared/version.gen.ts (gitignored) with the "Build n" version: +// n = commit count since the repo root, Sublime Text style. Regenerated by +// postinstall and by every dev/build/release script — never committed, since +// committing it would bump the very count it records. +import path from "node:path"; + +function git(args: string[]): string | null { + try { + const proc = Bun.spawnSync(["git", ...args], { + cwd: import.meta.dir, + stdout: "pipe", + stderr: "ignore", + }); + if (!proc.success) return null; + return proc.stdout.toString().trim() || null; + } catch { + return null; + } +} + +const number = Number(git(["rev-list", "--count", "HEAD"]) ?? 0); +const commit = git(["rev-parse", "--short", "HEAD"]) ?? "dev"; +const date = new Date().toISOString().slice(0, 10); + +let electrobun = "unknown"; +try { + const pkg = await Bun.file( + path.join(import.meta.dir, "..", "node_modules", "electrobun", "package.json"), + ).json(); + electrobun = pkg.version; +} catch { + // not installed yet (postinstall ordering) — fine +} + +const out = `// GENERATED by scripts/gen-version.ts — do not edit, do not commit. +export const BUILD = { + number: ${number}, + commit: ${JSON.stringify(commit)}, + date: ${JSON.stringify(date)}, + electrobun: ${JSON.stringify(electrobun)}, +} as const; +`; + +const target = path.join(import.meta.dir, "..", "src", "shared", "version.gen.ts"); +await Bun.write(target, out); +console.log(`[version] Build ${number} (${commit}) -> ${path.relative(process.cwd(), target)}`); diff --git a/src/bun/index.ts b/src/bun/index.ts index 03228c7..0396513 100644 --- a/src/bun/index.ts +++ b/src/bun/index.ts @@ -51,6 +51,7 @@ const events = { openEditor: (_ctx: EditorOpenContext) => {}, settingsChanged: (_settings: AppSettings) => {}, openConnectionManager: () => {}, + showAbout: () => {}, }; const rpc = createAppRPC({ registry, engine, store, settings, edits, contextMenu, events }); const url = await getMainViewUrl(); @@ -104,6 +105,20 @@ events.settingsChanged = (s) => { manager.broadcastSettings(s); }; events.openConnectionManager = () => manager.open(); + +function showAbout(): void { + const { build } = BRANDING; + void Utils.showMessageBox({ + type: "info", + title: BRANDING.appName, + message: BRANDING.appName, + detail: `${BRANDING.buildLabel} (${build.commit}) · ${build.date}\n\nElectrobun ${build.electrobun} · Bun ${Bun.version}`, + buttons: ["OK"], + defaultId: 0, + cancelId: 0, + }); +} +events.showAbout = showAbout; // clean editors close with the main window; dirty ones get to ask first, // and the modal manager has nothing to guard win.on("close", () => { @@ -204,6 +219,10 @@ ApplicationMenu.on("application-menu-clicked", (event) => { manager.open(); return; } + if (action === "about") { + showAbout(); + return; + } // the manager is modal to the main window — don't drive main behind it if (manager.isOpen()) return; rpc.send.menuAction({ action }); diff --git a/src/bun/menu.ts b/src/bun/menu.ts index a8cf349..ea0b6d4 100644 --- a/src/bun/menu.ts +++ b/src/bun/menu.ts @@ -1,4 +1,5 @@ import { ApplicationMenu, type ApplicationMenuItemConfig } from "electrobun/bun"; +import { BRANDING } from "../shared/branding"; import type { SavedConnection } from "../shared/types"; import type { ConnectionStore } from "./store/connections"; @@ -32,6 +33,8 @@ export async function applyApplicationMenu(store: ConnectionStore): Promise void; settingsChanged: (settings: AppSettings) => void; openConnectionManager: () => void; + showAbout: () => void; }; } @@ -294,6 +295,12 @@ export function createAppRPC({ events.openConnectionManager(); return {}; }), + + showAbout: () => + wrap(async () => { + events.showAbout(); + return {}; + }), }, }, }); diff --git a/src/shared/branding.ts b/src/shared/branding.ts index 87c8627..9be7137 100644 --- a/src/shared/branding.ts +++ b/src/shared/branding.ts @@ -1,12 +1,18 @@ // The single place to rebrand the app. Everything (window title, bundle name, // app-data directory, UI header, icon) reads from here. +import { BUILD } from "./version.gen"; + export const BRANDING = { appName: "miniSCP", identifier: "es.teknes.miniscp", // Previous app names: their app-data folders are migrated on boot // (see migrateLegacyDataDir in src/bun/store/paths.ts). legacyNames: ["BunSCP"], - version: "0.1.0", + // Sublime-style versioning: n = commit count (scripts/gen-version.ts). + // `version` stays semver-shaped for electrobun's config/updater. + version: `0.1.${BUILD.number}`, + buildLabel: `Build ${BUILD.number}`, + build: BUILD, // macOS build converts this .iconset folder into AppIcon.icns. // Regenerate from assets/icon.png with scripts/gen-iconset.sh. icon: "assets/icon.iconset", diff --git a/src/shared/rpc.ts b/src/shared/rpc.ts index c0f72f8..dbf65b1 100644 --- a/src/shared/rpc.ts +++ b/src/shared/rpc.ts @@ -137,6 +137,8 @@ export type AppRPC = { moveToTrash: { params: { path: string }; response: Result }; /** Native column-picker menu (right-click on the list headers). */ showColumnsMenu: { params: { visible: ColumnKey[] }; response: Result }; + /** Native About box (name, Build n, commit, engine versions). */ + showAbout: { params: Record; response: Result }; // editor windows /** Main window only: open (or focus) a native editor window. */ openEditor: { params: EditorOpenContext; response: Result }; diff --git a/ui/src/app/tab-bar/tab-bar.ts b/ui/src/app/tab-bar/tab-bar.ts index bb251ac..c9d43cd 100644 --- a/ui/src/app/tab-bar/tab-bar.ts +++ b/ui/src/app/tab-bar/tab-bar.ts @@ -1,5 +1,6 @@ import { Component, ElementRef, effect, inject, signal, viewChild } from '@angular/core'; import { NgIcon } from '@ng-icons/core'; +import { BRANDING } from '@shared/branding'; import type { AccentTone } from '@shared/types'; import { PanelsService } from '../panels.service'; import { IS_MAC_CHROME } from '../platform'; @@ -339,6 +340,14 @@ const CONCURRENCY_CHOICES = [2, 3, 4, 6, 8] as const; + + + } @@ -350,6 +359,8 @@ export class TabBar { private readonly rpc = inject(RpcService); readonly settingsOpen = signal(false); readonly isMacChrome = IS_MAC_CHROME; + readonly appName = BRANDING.appName; + readonly buildLabel = BRANDING.buildLabel; readonly themes = THEMES; readonly darkTones = DARK_TONES; readonly accents = ACCENTS; @@ -392,6 +403,10 @@ export class TabBar { void this.rpc.request.openConnectionManager({}); } + showAbout(): void { + void this.rpc.request.showAbout({}); + } + async pickEditorApp(): Promise { const res = await this.rpc.request.pickEditorApp({}); if (res.ok && res.path) this.settings.setEditorApp(res.path);