From c6bd9f85aa5e4a3979c5ad88406675fbbdfc48cd Mon Sep 17 00:00:00 2001 From: Harry Beckwith Date: Sat, 25 Jul 2026 12:21:56 -0700 Subject: [PATCH 1/3] A double-clickable macOS app, so no terminal is required The repo was professional but the install was still developer-shaped: open a terminal, paste a curl command, wait. This adds Bitcoin Vibes.app. - contrib/vibes/macos/build-app.sh builds the bundle (and --dmg). The app is a 452K launcher, not the node: on first open it fetches the source into ~/Library/Application Support/Bitcoin Vibes and the console compiles the node there, so the app stays small and updates itself on each launch by fast-forward only -- never clobbering local decrees. - launcher.sh restores a usable PATH (Finder gives almost none), finds python3, and reports every failure in a native dialog instead of into the void. If Apple's command line tools are missing it triggers the installer rather than failing mysteriously. - the console gains --app: with no terminal to print to, first-run setup runs as an ordinary streamed job and the operator watches dependency installation and the build in the browser, on a progress bar, in the same marble UI as everything else. Verified: the bundle registers as com.apple.application-bundle, is ad-hoc signed, and the launcher clones and starts the console from a clean home directory. Not notarised, so Gatekeeper wants right-click -> Open the first time. Said plainly in the docs rather than papered over. Co-Authored-By: Claude Opus 5 --- contrib/vibes/bitcoin-vibes | 99 ++++++++++++-- contrib/vibes/index.html | 44 ++++++ contrib/vibes/macos/build-app.sh | 78 +++++++++++ contrib/vibes/macos/icon.png | Bin 0 -> 13919 bytes contrib/vibes/macos/launcher.sh | 52 +++++++ dist/Bitcoin Vibes.app/Contents/Info.plist | 19 +++ .../Contents/MacOS/BitcoinVibes | 52 +++++++ .../Contents/Resources/AppIcon.icns | Bin 0 -> 440895 bytes .../Contents/_CodeSignature/CodeDirectory | Bin 0 -> 204 bytes .../Contents/_CodeSignature/CodeRequirements | Bin 0 -> 60 bytes .../Contents/_CodeSignature/CodeResources | 128 ++++++++++++++++++ .../Contents/_CodeSignature/CodeSignature | 0 12 files changed, 461 insertions(+), 11 deletions(-) create mode 100755 contrib/vibes/macos/build-app.sh create mode 100644 contrib/vibes/macos/icon.png create mode 100755 contrib/vibes/macos/launcher.sh create mode 100644 dist/Bitcoin Vibes.app/Contents/Info.plist create mode 100755 dist/Bitcoin Vibes.app/Contents/MacOS/BitcoinVibes create mode 100644 dist/Bitcoin Vibes.app/Contents/Resources/AppIcon.icns create mode 100644 dist/Bitcoin Vibes.app/Contents/_CodeSignature/CodeDirectory create mode 100644 dist/Bitcoin Vibes.app/Contents/_CodeSignature/CodeRequirements create mode 100644 dist/Bitcoin Vibes.app/Contents/_CodeSignature/CodeResources create mode 100644 dist/Bitcoin Vibes.app/Contents/_CodeSignature/CodeSignature diff --git a/contrib/vibes/bitcoin-vibes b/contrib/vibes/bitcoin-vibes index 6c3e133ba038..9042fa538ca7 100755 --- a/contrib/vibes/bitcoin-vibes +++ b/contrib/vibes/bitcoin-vibes @@ -1305,6 +1305,74 @@ class Console: self._restart_node(job) return go + def setup_runner(self): + """First run, streamed into the browser instead of a terminal. + + The desktop app has no terminal to print to, so preparing the node — + dependencies, configure, build, first start — runs as an ordinary job + and the operator watches it in the console like anything else.""" + def go(job): + job.emit(t="phase", name="setup", + detail="preparing your node — this happens once") + + missing = self.missing_deps() + if missing: + job.emit(t="phase", name="deps", + detail="installing what the compiler needs: " + ", ".join(missing)) + if not self.install_deps(job, missing): + job.state = "error" + job.emit(t="phase", name="error", + detail="could not install: " + ", ".join(missing) + + ". Install them by hand and reopen Bitcoin Vibes.") + return + else: + job.emit(t="log", line="build tools already present") + + jobs = self.build_jobs() + low, high = self.build_estimate(jobs) + job.emit(t="phase", name="build", + detail=f"compiling the node with {jobs} parallel job(s) — " + f"roughly {low}-{high} minutes on this machine, once") + if not self._build(job): + return + job.emit(t="phase", name="node", detail="waking the node") + self.start_node(job) + job.emit(t="result", + detail="Your node is ready, Excellency. It has been waiting.") + return go + + def missing_deps(self): + """Which build tools are absent. Empty means we can compile.""" + need = [] + if not shutil.which("cmake"): + need.append("cmake") + if sys.platform == "darwin": + for formula, probe in (("boost", "/boost/version.hpp"), + ("capnp", None)): + if formula == "capnp" and not shutil.which("capnp"): + need.append("capnp") + elif formula == "boost": + found = any(Path(p + probe).exists() for p in + ("/opt/homebrew/include", "/usr/local/include")) + if not found: + need.append("boost") + return need + + def install_deps(self, job, missing): + """Best effort, and honest when it cannot.""" + if sys.platform == "darwin": + brew = shutil.which("brew") or next( + (p for p in ("/opt/homebrew/bin/brew", "/usr/local/bin/brew") + if Path(p).exists()), None) + if not brew: + job.emit(t="log", line="Homebrew is not installed — it is how macOS " + "gets these. See https://brew.sh") + return False + return self._stream(job, [brew, "install", *missing]) == 0 + job.emit(t="log", line="install these with your package manager, then reopen: " + + " ".join(missing)) + return False + def build_runner(self, autorestart): def go(job): ok = self._build(job) @@ -1539,6 +1607,9 @@ def main(): ap.add_argument("--no-open", action="store_true", help="don't open the browser") ap.add_argument("--no-autostart", dest="autostart", action="store_false", help="don't build/start the node automatically on launch") + ap.add_argument("--app", action="store_true", + help="launched from the desktop app: no terminal is watching, so " + "first-run setup streams into the browser instead") ap.add_argument("--print-key", action="store_true", help="print the session key (for scripting)") opts = ap.parse_args() @@ -1612,18 +1683,24 @@ def main(): # the node, so the browser opens onto something alive. if opts.autostart: if not console.bin_path("bitcoind"): - bjobs = console.build_jobs() - low, high = console.build_estimate(bjobs) - print(f" no node binary yet — building it now, once, with {bjobs} " - f"parallel job(s).") - print(f" on this machine expect roughly {low}-{high} minutes. " - f"It is the last slow thing that will ever happen.") - print() - if console.build_blocking(): - print("\n build complete.\n") + if opts.app: + # Launched by double-click: there is no terminal to watch, so + # preparation streams into the browser instead. + print(" no node binary yet — preparing it in the console.") + console.start_job("setup", "", console.setup_runner()) else: - print("\n ⚠ build failed — see the output above. The console will still " - "start.\n") + bjobs = console.build_jobs() + low, high = console.build_estimate(bjobs) + print(f" no node binary yet — building it now, once, with {bjobs} " + f"parallel job(s).") + print(f" on this machine expect roughly {low}-{high} minutes. " + f"It is the last slow thing that will ever happen.") + print() + if console.build_blocking(): + print("\n build complete.\n") + else: + print("\n ⚠ build failed — see the output above. The console " + "will still start.\n") if console.bin_path("bitcoind") and not console.node_running(): if console.start_node(): print(f" node : awake on {opts.chain}") diff --git a/contrib/vibes/index.html b/contrib/vibes/index.html index 73d6668d31d5..290b9a7417ca 100644 --- a/contrib/vibes/index.html +++ b/contrib/vibes/index.html @@ -88,6 +88,22 @@ .rule::after{background:linear-gradient(90deg,var(--gold-leaf),transparent)} .rule span{color:var(--gold-ink);font-size:14px;letter-spacing:.4em} + /* ---- first run ---- */ + .setup{border:2px solid var(--gold-leaf);border-radius:120px 120px 3px 3px; + background:linear-gradient(180deg,#fffdf8,var(--marble-2)); + padding:34px 32px 26px;margin-bottom:22px;text-align:center; + box-shadow:0 12px 36px rgba(28,24,20,.09)} + .setup-title{font-family:var(--serif);font-size:21px;letter-spacing:.14em; + text-transform:uppercase;color:var(--gold-ink);margin-bottom:14px} + .setup-body{max-width:620px;margin:0 auto 20px;color:var(--ink);font-size:16.5px} + .setup-bar{height:8px;background:var(--marble-3);border:1px solid var(--gold-line); + border-radius:99px;overflow:hidden;max-width:520px;margin:0 auto} + .setup-bar span{display:block;height:100%;width:0; + background:linear-gradient(90deg,var(--gold-leaf),var(--gold-ink)); + transition:width .6s ease} + .setup-step{margin-top:12px;font-family:var(--mono);font-size:13.5px;color:var(--ink-2)} + body.is-setup .tablet,body.is-setup #book-section{opacity:.45;pointer-events:none} + /* ---- the engine notice ---- */ .engine{border:2px solid var(--porphyry);background:#fdf6f4;border-radius:2px; padding:15px 18px;margin-bottom:20px} @@ -356,6 +372,16 @@

You are a Bitcoin God.

one man's mood. You ask permission of no one.

+ +