mirror of
https://github.com/alphaXiv/OpenResearch.git
synced 2026-10-02 01:34:34 +08:00
* feat: scaffold downloadable macOS app (OpenResearch.app) Adds a native macOS `.app` bundle whose executable IS the `orx` binary. Launched from the bundle (double-click, no args) it enters GUI "app mode": an AppKit NSApplication + delegate run loop on the main thread — Dock icon and "OpenResearch" name from the bundle's Info.plist + .icns, and a Dock-icon click that reopens the dashboard in the browser — while the `orx up` server runs on background tokio worker threads. App mode is entered only when the executable lives in `.app/Contents/MacOS` AND argv is empty, so the bundled binary is still usable as a CLI. - src/commands/app.rs: bundle detection + AppKit delegate + background server - macos/Info.plist: bundle metadata (name, icon, identifier, version) - scripts/generate-icon.mjs: transparent-PNG rasterizer (from favicon.svg) - scripts/build-macos-app.sh: builds release orx, generates .icns, assembles OpenResearch.app into dist/ - objc2/objc2-app-kit gated under cfg(target_os = "macos") so musl Linux is untouched The bundle is unsigned; code-signing + notarization is a follow-up. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * feat: sign, notarize, and host OpenResearch.app via GitHub Releases Automates distribution of the macOS app as a signed, notarized DMG attached to each GitHub Release: - scripts/build-macos-app.sh: ORX_APP_UNIVERSAL=1 builds a universal (arm64 + x86_64) binary via lipo for distribution. - scripts/package-macos-app.sh: codesigns (hardened runtime, inside-out), notarizes + staples the .app (so it launches offline once dragged out of the DMG), packages a DMG, then notarizes + staples the DMG. Env-gated: runs unsigned locally, fully signed in CI. - .github/workflows/release-macos-app.yml: on the "Release" workflow completing (workflow_run — a GITHUB_TOKEN-created release: published event can't trigger workflows), a cheap ubuntu job gates on a real dispatched release + all signing secrets + the release existing, then a macOS job builds/signs/notarizes and uploads OpenResearch.dmg to that release. - macos/DISTRIBUTION.md: the one-time Apple setup + the six repo secrets. Inert until the signing secrets are configured, so it is safe to merge before the Apple Developer account exists. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * chore: gate macOS signing behind CODEOWNERS + a required-reviewer environment Hardens the release-signing pipeline for a public repo: - .github/CODEOWNERS: marks the release workflows and macOS signing scripts as owned so they can't change unreviewed (with branch protection's "Require review from Code Owners"). - release-macos-app.yml: the cert-using job now runs in the `release-signing` environment, so adding required reviewers to it pauses signing for human approval — the Developer ID cert is never used by an unreviewed change. - DISTRIBUTION.md: documents creating the environment + branch protection. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * chore: add @sox8502 to CODEOWNERS Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * security: environment-scoped signing secrets + SHA-pinned actions Hardens the release-signing pipeline: - The 6 signing secrets move from repo secrets to the `release-signing` environment, so only the reviewed, environment-gated macos-app job can read them — the cert secrets never exist in the cheap ubuntu gate. - The gate now keys on a non-secret repo variable MACOS_SIGNING_ENABLED instead of reading the secrets to detect configuration. - actions/checkout and actions/setup-node are pinned to commit SHAs so a moved tag can't inject code into the job that holds the Developer ID cert. - DISTRIBUTION.md updated: create the environment (required reviewers + main-only deployment branches), add environment secrets, set the variable. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * docs: trim DISTRIBUTION.md to repo-specific config Drop the generic Apple-portal walkthrough (enrolment, cert creation, export click-by-click) — Apple documents that. Keep only what's specific to this repo: the environment/secrets/variable, the local build+sign commands, and the download URL. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
84 lines
3.2 KiB
JavaScript
84 lines
3.2 KiB
JavaScript
// Rasterize the OpenResearch brand mark to a transparent-corner RGBA PNG.
|
|
//
|
|
// Source of truth for the shapes is ui/public/favicon.svg (a red squircle with
|
|
// a white right-triangle). QuickLook/`sips` flatten SVG transparency onto white,
|
|
// so we draw the geometry directly and emit straight-alpha RGBA — giving clean
|
|
// transparent corners for both the CLI Dock icon and the macOS .app iconset.
|
|
//
|
|
// Usage: node scripts/generate-icon.mjs <out.png> [size] (default size 1024)
|
|
// Requires Node >= 22.2 (uses the built-in zlib.crc32).
|
|
|
|
import zlib from 'node:zlib';
|
|
import { writeFileSync } from 'node:fs';
|
|
|
|
const out = process.argv[2];
|
|
if (!out) {
|
|
console.error('usage: node scripts/generate-icon.mjs <out.png> [size]');
|
|
process.exit(1);
|
|
}
|
|
const N = Number(process.argv[3] ?? 1024);
|
|
const SS = 4; // supersample factor per axis (anti-aliasing)
|
|
|
|
// squircle inset within the canvas (macOS-icon-style padding), scaled to N
|
|
const s = N / 1024;
|
|
const X = 88 * s, Y = 88 * s, W = 848 * s, H = 848 * s, R = 188 * s;
|
|
// brand triangle (favicon path, translate 88 + scale 8.48), right angle at B
|
|
const A = [218.38 * s, 230.31 * s], B = [218.38 * s, 805.62 * s], C = [793.69 * s, 805.62 * s];
|
|
const RED = [0x9a, 0x20, 0x36];
|
|
|
|
const clamp = (v, lo, hi) => (v < lo ? lo : v > hi ? hi : v);
|
|
function inSquircle(px, py) {
|
|
const cx = clamp(px, X + R, X + W - R), cy = clamp(py, Y + R, Y + H - R);
|
|
const dx = px - cx, dy = py - cy;
|
|
return dx * dx + dy * dy <= R * R;
|
|
}
|
|
function edge(p, a, b) {
|
|
return (p[0] - b[0]) * (a[1] - b[1]) - (a[0] - b[0]) * (p[1] - b[1]);
|
|
}
|
|
function inTriangle(px, py) {
|
|
const p = [px, py];
|
|
const d1 = edge(p, A, B), d2 = edge(p, B, C), d3 = edge(p, C, A);
|
|
const neg = d1 < 0 || d2 < 0 || d3 < 0, pos = d1 > 0 || d2 > 0 || d3 > 0;
|
|
return !(neg && pos);
|
|
}
|
|
|
|
const raw = Buffer.alloc(N * (N * 4 + 1)); // +1 filter byte per row
|
|
let o = 0;
|
|
for (let y = 0; y < N; y++) {
|
|
raw[o++] = 0; // PNG filter: none
|
|
for (let x = 0; x < N; x++) {
|
|
let r = 0, g = 0, b = 0, cov = 0;
|
|
for (let sy = 0; sy < SS; sy++) {
|
|
for (let sx = 0; sx < SS; sx++) {
|
|
const px = x + (sx + 0.5) / SS, py = y + (sy + 0.5) / SS;
|
|
if (inTriangle(px, py)) { r += 255; g += 255; b += 255; cov++; }
|
|
else if (inSquircle(px, py)) { r += RED[0]; g += RED[1]; b += RED[2]; cov++; }
|
|
}
|
|
}
|
|
const S = SS * SS;
|
|
raw[o++] = cov ? Math.round(r / cov) : 0;
|
|
raw[o++] = cov ? Math.round(g / cov) : 0;
|
|
raw[o++] = cov ? Math.round(b / cov) : 0;
|
|
raw[o++] = Math.round((255 * cov) / S);
|
|
}
|
|
}
|
|
|
|
function chunk(type, data) {
|
|
const len = Buffer.alloc(4); len.writeUInt32BE(data.length);
|
|
const td = Buffer.concat([Buffer.from(type, 'ascii'), data]);
|
|
const crc = Buffer.alloc(4); crc.writeUInt32BE(zlib.crc32(td) >>> 0);
|
|
return Buffer.concat([len, td, crc]);
|
|
}
|
|
const sig = Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]);
|
|
const ihdr = Buffer.alloc(13);
|
|
ihdr.writeUInt32BE(N, 0); ihdr.writeUInt32BE(N, 4);
|
|
ihdr[8] = 8; ihdr[9] = 6; // 8-bit, RGBA
|
|
const png = Buffer.concat([
|
|
sig,
|
|
chunk('IHDR', ihdr),
|
|
chunk('IDAT', zlib.deflateSync(raw, { level: 9 })),
|
|
chunk('IEND', Buffer.alloc(0)),
|
|
]);
|
|
writeFileSync(out, png);
|
|
console.log(`wrote ${out} (${N}x${N}, ${png.length} bytes)`);
|