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 <noreply@anthropic.com>
This commit is contained in:
@@ -6,3 +6,5 @@ artifacts/
|
|||||||
.DS_Store
|
.DS_Store
|
||||||
ui/dist/
|
ui/dist/
|
||||||
ui/.angular/
|
ui/.angular/
|
||||||
|
src/shared/version.gen.ts
|
||||||
|
release/
|
||||||
|
|||||||
+11
-5
@@ -3,13 +3,17 @@
|
|||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"workspaces": ["ui"],
|
"workspaces": [
|
||||||
|
"ui"
|
||||||
|
],
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "bun scripts/dev.ts",
|
"dev": "bun scripts/gen-version.ts && bun scripts/dev.ts",
|
||||||
"dev:ui": "bun run --cwd ui start",
|
"dev:ui": "bun run --cwd ui start",
|
||||||
"build:ui": "bun run --cwd ui build",
|
"build:ui": "bun run --cwd ui build",
|
||||||
"build": "bun run build:ui && bunx electrobun build",
|
"build": "bun scripts/gen-version.ts && bun run build:ui && bunx electrobun build",
|
||||||
"start": "bun run build:ui && bunx electrobun dev"
|
"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": {
|
"dependencies": {
|
||||||
"electrobun": "1.18.1",
|
"electrobun": "1.18.1",
|
||||||
@@ -22,5 +26,7 @@
|
|||||||
"@types/ssh2": "^1.15.5",
|
"@types/ssh2": "^1.15.5",
|
||||||
"typescript": "~5.9.3"
|
"typescript": "~5.9.3"
|
||||||
},
|
},
|
||||||
"trustedDependencies": ["@parcel/watcher"]
|
"trustedDependencies": [
|
||||||
|
"@parcel/watcher"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)}`);
|
||||||
@@ -51,6 +51,7 @@ const events = {
|
|||||||
openEditor: (_ctx: EditorOpenContext) => {},
|
openEditor: (_ctx: EditorOpenContext) => {},
|
||||||
settingsChanged: (_settings: AppSettings) => {},
|
settingsChanged: (_settings: AppSettings) => {},
|
||||||
openConnectionManager: () => {},
|
openConnectionManager: () => {},
|
||||||
|
showAbout: () => {},
|
||||||
};
|
};
|
||||||
const rpc = createAppRPC({ registry, engine, store, settings, edits, contextMenu, events });
|
const rpc = createAppRPC({ registry, engine, store, settings, edits, contextMenu, events });
|
||||||
const url = await getMainViewUrl();
|
const url = await getMainViewUrl();
|
||||||
@@ -104,6 +105,20 @@ events.settingsChanged = (s) => {
|
|||||||
manager.broadcastSettings(s);
|
manager.broadcastSettings(s);
|
||||||
};
|
};
|
||||||
events.openConnectionManager = () => manager.open();
|
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,
|
// clean editors close with the main window; dirty ones get to ask first,
|
||||||
// and the modal manager has nothing to guard
|
// and the modal manager has nothing to guard
|
||||||
win.on("close", () => {
|
win.on("close", () => {
|
||||||
@@ -204,6 +219,10 @@ ApplicationMenu.on("application-menu-clicked", (event) => {
|
|||||||
manager.open();
|
manager.open();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (action === "about") {
|
||||||
|
showAbout();
|
||||||
|
return;
|
||||||
|
}
|
||||||
// the manager is modal to the main window — don't drive main behind it
|
// the manager is modal to the main window — don't drive main behind it
|
||||||
if (manager.isOpen()) return;
|
if (manager.isOpen()) return;
|
||||||
rpc.send.menuAction({ action });
|
rpc.send.menuAction({ action });
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { ApplicationMenu, type ApplicationMenuItemConfig } from "electrobun/bun";
|
import { ApplicationMenu, type ApplicationMenuItemConfig } from "electrobun/bun";
|
||||||
|
import { BRANDING } from "../shared/branding";
|
||||||
import type { SavedConnection } from "../shared/types";
|
import type { SavedConnection } from "../shared/types";
|
||||||
import type { ConnectionStore } from "./store/connections";
|
import type { ConnectionStore } from "./store/connections";
|
||||||
|
|
||||||
@@ -32,6 +33,8 @@ export async function applyApplicationMenu(store: ConnectionStore): Promise<void
|
|||||||
ApplicationMenu.setApplicationMenu([
|
ApplicationMenu.setApplicationMenu([
|
||||||
{
|
{
|
||||||
submenu: [
|
submenu: [
|
||||||
|
{ label: `About ${BRANDING.appName}`, action: "about" },
|
||||||
|
{ type: "separator" },
|
||||||
{ role: "hide" },
|
{ role: "hide" },
|
||||||
{ role: "hideOthers" },
|
{ role: "hideOthers" },
|
||||||
{ role: "showAll" },
|
{ role: "showAll" },
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ export interface AppDeps {
|
|||||||
openEditor: (ctx: EditorOpenContext) => void;
|
openEditor: (ctx: EditorOpenContext) => void;
|
||||||
settingsChanged: (settings: AppSettings) => void;
|
settingsChanged: (settings: AppSettings) => void;
|
||||||
openConnectionManager: () => void;
|
openConnectionManager: () => void;
|
||||||
|
showAbout: () => void;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -294,6 +295,12 @@ export function createAppRPC({
|
|||||||
events.openConnectionManager();
|
events.openConnectionManager();
|
||||||
return {};
|
return {};
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
showAbout: () =>
|
||||||
|
wrap(async () => {
|
||||||
|
events.showAbout();
|
||||||
|
return {};
|
||||||
|
}),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,12 +1,18 @@
|
|||||||
// The single place to rebrand the app. Everything (window title, bundle name,
|
// The single place to rebrand the app. Everything (window title, bundle name,
|
||||||
// app-data directory, UI header, icon) reads from here.
|
// app-data directory, UI header, icon) reads from here.
|
||||||
|
import { BUILD } from "./version.gen";
|
||||||
|
|
||||||
export const BRANDING = {
|
export const BRANDING = {
|
||||||
appName: "miniSCP",
|
appName: "miniSCP",
|
||||||
identifier: "es.teknes.miniscp",
|
identifier: "es.teknes.miniscp",
|
||||||
// Previous app names: their app-data folders are migrated on boot
|
// Previous app names: their app-data folders are migrated on boot
|
||||||
// (see migrateLegacyDataDir in src/bun/store/paths.ts).
|
// (see migrateLegacyDataDir in src/bun/store/paths.ts).
|
||||||
legacyNames: ["BunSCP"],
|
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.
|
// macOS build converts this .iconset folder into AppIcon.icns.
|
||||||
// Regenerate from assets/icon.png with scripts/gen-iconset.sh.
|
// Regenerate from assets/icon.png with scripts/gen-iconset.sh.
|
||||||
icon: "assets/icon.iconset",
|
icon: "assets/icon.iconset",
|
||||||
|
|||||||
@@ -137,6 +137,8 @@ export type AppRPC = {
|
|||||||
moveToTrash: { params: { path: string }; response: Result };
|
moveToTrash: { params: { path: string }; response: Result };
|
||||||
/** Native column-picker menu (right-click on the list headers). */
|
/** Native column-picker menu (right-click on the list headers). */
|
||||||
showColumnsMenu: { params: { visible: ColumnKey[] }; response: Result };
|
showColumnsMenu: { params: { visible: ColumnKey[] }; response: Result };
|
||||||
|
/** Native About box (name, Build n, commit, engine versions). */
|
||||||
|
showAbout: { params: Record<string, never>; response: Result };
|
||||||
// editor windows
|
// editor windows
|
||||||
/** Main window only: open (or focus) a native editor window. */
|
/** Main window only: open (or focus) a native editor window. */
|
||||||
openEditor: { params: EditorOpenContext; response: Result };
|
openEditor: { params: EditorOpenContext; response: Result };
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { Component, ElementRef, effect, inject, signal, viewChild } from '@angular/core';
|
import { Component, ElementRef, effect, inject, signal, viewChild } from '@angular/core';
|
||||||
import { NgIcon } from '@ng-icons/core';
|
import { NgIcon } from '@ng-icons/core';
|
||||||
|
import { BRANDING } from '@shared/branding';
|
||||||
import type { AccentTone } from '@shared/types';
|
import type { AccentTone } from '@shared/types';
|
||||||
import { PanelsService } from '../panels.service';
|
import { PanelsService } from '../panels.service';
|
||||||
import { IS_MAC_CHROME } from '../platform';
|
import { IS_MAC_CHROME } from '../platform';
|
||||||
@@ -339,6 +340,14 @@ const CONCURRENCY_CHOICES = [2, 3, 4, 6, 8] as const;
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- about footer -->
|
||||||
|
<button
|
||||||
|
(click)="showAbout()"
|
||||||
|
class="mt-3 w-full border-t border-border pt-2 text-center text-[11px] text-muted-foreground hover:text-foreground"
|
||||||
|
>
|
||||||
|
{{ appName }} · {{ buildLabel }}
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
@@ -350,6 +359,8 @@ export class TabBar {
|
|||||||
private readonly rpc = inject(RpcService);
|
private readonly rpc = inject(RpcService);
|
||||||
readonly settingsOpen = signal(false);
|
readonly settingsOpen = signal(false);
|
||||||
readonly isMacChrome = IS_MAC_CHROME;
|
readonly isMacChrome = IS_MAC_CHROME;
|
||||||
|
readonly appName = BRANDING.appName;
|
||||||
|
readonly buildLabel = BRANDING.buildLabel;
|
||||||
readonly themes = THEMES;
|
readonly themes = THEMES;
|
||||||
readonly darkTones = DARK_TONES;
|
readonly darkTones = DARK_TONES;
|
||||||
readonly accents = ACCENTS;
|
readonly accents = ACCENTS;
|
||||||
@@ -392,6 +403,10 @@ export class TabBar {
|
|||||||
void this.rpc.request.openConnectionManager({});
|
void this.rpc.request.openConnectionManager({});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
showAbout(): void {
|
||||||
|
void this.rpc.request.showAbout({});
|
||||||
|
}
|
||||||
|
|
||||||
async pickEditorApp(): Promise<void> {
|
async pickEditorApp(): Promise<void> {
|
||||||
const res = await this.rpc.request.pickEditorApp({});
|
const res = await this.rpc.request.pickEditorApp({});
|
||||||
if (res.ok && res.path) this.settings.setEditorApp(res.path);
|
if (res.ok && res.path) this.settings.setEditorApp(res.path);
|
||||||
|
|||||||
Reference in New Issue
Block a user