diff --git a/.gitignore b/.gitignore
index 616cf1689a94..d999d0c7390b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -33,3 +33,6 @@ target/
# Python bytecode from the vibes console
__pycache__/
+
+# macOS app build output
+/dist/
diff --git a/README.md b/README.md
index ef17e03a31ab..852160eda48a 100644
--- a/README.md
+++ b/README.md
@@ -22,6 +22,14 @@ contact with the compiler.
Install
-------
+**On a Mac, and you would rather not see a terminal?**
+[Download Bitcoin Vibes.app](https://github.com/hotpixelgroup/bitcoin/releases/latest),
+drag it to Applications, open it. It does the rest and shows you what it is doing
+in your browser. (It is not notarised yet, so the first time you will need to
+right-click it and choose **Open** — macOS asks that of every small developer.)
+
+**Everyone else, one line:**
+
```bash
curl -fsSL https://raw.githubusercontent.com/hotpixelgroup/bitcoin/master/contrib/vibes/install.sh | sh
```
diff --git a/contrib/vibes/bitcoin-vibes b/contrib/vibes/bitcoin-vibes
index 6c3e133ba038..5eca9d64f782 100755
--- a/contrib/vibes/bitcoin-vibes
+++ b/contrib/vibes/bitcoin-vibes
@@ -1246,9 +1246,37 @@ class Console:
self.node_proc = subprocess.Popen(
[str(bitcoind), *self.chain_args(), "-server=1"],
stdout=self.node_log, stderr=self.node_log, start_new_session=True)
+
+ # Spawning is not starting. bitcoind exits early on a port already in
+ # use, a locked datadir or a full disk, and reporting success then
+ # leaves the operator staring at a node that is not there.
+ deadline = time.time() + 45
+ while time.time() < deadline:
+ if self.cli_json("uptime") is not None:
+ if job:
+ job.emit(t="phase", name="node",
+ detail=f"bitcoind started (pid {self.node_proc.pid})")
+ return True
+ if self.node_proc.poll() is not None:
+ break # it gave up before we did
+ time.sleep(0.5)
+
+ why = self._last_node_error() or "see .vibes/logs/bitcoind.log"
if job:
- job.emit(t="phase", name="node", detail=f"bitcoind started (pid {self.node_proc.pid})")
- return True
+ job.emit(t="log", line=f"bitcoind did not come up — {why}")
+ return False
+
+ def _last_node_error(self):
+ """The most recent complaint bitcoind made, for reporting upward."""
+ try:
+ lines = (self.logs_dir / "bitcoind.log").read_text(
+ errors="replace").splitlines()
+ except OSError:
+ return None
+ for line in reversed(lines[-60:]):
+ if "Error:" in line or "[error]" in line:
+ return line.split("Error:")[-1].split("[error]")[-1].strip()
+ return None
def stop_node(self, job=None, timeout=30):
if not self.node_running():
@@ -1305,6 +1333,80 @@ 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")
+ if not self.start_node(job):
+ job.state = "error"
+ job.emit(t="phase", name="error",
+ detail="The node was built, but it would not start. The usual "
+ "cause is another Bitcoin node already using this "
+ "network's ports. Full reason in the line above.")
+ return
+ 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 +1641,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 +1717,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.
✦
+
+
Preparing your node
+
This happens once. Bitcoin Vibes is
+ compiling a real Bitcoin node from source — the same source Bitcoin Core
+ ships, plus a text box. You can watch it below, or come back in a few
+ minutes; nothing is required of you.
+
+
starting…
+
+
Run this once, then return:
@@ -509,6 +535,16 @@
The Book of Decrees
function renderEvent(ev){
switch (ev.t) {
case "phase": {
+ // first-run setup drives the banner as well as the log
+ const setupSteps = {setup:[5,"getting ready"], deps:[15,"installing build tools"],
+ build:[35,"compiling the node — the slow part"],
+ node:[92,"waking the node"], done:[100,"ready"],
+ error:[100,"something went wrong"]};
+ if (setupSteps[ev.name] && document.body.classList.contains("is-setup")) {
+ const [pct, label] = setupSteps[ev.name];
+ $("setup-fill").style.width = pct + "%";
+ $("setup-step").textContent = ev.detail || label;
+ }
const names = {agent:"⛩ your word descends upon the codebase, which had been waiting",
commit:"✒ inscribing your decree where it can never be argued with",
build:"⚒ reality recompiles itself to your specification",
@@ -754,6 +790,14 @@ The Book of Decrees
$("nodebtn").dataset.action = n.running ? "stop" : "start";
$("minebtn").style.display = (n.running && s.chain === "regtest") ? "inline-block" : "none";
+ // first run: no node binary yet, so the console explains itself instead of
+ // presenting a decree box for a node that does not exist
+ const setting_up = !s.binary || (s.job && s.job.kind === "setup" && s.job.state === "running");
+ document.body.classList.toggle("is-setup", !!setting_up);
+ $("setup").style.display = setting_up ? "block" : "none";
+ if (!setting_up && $("setup-fill").style.width && $("setup-fill").style.width !== "100%")
+ $("setup-fill").style.width = "100%";
+
const eng = s.engine_ready || {ok:true};
$("engine").style.display = eng.ok ? "none" : "block";
if (!eng.ok){
diff --git a/contrib/vibes/macos/build-app.sh b/contrib/vibes/macos/build-app.sh
new file mode 100755
index 000000000000..060bca924ccf
--- /dev/null
+++ b/contrib/vibes/macos/build-app.sh
@@ -0,0 +1,78 @@
+#!/bin/sh
+# Build "Bitcoin Vibes.app" — a double-clickable macOS launcher.
+#
+# sh contrib/vibes/macos/build-app.sh -> dist/Bitcoin Vibes.app
+# sh contrib/vibes/macos/build-app.sh --dmg -> also dist/BitcoinVibes.dmg
+#
+# The bundle is a 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.
+set -eu
+
+cd "$(dirname "$0")/../../.."
+ROOT="$PWD"
+OUT="$ROOT/dist"
+APP="$OUT/Bitcoin Vibes.app"
+VERSION=$(sed -n 's/^set(CLIENT_VERSION_MAJOR \([0-9]*\))/\1/p' CMakeLists.txt).$(sed -n 's/^set(CLIENT_VERSION_MINOR \([0-9]*\))/\1/p' CMakeLists.txt)
+
+echo "→ building Bitcoin Vibes.app $VERSION"
+rm -rf "$APP"
+mkdir -p "$APP/Contents/MacOS" "$APP/Contents/Resources"
+
+cat > "$APP/Contents/Info.plist" <
+
+
+
+ CFBundleName Bitcoin Vibes
+ CFBundleDisplayName Bitcoin Vibes
+ CFBundleIdentifier group.hotpixel.bitcoinvibes
+ CFBundleVersion $VERSION
+ CFBundleShortVersionString$VERSION
+ CFBundleExecutable BitcoinVibes
+ CFBundleIconFile AppIcon
+ CFBundlePackageType APPL
+ LSMinimumSystemVersion 12.0
+ LSApplicationCategoryType public.app-category.finance
+ NSHumanReadableCopyright MIT. A fork of Bitcoin Core, maintained by Hot Pixel Group.
+
+ LSUIElement
+
+
+PLIST
+
+install -m 755 contrib/vibes/macos/launcher.sh "$APP/Contents/MacOS/BitcoinVibes"
+
+# Icon: reuse the site's mark so the dock, the tab and the social card agree.
+ICON_SRC="$ROOT/contrib/vibes/macos/icon.png"
+if [ -f "$ICON_SRC" ]; then
+ ICONSET=$(mktemp -d)/AppIcon.iconset
+ mkdir -p "$ICONSET"
+ for s in 16 32 128 256 512; do
+ sips -z $s $s "$ICON_SRC" --out "$ICONSET/icon_${s}x${s}.png" >/dev/null
+ d=$((s * 2))
+ sips -z $d $d "$ICON_SRC" --out "$ICONSET/icon_${s}x${s}@2x.png" >/dev/null
+ done
+ iconutil -c icns "$ICONSET" -o "$APP/Contents/Resources/AppIcon.icns"
+ echo " icon built"
+else
+ echo " note: no contrib/vibes/macos/icon.png — the app will use a blank icon"
+fi
+
+# Ad-hoc signature. Not notarised: Gatekeeper will still want a right-click →
+# Open the first time, which the README says plainly rather than pretending.
+codesign --force --deep --sign - "$APP" >/dev/null 2>&1 \
+ && echo " ad-hoc signed" || echo " note: could not sign (the app still runs)"
+
+echo "→ $APP"
+
+if [ "${1:-}" = "--dmg" ]; then
+ DMG="$OUT/BitcoinVibes.dmg"
+ rm -f "$DMG"
+ STAGE=$(mktemp -d)
+ cp -R "$APP" "$STAGE/"
+ ln -s /Applications "$STAGE/Applications"
+ hdiutil create -volname "Bitcoin Vibes" -srcfolder "$STAGE" -ov -format UDZO "$DMG" >/dev/null
+ rm -rf "$STAGE"
+ echo "→ $DMG"
+fi
diff --git a/contrib/vibes/macos/icon.png b/contrib/vibes/macos/icon.png
new file mode 100644
index 000000000000..4edae773f841
Binary files /dev/null and b/contrib/vibes/macos/icon.png differ
diff --git a/contrib/vibes/macos/launcher.sh b/contrib/vibes/macos/launcher.sh
new file mode 100755
index 000000000000..8ea3c930d584
--- /dev/null
+++ b/contrib/vibes/macos/launcher.sh
@@ -0,0 +1,52 @@
+#!/bin/sh
+# The executable inside Bitcoin Vibes.app.
+#
+# Double-clicked from Finder there is no terminal, no PATH worth trusting and
+# nowhere to print an error. So this script does the smallest possible amount of
+# work — find a source tree, find python, hand off — and reports anything that
+# goes wrong in a native dialog rather than into the void.
+set -u
+
+APP_SUPPORT="${VIBES_HOME:-$HOME/Library/Application Support/Bitcoin Vibes}"
+SRC="$APP_SUPPORT/source"
+LOG="$APP_SUPPORT/launch.log"
+REPO="${VIBES_REPO:-https://github.com/hotpixelgroup/bitcoin.git}"
+BRANCH="${VIBES_BRANCH:-master}"
+
+mkdir -p "$APP_SUPPORT"
+exec 2>>"$LOG"
+echo "--- launch $(date)" >>"$LOG"
+
+# Finder gives us a minimal PATH; put the usual homes for Homebrew and
+# user-installed tools back so git, cmake and claude can be found.
+PATH="/opt/homebrew/bin:/usr/local/bin:$HOME/.local/bin:/usr/bin:/bin:/usr/sbin:/sbin"
+export PATH
+
+say() { osascript -e "display dialog \"$1\" with title \"Bitcoin Vibes\" buttons {\"OK\"} default button 1 with icon note" >/dev/null 2>&1; }
+die() { osascript -e "display dialog \"$1\" with title \"Bitcoin Vibes\" buttons {\"OK\"} default button 1 with icon stop" >/dev/null 2>&1; exit 1; }
+
+PY=$(command -v python3 || true)
+[ -n "$PY" ] || die "Bitcoin Vibes needs Python 3, which normally ships with macOS.\n\nOpening Terminal and running: xcode-select --install\n\nwill install it along with the developer tools."
+
+# First run: fetch the source. Everything else assumes it is there.
+if [ ! -d "$SRC/.git" ]; then
+ if ! command -v git >/dev/null 2>&1; then
+ osascript -e 'display dialog "Bitcoin Vibes needs Apple'"'"'s command line tools (they include git and the compiler).\n\nClick OK and macOS will offer to install them. Reopen Bitcoin Vibes once it finishes." with title "Bitcoin Vibes" buttons {"OK"} default button 1 with icon note' >/dev/null 2>&1
+ xcode-select --install >/dev/null 2>&1
+ exit 0
+ fi
+ say "Bitcoin Vibes is fetching the node's source code.\n\nThis takes a minute. The console will open by itself when it is ready."
+ git clone --depth 1 --branch "$BRANCH" "$REPO" "$SRC" >>"$LOG" 2>&1 \
+ || die "Could not download Bitcoin Vibes.\n\nCheck your internet connection and try again.\n\nDetails: $LOG"
+else
+ # Keep up to date, but never clobber decrees the operator made locally.
+ git -C "$SRC" fetch --depth 1 origin "$BRANCH" >>"$LOG" 2>&1 || true
+ git -C "$SRC" merge --ff-only FETCH_HEAD >>"$LOG" 2>&1 || true
+fi
+
+CONSOLE="$SRC/contrib/vibes/bitcoin-vibes"
+[ -f "$CONSOLE" ] || die "The Bitcoin Vibes source looks incomplete.\n\nDelete this folder and reopen the app:\n$SRC"
+
+# Hand over. --app makes first-run setup stream into the browser, because from
+# here there is no terminal for it to print to.
+exec "$PY" -u "$CONSOLE" --app "$@" >>"$LOG" 2>&1
diff --git a/dist/Bitcoin Vibes.app/Contents/Info.plist b/dist/Bitcoin Vibes.app/Contents/Info.plist
new file mode 100644
index 000000000000..70dfdbb66db7
--- /dev/null
+++ b/dist/Bitcoin Vibes.app/Contents/Info.plist
@@ -0,0 +1,19 @@
+
+
+
+
+ CFBundleName Bitcoin Vibes
+ CFBundleDisplayName Bitcoin Vibes
+ CFBundleIdentifier group.hotpixel.bitcoinvibes
+ CFBundleVersion 31.99
+ CFBundleShortVersionString31.99
+ CFBundleExecutable BitcoinVibes
+ CFBundleIconFile AppIcon
+ CFBundlePackageType APPL
+ LSMinimumSystemVersion 12.0
+ LSApplicationCategoryType public.app-category.finance
+ NSHumanReadableCopyright MIT. A fork of Bitcoin Core, maintained by Hot Pixel Group.
+
+ LSUIElement
+
+
diff --git a/dist/Bitcoin Vibes.app/Contents/MacOS/BitcoinVibes b/dist/Bitcoin Vibes.app/Contents/MacOS/BitcoinVibes
new file mode 100755
index 000000000000..8ea3c930d584
--- /dev/null
+++ b/dist/Bitcoin Vibes.app/Contents/MacOS/BitcoinVibes
@@ -0,0 +1,52 @@
+#!/bin/sh
+# The executable inside Bitcoin Vibes.app.
+#
+# Double-clicked from Finder there is no terminal, no PATH worth trusting and
+# nowhere to print an error. So this script does the smallest possible amount of
+# work — find a source tree, find python, hand off — and reports anything that
+# goes wrong in a native dialog rather than into the void.
+set -u
+
+APP_SUPPORT="${VIBES_HOME:-$HOME/Library/Application Support/Bitcoin Vibes}"
+SRC="$APP_SUPPORT/source"
+LOG="$APP_SUPPORT/launch.log"
+REPO="${VIBES_REPO:-https://github.com/hotpixelgroup/bitcoin.git}"
+BRANCH="${VIBES_BRANCH:-master}"
+
+mkdir -p "$APP_SUPPORT"
+exec 2>>"$LOG"
+echo "--- launch $(date)" >>"$LOG"
+
+# Finder gives us a minimal PATH; put the usual homes for Homebrew and
+# user-installed tools back so git, cmake and claude can be found.
+PATH="/opt/homebrew/bin:/usr/local/bin:$HOME/.local/bin:/usr/bin:/bin:/usr/sbin:/sbin"
+export PATH
+
+say() { osascript -e "display dialog \"$1\" with title \"Bitcoin Vibes\" buttons {\"OK\"} default button 1 with icon note" >/dev/null 2>&1; }
+die() { osascript -e "display dialog \"$1\" with title \"Bitcoin Vibes\" buttons {\"OK\"} default button 1 with icon stop" >/dev/null 2>&1; exit 1; }
+
+PY=$(command -v python3 || true)
+[ -n "$PY" ] || die "Bitcoin Vibes needs Python 3, which normally ships with macOS.\n\nOpening Terminal and running: xcode-select --install\n\nwill install it along with the developer tools."
+
+# First run: fetch the source. Everything else assumes it is there.
+if [ ! -d "$SRC/.git" ]; then
+ if ! command -v git >/dev/null 2>&1; then
+ osascript -e 'display dialog "Bitcoin Vibes needs Apple'"'"'s command line tools (they include git and the compiler).\n\nClick OK and macOS will offer to install them. Reopen Bitcoin Vibes once it finishes." with title "Bitcoin Vibes" buttons {"OK"} default button 1 with icon note' >/dev/null 2>&1
+ xcode-select --install >/dev/null 2>&1
+ exit 0
+ fi
+ say "Bitcoin Vibes is fetching the node's source code.\n\nThis takes a minute. The console will open by itself when it is ready."
+ git clone --depth 1 --branch "$BRANCH" "$REPO" "$SRC" >>"$LOG" 2>&1 \
+ || die "Could not download Bitcoin Vibes.\n\nCheck your internet connection and try again.\n\nDetails: $LOG"
+else
+ # Keep up to date, but never clobber decrees the operator made locally.
+ git -C "$SRC" fetch --depth 1 origin "$BRANCH" >>"$LOG" 2>&1 || true
+ git -C "$SRC" merge --ff-only FETCH_HEAD >>"$LOG" 2>&1 || true
+fi
+
+CONSOLE="$SRC/contrib/vibes/bitcoin-vibes"
+[ -f "$CONSOLE" ] || die "The Bitcoin Vibes source looks incomplete.\n\nDelete this folder and reopen the app:\n$SRC"
+
+# Hand over. --app makes first-run setup stream into the browser, because from
+# here there is no terminal for it to print to.
+exec "$PY" -u "$CONSOLE" --app "$@" >>"$LOG" 2>&1
diff --git a/dist/Bitcoin Vibes.app/Contents/Resources/AppIcon.icns b/dist/Bitcoin Vibes.app/Contents/Resources/AppIcon.icns
new file mode 100644
index 000000000000..abe5ca7e68fb
Binary files /dev/null and b/dist/Bitcoin Vibes.app/Contents/Resources/AppIcon.icns differ
diff --git a/dist/Bitcoin Vibes.app/Contents/_CodeSignature/CodeDirectory b/dist/Bitcoin Vibes.app/Contents/_CodeSignature/CodeDirectory
new file mode 100644
index 000000000000..cc0e048d9be0
Binary files /dev/null and b/dist/Bitcoin Vibes.app/Contents/_CodeSignature/CodeDirectory differ
diff --git a/dist/Bitcoin Vibes.app/Contents/_CodeSignature/CodeRequirements b/dist/Bitcoin Vibes.app/Contents/_CodeSignature/CodeRequirements
new file mode 100644
index 000000000000..29fef9d63f3e
Binary files /dev/null and b/dist/Bitcoin Vibes.app/Contents/_CodeSignature/CodeRequirements differ
diff --git a/dist/Bitcoin Vibes.app/Contents/_CodeSignature/CodeResources b/dist/Bitcoin Vibes.app/Contents/_CodeSignature/CodeResources
new file mode 100644
index 000000000000..fa1675ca8e6b
--- /dev/null
+++ b/dist/Bitcoin Vibes.app/Contents/_CodeSignature/CodeResources
@@ -0,0 +1,128 @@
+
+
+
+
+ files
+
+ Resources/AppIcon.icns
+
+ 8Xo2kTHSuyXyReJFNUK29imGrEs=
+
+
+ files2
+
+ Resources/AppIcon.icns
+
+ hash2
+
+ 627Fi4bzEa4ctmdrNp0XFjXZCJsCWRbQIDeGcR2C0zc=
+
+
+
+ rules
+
+ ^Resources/
+
+ ^Resources/.*\.lproj/
+
+ optional
+
+ weight
+ 1000
+
+ ^Resources/.*\.lproj/locversion.plist$
+
+ omit
+
+ weight
+ 1100
+
+ ^Resources/Base\.lproj/
+
+ weight
+ 1010
+
+ ^version.plist$
+
+
+ rules2
+
+ .*\.dSYM($|/)
+
+ weight
+ 11
+
+ ^(.*/)?\.DS_Store$
+
+ omit
+
+ weight
+ 2000
+
+ ^(Frameworks|SharedFrameworks|PlugIns|Plug-ins|XPCServices|Helpers|MacOS|Library/(Automator|Spotlight|LoginItems))/
+
+ nested
+
+ weight
+ 10
+
+ ^.*
+
+ ^Info\.plist$
+
+ omit
+
+ weight
+ 20
+
+ ^PkgInfo$
+
+ omit
+
+ weight
+ 20
+
+ ^Resources/
+
+ weight
+ 20
+
+ ^Resources/.*\.lproj/
+
+ optional
+
+ weight
+ 1000
+
+ ^Resources/.*\.lproj/locversion.plist$
+
+ omit
+
+ weight
+ 1100
+
+ ^Resources/Base\.lproj/
+
+ weight
+ 1010
+
+ ^[^/]+$
+
+ nested
+
+ weight
+ 10
+
+ ^embedded\.provisionprofile$
+
+ weight
+ 20
+
+ ^version\.plist$
+
+ weight
+ 20
+
+
+
+
diff --git a/dist/Bitcoin Vibes.app/Contents/_CodeSignature/CodeSignature b/dist/Bitcoin Vibes.app/Contents/_CodeSignature/CodeSignature
new file mode 100644
index 000000000000..e69de29bb2d1
diff --git a/doc/vibes.md b/doc/vibes.md
index 531a0406b32e..c429c121a3f0 100644
--- a/doc/vibes.md
+++ b/doc/vibes.md
@@ -24,6 +24,24 @@ now.
## Quickstart
+### The Mac app, if you would rather not see a terminal
+
+[Download it](https://github.com/hotpixelgroup/bitcoin/releases/latest), drag it
+to Applications, double-click. On first open it fetches the source, installs
+what the compiler needs, builds the node and opens the console — all of it
+reported in the browser on a progress bar, because an app with no terminal has
+no business printing into one.
+
+It is a launcher, not the node: the source lives in `~/Library/Application
+Support/Bitcoin Vibes`, and every launch fast-forwards it without touching
+decrees you made locally.
+
+Not notarised yet, so the first launch needs a right-click → **Open**. Build it
+yourself with `sh contrib/vibes/macos/build-app.sh` if you would rather not
+trust a download.
+
+### Everyone else
+
One line. It installs the build dependencies, fetches the source, builds the
node, sets up the vibe engine, and opens the console: