From 0133979627a3d0fb37baa97a4ae66ca8b27d65e9 Mon Sep 17 00:00:00 2001 From: shocknet-justin Date: Mon, 3 Nov 2025 22:14:28 -0500 Subject: [PATCH 01/37] terminal QR --- package-lock.json | 9 +++++++++ package.json | 1 + scripts/extract_nprofile.sh | 6 ++++++ scripts/qr_generator.js | 15 +++++++++++++++ 4 files changed, 31 insertions(+) create mode 100755 scripts/qr_generator.js diff --git a/package-lock.json b/package-lock.json index 4435ce90..ed242466 100644 --- a/package-lock.json +++ b/package-lock.json @@ -36,6 +36,7 @@ "lodash": "^4.17.21", "nostr-tools": "^2.13.0", "pg": "^8.4.0", + "qrcode-terminal": "^0.12.0", "reflect-metadata": "^0.2.2", "rimraf": "^3.0.2", "rxjs": "^7.5.5", @@ -5730,6 +5731,14 @@ "node": ">=6" } }, + "node_modules/qrcode-terminal": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/qrcode-terminal/-/qrcode-terminal-0.12.0.tgz", + "integrity": "sha512-EXtzRZmC+YGmGlDFbXKxQiMZNwCLEO6BANKXG4iCtSIM0yqc/pappSx3RIKr4r0uh5JsBckOXeKrB3Iz7mdQpQ==", + "bin": { + "qrcode-terminal": "bin/qrcode-terminal.js" + } + }, "node_modules/qs": { "version": "6.13.0", "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", diff --git a/package.json b/package.json index a71062ba..123cd17c 100644 --- a/package.json +++ b/package.json @@ -54,6 +54,7 @@ "lodash": "^4.17.21", "nostr-tools": "^2.13.0", "pg": "^8.4.0", + "qrcode-terminal": "^0.12.0", "reflect-metadata": "^0.2.2", "rimraf": "^3.0.2", "rxjs": "^7.5.5", diff --git a/scripts/extract_nprofile.sh b/scripts/extract_nprofile.sh index 704b7b36..3ddf06cd 100644 --- a/scripts/extract_nprofile.sh +++ b/scripts/extract_nprofile.sh @@ -108,12 +108,18 @@ get_log_info() { log "A node admin has not yet enrolled via Nostr." log "Paste this string into ShockWallet as a node source to connect as administrator:" log "${SECONDARY_COLOR}$admin_connect${RESET_COLOR}" + echo "" + log "Or scan this QR code with ShockWallet:" + node "$INSTALL_DIR/scripts/qr_generator.js" "$admin_connect" 2>/dev/null || log "QR code generation unavailable" break fi elif [ -f "$DATA_DIR/app.nprofile" ]; then app_nprofile=$(cat "$DATA_DIR/app.nprofile") log "Node is already set up. Use this nprofile to invite guest users:" log "${SECONDARY_COLOR}$app_nprofile${RESET_COLOR}" + echo "" + log "Or scan this QR code with ShockWallet:" + node "$INSTALL_DIR/scripts/qr_generator.js" "$app_nprofile" 2>/dev/null || log "QR code generation unavailable" break fi sleep $WAIT_INTERVAL diff --git a/scripts/qr_generator.js b/scripts/qr_generator.js new file mode 100755 index 00000000..a4b4a865 --- /dev/null +++ b/scripts/qr_generator.js @@ -0,0 +1,15 @@ +#!/usr/bin/env node + +import qrcode from 'qrcode-terminal'; + +const text = process.argv[2]; + +if (!text) { + console.error('Usage: qr_generator.js '); + process.exit(1); +} + +qrcode.generate(text, { small: true }, (qr) => { + console.log(qr); +}); + From 64dbd2f4601cab9e9cfde6d9d6d513c2fc1fe8ae Mon Sep 17 00:00:00 2001 From: shocknet-justin Date: Tue, 4 Nov 2025 00:24:13 -0500 Subject: [PATCH 02/37] bump --- scripts/install.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/install.sh b/scripts/install.sh index 171d0b1c..383298ed 100755 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -11,7 +11,7 @@ log() { echo -e "$(echo "$message" | sed 's/\\e\[[0-9;]*m//g')" >> "$TMP_LOG_FILE" } -SCRIPT_VERSION="0.2.2" +SCRIPT_VERSION="0.2.3 REPO="shocknet/Lightning.Pub" BRANCH="master" From 3502858ce2ebdc1844e829de7ec75ed708f3753a Mon Sep 17 00:00:00 2001 From: shocknet-justin Date: Tue, 4 Nov 2025 15:15:50 -0500 Subject: [PATCH 03/37] curl checks --- scripts/install.sh | 2 +- scripts/install_nodejs.sh | 24 ++++++++++++++++++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/scripts/install.sh b/scripts/install.sh index 383298ed..90320715 100755 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -13,7 +13,7 @@ log() { SCRIPT_VERSION="0.2.3 REPO="shocknet/Lightning.Pub" -BRANCH="master" +BRANCH="wizard-update cleanup() { log "Cleaning up temporary files..." diff --git a/scripts/install_nodejs.sh b/scripts/install_nodejs.sh index 06601a20..ca4e196a 100755 --- a/scripts/install_nodejs.sh +++ b/scripts/install_nodejs.sh @@ -8,6 +8,30 @@ install_nodejs() { log "${PRIMARY_COLOR}Checking${RESET_COLOR} for Node.js..." MINIMUM_VERSION="18.0.0" + # Check for curl (required by nvm installer) + if ! command -v curl &> /dev/null; then + log "ERROR: curl is required for Node.js installation" + log "" + log "Please install curl first:" + log " Debian/Ubuntu: sudo apt install curl" + log " Fedora/RHEL: sudo dnf install curl" + log " Arch: sudo pacman -S curl" + log "" + exit 1 + fi + + # Check for snap curl which cannot access hidden folders + if snap list 2>/dev/null | grep -q "^curl "; then + log "ERROR: Snap curl detected" + log "" + log "Snap curl cannot access hidden folders needed for Node.js installation." + log "Please remove snap curl and install native curl:" + log " sudo snap remove curl" + log " sudo apt install curl" + log "" + exit 1 + fi + # Load nvm if it already exists export NVM_DIR="${NVM_DIR}" [ -s "${NVM_DIR}/nvm.sh" ] && \. "${NVM_DIR}/nvm.sh" From ddda6030ba461411e613b81af05f02b274b0fefd Mon Sep 17 00:00:00 2001 From: shocknet-justin Date: Tue, 4 Nov 2025 15:19:41 -0500 Subject: [PATCH 04/37] only check snap curl --- scripts/install_nodejs.sh | 14 +------------- 1 file changed, 1 insertion(+), 13 deletions(-) diff --git a/scripts/install_nodejs.sh b/scripts/install_nodejs.sh index ca4e196a..6e97ea61 100755 --- a/scripts/install_nodejs.sh +++ b/scripts/install_nodejs.sh @@ -8,19 +8,7 @@ install_nodejs() { log "${PRIMARY_COLOR}Checking${RESET_COLOR} for Node.js..." MINIMUM_VERSION="18.0.0" - # Check for curl (required by nvm installer) - if ! command -v curl &> /dev/null; then - log "ERROR: curl is required for Node.js installation" - log "" - log "Please install curl first:" - log " Debian/Ubuntu: sudo apt install curl" - log " Fedora/RHEL: sudo dnf install curl" - log " Arch: sudo pacman -S curl" - log "" - exit 1 - fi - - # Check for snap curl which cannot access hidden folders + # Check for snap curl which cannot access hidden folders, nvm falls back to wget if curl is not installed if snap list 2>/dev/null | grep -q "^curl "; then log "ERROR: Snap curl detected" log "" From 6b9efb013719ce15179975e9afe4ff4a56e8be13 Mon Sep 17 00:00:00 2001 From: shocknet-justin Date: Tue, 4 Nov 2025 21:43:47 -0500 Subject: [PATCH 05/37] typo --- scripts/install.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/install.sh b/scripts/install.sh index 90320715..0c6721d8 100755 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -11,9 +11,9 @@ log() { echo -e "$(echo "$message" | sed 's/\\e\[[0-9;]*m//g')" >> "$TMP_LOG_FILE" } -SCRIPT_VERSION="0.2.3 +SCRIPT_VERSION="0.2.3" REPO="shocknet/Lightning.Pub" -BRANCH="wizard-update +BRANCH="wizard-update" cleanup() { log "Cleaning up temporary files..." From ff5d7ec2838ef9c6c31313c20da089e800c4cee0 Mon Sep 17 00:00:00 2001 From: shocknet-justin Date: Wed, 5 Nov 2025 14:30:10 -0500 Subject: [PATCH 06/37] readme --- README.md | 136 ++++++++++++++++++++++++++++++++++++++++++++++----- pub_logo.png | Bin 9577 -> 26499 bytes 2 files changed, 125 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 60b2da0a..0b339336 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,11 @@ # Lightning.Pub -![Lightning.Pub](https://github.com/shocknet/Lightning.Pub/raw/master/pub_logo.png) +![Lightning.Pub](https://github.com/shocknet/Lightning.Pub/raw/wizard-update/pub_logo.png) ![GitHub last commit](https://img.shields.io/github/last-commit/shocknet/Lightning.Pub?style=flat-square) [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square)](http://makeapullrequest.com) [![Chat](https://img.shields.io/badge/chat-on%20Telegram-blue?style=flat-square)](https://t.me/ShockBTC) -![X (formerly Twitter) Follow](https://img.shields.io/twitter/follow/ShockBTC?style=flat-square&logo=bitcoin) +![Xitter](https://img.shields.io/twitter/follow/ShockBTC?style=flat-square&logo=bitcoin) ### Don't just run a Lightning Node, run a Lightning Pub. @@ -15,7 +15,9 @@ Lightning payments open the door to a new internet, but because of UX challenges It may come as a surprise that the biggest hurdle to more adoption via Family and SMB Lightning nodes hasn't been with Bitcoin/Lightning node management itself, as we've seen that liquidity is easily automated, but rather the legacy baggage of traditional Client-Server web infrastructure. Things like IP4, Reverse Proxies, DNS, Firewalls and SSL certificates, all require a personal configuration that is a hurdle for most. -Tor as a workaround has proven too slow and unreliable, and a dead-end for clearnet-web usecases. Mobile node are easy but channels for every device is expensive and unscalable, and the UX that suffers from the limitations of the node not being an always-online server. +Tor as a workaround has proven too slow and unreliable, and a dead-end for clearnet-web usecases. + +Mobile nodes are easy to use for spending, but channels for every device is expensive and unscalable. UX suffers from the limitations of the node not being an always-online server, which also makes them largely useless for merchants and routing services that earn revenue while you sleep. Pub solves these challenges with a P2P-like design that is also web-friendly, by implementing a full RPC that is Nostr-native. Being Nostr-native eliminates the complexity of configuring your node like a server by using commodity Nostr relays. These relays, unlike LNURL proxies, are trustless by nature of Nostr's own encryption spec (NIP44). @@ -31,18 +33,27 @@ By solving the networking and programability hurdles, Pub provides Lightning wit - [Docker Installation](#docker-installation) - [Manual CLI Installation](#manual-cli-installation) - [Usage Notes](#usage-notes) -- [Misc](#support-development) + - [Connecting to ShockWallet](#connecting-to-shockwallet) + - [Lightning Address](#lightning-address) + - [Node Type](#node-type) + - [Running Your Own Nostr Relay](#running-your-own-nostr-relay) + - [Bootstrap vs Self-Hosted Mode](#bootstrap-vs-self-hosted-mode) + - [Configuration](#configuration) +- [Support Development](#support-development) ## Features +- **Zero Network Configuration Required** - No port forwarding, Tor setup, firewall rules, or DNS configuration needed! Pub uses Nostr relays for all communication, making it perfect for users who want sovereignty without networking complexity. - Wrapper for [`LND`](https://github.com/lightningnetwork/lnd/releases) that can serve accounts over LNURL and NOSTR - A growing number of [methods](https://github.com/shocknet/Lightning.Pub/blob/master/proto/autogenerated/client.md) - Automated Channels - Receives quotes from multiple LSPs including Zeus, Voltage, and Flashsats - Bootstrap Peering - A pub node may trust another pub node until it can afford a channel + - Can be disabled via environment variable for full sovereignty - Accounting SubLayers for Application Pools and Users - - A fee regime allows applications owners to monetize users, or node operators to host distinctly monetized applications. + - A fee regime allows applications owners to monetize users, or node operators to host distinctly monetized applications. +- **Complete Lightning Solution** - No additional LN node or Bitcoin full node required (uses Neutrino) ![Accounts](https://github.com/shocknet/Lightning.Pub/raw/master/accounting_layers.png) @@ -74,8 +85,17 @@ This method installs all dependencies and creates user-level systemd services. **Platform Support:** - ✅ **Debian/Ubuntu**: Fully tested and supported -- ⚠️ **Arch/Fedora**: Should work but untested - please report issues -- 🚧 **macOS**: Basic support stubbed in but completely untested - help wanted +- ✅ **Arch/Fedora**: Fully tested and supported +- 🚧 **macOS**: Basic support stubbed in, but untested. Help wanted. + +> [!IMPORTANT] +> **System Requirements:** +> - **RAM**: Minimum 2GB burstable in headless containers or VPS. 4+GB recommended for full Linux Desktop OS. +> - **Storage**: 20GB of space for compact blocks. +> - **Network**: No port forwarding, Tor, or firewall configuration needed! Pub uses Nostr relays for communication to eliminate traditional server networking requirements. + +> [!TIP] +> **Bundled Node**: The Lightning.Pub install script provides a complete Lightning solution. You do NOT need to a full Bitcoin or other node, perfect small devices like Raspberry Pi. To start, run the following command: @@ -90,7 +110,11 @@ It should look like this in a minute or so **Note:** The installation is now confined to user-space, meaning: - No sudo required for installation - All data stored in `$HOME/lightning_pub/` -- Logs available at `$HOME/lightning_pub/install.log` + +**After Installation:** +- The installer will display an admin connection string (nprofile and secret separated by a colon) and a **QR code** for easy mobile setup +- You can also access the connection info via web browser on Start9 and Umbrel appliances (releases forthcoming) +- Copy the connection string or scan the QR code with ShockWallet to connect as administrator **⚠️ Migration from Previous Versions:** Previous system-wide installations (as of 8.27.2025) need some manual intervention: @@ -113,6 +137,32 @@ These are controversial, so we don't include them. You can however add a line to **Note:** The installer will only restart services if version checks deem necessary. +#### Troubleshooting + +If the installation fails or services don't start properly, use these commands to diagnose: + +```bash +# Check service status +systemctl --user status lnd +systemctl --user status lightning_pub + +# View logs +journalctl --user-unit lnd -f +journalctl --user-unit lightning_pub -f + +# Restart services if needed +systemctl --user restart lnd +systemctl --user restart lightning_pub + +# Retrieve admin connection string (if installation completed but you need to find it again) +cat ~/lightning_pub/admin.connect + +# Reset admin access (generates new admin.connect automatically) +rm ~/lightning_pub/admin.npub +sleep 1 # Wait briefly for new admin.connect to re-generate +cat ~/lightning_pub/admin.connect +``` + ### Docker Installation See the [Docker Installation Guide](DOCKER.md). @@ -145,11 +195,75 @@ npm start ## Usage Notes -Connect with ShockWallet ([wallet2](https://github.com/shocknet/wallet2)) using the wallet admin string that gets logged at startup. Simply copy/paste the string into the node connection screen. +### Connecting to ShockWallet -The nprofile of the node can also be used to send invitation links to guests via the web version of ShockWallet. +**For Administrators:** +1. After installation, you'll see an admin connection string (format: `nprofile1...:token`) and a QR code +2. **Option 1**: Scan the QR code with ShockWallet mobile app +3. **Option 2**: Copy/paste the connection string into ShockWallet's node connection screen -**Note that connecting with wallet will create an account on the node, it will not show or have access to the full LND balance. Allocating existing funds to the admin user will be added to the operator dashboard in a future release.** +**For Guest Users:** +- The nprofile of the node (without the admin token) can be used to send invitation links to guests via the web version of ShockWallet +- The nprofile is stored in `$HOME/lightning_pub/app.nprofile` or the administrator can copy a link from the "Invite" page in ShockWallet + +> [!NOTE] +> Connecting with wallet will create an account on the node, it will not show or have access to the full LND balance. Allocating existing funds to the admin user will be added to the operator dashboard in a future release. + +### Lightning Address + +When you run your own Lightning Pub, obtaining a Lightning Address is fully automated in ShockWallet. The wallet automatically: +1. Takes the CLINK offer from your Pub +2. Enrolls it at a LNURL bridge (creates a `@shockwallet.app` address) +3. This makes the Lightning Address trustless when payers support CLINK as it uses Nostr for communication instead of trusting the bridge to serve the correct invoice. + +> [!TIP] +> **CLINK Integration**: Your Pub's CLINK offers enable ShockWallet to connect to CLINK-compatible services, like [Stacker News](https://stacker.news), allowing you to send and receive payments without additional setup. + +**Alternative Options for Custom Lightning Addresses:** + +1. **Run your own [Bridgelet](https://github.com/shocknet/bridgelet)**: A minimalist LNURL-P and Lightning Address bridge service that uses CLINK Offers. This gives you full control over your Lightning Address domain without trusting third-party bridges. + +2. **Enable LNURL directly on Pub**: Set `SERVICE_URL` to your domain (e.g., `https://yourdomain.com`). Pub will serve LNURL callbacks directly. Requires an SSL reverse proxy pointing your domain to Pub's port. + +### Running Your Own Nostr Relay + +By default, Lightning.Pub uses the ShockNet relay. To use your own Nostr relay: + +1. Set the `NOSTR_RELAYS` environment variable in your `.env` file: + ```bash + NOSTR_RELAYS=wss://your-relay-url.com + ``` +2. Multiple relays can be specified (space-separated): + ```bash + NOSTR_RELAYS="wss://relay1.com wss://relay2.com wss://relay3.com" + ``` +3. The wizard interface (coming soon for Start9/Umbrel) will make this graphical + +See `env.example` for all available configuration options. + +### Bootstrap Liquidity Provider + +By default, Lightning.Pub uses a bootstrap liquidity provider that provides initial channel funding as a service credit until you can afford your own channels. Pub compares rates from top LSPs and automatically requests a channel when needed. + +You can disable this for full sovereignty via `DISABLE_LIQUIDITY_PROVIDER=true` in `.env`, or re-point to any other Lightning Pub (not just ShockNet's) via `LIQUIDITY_PROVIDER_PUB`. + +### Configuration + +Copy `env.example` to `.env` and customize settings: +```bash +cp env.example .env +nano .env # or use your preferred editor +``` + +> [!IMPORTANT] +> Environment variables set in `.env` will override any settings configured via the wizard or stored in the settings database table. + +Key settings: +- `NOSTR_RELAYS`: Custom Nostr relay(s) to use +- `DISABLE_LIQUIDITY_PROVIDER`: Set to `true` to disable bootstrap liquidity provider (alternatively, set `LIQUIDITY_PROVIDER_PUB=null` or to the nprofile of a preferred Pub instance) +- `BRIDGE_URL`: Configure which CLINK -> Suggets a LNURL bridge to wallets to use for Lightning Address enrollment (default: `https://shockwallet.app`) +- `SERVICE_URL`: Your domain URL for enabling LNURL directly on Pub (requires SSL via a reverse-proxy pointing to Pub's port) +- See `env.example` for complete documentation Additional docs are WIP at [docs.shock.network](https://docs.shock.network) diff --git a/pub_logo.png b/pub_logo.png index 65bf00400f8463528fdb1cb880fc8fa941fdaa92..13b0c0428a3d4fb080e09b1d1aace678ffc16fb1 100644 GIT binary patch literal 26499 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 Date: Wed, 5 Nov 2025 15:56:43 -0500 Subject: [PATCH 07/37] readme --- README.md | 575 ++++++++++++++++++++++++++++-------------------------- 1 file changed, 295 insertions(+), 280 deletions(-) diff --git a/README.md b/README.md index 0b339336..baf8769d 100644 --- a/README.md +++ b/README.md @@ -1,280 +1,295 @@ -# Lightning.Pub - -![Lightning.Pub](https://github.com/shocknet/Lightning.Pub/raw/wizard-update/pub_logo.png) - -![GitHub last commit](https://img.shields.io/github/last-commit/shocknet/Lightning.Pub?style=flat-square) -[![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square)](http://makeapullrequest.com) -[![Chat](https://img.shields.io/badge/chat-on%20Telegram-blue?style=flat-square)](https://t.me/ShockBTC) -![Xitter](https://img.shields.io/twitter/follow/ShockBTC?style=flat-square&logo=bitcoin) - -### Don't just run a Lightning Node, run a Lightning Pub. - -"Pub" is a [Nostr](https://nostr.info)-native account system designed to make running Lightning infrastructure for your friends/family/customers easier than previously thought possible. - -Lightning payments open the door to a new internet, but because of UX challenges with sovereignty we've seen a much slower uptake than we should for something so amazing. - -It may come as a surprise that the biggest hurdle to more adoption via Family and SMB Lightning nodes hasn't been with Bitcoin/Lightning node management itself, as we've seen that liquidity is easily automated, but rather the legacy baggage of traditional Client-Server web infrastructure. Things like IP4, Reverse Proxies, DNS, Firewalls and SSL certificates, all require a personal configuration that is a hurdle for most. - -Tor as a workaround has proven too slow and unreliable, and a dead-end for clearnet-web usecases. - -Mobile nodes are easy to use for spending, but channels for every device is expensive and unscalable. UX suffers from the limitations of the node not being an always-online server, which also makes them largely useless for merchants and routing services that earn revenue while you sleep. - -Pub solves these challenges with a P2P-like design that is also web-friendly, by implementing a full RPC that is Nostr-native. Being Nostr-native eliminates the complexity of configuring your node like a server by using commodity Nostr relays. These relays, unlike LNURL proxies, are trustless by nature of Nostr's own encryption spec (NIP44). - -Additionally, support for optional services are integrated into Pub for operators seeking backward compatibility with legacy LNURLs and Lightning Addresses. - -By solving the networking and programability hurdles, Pub provides Lightning with a 3rd Layer that enables node-runners, Busineses, and Uncle Jims to more easily bring their personal network into Bitcoin's permissionless economy. In doing so, Pub runners can keep the Lightning Network decentralized, with custodial scaling that is free of fiat rails, large banks, and other forms of high-time-preference shitcoinery. - -## Table of Contents -- [Features](#features) -- [Planned Features](#planned-features) -- [Installation](#installation) - - [One-Line Deployment](#one-line-deployment) - - [Docker Installation](#docker-installation) - - [Manual CLI Installation](#manual-cli-installation) -- [Usage Notes](#usage-notes) - - [Connecting to ShockWallet](#connecting-to-shockwallet) - - [Lightning Address](#lightning-address) - - [Node Type](#node-type) - - [Running Your Own Nostr Relay](#running-your-own-nostr-relay) - - [Bootstrap vs Self-Hosted Mode](#bootstrap-vs-self-hosted-mode) - - [Configuration](#configuration) -- [Support Development](#support-development) - -## Features - -- **Zero Network Configuration Required** - No port forwarding, Tor setup, firewall rules, or DNS configuration needed! Pub uses Nostr relays for all communication, making it perfect for users who want sovereignty without networking complexity. -- Wrapper for [`LND`](https://github.com/lightningnetwork/lnd/releases) that can serve accounts over LNURL and NOSTR -- A growing number of [methods](https://github.com/shocknet/Lightning.Pub/blob/master/proto/autogenerated/client.md) -- Automated Channels - - Receives quotes from multiple LSPs including Zeus, Voltage, and Flashsats -- Bootstrap Peering - - A pub node may trust another pub node until it can afford a channel - - Can be disabled via environment variable for full sovereignty -- Accounting SubLayers for Application Pools and Users - - A fee regime allows applications owners to monetize users, or node operators to host distinctly monetized applications. -- **Complete Lightning Solution** - No additional LN node or Bitcoin full node required (uses Neutrino) - -![Accounts](https://github.com/shocknet/Lightning.Pub/raw/master/accounting_layers.png) - -- Connecting via ShockWallet is as easy as pasting an nprofile -- Or use a link to share your nprofile with friends and family - -Connect Wallet Invite Guests - -## Planned Features - -- [x] A management dashboard is actively being integrated into [ShockWallet](https://github.com/shocknet/wallet2) -- [x] Nostr native [CLINK](https://clinkme.dev) "offers" -- [x] Encrypted Push Notifications -- [ ] P2P "LSP" coordination for channel batching over Nostr -- [ ] Swap integration -- [ ] High-Availabilty / Clustering - -Dashboard Wireframe: - -Pub Dashboard - -## Installation - -### One-Line Deployment - -Paste one-line and have a Pub node in under 2 minutes. It uses neutrino so you can run it on a $5 VPS or old laptop. - -This method installs all dependencies and creates user-level systemd services. - -**Platform Support:** -- ✅ **Debian/Ubuntu**: Fully tested and supported -- ✅ **Arch/Fedora**: Fully tested and supported -- 🚧 **macOS**: Basic support stubbed in, but untested. Help wanted. - -> [!IMPORTANT] -> **System Requirements:** -> - **RAM**: Minimum 2GB burstable in headless containers or VPS. 4+GB recommended for full Linux Desktop OS. -> - **Storage**: 20GB of space for compact blocks. -> - **Network**: No port forwarding, Tor, or firewall configuration needed! Pub uses Nostr relays for communication to eliminate traditional server networking requirements. - -> [!TIP] -> **Bundled Node**: The Lightning.Pub install script provides a complete Lightning solution. You do NOT need to a full Bitcoin or other node, perfect small devices like Raspberry Pi. - -To start, run the following command: - -```bash -wget -qO- https://deploy.lightning.pub | bash -``` - -It should look like this in a minute or so - -![One-Line Deployment](https://raw.githubusercontent.com/shocknet/Lightning.Pub/master/one-liner.png) - -**Note:** The installation is now confined to user-space, meaning: -- No sudo required for installation -- All data stored in `$HOME/lightning_pub/` - -**After Installation:** -- The installer will display an admin connection string (nprofile and secret separated by a colon) and a **QR code** for easy mobile setup -- You can also access the connection info via web browser on Start9 and Umbrel appliances (releases forthcoming) -- Copy the connection string or scan the QR code with ShockWallet to connect as administrator - -**⚠️ Migration from Previous Versions:** -Previous system-wide installations (as of 8.27.2025) need some manual intervention: -1. Stop existing services: `sudo systemctl stop lnd lightning_pub` -2. Disable services: `sudo systemctl disable lnd lightning_pub` -3. Remove old systemd units: `sudo rm /etc/systemd/system/lnd.service /etc/systemd/system/lightning_pub.service` -4. Reload systemd: `sudo systemctl daemon-reload` -5. Run the new installer: `wget -qO- https://deploy.lightning.pub | bash` - -Please report any issues to the [issue tracker](https://github.com/shocknet/Lightning.Pub/issues). - -#### Automatic updates - -These are controversial, so we don't include them. You can however add a line to your crontab to re-run the installer on your time preference and it will gracefully handle updating: - -```bash -# Add to user's crontab (crontab -e) - runs weekly on Sunday at 2 AM -0 2 * * 0 wget -qO- https://deploy.lightning.pub | bash -``` - -**Note:** The installer will only restart services if version checks deem necessary. - -#### Troubleshooting - -If the installation fails or services don't start properly, use these commands to diagnose: - -```bash -# Check service status -systemctl --user status lnd -systemctl --user status lightning_pub - -# View logs -journalctl --user-unit lnd -f -journalctl --user-unit lightning_pub -f - -# Restart services if needed -systemctl --user restart lnd -systemctl --user restart lightning_pub - -# Retrieve admin connection string (if installation completed but you need to find it again) -cat ~/lightning_pub/admin.connect - -# Reset admin access (generates new admin.connect automatically) -rm ~/lightning_pub/admin.npub -sleep 1 # Wait briefly for new admin.connect to re-generate -cat ~/lightning_pub/admin.connect -``` - -### Docker Installation - -See the [Docker Installation Guide](DOCKER.md). - -### Manual CLI Installation - -1. Run [LND](https://github.com/lightningnetwork/lnd/releases) if you aren't already: - -```ssh -./lnd --bitcoin.active --bitcoin.mainnet --bitcoin.node=neutrino --neutrino.addpeer=neutrino.shock.network --feeurl=https://nodes.lightning.computer/fees/v1/btc-fee-estimates.json -``` - -2. Download and Install Lightning.Pub: - -```ssh -git clone https://github.com/shocknet/Lightning.Pub && cd Lightning.Pub && npm i -``` - -3. Configure values in the env file: - -```ssh -cp env.example .env && nano .env -``` - -4. Start the service: - -```ssh -npm start -``` - -## Usage Notes - -### Connecting to ShockWallet - -**For Administrators:** -1. After installation, you'll see an admin connection string (format: `nprofile1...:token`) and a QR code -2. **Option 1**: Scan the QR code with ShockWallet mobile app -3. **Option 2**: Copy/paste the connection string into ShockWallet's node connection screen - -**For Guest Users:** -- The nprofile of the node (without the admin token) can be used to send invitation links to guests via the web version of ShockWallet -- The nprofile is stored in `$HOME/lightning_pub/app.nprofile` or the administrator can copy a link from the "Invite" page in ShockWallet - -> [!NOTE] -> Connecting with wallet will create an account on the node, it will not show or have access to the full LND balance. Allocating existing funds to the admin user will be added to the operator dashboard in a future release. - -### Lightning Address - -When you run your own Lightning Pub, obtaining a Lightning Address is fully automated in ShockWallet. The wallet automatically: -1. Takes the CLINK offer from your Pub -2. Enrolls it at a LNURL bridge (creates a `@shockwallet.app` address) -3. This makes the Lightning Address trustless when payers support CLINK as it uses Nostr for communication instead of trusting the bridge to serve the correct invoice. - -> [!TIP] -> **CLINK Integration**: Your Pub's CLINK offers enable ShockWallet to connect to CLINK-compatible services, like [Stacker News](https://stacker.news), allowing you to send and receive payments without additional setup. - -**Alternative Options for Custom Lightning Addresses:** - -1. **Run your own [Bridgelet](https://github.com/shocknet/bridgelet)**: A minimalist LNURL-P and Lightning Address bridge service that uses CLINK Offers. This gives you full control over your Lightning Address domain without trusting third-party bridges. - -2. **Enable LNURL directly on Pub**: Set `SERVICE_URL` to your domain (e.g., `https://yourdomain.com`). Pub will serve LNURL callbacks directly. Requires an SSL reverse proxy pointing your domain to Pub's port. - -### Running Your Own Nostr Relay - -By default, Lightning.Pub uses the ShockNet relay. To use your own Nostr relay: - -1. Set the `NOSTR_RELAYS` environment variable in your `.env` file: - ```bash - NOSTR_RELAYS=wss://your-relay-url.com - ``` -2. Multiple relays can be specified (space-separated): - ```bash - NOSTR_RELAYS="wss://relay1.com wss://relay2.com wss://relay3.com" - ``` -3. The wizard interface (coming soon for Start9/Umbrel) will make this graphical - -See `env.example` for all available configuration options. - -### Bootstrap Liquidity Provider - -By default, Lightning.Pub uses a bootstrap liquidity provider that provides initial channel funding as a service credit until you can afford your own channels. Pub compares rates from top LSPs and automatically requests a channel when needed. - -You can disable this for full sovereignty via `DISABLE_LIQUIDITY_PROVIDER=true` in `.env`, or re-point to any other Lightning Pub (not just ShockNet's) via `LIQUIDITY_PROVIDER_PUB`. - -### Configuration - -Copy `env.example` to `.env` and customize settings: -```bash -cp env.example .env -nano .env # or use your preferred editor -``` - -> [!IMPORTANT] -> Environment variables set in `.env` will override any settings configured via the wizard or stored in the settings database table. - -Key settings: -- `NOSTR_RELAYS`: Custom Nostr relay(s) to use -- `DISABLE_LIQUIDITY_PROVIDER`: Set to `true` to disable bootstrap liquidity provider (alternatively, set `LIQUIDITY_PROVIDER_PUB=null` or to the nprofile of a preferred Pub instance) -- `BRIDGE_URL`: Configure which CLINK -> Suggets a LNURL bridge to wallets to use for Lightning Address enrollment (default: `https://shockwallet.app`) -- `SERVICE_URL`: Your domain URL for enabling LNURL directly on Pub (requires SSL via a reverse-proxy pointing to Pub's port) -- See `env.example` for complete documentation - -Additional docs are WIP at [docs.shock.network](https://docs.shock.network) - -## Support Development - -> [!IMPORTANT] -> ShockWallet and Lightning.Pub are free software. If you would like to see continued development, please show your [**support**](https://github.com/sponsors/shocknet) 😊 - -License - -## Warning - -> [!WARNING] -> While this software has been used in a high-profile production environment for several years, it should still be considered bleeding edge. Special care has been taken to mitigate the risk of drainage attacks, which is a common risk to all Lightning APIs. An integrated Watchdog service will terminate spends if it detects a discrepancy between LND and the database, for this reason **IT IS NOT RECOMMENDED TO USE PUB ALONGSIDE OTHER ACCOUNT SYSTEMS** such as AlbyHub, LNBits, or BTCPay - this watchdog may however be disabled. While we give the utmost care and attention to security, **the internet is an adversarial environment and SECURITY/RELIABILITY ARE NOT GUARANTEED- USE AT YOUR OWN RISK**. +# Lightning.Pub + +![Lightning.Pub](https://github.com/shocknet/Lightning.Pub/raw/wizard-update/pub_logo.png) + +![GitHub last commit](https://img.shields.io/github/last-commit/shocknet/Lightning.Pub?style=flat-square) +[![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square)](http://makeapullrequest.com) +[![Chat](https://img.shields.io/badge/chat-on%20Telegram-blue?style=flat-square)](https://t.me/ShockBTC) +![Xitter](https://img.shields.io/twitter/follow/ShockBTC?style=flat-square&logo=bitcoin) + +### Don't just run a Lightning Node, run a Lightning Pub. + +"Pub" is a [Nostr](https://nostr.info)-native account system designed to make running Lightning infrastructure for your friends/family/customers easier than previously thought possible. + +Lightning payments open the door to a new internet, but because of UX challenges with sovereignty we've seen a much slower uptake than we should for something so amazing. + +It may come as a surprise that the biggest hurdle to more adoption via Family and SMB Lightning nodes hasn't been with Bitcoin/Lightning node management itself, as we've seen that liquidity is easily automated, but rather the legacy baggage of traditional Client-Server web infrastructure. Things like IP4, Reverse Proxies, DNS, Firewalls and SSL certificates, all require a personal configuration that is a hurdle for most. + +Tor as a workaround has proven too slow and unreliable, and a dead-end for clearnet-web usecases. + +Mobile nodes are easy to use for spending, but channels for every device is expensive and unscalable. UX suffers from the limitations of the node not being an always-online server, which also makes them largely useless for merchants and routing services that earn revenue while you sleep. + +Pub solves these challenges with a P2P-like design that is also web-friendly, by implementing a full RPC that is Nostr-native. Being Nostr-native eliminates the complexity of configuring your node like a server by using commodity Nostr relays. These relays, unlike LNURL proxies, are trustless by nature of Nostr's own encryption spec (NIP44). + +Additionally, support for optional services are integrated into Pub for operators seeking backward compatibility with legacy LNURLs and Lightning Addresses. + +By solving the networking and programability hurdles, Pub provides Lightning with a 3rd Layer that enables node-runners, Busineses, and Uncle Jims to more easily bring their personal network into Bitcoin's permissionless economy. In doing so, Pub runners can keep the Lightning Network decentralized, with custodial scaling that is free of fiat rails, large banks, and other forms of high-time-preference shitcoinery. + +## Table of Contents +- [Features](#features) +- [Planned Features](#planned-features) +- [Installation](#installation) + - [One-Line Deployment](#one-line-deployment) + - [Docker Installation](#docker-installation) + - [Manual CLI Installation](#manual-cli-installation) +- [Usage Notes](#usage-notes) + - [Connecting to ShockWallet](#connecting-to-shockwallet) + - [Lightning Address](#lightning-address) +- [Advanced Configuration](#advanced-configuration) + - [Custom Nostr Relay](#custom-nostr-relay) + - [Bootstrap Liquidity Provider](#bootstrap-liquidity-provider) + - [Custom Lightning Address Domain](#custom-lightning-address-domain) +- [Support Development](#support-development) + +## Features + +- **Zero Network Configuration Required** - No port forwarding, Tor setup, firewall rules, or DNS configuration needed! Pub uses Nostr relays for all communication, making it perfect for users who want sovereignty without networking complexity. +- Wrapper for [`LND`](https://github.com/lightningnetwork/lnd/releases) that can serve accounts over LNURL and NOSTR +- A growing number of [methods](https://github.com/shocknet/Lightning.Pub/blob/master/proto/autogenerated/client.md) +- Automated Channels + - Receives quotes from multiple LSPs including Zeus, Voltage, and Flashsats +- Bootstrap Peering + - A pub node may trust another pub node until it can afford a channel + - Can be disabled via environment variable for full sovereignty +- Accounting SubLayers for Application Pools and Users + - A fee regime allows applications owners to monetize users, or node operators to host distinctly monetized applications. +- **Complete Lightning Solution** - No additional LN node or Bitcoin full node required (uses Neutrino) + +![Accounts](https://github.com/shocknet/Lightning.Pub/raw/master/accounting_layers.png) + +- Connecting via ShockWallet is as easy as pasting an nprofile +- Or use a link to share your nprofile with friends and family + +Connect Wallet Invite Guests + +## Planned Features + +- [x] A management dashboard is actively being integrated into [ShockWallet](https://github.com/shocknet/wallet2) +- [x] Nostr native [CLINK](https://clinkme.dev) "offers" +- [x] Encrypted Push Notifications +- [ ] Swap integration +- [ ] P2P "LSP" coordination for channel batching over Nostr +- [ ] High-Availabilty / Clustering + +Dashboard Wireframe: + +Pub Dashboard + +## Installation + +### One-Line Deployment + +Paste one-line and have a Pub node in under 2 minutes. It uses neutrino so you can run it on a $5 VPS or old laptop. + +This method installs all dependencies and creates user-level systemd services. + +**Platform Support:** +- ✅ **Debian/Ubuntu**: Fully tested and supported +- ✅ **Arch/Fedora**: Fully tested and supported +- 🚧 **macOS**: Basic support stubbed in, but untested. Help wanted. + +> [!IMPORTANT] +> **System Requirements:** +> - **RAM**: Minimum 2GB burstable in headless containers or VPS. 4+GB recommended for full Linux Desktop OS. +> - **Storage**: 20GB of space for compact blocks. +> - **Network**: No port forwarding, Tor, or firewall configuration needed! Pub uses Nostr relays for communication to eliminate traditional server networking requirements. + +> [!TIP] +> **Bundled Node**: The Lightning.Pub install script provides a complete Lightning solution. You do NOT need to a full Bitcoin or other node, perfect small devices like Raspberry Pi. + +To start, run the following command: + +```bash +wget -qO- https://deploy.lightning.pub | bash +``` + +It should look like this in a minute or so + +![One-Line Deployment](https://raw.githubusercontent.com/shocknet/Lightning.Pub/master/one-liner.png) + +**Note:** The installation is now confined to user-space, meaning: +- No sudo required for installation +- All data stored in `$HOME/lightning_pub/` + +**After Installation:** +- The installer will display an admin connection string (nprofile and secret separated by a colon) and a **QR code** for easy mobile setup +- You can also access the connection info via web browser on Start9 and Umbrel appliances (releases forthcoming) +- Copy the connection string or scan the QR code with ShockWallet to connect as administrator + +**⚠️ Migration from Previous Versions:** +Previous system-wide installations (as of 8.27.2025) need some manual intervention: +1. Stop existing services: `sudo systemctl stop lnd lightning_pub` +2. Disable services: `sudo systemctl disable lnd lightning_pub` +3. Remove old systemd units: `sudo rm /etc/systemd/system/lnd.service /etc/systemd/system/lightning_pub.service` +4. Reload systemd: `sudo systemctl daemon-reload` +5. Run the new installer: `wget -qO- https://deploy.lightning.pub | bash` + +Please report any issues to the [issue tracker](https://github.com/shocknet/Lightning.Pub/issues). + +#### Automatic updates + +These are controversial, so we don't include them. You can however add a line to your crontab to re-run the installer on your time preference and it will gracefully handle updating: + +```bash +# Add to user's crontab (crontab -e) - runs weekly on Sunday at 2 AM +0 2 * * 0 wget -qO- https://deploy.lightning.pub | bash +``` + +**Note:** The installer will only restart services if version checks deem necessary. + +#### Troubleshooting + +If the installation fails or services don't start properly, use these commands to diagnose: + +```bash +# Check service status +systemctl --user status lnd +systemctl --user status lightning_pub + +# View logs +journalctl --user-unit lnd -f +journalctl --user-unit lightning_pub -f + +# Restart services if needed +systemctl --user restart lnd +systemctl --user restart lightning_pub + +# Retrieve admin connection string (if installation completed but you need to find it again) +cat ~/lightning_pub/admin.connect + +# Reset admin access (generates new admin.connect automatically) +rm ~/lightning_pub/admin.npub +sleep 1 # Wait briefly for new admin.connect to re-generate +cat ~/lightning_pub/admin.connect +``` + +### Docker Installation + +See the [Docker Installation Guide](DOCKER.md). + +### Manual CLI Installation + +1. Run [LND](https://github.com/lightningnetwork/lnd/releases) if you aren't already: + +```ssh +./lnd --bitcoin.active --bitcoin.mainnet --bitcoin.node=neutrino --neutrino.addpeer=neutrino.shock.network --feeurl=https://nodes.lightning.computer/fees/v1/btc-fee-estimates.json +``` + +2. Download and Install Lightning.Pub: + +```ssh +git clone https://github.com/shocknet/Lightning.Pub && cd Lightning.Pub && npm i +``` + +3. Configure values in the env file: + +```ssh +cp env.example .env && nano .env +``` + +4. Start the service: + +```ssh +npm start +``` + +## Usage Notes + +### Connecting to ShockWallet + +**For Administrators:** +1. After installation, you'll see an admin connection string (format: `nprofile1...:token`) and a QR code +2. **Option 1**: Scan the QR code with ShockWallet mobile app +3. **Option 2**: Copy/paste the connection string into ShockWallet's node connection screen + +**For Guest Users:** +- The nprofile of the node (without the admin token) can be used to send invitation links to guests via the web version of ShockWallet +- The nprofile is stored in `$HOME/lightning_pub/app.nprofile` or the administrator can copy a link from the "Invite" page in ShockWallet + +> [!NOTE] +> Connecting with wallet will create an account on the node, it will not show or have access to the full LND balance. Allocating existing funds to the admin user will be added to the operator dashboard in a future release. + +### Lightning Address + +When you run your own Lightning Pub, obtaining a Lightning Address is fully automated in ShockWallet. The wallet automatically: +1. Takes the CLINK offer from your Pub +2. Enrolls it at a LNURL bridge (creates a `@shockwallet.app` address) +3. This makes the Lightning Address trustless when payers support CLINK as it uses Nostr for communication instead of trusting the bridge to serve the correct invoice. + +> [!TIP] +> **CLINK Integration**: Your Pub's CLINK offers enable ShockWallet to connect to CLINK-compatible services, like [Stacker News](https://stacker.news), allowing you to send and receive payments without additional setup. + +For custom Lightning Address domains, see the [Advanced Configuration](#advanced-configuration) section. + +## Advanced Configuration + +Copy `env.example` to `.env` and customize settings: +```bash +cp env.example .env +nano .env # or use your preferred editor +``` + +> [!IMPORTANT] +> Environment variables set in `.env` will override any settings configured via the wizard or stored in the settings database table. + +### Custom Nostr Relay + +By default, Lightning.Pub uses the ShockNet relay. To use your own: + +```bash +# Single relay +NOSTR_RELAYS=wss://your-relay-url.com + +# Multiple relays (space-separated) +NOSTR_RELAYS="wss://relay1.com wss://relay2.com wss://relay3.com" +``` + +The wizard interface (coming soon for Start9/Umbrel) will make this graphical. + +### Bootstrap Liquidity Provider + +By default, Lightning.Pub uses a bootstrap liquidity provider that provides initial channel funding as a service credit until you can afford your own channels. Pub compares rates from top LSPs and automatically requests a channel when needed. + +```bash +# Disable for full sovereignty +DISABLE_LIQUIDITY_PROVIDER=true + +# Or point to a different Pub instance +LIQUIDITY_PROVIDER_PUB=nprofile1... +``` + +### Custom Lightning Address Domain + +By default, ShockWallet automatically enrolls your CLINK offer at `@shockwallet.app`. For custom domains, you have two options: + +**Option 1: Run your own [Bridgelet](https://github.com/shocknet/bridgelet)** + +A minimalist LNURL-P and Lightning Address bridge service that uses CLINK Offers. This gives you full control over your Lightning Address domain without trusting third-party bridges. + +**Option 2: Enable LNURL directly on Pub** + +```bash +# Configure which LNURL bridge to suggest to wallets (default: https://shockwallet.app) +BRIDGE_URL=https://your-bridge.com + +# Enable LNURL callbacks directly on Pub (requires SSL reverse proxy pointing to Pub's port) +SERVICE_URL=https://yourdomain.com +``` + +### Complete Reference + +See `env.example` for complete documentation of all available settings. + +Additional docs are WIP at [docs.shock.network](https://docs.shock.network) + +## Support Development + +> [!IMPORTANT] +> ShockWallet and Lightning.Pub are free software. If you would like to see continued development, please show your [**support**](https://github.com/sponsors/shocknet) 😊 + +License + +## Warning + +> [!WARNING] +> While this software has been used in a high-profile production environment for several years, it should still be considered bleeding edge. Special care has been taken to mitigate the risk of drainage attacks, which is a common risk to all Lightning APIs. An integrated Watchdog service will terminate spends if it detects a discrepancy between LND and the database, for this reason **IT IS NOT RECOMMENDED TO USE PUB ALONGSIDE OTHER ACCOUNT SYSTEMS** such as AlbyHub, LNBits, or BTCPay - this watchdog may however be disabled. While we give the utmost care and attention to security, **the internet is an adversarial environment and SECURITY/RELIABILITY ARE NOT GUARANTEED- USE AT YOUR OWN RISK**. From 328529b90cd13169e462b976336a8f5a7d35746e Mon Sep 17 00:00:00 2001 From: shocknet-justin Date: Wed, 5 Nov 2025 16:37:12 -0500 Subject: [PATCH 08/37] readme --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index baf8769d..6f23327f 100644 --- a/README.md +++ b/README.md @@ -60,14 +60,14 @@ By solving the networking and programability hurdles, Pub provides Lightning wit - Connecting via ShockWallet is as easy as pasting an nprofile - Or use a link to share your nprofile with friends and family -Connect Wallet Invite Guests +Connect Wallet Invite Guests ## Planned Features - [x] A management dashboard is actively being integrated into [ShockWallet](https://github.com/shocknet/wallet2) - [x] Nostr native [CLINK](https://clinkme.dev) "offers" - [x] Encrypted Push Notifications -- [ ] Swap integration +- [_] Swap integration (in progress) - [ ] P2P "LSP" coordination for channel batching over Nostr - [ ] High-Availabilty / Clustering @@ -280,7 +280,7 @@ SERVICE_URL=https://yourdomain.com See `env.example` for complete documentation of all available settings. -Additional docs are WIP at [docs.shock.network](https://docs.shock.network) +For additional documentation, guides, and FAQs, visit [docs.shock.network](https://docs.shock.network) or contribute to the [docs repository](https://github.com/shocknet/docs). ## Support Development From da5113ec05353ba41d21624233ac60d4f4288819 Mon Sep 17 00:00:00 2001 From: shocknet-justin Date: Wed, 26 Nov 2025 00:30:24 -0500 Subject: [PATCH 09/37] mac untested --- scripts/check_homebrew.sh | 11 --- scripts/create_launchd_plist.sh | 43 ---------- scripts/handle_macos.sh | 148 ++++++++++++++++++++++++++++++++ scripts/install.sh | 5 +- scripts/install_lnd.sh | 19 ++-- scripts/install_nodejs.sh | 3 +- scripts/start_services_mac.sh | 17 ---- scripts/utils.sh | 35 +++++++- 8 files changed, 198 insertions(+), 83 deletions(-) delete mode 100755 scripts/check_homebrew.sh delete mode 100755 scripts/create_launchd_plist.sh create mode 100644 scripts/handle_macos.sh delete mode 100755 scripts/start_services_mac.sh diff --git a/scripts/check_homebrew.sh b/scripts/check_homebrew.sh deleted file mode 100755 index fd58489e..00000000 --- a/scripts/check_homebrew.sh +++ /dev/null @@ -1,11 +0,0 @@ -#!/bin/bash - -check_homebrew() { - if ! command -v brew &> /dev/null; then - log "${PRIMARY_COLOR}Homebrew not found. Installing Homebrew...${RESET_COLOR}" - /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" || { - log "${PRIMARY_COLOR}Failed to install Homebrew.${RESET_COLOR}" - exit 1 - } - fi -} diff --git a/scripts/create_launchd_plist.sh b/scripts/create_launchd_plist.sh deleted file mode 100755 index 9964a1fe..00000000 --- a/scripts/create_launchd_plist.sh +++ /dev/null @@ -1,43 +0,0 @@ -#!/bin/bash - -create_launchd_plist() { - create_plist() { - local plist_path=$1 - local label=$2 - local program_args=$3 - local working_dir=$4 - - if [ -f "$plist_path" ]; then - log "${PRIMARY_COLOR}${label} already exists. Skipping creation.${RESET_COLOR}" - else - cat < "$plist_path" - - - - - Label - ${label} - ProgramArguments - - ${program_args} - - WorkingDirectory - ${working_dir} - RunAtLoad - - KeepAlive - - - -EOF - fi - } - USER_HOME=$(eval echo ~$(whoami)) - NVM_DIR="$([ -z "${XDG_CONFIG_HOME-}" ] && printf %s "${USER_HOME}/.nvm" || printf %s "${XDG_CONFIG_HOME}/nvm")" - LAUNCH_AGENTS_DIR="${USER_HOME}/Library/LaunchAgents" - - create_plist "${LAUNCH_AGENTS_DIR}/local.lnd.plist" "local.lnd" "${USER_HOME}/lnd/lnd" "" - create_plist "${LAUNCH_AGENTS_DIR}/local.lightning_pub.plist" "local.lightning_pub" "/bin/bash-csource ${NVM_DIR}/nvm.sh && npm start" "${USER_HOME}/lightning_pub" - - log "${PRIMARY_COLOR}Created launchd plists. Please load them using launchctl.${RESET_COLOR}" -} diff --git a/scripts/handle_macos.sh b/scripts/handle_macos.sh new file mode 100644 index 00000000..c9e43f20 --- /dev/null +++ b/scripts/handle_macos.sh @@ -0,0 +1,148 @@ +#!/bin/bash + +# macOS-specific installation handler +# Called from install.sh when OS=Mac + +handle_macos() { + local REPO_URL="$1" + + export INSTALL_DIR="$HOME/lightning_pub" + export LAUNCH_AGENTS_DIR="$HOME/Library/LaunchAgents" + mkdir -p "$LAUNCH_AGENTS_DIR" + + # Install Homebrew if needed + if ! command -v brew &> /dev/null; then + log "${PRIMARY_COLOR}Installing Homebrew...${RESET_COLOR}" + /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" || { + log_error "Failed to install Homebrew" 1 + } + fi + + # Install LND + log "${PRIMARY_COLOR}Installing${RESET_COLOR} ${SECONDARY_COLOR}LND${RESET_COLOR}..." + lnd_output=$(install_lnd) + install_result=$? + + if [ $install_result -ne 0 ]; then + log_error "LND installation failed" $install_result + fi + + lnd_status=$(echo "$lnd_output" | grep "LND_STATUS:" | cut -d':' -f2) + + case $lnd_status in + 0) log "LND fresh installation completed successfully." ;; + 1) log "LND upgrade completed successfully." ;; + 2) log "LND is already up-to-date. No action needed." ;; + *) log "WARNING: Unexpected status from install_lnd: $lnd_status" ;; + esac + + # Install Node.js + install_nodejs || log_error "Failed to install Node.js" 1 + + # Install Lightning.Pub + install_lightning_pub "$REPO_URL" || pub_install_status=$? + + case ${pub_install_status:-0} in + 0) + log "Lightning.Pub fresh installation completed successfully." + pub_upgrade_status=0 + ;; + 100) + log "Lightning.Pub upgrade completed successfully." + pub_upgrade_status=100 + ;; + 2) + log "Lightning.Pub is already up-to-date. No action needed." + pub_upgrade_status=2 + ;; + *) + log_error "Lightning.Pub installation failed with exit code $pub_install_status" "$pub_install_status" + ;; + esac + + # Start services using launchd + if [ "$pub_upgrade_status" -eq 0 ] || [ "$pub_upgrade_status" -eq 100 ]; then + log "Starting services..." + if [ "$lnd_status" = "0" ] || [ "$lnd_status" = "1" ]; then + log "Note: LND may take several minutes to sync block headers depending on network conditions." + fi + + create_launchd_plists + + # Start/restart services + for svc in local.lnd local.lightning_pub; do + local plist="$LAUNCH_AGENTS_DIR/${svc}.plist" + if launchctl list 2>/dev/null | grep -q "$svc"; then + launchctl unload "$plist" 2>/dev/null || true + fi + launchctl load "$plist" + done + log "${SECONDARY_COLOR}LND${RESET_COLOR} and ${SECONDARY_COLOR}Lightning.Pub${RESET_COLOR} services started." + + TIMESTAMP_FILE=$(mktemp) + export TIMESTAMP_FILE + get_log_info || log_error "Failed to get log info" 1 + fi + + log "Installation process completed successfully" + + if [ -d "$HOME/lightning_pub" ]; then + mv "$TMP_LOG_FILE" "$HOME/lightning_pub/install.log" + chmod 600 "$HOME/lightning_pub/install.log" + else + rm -f "$TMP_LOG_FILE" + fi +} + +create_launchd_plists() { + local NVM_DIR="$HOME/.nvm" + + # LND plist + if [ ! -f "$LAUNCH_AGENTS_DIR/local.lnd.plist" ]; then + cat > "$LAUNCH_AGENTS_DIR/local.lnd.plist" < + + + + Label + local.lnd + ProgramArguments + + $HOME/lnd/lnd + + RunAtLoad + + KeepAlive + + + +EOF + fi + + # Lightning.Pub plist + if [ ! -f "$LAUNCH_AGENTS_DIR/local.lightning_pub.plist" ]; then + cat > "$LAUNCH_AGENTS_DIR/local.lightning_pub.plist" < + + + + Label + local.lightning_pub + ProgramArguments + + /bin/bash + -c + source $NVM_DIR/nvm.sh && npm start + + WorkingDirectory + $INSTALL_DIR + RunAtLoad + + KeepAlive + + + +EOF + fi +} + diff --git a/scripts/install.sh b/scripts/install.sh index 171d0b1c..b25e79f0 100755 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -11,7 +11,7 @@ log() { echo -e "$(echo "$message" | sed 's/\\e\[[0-9;]*m//g')" >> "$TMP_LOG_FILE" } -SCRIPT_VERSION="0.2.2" +SCRIPT_VERSION="0.3.0" REPO="shocknet/Lightning.Pub" BRANCH="master" @@ -36,6 +36,7 @@ modules=( "install_nodejs" "install_lightning_pub" "start_services" + "handle_macos" "extract_nprofile" ) @@ -91,7 +92,7 @@ log "Detected OS: $OS" log "Detected ARCH: $ARCH" if [ "$OS" = "Mac" ]; then - log_error "macOS is not currently supported by this install script. Please use a Linux-based system." 1 + handle_macos "$REPO_URL" else # Explicit kickoff log for LND so the flow is clear in the install log log "${PRIMARY_COLOR}Installing${RESET_COLOR} ${SECONDARY_COLOR}LND${RESET_COLOR}..." diff --git a/scripts/install_lnd.sh b/scripts/install_lnd.sh index af53650c..7c09c725 100755 --- a/scripts/install_lnd.sh +++ b/scripts/install_lnd.sh @@ -9,15 +9,17 @@ install_lnd() { USER_NAME=$(whoami) log "Checking latest LND version..." - LND_VERSION=$(wget -qO- https://api.github.com/repos/lightningnetwork/lnd/releases/latest | grep -oP '"tag_name": "\K(.*)(?=")') + local api_response=$(wget -qO- https://api.github.com/repos/lightningnetwork/lnd/releases/latest) + LND_VERSION=$(json_value "tag_name" "$api_response") log "Latest LND version: $LND_VERSION" - LND_URL="https://github.com/lightningnetwork/lnd/releases/download/${LND_VERSION}/lnd-${OS}-${ARCH}-${LND_VERSION}.tar.gz" + local LND_OS="$OS"; [ "$OS" = "Mac" ] && LND_OS="darwin" + LND_URL="https://github.com/lightningnetwork/lnd/releases/download/${LND_VERSION}/lnd-${LND_OS}-${ARCH}-${LND_VERSION}.tar.gz" # Check if LND is already installed if [ -d "$USER_HOME/lnd" ]; then log "LND directory found. Checking current version..." - CURRENT_VERSION=$("$USER_HOME/lnd/lnd" --version | grep -oP 'version \K[^\s]+') + CURRENT_VERSION=$("$USER_HOME/lnd/lnd" --version | awk '/version/ {print $3}') log "Current LND version: $CURRENT_VERSION" if [ "$CURRENT_VERSION" == "${LND_VERSION#v}" ]; then @@ -60,12 +62,13 @@ install_lnd() { log "${PRIMARY_COLOR}Stopping${RESET_COLOR} ${SECONDARY_COLOR}LND${RESET_COLOR} user service..." systemctl --user stop lnd fi - else - log "${PRIMARY_COLOR}Please stop ${SECONDARY_COLOR}LND${RESET_COLOR} manually if it is running.${RESET_COLOR}" + elif [ "$OS" = "Mac" ] && launchctl list 2>/dev/null | grep -q "local.lnd"; then + log "${PRIMARY_COLOR}Stopping${RESET_COLOR} ${SECONDARY_COLOR}LND${RESET_COLOR} launchd service..." + launchctl unload "$USER_HOME/Library/LaunchAgents/local.lnd.plist" 2>/dev/null || true fi log "Extracting LND..." - LND_TMP_DIR=$(mktemp -d -p "$USER_HOME") + LND_TMP_DIR=$(mktemp_in "$USER_HOME") tar -xzf "$USER_HOME/lnd.tar.gz" -C "$LND_TMP_DIR" --strip-components=1 > /dev/null || { log "${PRIMARY_COLOR}Failed to extract LND.${RESET_COLOR}" @@ -106,7 +109,7 @@ install_lnd() { # Port conflict resolution. local lnd_port=9735 - if ! is_port_available $lnd_port; then + if [ "$OS" = "Linux" ] && ! is_port_available $lnd_port; then # The port is occupied. We should intervene if our service is either in a failed state # or not active at all (which covers fresh installs and failure loops). if systemctl --user -q is-failed lnd.service 2>/dev/null || ! systemctl --user -q is-active lnd.service 2>/dev/null; then @@ -114,7 +117,7 @@ install_lnd() { lnd_port_new=$(find_available_port $lnd_port) log "Configuring LND to use new port $lnd_port_new." - sed -i '/^listen=/d' $USER_HOME/.lnd/lnd.conf + sed_i '/^listen=/d' $USER_HOME/.lnd/lnd.conf echo "listen=0.0.0.0:$lnd_port_new" >> $USER_HOME/.lnd/lnd.conf log "LND configuration updated. The service will be restarted by the installer." else diff --git a/scripts/install_nodejs.sh b/scripts/install_nodejs.sh index 06601a20..786315aa 100755 --- a/scripts/install_nodejs.sh +++ b/scripts/install_nodejs.sh @@ -13,7 +13,8 @@ install_nodejs() { [ -s "${NVM_DIR}/nvm.sh" ] && \. "${NVM_DIR}/nvm.sh" if ! command -v nvm &> /dev/null; then - NVM_VERSION=$(wget -qO- https://api.github.com/repos/nvm-sh/nvm/releases/latest | grep -oP '"tag_name": "\K(.*)(?=")') + local nvm_api=$(wget -qO- https://api.github.com/repos/nvm-sh/nvm/releases/latest) + NVM_VERSION=$(json_value "tag_name" "$nvm_api") wget -qO- https://raw.githubusercontent.com/nvm-sh/nvm/${NVM_VERSION}/install.sh | bash > /dev/null 2>&1 export NVM_DIR="${NVM_DIR}" [ -s "${NVM_DIR}/nvm.sh" ] && \. "${NVM_DIR}/nvm.sh" diff --git a/scripts/start_services_mac.sh b/scripts/start_services_mac.sh deleted file mode 100755 index 81f817c1..00000000 --- a/scripts/start_services_mac.sh +++ /dev/null @@ -1,17 +0,0 @@ -#!/bin/bash - -start_services_mac() { - create_launchd_plist - launchctl load "${LAUNCH_AGENTS_DIR}/local.lnd.plist" - launchctl load "${LAUNCH_AGENTS_DIR}/local.lightning_pub.plist" - log "${SECONDARY_COLOR}LND${RESET_COLOR} and ${SECONDARY_COLOR}Lightning.Pub${RESET_COLOR} services started using launchd." -} - -handle_macos() { - check_homebrew - install_rsync_mac - install_nodejs - install_lightning_pub - create_launchd_plist - start_services_mac -} diff --git a/scripts/utils.sh b/scripts/utils.sh index 87dc5df6..0d7d9c39 100755 --- a/scripts/utils.sh +++ b/scripts/utils.sh @@ -29,10 +29,43 @@ detect_os_arch() { } check_deps() { - for cmd in wget grep stat tar sha256sum; do + for cmd in wget grep stat tar; do if ! command -v $cmd &> /dev/null; then log "Missing system dependency: $cmd. Install $cmd via your package manager and retry." exit 1 fi done + if ! command -v sha256sum &> /dev/null && ! command -v shasum &> /dev/null; then + log "Missing system dependency: sha256sum or shasum." + exit 1 + fi +} + +# Cross-platform sed in-place +sed_i() { + if [ "$OS" = "Mac" ]; then + sed -i '' "$@" + else + sed -i "$@" + fi +} + +# Cross-platform mktemp with parent directory +mktemp_in() { + local parent="$1" + if [ "$OS" = "Mac" ]; then + mktemp -d "${parent}/tmp.XXXXXX" + else + mktemp -d -p "$parent" + fi +} + +# Extract JSON value (cross-platform) +json_value() { + local key="$1" + if [ "$OS" = "Mac" ]; then + echo "$2" | awk -F"\"${key}\"[[:space:]]*:[[:space:]]*\"" '{print $2}' | awk -F'"' '{print $1}' | head -1 + else + echo "$2" | grep -oP "\"${key}\": \"\\K[^\"]+" + fi } \ No newline at end of file From 20dd93d8f0a386a8a4c18e55ccddbd96b5820c92 Mon Sep 17 00:00:00 2001 From: shocknet-justin Date: Wed, 26 Nov 2025 12:26:22 -0500 Subject: [PATCH 10/37] temp branch --- scripts/install.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/install.sh b/scripts/install.sh index b25e79f0..8f54170f 100755 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -13,7 +13,7 @@ log() { SCRIPT_VERSION="0.3.0" REPO="shocknet/Lightning.Pub" -BRANCH="master" +BRANCH="mac-install" cleanup() { log "Cleaning up temporary files..." From ba1984c106904086f1fbb06170a80e6ca6da3b82 Mon Sep 17 00:00:00 2001 From: shocknet-justin Date: Wed, 26 Nov 2025 12:39:56 -0500 Subject: [PATCH 11/37] curl fallback for mac --- scripts/handle_macos.sh | 9 +++----- scripts/install.sh | 15 ++++++++++++- scripts/install_lightning_pub.sh | 4 ++-- scripts/install_lnd.sh | 4 ++-- scripts/install_nodejs.sh | 4 ++-- scripts/utils.sh | 36 +++++++++++++++++++++++++++++++- 6 files changed, 58 insertions(+), 14 deletions(-) diff --git a/scripts/handle_macos.sh b/scripts/handle_macos.sh index c9e43f20..9474ae55 100644 --- a/scripts/handle_macos.sh +++ b/scripts/handle_macos.sh @@ -10,14 +10,11 @@ handle_macos() { export LAUNCH_AGENTS_DIR="$HOME/Library/LaunchAgents" mkdir -p "$LAUNCH_AGENTS_DIR" - # Install Homebrew if needed + # Install Homebrew if needed (skipped if not found, we don't force it) if ! command -v brew &> /dev/null; then - log "${PRIMARY_COLOR}Installing Homebrew...${RESET_COLOR}" - /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" || { - log_error "Failed to install Homebrew" 1 - } + log "${PRIMARY_COLOR}Homebrew not found. Proceeding without it...${RESET_COLOR}" fi - + # Install LND log "${PRIMARY_COLOR}Installing${RESET_COLOR} ${SECONDARY_COLOR}LND${RESET_COLOR}..." lnd_output=$(install_lnd) diff --git a/scripts/install.sh b/scripts/install.sh index 8f54170f..882df457 100755 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -28,6 +28,19 @@ log_error() { exit $2 } +# Helper to download files using wget or curl +download() { + local url="$1" + local dest="$2" + if command -v wget &> /dev/null; then + wget -q "$url" -O "$dest" + elif command -v curl &> /dev/null; then + curl -sL "$url" -o "$dest" + else + log_error "Neither wget nor curl found. Please install one." 1 + fi +} + modules=( "utils" @@ -66,7 +79,7 @@ SCRIPTS_URL="${BASE_URL}/scripts/" TMP_DIR=$(mktemp -d) for module in "${modules[@]}"; do - wget -q "${SCRIPTS_URL}${module}.sh" -O "${TMP_DIR}/${module}.sh" || log_error "Failed to download ${module}.sh" 1 + download "${SCRIPTS_URL}${module}.sh" "${TMP_DIR}/${module}.sh" || log_error "Failed to download ${module}.sh" 1 source "${TMP_DIR}/${module}.sh" || log_error "Failed to source ${module}.sh" 1 done diff --git a/scripts/install_lightning_pub.sh b/scripts/install_lightning_pub.sh index a5cb442c..36a959e7 100755 --- a/scripts/install_lightning_pub.sh +++ b/scripts/install_lightning_pub.sh @@ -19,7 +19,7 @@ install_lightning_pub() { USER_HOME=$HOME USER_NAME=$(whoami) - wget -q $REPO_URL -O $USER_HOME/lightning_pub.tar.gz > /dev/null 2>&1 || { + download "$REPO_URL" "$USER_HOME/lightning_pub.tar.gz" > /dev/null 2>&1 || { log "${PRIMARY_COLOR}Failed to download Lightning.Pub.${RESET_COLOR}" return 1 } @@ -37,7 +37,7 @@ install_lightning_pub() { log "Existing installation found. Checking for updates..." # Check if update is needed by comparing commit hashes - API_RESPONSE=$(wget -qO- "https://api.github.com/repos/${REPO}/commits/${BRANCH}" 2>&1 | tee /tmp/api_response.log) + API_RESPONSE=$(download_stdout "https://api.github.com/repos/${REPO}/commits/${BRANCH}" 2>&1 | tee /tmp/api_response.log) if grep -q '"message"[[:space:]]*:[[:space:]]*"API rate limit exceeded"' <<< "$API_RESPONSE"; then log_error "GitHub API rate limit exceeded. Please wait a while before trying again." 1 fi diff --git a/scripts/install_lnd.sh b/scripts/install_lnd.sh index 7c09c725..c525572b 100755 --- a/scripts/install_lnd.sh +++ b/scripts/install_lnd.sh @@ -9,7 +9,7 @@ install_lnd() { USER_NAME=$(whoami) log "Checking latest LND version..." - local api_response=$(wget -qO- https://api.github.com/repos/lightningnetwork/lnd/releases/latest) + local api_response=$(download_stdout "https://api.github.com/repos/lightningnetwork/lnd/releases/latest") LND_VERSION=$(json_value "tag_name" "$api_response") log "Latest LND version: $LND_VERSION" @@ -51,7 +51,7 @@ install_lnd() { log "${PRIMARY_COLOR}Downloading${RESET_COLOR} ${SECONDARY_COLOR}LND${RESET_COLOR}..." # Start the download - wget -q $LND_URL -O $USER_HOME/lnd.tar.gz || { + download "$LND_URL" "$USER_HOME/lnd.tar.gz" || { log "${PRIMARY_COLOR}Failed to download LND.${RESET_COLOR}" exit 1 } diff --git a/scripts/install_nodejs.sh b/scripts/install_nodejs.sh index 786315aa..53824e3c 100755 --- a/scripts/install_nodejs.sh +++ b/scripts/install_nodejs.sh @@ -13,9 +13,9 @@ install_nodejs() { [ -s "${NVM_DIR}/nvm.sh" ] && \. "${NVM_DIR}/nvm.sh" if ! command -v nvm &> /dev/null; then - local nvm_api=$(wget -qO- https://api.github.com/repos/nvm-sh/nvm/releases/latest) + local nvm_api=$(download_stdout "https://api.github.com/repos/nvm-sh/nvm/releases/latest") NVM_VERSION=$(json_value "tag_name" "$nvm_api") - wget -qO- https://raw.githubusercontent.com/nvm-sh/nvm/${NVM_VERSION}/install.sh | bash > /dev/null 2>&1 + download_stdout "https://raw.githubusercontent.com/nvm-sh/nvm/${NVM_VERSION}/install.sh" | bash > /dev/null 2>&1 export NVM_DIR="${NVM_DIR}" [ -s "${NVM_DIR}/nvm.sh" ] && \. "${NVM_DIR}/nvm.sh" fi diff --git a/scripts/utils.sh b/scripts/utils.sh index 0d7d9c39..7f5c8ccb 100755 --- a/scripts/utils.sh +++ b/scripts/utils.sh @@ -29,12 +29,19 @@ detect_os_arch() { } check_deps() { - for cmd in wget grep stat tar; do + for cmd in grep stat tar; do if ! command -v $cmd &> /dev/null; then log "Missing system dependency: $cmd. Install $cmd via your package manager and retry." exit 1 fi done + + # Check for wget or curl (one is required) + if ! command -v wget &> /dev/null && ! command -v curl &> /dev/null; then + log "Missing system dependency: wget or curl. Install via your package manager and retry." + exit 1 + fi + if ! command -v sha256sum &> /dev/null && ! command -v shasum &> /dev/null; then log "Missing system dependency: sha256sum or shasum." exit 1 @@ -68,4 +75,31 @@ json_value() { else echo "$2" | grep -oP "\"${key}\": \"\\K[^\"]+" fi +} + +# Download file (wget or curl) +download() { + local url="$1" + local dest="$2" + if command -v wget &> /dev/null; then + wget -q "$url" -O "$dest" + elif command -v curl &> /dev/null; then + curl -sL "$url" -o "$dest" + else + log "Error: Neither wget nor curl found." + return 1 + fi +} + +# Download to stdout (wget or curl) +download_stdout() { + local url="$1" + if command -v wget &> /dev/null; then + wget -qO- "$url" + elif command -v curl &> /dev/null; then + curl -sL "$url" + else + log "Error: Neither wget nor curl found." + return 1 + fi } \ No newline at end of file From f138b71406b2ac899510f6415d0f2f9419a017e3 Mon Sep 17 00:00:00 2001 From: shocknet-justin Date: Wed, 26 Nov 2025 12:48:13 -0500 Subject: [PATCH 12/37] mac debug --- scripts/handle_macos.sh | 1 + scripts/install.sh | 16 +++++++++------- scripts/install_lnd.sh | 5 +++++ scripts/utils.sh | 7 +++++-- 4 files changed, 20 insertions(+), 9 deletions(-) diff --git a/scripts/handle_macos.sh b/scripts/handle_macos.sh index 9474ae55..9c32f7ab 100644 --- a/scripts/handle_macos.sh +++ b/scripts/handle_macos.sh @@ -21,6 +21,7 @@ handle_macos() { install_result=$? if [ $install_result -ne 0 ]; then + printf "%s\n" "$lnd_output" log_error "LND installation failed" $install_result fi diff --git a/scripts/install.sh b/scripts/install.sh index 882df457..87d9842f 100755 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -6,9 +6,10 @@ TMP_LOG_FILE=$(mktemp) log() { local message="$(date '+%Y-%m-%d %H:%M:%S') $1" - echo -e "$message" - # Write to the temporary log file. - echo -e "$(echo "$message" | sed 's/\\e\[[0-9;]*m//g')" >> "$TMP_LOG_FILE" + # Use printf for cross-platform compatibility (macOS echo -e issues) + printf "%b\n" "$message" + # Write to the temporary log file (strip colors) + echo "$message" | sed 's/\\e\[[0-9;]*m//g' >> "$TMP_LOG_FILE" } SCRIPT_VERSION="0.3.0" @@ -35,7 +36,7 @@ download() { if command -v wget &> /dev/null; then wget -q "$url" -O "$dest" elif command -v curl &> /dev/null; then - curl -sL "$url" -o "$dest" + curl -fsL "$url" -o "$dest" else log_error "Neither wget nor curl found. Please install one." 1 fi @@ -112,9 +113,10 @@ else lnd_output=$(install_lnd) install_result=$? - if [ $install_result -ne 0 ]; then - log_error "LND installation failed" $install_result - fi +if [ $install_result -ne 0 ]; then + printf "%s\n" "$lnd_output" + log_error "LND installation failed" $install_result +fi lnd_status=$(echo "$lnd_output" | grep "LND_STATUS:" | cut -d':' -f2) diff --git a/scripts/install_lnd.sh b/scripts/install_lnd.sh index c525572b..ba1cb7d6 100755 --- a/scripts/install_lnd.sh +++ b/scripts/install_lnd.sh @@ -11,6 +11,11 @@ install_lnd() { log "Checking latest LND version..." local api_response=$(download_stdout "https://api.github.com/repos/lightningnetwork/lnd/releases/latest") LND_VERSION=$(json_value "tag_name" "$api_response") + + if [ -z "$LND_VERSION" ]; then + log "${PRIMARY_COLOR}Failed to fetch latest LND version.${RESET_COLOR}" + exit 1 + fi log "Latest LND version: $LND_VERSION" local LND_OS="$OS"; [ "$OS" = "Mac" ] && LND_OS="darwin" diff --git a/scripts/utils.sh b/scripts/utils.sh index 7f5c8ccb..30b88fe5 100755 --- a/scripts/utils.sh +++ b/scripts/utils.sh @@ -84,7 +84,10 @@ download() { if command -v wget &> /dev/null; then wget -q "$url" -O "$dest" elif command -v curl &> /dev/null; then - curl -sL "$url" -o "$dest" + # -f: fail on HTTP errors (404/500) + # -s: silent + # -L: follow redirects + curl -fsL "$url" -o "$dest" else log "Error: Neither wget nor curl found." return 1 @@ -97,7 +100,7 @@ download_stdout() { if command -v wget &> /dev/null; then wget -qO- "$url" elif command -v curl &> /dev/null; then - curl -sL "$url" + curl -fsL "$url" else log "Error: Neither wget nor curl found." return 1 From 6dc313e3c04ecf1283c53cf0dbb633c2418150dd Mon Sep 17 00:00:00 2001 From: shocknet-justin Date: Wed, 26 Nov 2025 13:41:40 -0500 Subject: [PATCH 13/37] mac debug --- scripts/handle_macos.sh | 8 +++++--- scripts/install.sh | 14 ++++++++------ scripts/install_lnd.sh | 8 ++++++-- 3 files changed, 19 insertions(+), 11 deletions(-) diff --git a/scripts/handle_macos.sh b/scripts/handle_macos.sh index 9c32f7ab..8fecd185 100644 --- a/scripts/handle_macos.sh +++ b/scripts/handle_macos.sh @@ -17,15 +17,17 @@ handle_macos() { # Install LND log "${PRIMARY_COLOR}Installing${RESET_COLOR} ${SECONDARY_COLOR}LND${RESET_COLOR}..." - lnd_output=$(install_lnd) + LND_STATUS_FILE=$(mktemp) + install_lnd "$LND_STATUS_FILE" install_result=$? if [ $install_result -ne 0 ]; then - printf "%s\n" "$lnd_output" + rm -f "$LND_STATUS_FILE" log_error "LND installation failed" $install_result fi - lnd_status=$(echo "$lnd_output" | grep "LND_STATUS:" | cut -d':' -f2) + lnd_status=$(cat "$LND_STATUS_FILE") + rm -f "$LND_STATUS_FILE" case $lnd_status in 0) log "LND fresh installation completed successfully." ;; diff --git a/scripts/install.sh b/scripts/install.sh index 87d9842f..e2121917 100755 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -110,15 +110,17 @@ if [ "$OS" = "Mac" ]; then else # Explicit kickoff log for LND so the flow is clear in the install log log "${PRIMARY_COLOR}Installing${RESET_COLOR} ${SECONDARY_COLOR}LND${RESET_COLOR}..." - lnd_output=$(install_lnd) + LND_STATUS_FILE=$(mktemp) + install_lnd "$LND_STATUS_FILE" install_result=$? -if [ $install_result -ne 0 ]; then - printf "%s\n" "$lnd_output" - log_error "LND installation failed" $install_result -fi + if [ $install_result -ne 0 ]; then + rm -f "$LND_STATUS_FILE" + log_error "LND installation failed" $install_result + fi - lnd_status=$(echo "$lnd_output" | grep "LND_STATUS:" | cut -d':' -f2) + lnd_status=$(cat "$LND_STATUS_FILE") + rm -f "$LND_STATUS_FILE" case $lnd_status in 0) log "LND fresh installation completed successfully." ;; diff --git a/scripts/install_lnd.sh b/scripts/install_lnd.sh index ba1cb7d6..be2fdd14 100755 --- a/scripts/install_lnd.sh +++ b/scripts/install_lnd.sh @@ -1,6 +1,7 @@ #!/bin/bash install_lnd() { + local status_file="$1" local lnd_status=0 log "Starting LND installation/check process..." @@ -134,7 +135,10 @@ install_lnd() { fi log "LND installation/check process complete. Status: $lnd_status" - # Echo the LND status - echo "LND_STATUS:$lnd_status" + + if [ -n "$status_file" ]; then + echo "$lnd_status" > "$status_file" + fi + return 0 # Always return 0 to indicate success } \ No newline at end of file From 438bea47a7f8fab9fa440f0971f0081b3bdc711c Mon Sep 17 00:00:00 2001 From: shocknet-justin Date: Wed, 26 Nov 2025 13:57:06 -0500 Subject: [PATCH 14/37] mac debug --- scripts/install_lnd.sh | 5 +++-- scripts/install_nodejs.sh | 4 ++++ 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/scripts/install_lnd.sh b/scripts/install_lnd.sh index be2fdd14..459e7eb4 100755 --- a/scripts/install_lnd.sh +++ b/scripts/install_lnd.sh @@ -14,8 +14,9 @@ install_lnd() { LND_VERSION=$(json_value "tag_name" "$api_response") if [ -z "$LND_VERSION" ]; then - log "${PRIMARY_COLOR}Failed to fetch latest LND version.${RESET_COLOR}" - exit 1 + # Fallback to a known stable version if GitHub API fails (e.g. rate limit) + LND_VERSION="v0.18.3-beta" + log "${PRIMARY_COLOR}Warning: Failed to fetch latest LND version from GitHub. Using fallback: ${LND_VERSION}${RESET_COLOR}" fi log "Latest LND version: $LND_VERSION" diff --git a/scripts/install_nodejs.sh b/scripts/install_nodejs.sh index 53824e3c..ce1b47eb 100755 --- a/scripts/install_nodejs.sh +++ b/scripts/install_nodejs.sh @@ -15,6 +15,10 @@ install_nodejs() { if ! command -v nvm &> /dev/null; then local nvm_api=$(download_stdout "https://api.github.com/repos/nvm-sh/nvm/releases/latest") NVM_VERSION=$(json_value "tag_name" "$nvm_api") + if [ -z "$NVM_VERSION" ]; then + NVM_VERSION="v0.39.7" + log "Warning: Failed to fetch latest NVM version. Using fallback: $NVM_VERSION" + fi download_stdout "https://raw.githubusercontent.com/nvm-sh/nvm/${NVM_VERSION}/install.sh" | bash > /dev/null 2>&1 export NVM_DIR="${NVM_DIR}" [ -s "${NVM_DIR}/nvm.sh" ] && \. "${NVM_DIR}/nvm.sh" From 688033769dada55cfc652504c181c81cd4176b10 Mon Sep 17 00:00:00 2001 From: shocknet-justin Date: Wed, 26 Nov 2025 16:25:43 -0500 Subject: [PATCH 15/37] mac restore --- scripts/install_lnd.sh | 8 +++----- scripts/install_nodejs.sh | 7 +++---- scripts/utils.sh | 25 ++++++++++++------------- 3 files changed, 18 insertions(+), 22 deletions(-) diff --git a/scripts/install_lnd.sh b/scripts/install_lnd.sh index 459e7eb4..3a0d47b9 100755 --- a/scripts/install_lnd.sh +++ b/scripts/install_lnd.sh @@ -10,13 +10,11 @@ install_lnd() { USER_NAME=$(whoami) log "Checking latest LND version..." - local api_response=$(download_stdout "https://api.github.com/repos/lightningnetwork/lnd/releases/latest") - LND_VERSION=$(json_value "tag_name" "$api_response") + LND_VERSION=$(get_latest_release_tag "lightningnetwork/lnd") if [ -z "$LND_VERSION" ]; then - # Fallback to a known stable version if GitHub API fails (e.g. rate limit) - LND_VERSION="v0.18.3-beta" - log "${PRIMARY_COLOR}Warning: Failed to fetch latest LND version from GitHub. Using fallback: ${LND_VERSION}${RESET_COLOR}" + log "${PRIMARY_COLOR}Failed to fetch latest LND version.${RESET_COLOR}" + exit 1 fi log "Latest LND version: $LND_VERSION" diff --git a/scripts/install_nodejs.sh b/scripts/install_nodejs.sh index ce1b47eb..da7fe411 100755 --- a/scripts/install_nodejs.sh +++ b/scripts/install_nodejs.sh @@ -13,11 +13,10 @@ install_nodejs() { [ -s "${NVM_DIR}/nvm.sh" ] && \. "${NVM_DIR}/nvm.sh" if ! command -v nvm &> /dev/null; then - local nvm_api=$(download_stdout "https://api.github.com/repos/nvm-sh/nvm/releases/latest") - NVM_VERSION=$(json_value "tag_name" "$nvm_api") + NVM_VERSION=$(get_latest_release_tag "nvm-sh/nvm") if [ -z "$NVM_VERSION" ]; then - NVM_VERSION="v0.39.7" - log "Warning: Failed to fetch latest NVM version. Using fallback: $NVM_VERSION" + log "Failed to fetch latest NVM version." + return 1 fi download_stdout "https://raw.githubusercontent.com/nvm-sh/nvm/${NVM_VERSION}/install.sh" | bash > /dev/null 2>&1 export NVM_DIR="${NVM_DIR}" diff --git a/scripts/utils.sh b/scripts/utils.sh index 30b88fe5..8c820e4a 100755 --- a/scripts/utils.sh +++ b/scripts/utils.sh @@ -81,28 +81,27 @@ json_value() { download() { local url="$1" local dest="$2" - if command -v wget &> /dev/null; then - wget -q "$url" -O "$dest" - elif command -v curl &> /dev/null; then - # -f: fail on HTTP errors (404/500) - # -s: silent - # -L: follow redirects + if [ "$OS" = "Mac" ]; then curl -fsL "$url" -o "$dest" else - log "Error: Neither wget nor curl found." - return 1 + wget -q "$url" -O "$dest" fi } # Download to stdout (wget or curl) download_stdout() { local url="$1" - if command -v wget &> /dev/null; then - wget -qO- "$url" - elif command -v curl &> /dev/null; then + if [ "$OS" = "Mac" ]; then curl -fsL "$url" else - log "Error: Neither wget nor curl found." - return 1 + wget -qO- "$url" fi +} + +# Get latest release tag from GitHub (via API) +get_latest_release_tag() { + local repo="$1" + local url="https://api.github.com/repos/${repo}/releases/latest" + local api_json=$(download_stdout "$url") + json_value "tag_name" "$api_json" } \ No newline at end of file From dd4d1562296c4d921fb4234b9287dec0bce2869d Mon Sep 17 00:00:00 2001 From: shocknet-justin Date: Wed, 26 Nov 2025 17:02:41 -0500 Subject: [PATCH 16/37] move os detection --- scripts/install.sh | 105 +++++++++++++++++++++++++++++++++++++++++--- scripts/utils.sh | 107 --------------------------------------------- 2 files changed, 99 insertions(+), 113 deletions(-) delete mode 100755 scripts/utils.sh diff --git a/scripts/install.sh b/scripts/install.sh index e2121917..6c8e7c76 100755 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -29,22 +29,115 @@ log_error() { exit $2 } -# Helper to download files using wget or curl +# Detect OS early for bootstrap +OS="$(uname -s)" +case "$OS" in + Linux*) OS=Linux;; + Darwin*) OS=Mac;; + *) OS="UNKNOWN" +esac + +detect_os_arch() { + # OS already detected above, but we need ARCH and systemctl check + ARCH="$(uname -m)" + case "$ARCH" in + x86_64) ARCH=amd64;; + arm64|aarch64|armv8*) ARCH=arm64;; + armv7*) ARCH=armv7;; + *) ARCH="UNKNOWN" + esac + + if [ "$OS" = "Linux" ] && command -v systemctl &> /dev/null; then + SYSTEMCTL_AVAILABLE=true + else + SYSTEMCTL_AVAILABLE=false + fi +} + +PRIMARY_COLOR="\e[38;5;208m" +SECONDARY_COLOR="\e[38;5;165m" +RESET_COLOR="\e[0m" + +check_deps() { + for cmd in grep stat tar; do + if ! command -v $cmd &> /dev/null; then + log "Missing system dependency: $cmd. Install $cmd via your package manager and retry." + exit 1 + fi + done + + # Check for wget or curl (one is required) + if ! command -v wget &> /dev/null && ! command -v curl &> /dev/null; then + log "Missing system dependency: wget or curl. Install via your package manager and retry." + exit 1 + fi + + if ! command -v sha256sum &> /dev/null && ! command -v shasum &> /dev/null; then + log "Missing system dependency: sha256sum or shasum." + exit 1 + fi +} + +# Cross-platform sed in-place +sed_i() { + if [ "$OS" = "Mac" ]; then + sed -i '' "$@" + else + sed -i "$@" + fi +} + +# Cross-platform mktemp with parent directory +mktemp_in() { + local parent="$1" + if [ "$OS" = "Mac" ]; then + mktemp -d "${parent}/tmp.XXXXXX" + else + mktemp -d -p "$parent" + fi +} + +# Extract JSON value (cross-platform) +json_value() { + local key="$1" + if [ "$OS" = "Mac" ]; then + echo "$2" | awk -F"\"${key}\"[[:space:]]*:[[:space:]]*\"" '{print $2}' | awk -F'"' '{print $1}' | head -1 + else + echo "$2" | grep -oP "\"${key}\": \"\\K[^\"]+" + fi +} + +# Download file (wget or curl) download() { local url="$1" local dest="$2" - if command -v wget &> /dev/null; then - wget -q "$url" -O "$dest" - elif command -v curl &> /dev/null; then + if [ "$OS" = "Mac" ]; then curl -fsL "$url" -o "$dest" else - log_error "Neither wget nor curl found. Please install one." 1 + wget -q "$url" -O "$dest" fi } +# Download to stdout (wget or curl) +download_stdout() { + local url="$1" + if [ "$OS" = "Mac" ]; then + curl -fsL "$url" + else + wget -qO- "$url" + fi +} + +# Get latest release tag from GitHub (via API) +get_latest_release_tag() { + local repo="$1" + local url="https://api.github.com/repos/${repo}/releases/latest" + local api_json=$(download_stdout "$url") + json_value "tag_name" "$api_json" +} + modules=( - "utils" "ports" "install_lnd" "install_nodejs" diff --git a/scripts/utils.sh b/scripts/utils.sh deleted file mode 100755 index 8c820e4a..00000000 --- a/scripts/utils.sh +++ /dev/null @@ -1,107 +0,0 @@ -#!/bin/bash - -PRIMARY_COLOR="\e[38;5;208m" -SECONDARY_COLOR="\e[38;5;165m" -RESET_COLOR="\e[0m" - -detect_os_arch() { - OS="$(uname -s)" - ARCH="$(uname -m)" - case "$OS" in - Linux*) OS=Linux;; - Darwin*) OS=Mac;; - CYGWIN*) OS=Cygwin;; - MINGW*) OS=MinGw;; - *) OS="UNKNOWN" - esac - case "$ARCH" in - x86_64) ARCH=amd64;; - arm64|aarch64|armv8*) ARCH=arm64;; - armv7*) ARCH=armv7;; - *) ARCH="UNKNOWN" - esac - - if [ "$OS" = "Linux" ] && command -v systemctl &> /dev/null; then - SYSTEMCTL_AVAILABLE=true - else - SYSTEMCTL_AVAILABLE=false - fi -} - -check_deps() { - for cmd in grep stat tar; do - if ! command -v $cmd &> /dev/null; then - log "Missing system dependency: $cmd. Install $cmd via your package manager and retry." - exit 1 - fi - done - - # Check for wget or curl (one is required) - if ! command -v wget &> /dev/null && ! command -v curl &> /dev/null; then - log "Missing system dependency: wget or curl. Install via your package manager and retry." - exit 1 - fi - - if ! command -v sha256sum &> /dev/null && ! command -v shasum &> /dev/null; then - log "Missing system dependency: sha256sum or shasum." - exit 1 - fi -} - -# Cross-platform sed in-place -sed_i() { - if [ "$OS" = "Mac" ]; then - sed -i '' "$@" - else - sed -i "$@" - fi -} - -# Cross-platform mktemp with parent directory -mktemp_in() { - local parent="$1" - if [ "$OS" = "Mac" ]; then - mktemp -d "${parent}/tmp.XXXXXX" - else - mktemp -d -p "$parent" - fi -} - -# Extract JSON value (cross-platform) -json_value() { - local key="$1" - if [ "$OS" = "Mac" ]; then - echo "$2" | awk -F"\"${key}\"[[:space:]]*:[[:space:]]*\"" '{print $2}' | awk -F'"' '{print $1}' | head -1 - else - echo "$2" | grep -oP "\"${key}\": \"\\K[^\"]+" - fi -} - -# Download file (wget or curl) -download() { - local url="$1" - local dest="$2" - if [ "$OS" = "Mac" ]; then - curl -fsL "$url" -o "$dest" - else - wget -q "$url" -O "$dest" - fi -} - -# Download to stdout (wget or curl) -download_stdout() { - local url="$1" - if [ "$OS" = "Mac" ]; then - curl -fsL "$url" - else - wget -qO- "$url" - fi -} - -# Get latest release tag from GitHub (via API) -get_latest_release_tag() { - local repo="$1" - local url="https://api.github.com/repos/${repo}/releases/latest" - local api_json=$(download_stdout "$url") - json_value "tag_name" "$api_json" -} \ No newline at end of file From 8e0bb785278be27fec35bad15506f4d290d48861 Mon Sep 17 00:00:00 2001 From: shocknet-justin Date: Wed, 26 Nov 2025 17:08:10 -0500 Subject: [PATCH 17/37] tests ignore script and readme --- .github/workflows/test.yaml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 37ce4f05..e5b26019 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -1,5 +1,9 @@ name: Docker Compose Actions Workflow -on: push +on: + push: + paths-ignore: + - 'scripts/**' + - 'README.md' jobs: test: runs-on: ubuntu-latest From 8924adedeb853495863ba3724038f7ef2ab982df Mon Sep 17 00:00:00 2001 From: shocknet-justin Date: Wed, 26 Nov 2025 17:10:27 -0500 Subject: [PATCH 18/37] parsing --- scripts/handle_macos.sh | 5 ----- scripts/install.sh | 2 +- 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/scripts/handle_macos.sh b/scripts/handle_macos.sh index 8fecd185..55b33d01 100644 --- a/scripts/handle_macos.sh +++ b/scripts/handle_macos.sh @@ -10,11 +10,6 @@ handle_macos() { export LAUNCH_AGENTS_DIR="$HOME/Library/LaunchAgents" mkdir -p "$LAUNCH_AGENTS_DIR" - # Install Homebrew if needed (skipped if not found, we don't force it) - if ! command -v brew &> /dev/null; then - log "${PRIMARY_COLOR}Homebrew not found. Proceeding without it...${RESET_COLOR}" - fi - # Install LND log "${PRIMARY_COLOR}Installing${RESET_COLOR} ${SECONDARY_COLOR}LND${RESET_COLOR}..." LND_STATUS_FILE=$(mktemp) diff --git a/scripts/install.sh b/scripts/install.sh index 6c8e7c76..e8bb749b 100755 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -101,7 +101,7 @@ mktemp_in() { json_value() { local key="$1" if [ "$OS" = "Mac" ]; then - echo "$2" | awk -F"\"${key}\"[[:space:]]*:[[:space:]]*\"" '{print $2}' | awk -F'"' '{print $1}' | head -1 + echo "$2" | grep "\"${key}\"" | awk -F'"' '{print $4}' | head -1 else echo "$2" | grep -oP "\"${key}\": \"\\K[^\"]+" fi From def5bde7ffb565a23b881817b2ec512f1757f7ee Mon Sep 17 00:00:00 2001 From: shocknet-justin Date: Wed, 26 Nov 2025 17:18:33 -0500 Subject: [PATCH 19/37] fix lnd update --- scripts/install_lnd.sh | 18 ++---------------- 1 file changed, 2 insertions(+), 16 deletions(-) diff --git a/scripts/install_lnd.sh b/scripts/install_lnd.sh index 3a0d47b9..1f3e5f18 100755 --- a/scripts/install_lnd.sh +++ b/scripts/install_lnd.sh @@ -31,22 +31,8 @@ install_lnd() { log "${SECONDARY_COLOR}LND${RESET_COLOR} is already up-to-date (version $CURRENT_VERSION)." lnd_status=2 # Set status to 2 to indicate no action needed else - if [ "$SKIP_PROMPT" != true ]; then - read -p "LND version $CURRENT_VERSION is installed. Do you want to upgrade to version $LND_VERSION? (y/N): " response - case "$response" in - [yY][eE][sS]|[yY]) - log "${PRIMARY_COLOR}Upgrading${RESET_COLOR} ${SECONDARY_COLOR}LND${RESET_COLOR} from version $CURRENT_VERSION to $LND_VERSION..." - lnd_status=1 # Set status to 1 to indicate upgrade - ;; - *) - log "$(date '+%Y-%m-%d %H:%M:%S') Upgrade cancelled." - lnd_status=2 # Set status to 2 to indicate no action needed - ;; - esac - else - log "${PRIMARY_COLOR}Upgrading${RESET_COLOR} ${SECONDARY_COLOR}LND${RESET_COLOR} from version $CURRENT_VERSION to $LND_VERSION..." - lnd_status=1 # Set status to 1 to indicate upgrade - fi + log "${PRIMARY_COLOR}Upgrading${RESET_COLOR} ${SECONDARY_COLOR}LND${RESET_COLOR} from version $CURRENT_VERSION to $LND_VERSION..." + lnd_status=1 # Set status to 1 to indicate upgrade fi else log "LND not found. Proceeding with fresh installation..." From 2d74e7edaaf294f2e2bbbcb849efd69eefd8da15 Mon Sep 17 00:00:00 2001 From: shocknet-justin Date: Wed, 26 Nov 2025 19:28:23 -0500 Subject: [PATCH 20/37] mac nvm debug --- scripts/install_nodejs.sh | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/scripts/install_nodejs.sh b/scripts/install_nodejs.sh index da7fe411..56e2e7f7 100755 --- a/scripts/install_nodejs.sh +++ b/scripts/install_nodejs.sh @@ -18,9 +18,17 @@ install_nodejs() { log "Failed to fetch latest NVM version." return 1 fi + log "Installing NVM ${NVM_VERSION}..." download_stdout "https://raw.githubusercontent.com/nvm-sh/nvm/${NVM_VERSION}/install.sh" | bash > /dev/null 2>&1 - export NVM_DIR="${NVM_DIR}" - [ -s "${NVM_DIR}/nvm.sh" ] && \. "${NVM_DIR}/nvm.sh" + fi + + # Source NVM + export NVM_DIR="${NVM_DIR}" + [ -s "${NVM_DIR}/nvm.sh" ] && \. "${NVM_DIR}/nvm.sh" + + if ! command -v nvm &> /dev/null; then + log "NVM installation failed." + return 1 fi if command -v node &> /dev/null; then @@ -35,8 +43,8 @@ install_nodejs() { log "Node.js is not installed. ${PRIMARY_COLOR}Installing the LTS version...${RESET_COLOR}" fi - # Silence all nvm output to keep installer logs clean - if ! bash -c "source ${NVM_DIR}/nvm.sh && nvm install --lts" >/dev/null 2>&1; then + # Install Node.js LTS + if ! nvm install --lts > /dev/null 2>&1; then log "${PRIMARY_COLOR}Failed to install Node.js.${RESET_COLOR}" return 1 fi From ed38dc28b59196830eebc3eea9f5aaf64cbca6ab Mon Sep 17 00:00:00 2001 From: shocknet-justin Date: Wed, 26 Nov 2025 19:30:05 -0500 Subject: [PATCH 21/37] mac nvm debug --- scripts/install_nodejs.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/install_nodejs.sh b/scripts/install_nodejs.sh index 56e2e7f7..48451ad5 100755 --- a/scripts/install_nodejs.sh +++ b/scripts/install_nodejs.sh @@ -19,7 +19,7 @@ install_nodejs() { return 1 fi log "Installing NVM ${NVM_VERSION}..." - download_stdout "https://raw.githubusercontent.com/nvm-sh/nvm/${NVM_VERSION}/install.sh" | bash > /dev/null 2>&1 + download_stdout "https://raw.githubusercontent.com/nvm-sh/nvm/${NVM_VERSION}/install.sh" | bash fi # Source NVM From 35ac191dd7ca226f79cb86e7f6ffb8104692bb33 Mon Sep 17 00:00:00 2001 From: shocknet-justin Date: Wed, 26 Nov 2025 21:55:50 -0500 Subject: [PATCH 22/37] mac nodejs --- scripts/install_nodejs.sh | 84 ++++++++++++++++++++++++++++++++++----- 1 file changed, 73 insertions(+), 11 deletions(-) diff --git a/scripts/install_nodejs.sh b/scripts/install_nodejs.sh index 48451ad5..68cfd88e 100755 --- a/scripts/install_nodejs.sh +++ b/scripts/install_nodejs.sh @@ -2,14 +2,80 @@ install_nodejs() { USER_HOME=$HOME - USER_NAME=$(whoami) - - export NVM_DIR="$USER_HOME/.nvm" + MINIMUM_VERSION="24.0.0" + log "${PRIMARY_COLOR}Checking${RESET_COLOR} for Node.js..." - MINIMUM_VERSION="18.0.0" + + if [ "$OS" = "Mac" ]; then + install_nodejs_mac + else + install_nodejs_linux + fi +} + +install_nodejs_mac() { + # Check if node exists and meets minimum version + if command -v node &> /dev/null; then + NODE_VERSION=$(node -v | sed 's/v//') + if [ "$(printf '%s\n' "$MINIMUM_VERSION" "$NODE_VERSION" | sort -V | head -n1)" = "$MINIMUM_VERSION" ]; then + log "Node.js is already installed and meets the minimum version requirement." + return 0 + fi + fi + + log "Node.js is not installed or outdated. ${PRIMARY_COLOR}Installing...${RESET_COLOR}" + + # Get latest LTS version from Node.js + local node_index=$(download_stdout "https://nodejs.org/dist/index.json") + local lts_version=$(echo "$node_index" | grep -o '"version":"v[0-9.]*","date":"[^"]*","files":\[[^]]*\],"npm":"[^"]*","v8":"[^"]*","uv":"[^"]*","zlib":"[^"]*","openssl":"[^"]*","modules":"[^"]*","lts":"[^"]*"' | grep -v '"lts":false' | head -1 | grep -o '"version":"v[^"]*"' | awk -F'"' '{print $4}') + + if [ -z "$lts_version" ]; then + log "Failed to fetch Node.js LTS version." + return 1 + fi + + log "Installing Node.js ${lts_version}..." + + local node_arch="x64" + [ "$ARCH" = "arm64" ] && node_arch="arm64" + + local node_url="https://nodejs.org/dist/${lts_version}/node-${lts_version}-darwin-${node_arch}.tar.gz" + local node_tar="$USER_HOME/node.tar.gz" + + download "$node_url" "$node_tar" || { + log "Failed to download Node.js." + return 1 + } + + # Extract to ~/node + rm -rf "$USER_HOME/node" + mkdir -p "$USER_HOME/node" + tar -xzf "$node_tar" -C "$USER_HOME/node" --strip-components=1 || { + log "Failed to extract Node.js." + rm -f "$node_tar" + return 1 + } + rm -f "$node_tar" + + # Add to PATH for current session + export PATH="$USER_HOME/node/bin:$PATH" + + # Add to shell profile if not already there + local shell_profile="$USER_HOME/.zshrc" + [ -f "$USER_HOME/.bash_profile" ] && shell_profile="$USER_HOME/.bash_profile" + + if ! grep -q 'node/bin' "$shell_profile" 2>/dev/null; then + echo 'export PATH="$HOME/node/bin:$PATH"' >> "$shell_profile" + fi + + log "Node.js ${lts_version} installation completed." + return 0 +} + +install_nodejs_linux() { + export NVM_DIR="$USER_HOME/.nvm" # Load nvm if it already exists - export NVM_DIR="${NVM_DIR}" [ -s "${NVM_DIR}/nvm.sh" ] && \. "${NVM_DIR}/nvm.sh" if ! command -v nvm &> /dev/null; then @@ -19,13 +85,10 @@ install_nodejs() { return 1 fi log "Installing NVM ${NVM_VERSION}..." - download_stdout "https://raw.githubusercontent.com/nvm-sh/nvm/${NVM_VERSION}/install.sh" | bash + download_stdout "https://raw.githubusercontent.com/nvm-sh/nvm/${NVM_VERSION}/install.sh" | bash > /dev/null 2>&1 + [ -s "${NVM_DIR}/nvm.sh" ] && \. "${NVM_DIR}/nvm.sh" fi - # Source NVM - export NVM_DIR="${NVM_DIR}" - [ -s "${NVM_DIR}/nvm.sh" ] && \. "${NVM_DIR}/nvm.sh" - if ! command -v nvm &> /dev/null; then log "NVM installation failed." return 1 @@ -43,7 +106,6 @@ install_nodejs() { log "Node.js is not installed. ${PRIMARY_COLOR}Installing the LTS version...${RESET_COLOR}" fi - # Install Node.js LTS if ! nvm install --lts > /dev/null 2>&1; then log "${PRIMARY_COLOR}Failed to install Node.js.${RESET_COLOR}" return 1 From 740f5188959a6aa64c078f9a4f60bfc72f6d4809 Mon Sep 17 00:00:00 2001 From: shocknet-justin Date: Wed, 26 Nov 2025 22:23:30 -0500 Subject: [PATCH 23/37] mac wrappers and aliases --- scripts/extract_nprofile.sh | 12 +++++++-- scripts/handle_macos.sh | 54 ++++++++++++++++++++++++++++++++++--- 2 files changed, 60 insertions(+), 6 deletions(-) diff --git a/scripts/extract_nprofile.sh b/scripts/extract_nprofile.sh index 704b7b36..c5b86610 100644 --- a/scripts/extract_nprofile.sh +++ b/scripts/extract_nprofile.sh @@ -16,7 +16,11 @@ get_log_info() { fi # Get the modification time of the timestamp file as a UNIX timestamp - ref_timestamp=$(stat -c %Y "$TIMESTAMP_FILE") + if [ "$OS" = "Mac" ]; then + ref_timestamp=$(stat -f %m "$TIMESTAMP_FILE") + else + ref_timestamp=$(stat -c %Y "$TIMESTAMP_FILE") + fi # Wait for a new unlocker log file to be created while [ $(($(date +%s) - START_TIME)) -lt $MAX_WAIT_TIME ]; do @@ -24,7 +28,11 @@ get_log_info() { # Loop through log files and check their modification time for log_file in "${LOG_DIR}/components/"unlocker_*.log; do if [ -f "$log_file" ]; then - file_timestamp=$(stat -c %Y "$log_file") + if [ "$OS" = "Mac" ]; then + file_timestamp=$(stat -f %m "$log_file") + else + file_timestamp=$(stat -c %Y "$log_file") + fi if [ "$file_timestamp" -gt "$ref_timestamp" ]; then latest_unlocker_log="$log_file" break # Found the newest log file diff --git a/scripts/handle_macos.sh b/scripts/handle_macos.sh index 55b33d01..e5a9d9c0 100644 --- a/scripts/handle_macos.sh +++ b/scripts/handle_macos.sh @@ -91,7 +91,47 @@ handle_macos() { create_launchd_plists() { local NVM_DIR="$HOME/.nvm" + local NODE_BIN="$HOME/node/bin" + # Create wrapper scripts so macOS shows proper names in Background Items + mkdir -p "$HOME/.local/bin" + + # LND wrapper + cat > "$HOME/.local/bin/LND" < "$HOME/.local/bin/Lightning.Pub" </dev/null; then + cat >> "$shell_profile" <<'ALIASES' + +# Lightning.Pub service management +alias lpub-start='launchctl load ~/Library/LaunchAgents/local.lightning_pub.plist ~/Library/LaunchAgents/local.lnd.plist' +alias lpub-stop='launchctl unload ~/Library/LaunchAgents/local.lightning_pub.plist ~/Library/LaunchAgents/local.lnd.plist' +alias lpub-restart='lpub-stop; lpub-start' +alias lpub-log='tail -f ~/Library/Logs/Lightning.Pub/pub.log' +alias lnd-log='tail -f ~/Library/Logs/Lightning.Pub/lnd.log' +alias lpub-status='launchctl list | grep local.lightning_pub; launchctl list | grep local.lnd' +ALIASES + fi + + # Create log directory + mkdir -p "$HOME/Library/Logs/Lightning.Pub" + # LND plist if [ ! -f "$LAUNCH_AGENTS_DIR/local.lnd.plist" ]; then cat > "$LAUNCH_AGENTS_DIR/local.lnd.plist" <local.lnd ProgramArguments - $HOME/lnd/lnd + $HOME/.local/bin/LND RunAtLoad KeepAlive + StandardOutPath + $HOME/Library/Logs/Lightning.Pub/lnd.log + StandardErrorPath + $HOME/Library/Logs/Lightning.Pub/lnd.log EOF @@ -125,9 +169,7 @@ EOF local.lightning_pub ProgramArguments - /bin/bash - -c - source $NVM_DIR/nvm.sh && npm start + $HOME/.local/bin/Lightning.Pub WorkingDirectory $INSTALL_DIR @@ -135,6 +177,10 @@ EOF KeepAlive + StandardOutPath + $HOME/Library/Logs/Lightning.Pub/pub.log + StandardErrorPath + $HOME/Library/Logs/Lightning.Pub/pub.log EOF From 471464905fb9685ae51032ba7b3575de4bfb9255 Mon Sep 17 00:00:00 2001 From: shocknet-justin Date: Wed, 26 Nov 2025 22:36:12 -0500 Subject: [PATCH 24/37] mac fixes --- scripts/install.sh | 2 +- scripts/install_lnd.sh | 35 +++++++++++++++++++++-------------- scripts/install_nodejs.sh | 2 +- 3 files changed, 23 insertions(+), 16 deletions(-) diff --git a/scripts/install.sh b/scripts/install.sh index e8bb749b..9797d212 100755 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -101,7 +101,7 @@ mktemp_in() { json_value() { local key="$1" if [ "$OS" = "Mac" ]; then - echo "$2" | grep "\"${key}\"" | awk -F'"' '{print $4}' | head -1 + echo "$2" | grep "\"${key}\"" | awk -F'"' '{print $4; exit}' else echo "$2" | grep -oP "\"${key}\": \"\\K[^\"]+" fi diff --git a/scripts/install_lnd.sh b/scripts/install_lnd.sh index 1f3e5f18..13c87f59 100755 --- a/scripts/install_lnd.sh +++ b/scripts/install_lnd.sh @@ -9,6 +9,13 @@ install_lnd() { USER_HOME=$HOME USER_NAME=$(whoami) + # LND data directory (Mac uses Application Support, Linux uses .lnd) + if [ "$OS" = "Mac" ]; then + LND_DIR="$USER_HOME/Library/Application Support/Lnd" + else + LND_DIR="$USER_HOME/.lnd" + fi + log "Checking latest LND version..." LND_VERSION=$(get_latest_release_tag "lightningnetwork/lnd") @@ -80,23 +87,23 @@ install_lnd() { exit 1 } - # Create .lnd directory if it doesn't exist - mkdir -p $USER_HOME/.lnd + # Create LND data directory if it doesn't exist + mkdir -p "$LND_DIR" # Ensure lnd.conf exists. - touch $USER_HOME/.lnd/lnd.conf + touch "$LND_DIR/lnd.conf" # Check for and add default settings only if the keys are missing. - grep -q "^bitcoin.mainnet=" $USER_HOME/.lnd/lnd.conf || echo "bitcoin.mainnet=true" >> $USER_HOME/.lnd/lnd.conf - grep -q "^bitcoin.node=" $USER_HOME/.lnd/lnd.conf || echo "bitcoin.node=neutrino" >> $USER_HOME/.lnd/lnd.conf - grep -q "^neutrino.addpeer=neutrino.shock.network" $USER_HOME/.lnd/lnd.conf || echo "neutrino.addpeer=neutrino.shock.network" >> $USER_HOME/.lnd/lnd.conf - grep -q "^neutrino.addpeer=asia.blixtwallet.com" $USER_HOME/.lnd/lnd.conf || echo "neutrino.addpeer=asia.blixtwallet.com" >> $USER_HOME/.lnd/lnd.conf - grep -q "^neutrino.addpeer=europe.blixtwallet.com" $USER_HOME/.lnd/lnd.conf || echo "neutrino.addpeer=europe.blixtwallet.com" >> $USER_HOME/.lnd/lnd.conf - grep -q "^neutrino.addpeer=btcd.lnolymp.us" $USER_HOME/.lnd/lnd.conf || echo "neutrino.addpeer=btcd.lnolymp.us" >> $USER_HOME/.lnd/lnd.conf - grep -q "^neutrino.addpeer=btcd-mainnet.lightning.computer" $USER_HOME/.lnd/lnd.conf || echo "neutrino.addpeer=btcd-mainnet.lightning.computer" >> $USER_HOME/.lnd/lnd.conf - grep -q "^fee.url=" $USER_HOME/.lnd/lnd.conf || echo "fee.url=https://nodes.lightning.computer/fees/v1/btc-fee-estimates.json" >> $USER_HOME/.lnd/lnd.conf + grep -q "^bitcoin.mainnet=" "$LND_DIR/lnd.conf" || echo "bitcoin.mainnet=true" >> "$LND_DIR/lnd.conf" + grep -q "^bitcoin.node=" "$LND_DIR/lnd.conf" || echo "bitcoin.node=neutrino" >> "$LND_DIR/lnd.conf" + grep -q "^neutrino.addpeer=neutrino.shock.network" "$LND_DIR/lnd.conf" || echo "neutrino.addpeer=neutrino.shock.network" >> "$LND_DIR/lnd.conf" + grep -q "^neutrino.addpeer=asia.blixtwallet.com" "$LND_DIR/lnd.conf" || echo "neutrino.addpeer=asia.blixtwallet.com" >> "$LND_DIR/lnd.conf" + grep -q "^neutrino.addpeer=europe.blixtwallet.com" "$LND_DIR/lnd.conf" || echo "neutrino.addpeer=europe.blixtwallet.com" >> "$LND_DIR/lnd.conf" + grep -q "^neutrino.addpeer=btcd.lnolymp.us" "$LND_DIR/lnd.conf" || echo "neutrino.addpeer=btcd.lnolymp.us" >> "$LND_DIR/lnd.conf" + grep -q "^neutrino.addpeer=btcd-mainnet.lightning.computer" "$LND_DIR/lnd.conf" || echo "neutrino.addpeer=btcd-mainnet.lightning.computer" >> "$LND_DIR/lnd.conf" + grep -q "^fee.url=" "$LND_DIR/lnd.conf" || echo "fee.url=https://nodes.lightning.computer/fees/v1/btc-fee-estimates.json" >> "$LND_DIR/lnd.conf" - chmod 600 $USER_HOME/.lnd/lnd.conf + chmod 600 "$LND_DIR/lnd.conf" # Port conflict resolution. local lnd_port=9735 @@ -108,8 +115,8 @@ install_lnd() { lnd_port_new=$(find_available_port $lnd_port) log "Configuring LND to use new port $lnd_port_new." - sed_i '/^listen=/d' $USER_HOME/.lnd/lnd.conf - echo "listen=0.0.0.0:$lnd_port_new" >> $USER_HOME/.lnd/lnd.conf + sed_i '/^listen=/d' "$LND_DIR/lnd.conf" + echo "listen=0.0.0.0:$lnd_port_new" >> "$LND_DIR/lnd.conf" log "LND configuration updated. The service will be restarted by the installer." else log "Port $lnd_port is in use by a healthy LND service (assumed to be our own). No changes will be made." diff --git a/scripts/install_nodejs.sh b/scripts/install_nodejs.sh index 68cfd88e..14ded9cf 100755 --- a/scripts/install_nodejs.sh +++ b/scripts/install_nodejs.sh @@ -27,7 +27,7 @@ install_nodejs_mac() { # Get latest LTS version from Node.js local node_index=$(download_stdout "https://nodejs.org/dist/index.json") - local lts_version=$(echo "$node_index" | grep -o '"version":"v[0-9.]*","date":"[^"]*","files":\[[^]]*\],"npm":"[^"]*","v8":"[^"]*","uv":"[^"]*","zlib":"[^"]*","openssl":"[^"]*","modules":"[^"]*","lts":"[^"]*"' | grep -v '"lts":false' | head -1 | grep -o '"version":"v[^"]*"' | awk -F'"' '{print $4}') + local lts_version=$(echo "$node_index" | grep -o '"version":"v[0-9.]*"[^}]*"lts":"[^"]*"' | grep -v '"lts":false' | awk -F'"' '{print $4; exit}') if [ -z "$lts_version" ]; then log "Failed to fetch Node.js LTS version." From 67425e6c221a62f0da98e7c1a49bc078ce29e954 Mon Sep 17 00:00:00 2001 From: shocknet-justin Date: Wed, 26 Nov 2025 23:07:06 -0500 Subject: [PATCH 25/37] mac fixes --- scripts/install_lightning_pub.sh | 9 +++++++++ scripts/install_nodejs.sh | 2 +- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/scripts/install_lightning_pub.sh b/scripts/install_lightning_pub.sh index 36a959e7..4577a485 100755 --- a/scripts/install_lightning_pub.sh +++ b/scripts/install_lightning_pub.sh @@ -89,6 +89,15 @@ install_lightning_pub() { upgrade_status=0 mkdir -p "$(dirname "$INSTALL_DIR")" mv "$EXTRACT_DIR" "$INSTALL_DIR" + + # Create .env with OS-specific LND paths + if [ "$OS" = "Mac" ]; then + cat > "$INSTALL_DIR/.env" < /dev/null; then NODE_VERSION=$(node -v | sed 's/v//') - if [ "$(printf '%s\n' "$MINIMUM_VERSION" "$NODE_VERSION" | sort -V | head -n1)" = "$MINIMUM_VERSION" ]; then + if [ "$(printf '%s\n' "$MINIMUM_VERSION" "$NODE_VERSION" | sort -V | awk 'NR==1')" = "$MINIMUM_VERSION" ]; then log "Node.js is already installed and meets the minimum version requirement." return 0 fi From 1f119e1419b1e8c89ab45a54d0e99c99ce59ae16 Mon Sep 17 00:00:00 2001 From: shocknet-justin Date: Wed, 26 Nov 2025 23:34:07 -0500 Subject: [PATCH 26/37] mac fixes --- scripts/install_lightning_pub.sh | 1 + scripts/install_lnd.sh | 12 ++---------- scripts/install_nodejs.sh | 7 +++---- 3 files changed, 6 insertions(+), 14 deletions(-) diff --git a/scripts/install_lightning_pub.sh b/scripts/install_lightning_pub.sh index 4577a485..d05765fc 100755 --- a/scripts/install_lightning_pub.sh +++ b/scripts/install_lightning_pub.sh @@ -96,6 +96,7 @@ install_lightning_pub() { LND_ADDRESS=127.0.0.1:10009 LND_CERT_PATH=$HOME/Library/Application Support/Lnd/tls.cert LND_MACAROON_PATH=$HOME/Library/Application Support/Lnd/data/chain/bitcoin/mainnet/admin.macaroon +LND_LOG_DIR=$HOME/Library/Application Support/Lnd/logs/bitcoin/mainnet EOF fi fi diff --git a/scripts/install_lnd.sh b/scripts/install_lnd.sh index 13c87f59..126471b0 100755 --- a/scripts/install_lnd.sh +++ b/scripts/install_lnd.sh @@ -4,8 +4,6 @@ install_lnd() { local status_file="$1" local lnd_status=0 - log "Starting LND installation/check process..." - USER_HOME=$HOME USER_NAME=$(whoami) @@ -16,33 +14,27 @@ install_lnd() { LND_DIR="$USER_HOME/.lnd" fi - log "Checking latest LND version..." LND_VERSION=$(get_latest_release_tag "lightningnetwork/lnd") if [ -z "$LND_VERSION" ]; then log "${PRIMARY_COLOR}Failed to fetch latest LND version.${RESET_COLOR}" exit 1 fi - log "Latest LND version: $LND_VERSION" local LND_OS="$OS"; [ "$OS" = "Mac" ] && LND_OS="darwin" LND_URL="https://github.com/lightningnetwork/lnd/releases/download/${LND_VERSION}/lnd-${LND_OS}-${ARCH}-${LND_VERSION}.tar.gz" # Check if LND is already installed if [ -d "$USER_HOME/lnd" ]; then - log "LND directory found. Checking current version..." CURRENT_VERSION=$("$USER_HOME/lnd/lnd" --version | awk '/version/ {print $3}') - log "Current LND version: $CURRENT_VERSION" if [ "$CURRENT_VERSION" == "${LND_VERSION#v}" ]; then - log "${SECONDARY_COLOR}LND${RESET_COLOR} is already up-to-date (version $CURRENT_VERSION)." + log "${SECONDARY_COLOR}LND${RESET_COLOR} is already up-to-date (${LND_VERSION})." lnd_status=2 # Set status to 2 to indicate no action needed else - log "${PRIMARY_COLOR}Upgrading${RESET_COLOR} ${SECONDARY_COLOR}LND${RESET_COLOR} from version $CURRENT_VERSION to $LND_VERSION..." + log "${PRIMARY_COLOR}Upgrading${RESET_COLOR} ${SECONDARY_COLOR}LND${RESET_COLOR} from ${CURRENT_VERSION} to ${LND_VERSION}..." lnd_status=1 # Set status to 1 to indicate upgrade fi - else - log "LND not found. Proceeding with fresh installation..." fi if [ $lnd_status -eq 0 ] || [ $lnd_status -eq 1 ]; then diff --git a/scripts/install_nodejs.sh b/scripts/install_nodejs.sh index e23dc11a..aebcc368 100755 --- a/scripts/install_nodejs.sh +++ b/scripts/install_nodejs.sh @@ -23,18 +23,17 @@ install_nodejs_mac() { fi fi - log "Node.js is not installed or outdated. ${PRIMARY_COLOR}Installing...${RESET_COLOR}" - # Get latest LTS version from Node.js local node_index=$(download_stdout "https://nodejs.org/dist/index.json") - local lts_version=$(echo "$node_index" | grep -o '"version":"v[0-9.]*"[^}]*"lts":"[^"]*"' | grep -v '"lts":false' | awk -F'"' '{print $4; exit}') + local lts_line=$(printf '%s' "$node_index" | grep -o '"version":"v[0-9.]*"[^}]*"lts":"[A-Za-z]*"' | grep -v '"lts":false' | head -1) + local lts_version=$(printf '%s' "$lts_line" | awk -F'"' '{print $4}') if [ -z "$lts_version" ]; then log "Failed to fetch Node.js LTS version." return 1 fi - log "Installing Node.js ${lts_version}..." + log "${PRIMARY_COLOR}Installing${RESET_COLOR} Node.js ${lts_version}..." local node_arch="x64" [ "$ARCH" = "arm64" ] && node_arch="arm64" From d81f81bf87e1572496c49dd17b827999bbb7e005 Mon Sep 17 00:00:00 2001 From: shocknet-justin Date: Wed, 26 Nov 2025 23:40:25 -0500 Subject: [PATCH 27/37] default mac paths --- scripts/install_lightning_pub.sh | 9 --------- src/services/main/settings.ts | 13 ++++++++++--- 2 files changed, 10 insertions(+), 12 deletions(-) diff --git a/scripts/install_lightning_pub.sh b/scripts/install_lightning_pub.sh index d05765fc..cbb902c8 100755 --- a/scripts/install_lightning_pub.sh +++ b/scripts/install_lightning_pub.sh @@ -90,15 +90,6 @@ install_lightning_pub() { mkdir -p "$(dirname "$INSTALL_DIR")" mv "$EXTRACT_DIR" "$INSTALL_DIR" - # Create .env with OS-specific LND paths - if [ "$OS" = "Mac" ]; then - cat > "$INSTALL_DIR/.env" < { return path.join(homeDir, filepath); } +const lndDir = () => { + if (os.platform() === 'darwin') { + return path.join(os.homedir(), 'Library', 'Application Support', 'Lnd'); + } + return resolveHome('/.lnd'); +} + export const LoadLndNodeSettingsFromEnv = (dbEnv: Record, addToDb?: EnvCacher): LndNodeSettings => { return { lndAddr: chooseEnv('LND_ADDRESS', dbEnv, "127.0.0.1:10009", addToDb), - lndCertPath: chooseEnv('LND_CERT_PATH', dbEnv, resolveHome("/.lnd/tls.cert"), addToDb), - lndMacaroonPath: chooseEnv('LND_MACAROON_PATH', dbEnv, resolveHome("/.lnd/data/chain/bitcoin/mainnet/admin.macaroon"), addToDb), + lndCertPath: chooseEnv('LND_CERT_PATH', dbEnv, path.join(lndDir(), "tls.cert"), addToDb), + lndMacaroonPath: chooseEnv('LND_MACAROON_PATH', dbEnv, path.join(lndDir(), "data", "chain", "bitcoin", "mainnet", "admin.macaroon"), addToDb), } } export const LoadLndSettingsFromEnv = (dbEnv: Record, addToDb?: EnvCacher): LndSettings => { const feeRateBps: number = chooseEnvInt('OUTBOUND_MAX_FEE_BPS', dbEnv, 60, addToDb) return { - lndLogDir: chooseEnv('LND_LOG_DIR', dbEnv, resolveHome("/.lnd/logs/bitcoin/mainnet/lnd.log"), addToDb), + lndLogDir: chooseEnv('LND_LOG_DIR', dbEnv, path.join(lndDir(), "logs", "bitcoin", "mainnet", "lnd.log"), addToDb), feeRateBps: feeRateBps, feeRateLimit: feeRateBps / 10000, feeFixedLimit: chooseEnvInt('OUTBOUND_MAX_FEE_EXTRA_SATS', dbEnv, 100, addToDb), From 8cbe5896466af23f1a8efd22a0f08cf47a082e2f Mon Sep 17 00:00:00 2001 From: shocknet-justin Date: Thu, 27 Nov 2025 00:02:26 -0500 Subject: [PATCH 28/37] clean logs --- scripts/install_lnd.sh | 10 ++-------- scripts/install_nodejs.sh | 7 ++++--- 2 files changed, 6 insertions(+), 11 deletions(-) diff --git a/scripts/install_lnd.sh b/scripts/install_lnd.sh index 126471b0..bdb97310 100755 --- a/scripts/install_lnd.sh +++ b/scripts/install_lnd.sh @@ -57,7 +57,6 @@ install_lnd() { launchctl unload "$USER_HOME/Library/LaunchAgents/local.lnd.plist" 2>/dev/null || true fi - log "Extracting LND..." LND_TMP_DIR=$(mktemp_in "$USER_HOME") tar -xzf "$USER_HOME/lnd.tar.gz" -C "$LND_TMP_DIR" --strip-components=1 > /dev/null || { @@ -69,10 +68,7 @@ install_lnd() { rm "$USER_HOME/lnd.tar.gz" - if [ -d "$USER_HOME/lnd" ]; then - log "Removing old LND directory..." - rm -rf "$USER_HOME/lnd" - fi + rm -rf "$USER_HOME/lnd" 2>/dev/null || true mv "$LND_TMP_DIR" "$USER_HOME/lnd" || { log "${PRIMARY_COLOR}Failed to move new LND version into place.${RESET_COLOR}" @@ -115,11 +111,9 @@ install_lnd() { fi fi - log "${SECONDARY_COLOR}LND${RESET_COLOR} installation and configuration completed." + log "${SECONDARY_COLOR}LND${RESET_COLOR} installed successfully." fi - log "LND installation/check process complete. Status: $lnd_status" - if [ -n "$status_file" ]; then echo "$lnd_status" > "$status_file" fi diff --git a/scripts/install_nodejs.sh b/scripts/install_nodejs.sh index aebcc368..e9c96aaa 100755 --- a/scripts/install_nodejs.sh +++ b/scripts/install_nodejs.sh @@ -24,9 +24,10 @@ install_nodejs_mac() { fi # Get latest LTS version from Node.js - local node_index=$(download_stdout "https://nodejs.org/dist/index.json") - local lts_line=$(printf '%s' "$node_index" | grep -o '"version":"v[0-9.]*"[^}]*"lts":"[A-Za-z]*"' | grep -v '"lts":false' | head -1) - local lts_version=$(printf '%s' "$lts_line" | awk -F'"' '{print $4}') + local node_tmp=$(mktemp) + download_stdout "https://nodejs.org/dist/index.json" > "$node_tmp" + local lts_version=$(grep -o '"version":"v[0-9.]*"[^}]*"lts":"[A-Za-z]*"' "$node_tmp" | grep -v '"lts":false' | head -1 | awk -F'"' '{print $4}') + rm -f "$node_tmp" if [ -z "$lts_version" ]; then log "Failed to fetch Node.js LTS version." From bf466126f5e5c167aa75549aefc516234120cb4f Mon Sep 17 00:00:00 2001 From: shocknet-justin Date: Thu, 27 Nov 2025 00:33:39 -0500 Subject: [PATCH 29/37] mac alias hint --- scripts/handle_macos.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/scripts/handle_macos.sh b/scripts/handle_macos.sh index e5a9d9c0..30206511 100644 --- a/scripts/handle_macos.sh +++ b/scripts/handle_macos.sh @@ -80,6 +80,7 @@ handle_macos() { fi log "Installation process completed successfully" + log "Run 'source ~/.zshrc' or open a new terminal to use lpub-status, lpub-log, etc." if [ -d "$HOME/lightning_pub" ]; then mv "$TMP_LOG_FILE" "$HOME/lightning_pub/install.log" From 0baa343c3ec674dc90a90944a1943bfb1c8dfa1f Mon Sep 17 00:00:00 2001 From: shocknet-justin Date: Thu, 27 Nov 2025 00:44:35 -0500 Subject: [PATCH 30/37] clean logs --- scripts/handle_macos.sh | 7 ------- scripts/install.sh | 9 +-------- 2 files changed, 1 insertion(+), 15 deletions(-) diff --git a/scripts/handle_macos.sh b/scripts/handle_macos.sh index 30206511..e2d05c08 100644 --- a/scripts/handle_macos.sh +++ b/scripts/handle_macos.sh @@ -23,13 +23,6 @@ handle_macos() { lnd_status=$(cat "$LND_STATUS_FILE") rm -f "$LND_STATUS_FILE" - - case $lnd_status in - 0) log "LND fresh installation completed successfully." ;; - 1) log "LND upgrade completed successfully." ;; - 2) log "LND is already up-to-date. No action needed." ;; - *) log "WARNING: Unexpected status from install_lnd: $lnd_status" ;; - esac # Install Node.js install_nodejs || log_error "Failed to install Node.js" 1 diff --git a/scripts/install.sh b/scripts/install.sh index 9797d212..c101c888 100755 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -214,14 +214,7 @@ else lnd_status=$(cat "$LND_STATUS_FILE") rm -f "$LND_STATUS_FILE" - - case $lnd_status in - 0) log "LND fresh installation completed successfully." ;; - 1) log "LND upgrade completed successfully." ;; - 2) log "LND is already up-to-date. No action needed." ;; - *) log "WARNING: Unexpected status from install_lnd: $lnd_status" ;; - esac - + install_nodejs || log_error "Failed to install Node.js" 1 # Run install_lightning_pub and capture its exit code directly. From 39466ef4a760b5540c4b10675d6019c4621366d7 Mon Sep 17 00:00:00 2001 From: shocknet-justin Date: Thu, 27 Nov 2025 01:00:11 -0500 Subject: [PATCH 31/37] mac node bin --- scripts/install_nodejs.sh | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/scripts/install_nodejs.sh b/scripts/install_nodejs.sh index e9c96aaa..4813d3f1 100755 --- a/scripts/install_nodejs.sh +++ b/scripts/install_nodejs.sh @@ -14,9 +14,11 @@ install_nodejs() { } install_nodejs_mac() { + local NODE_BIN="$USER_HOME/node/bin/node" + # Check if node exists and meets minimum version - if command -v node &> /dev/null; then - NODE_VERSION=$(node -v | sed 's/v//') + if [ -x "$NODE_BIN" ]; then + NODE_VERSION=$("$NODE_BIN" -v | sed 's/v//') if [ "$(printf '%s\n' "$MINIMUM_VERSION" "$NODE_VERSION" | sort -V | awk 'NR==1')" = "$MINIMUM_VERSION" ]; then log "Node.js is already installed and meets the minimum version requirement." return 0 From 519a4575c19b33fb8cc46f4d0964e26ca51eb355 Mon Sep 17 00:00:00 2001 From: shocknet-justin Date: Thu, 27 Nov 2025 01:02:44 -0500 Subject: [PATCH 32/37] script logs --- scripts/extract_nprofile.sh | 2 +- scripts/install_lightning_pub.sh | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/extract_nprofile.sh b/scripts/extract_nprofile.sh index c5b86610..4b8b1ed4 100644 --- a/scripts/extract_nprofile.sh +++ b/scripts/extract_nprofile.sh @@ -120,7 +120,7 @@ get_log_info() { fi elif [ -f "$DATA_DIR/app.nprofile" ]; then app_nprofile=$(cat "$DATA_DIR/app.nprofile") - log "Node is already set up. Use this nprofile to invite guest users:" + log "${SECONDARY_COLOR}Lightning.Pub${RESET_COLOR} is already set up. Use this nprofile to invite guest users:" log "${SECONDARY_COLOR}$app_nprofile${RESET_COLOR}" break fi diff --git a/scripts/install_lightning_pub.sh b/scripts/install_lightning_pub.sh index cbb902c8..81d0839b 100755 --- a/scripts/install_lightning_pub.sh +++ b/scripts/install_lightning_pub.sh @@ -34,7 +34,7 @@ install_lightning_pub() { # Decide flow based on whether a valid previous installation exists. if [ -f "$INSTALL_DIR/.installed_commit" ] || [ -f "$INSTALL_DIR/db.sqlite" ]; then # --- UPGRADE PATH --- - log "Existing installation found. Checking for updates..." + log "Existing ${SECONDARY_COLOR}Lightning.Pub${RESET_COLOR} installation found. Checking for updates..." # Check if update is needed by comparing commit hashes API_RESPONSE=$(download_stdout "https://api.github.com/repos/${REPO}/commits/${BRANCH}" 2>&1 | tee /tmp/api_response.log) From eacf948096cd5458a2e8e87bc748f8698f0632e7 Mon Sep 17 00:00:00 2001 From: shocknet-justin Date: Thu, 27 Nov 2025 01:05:32 -0500 Subject: [PATCH 33/37] mac node path --- scripts/install_lightning_pub.sh | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/scripts/install_lightning_pub.sh b/scripts/install_lightning_pub.sh index 81d0839b..8018ec74 100755 --- a/scripts/install_lightning_pub.sh +++ b/scripts/install_lightning_pub.sh @@ -93,9 +93,13 @@ install_lightning_pub() { fi - # Load nvm and npm - export NVM_DIR="${NVM_DIR}" - [ -s "${NVM_DIR}/nvm.sh" ] && \. "${NVM_DIR}/nvm.sh" + # Load nvm (Linux) or add node to PATH (Mac) + if [ "$OS" = "Mac" ]; then + export PATH="$HOME/node/bin:$PATH" + else + export NVM_DIR="${NVM_DIR}" + [ -s "${NVM_DIR}/nvm.sh" ] && \. "${NVM_DIR}/nvm.sh" + fi cd "$INSTALL_DIR" From 62428309feec5d63f727e0998fbe5d77d7db9d6e Mon Sep 17 00:00:00 2001 From: shocknet-justin Date: Thu, 27 Nov 2025 01:12:52 -0500 Subject: [PATCH 34/37] logs --- scripts/handle_macos.sh | 1 - scripts/install.sh | 1 - 2 files changed, 2 deletions(-) diff --git a/scripts/handle_macos.sh b/scripts/handle_macos.sh index e2d05c08..b6ae28ed 100644 --- a/scripts/handle_macos.sh +++ b/scripts/handle_macos.sh @@ -40,7 +40,6 @@ handle_macos() { pub_upgrade_status=100 ;; 2) - log "Lightning.Pub is already up-to-date. No action needed." pub_upgrade_status=2 ;; *) diff --git a/scripts/install.sh b/scripts/install.sh index c101c888..340afa18 100755 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -231,7 +231,6 @@ else pub_upgrade_status=100 # Indicates an upgrade, services should restart ;; 2) - log "Lightning.Pub is already up-to-date. No action needed." pub_upgrade_status=2 # Special status to skip service restart ;; *) From 603f0532f74f560564e5f24157c0b5bc52be2844 Mon Sep 17 00:00:00 2001 From: shocknet-justin Date: Thu, 27 Nov 2025 01:34:25 -0500 Subject: [PATCH 35/37] image branch --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 6f23327f..d027073f 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Lightning.Pub -![Lightning.Pub](https://github.com/shocknet/Lightning.Pub/raw/wizard-update/pub_logo.png) +![Lightning.Pub](https://github.com/shocknet/Lightning.Pub/raw/mac-install/pub_logo.png) ![GitHub last commit](https://img.shields.io/github/last-commit/shocknet/Lightning.Pub?style=flat-square) [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square)](http://makeapullrequest.com) From 7b85c5adb972a3d73372cfbf84edf148f608ac7a Mon Sep 17 00:00:00 2001 From: shocknet-justin Date: Wed, 3 Dec 2025 15:09:00 -0500 Subject: [PATCH 36/37] branch and readme --- README.md | 56 ++++++++++++++++++++++++++++++++++++++-------- scripts/install.sh | 2 +- 2 files changed, 48 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index d027073f..a5d28d07 100644 --- a/README.md +++ b/README.md @@ -81,12 +81,12 @@ Dashboard Wireframe: Paste one-line and have a Pub node in under 2 minutes. It uses neutrino so you can run it on a $5 VPS or old laptop. -This method installs all dependencies and creates user-level systemd services. +This method installs all dependencies and creates user-level services (systemd on Linux, launchd on macOS). **Platform Support:** - ✅ **Debian/Ubuntu**: Fully tested and supported - ✅ **Arch/Fedora**: Fully tested and supported -- 🚧 **macOS**: Basic support stubbed in, but untested. Help wanted. +- ✅ **macOS**: Fully supported with launchd service management > [!IMPORTANT] > **System Requirements:** @@ -99,10 +99,16 @@ This method installs all dependencies and creates user-level systemd services. To start, run the following command: +**Linux:** ```bash wget -qO- https://deploy.lightning.pub | bash ``` +**macOS:** +```bash +curl -fsSL https://deploy.lightning.pub | bash +``` + It should look like this in a minute or so ![One-Line Deployment](https://raw.githubusercontent.com/shocknet/Lightning.Pub/master/one-liner.png) @@ -110,11 +116,14 @@ It should look like this in a minute or so **Note:** The installation is now confined to user-space, meaning: - No sudo required for installation - All data stored in `$HOME/lightning_pub/` +- **Linux**: Services managed via systemd (user-level) +- **macOS**: Services managed via launchd with convenient aliases **After Installation:** -- The installer will display an admin connection string (nprofile and secret separated by a colon) and a **QR code** for easy mobile setup -- You can also access the connection info via web browser on Start9 and Umbrel appliances (releases forthcoming) -- Copy the connection string or scan the QR code with ShockWallet to connect as administrator +- The installer will display an admin connection string (nprofile and secret separated by a colon) and a **terminal QR code** for easy mobile setup +- The QR code is displayed directly in your terminal - simply scan it with ShockWallet's node connetion flows +- You can also copy/paste the connection string into ShockWallet if you prefer +- Connection info is also available via web browser on Start9 and Umbrel appliances (releases forthcoming) **⚠️ Migration from Previous Versions:** Previous system-wide installations (as of 8.27.2025) need some manual intervention: @@ -130,17 +139,25 @@ Please report any issues to the [issue tracker](https://github.com/shocknet/Ligh These are controversial, so we don't include them. You can however add a line to your crontab to re-run the installer on your time preference and it will gracefully handle updating: +**Linux:** ```bash # Add to user's crontab (crontab -e) - runs weekly on Sunday at 2 AM 0 2 * * 0 wget -qO- https://deploy.lightning.pub | bash ``` +**macOS:** +```bash +# Add to user's crontab (crontab -e) - runs weekly on Sunday at 2 AM +0 2 * * 0 curl -fsSL https://deploy.lightning.pub | bash +``` + **Note:** The installer will only restart services if version checks deem necessary. #### Troubleshooting If the installation fails or services don't start properly, use these commands to diagnose: +**Linux:** ```bash # Check service status systemctl --user status lnd @@ -153,7 +170,27 @@ journalctl --user-unit lightning_pub -f # Restart services if needed systemctl --user restart lnd systemctl --user restart lightning_pub +``` +**macOS:** +After installation, run `source ~/.zshrc` (or `source ~/.bash_profile`) to enable the convenience aliases, or open a new terminal. Then use: + +```bash +# Check service status +lpub-status + +# View logs +lpub-log # Lightning.Pub logs +lnd-log # LND logs + +# Control services +lpub-start # Start both services +lpub-stop # Stop both services +lpub-restart # Restart both services +``` + +**All Platforms:** +```bash # Retrieve admin connection string (if installation completed but you need to find it again) cat ~/lightning_pub/admin.connect @@ -198,13 +235,14 @@ npm start ### Connecting to ShockWallet **For Administrators:** -1. After installation, you'll see an admin connection string (format: `nprofile1...:token`) and a QR code -2. **Option 1**: Scan the QR code with ShockWallet mobile app +1. After installation, you'll see an admin connection string (format: `nprofile1...:token`) and a **terminal QR code** containing the same admin connection string +2. **Option 1**: Scan the terminal QR code directly with ShockWallet's "Add Source" feature (mobile or web) to pair as administrator 3. **Option 2**: Copy/paste the connection string into ShockWallet's node connection screen **For Guest Users:** -- The nprofile of the node (without the admin token) can be used to send invitation links to guests via the web version of ShockWallet -- The nprofile is stored in `$HOME/lightning_pub/app.nprofile` or the administrator can copy a link from the "Invite" page in ShockWallet +- The guest nprofile (without admin token) is stored in `$HOME/lightning_pub/app.nprofile` +- Share the nprofile string with guests directly, or use invitation links from the "Invite" page in ShockWallet +- Guests paste the nprofile string into ShockWallet's "Add Source" feature to connect > [!NOTE] > Connecting with wallet will create an account on the node, it will not show or have access to the full LND balance. Allocating existing funds to the admin user will be added to the operator dashboard in a future release. diff --git a/scripts/install.sh b/scripts/install.sh index 340afa18..42c5858a 100755 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -14,7 +14,7 @@ log() { SCRIPT_VERSION="0.3.0" REPO="shocknet/Lightning.Pub" -BRANCH="mac-install" +BRANCH="master" cleanup() { log "Cleaning up temporary files..." From f73b4ca026c82210191e7b6039fe9baf61cb8262 Mon Sep 17 00:00:00 2001 From: shocknet-justin Date: Wed, 3 Dec 2025 16:18:15 -0500 Subject: [PATCH 37/37] readme --- README.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index a5d28d07..bc01e17b 100644 --- a/README.md +++ b/README.md @@ -84,8 +84,10 @@ Paste one-line and have a Pub node in under 2 minutes. It uses neutrino so you c This method installs all dependencies and creates user-level services (systemd on Linux, launchd on macOS). **Platform Support:** -- ✅ **Debian/Ubuntu**: Fully tested and supported -- ✅ **Arch/Fedora**: Fully tested and supported +- ✅ **Debian**: Fully tested and supported +- ✅ **Ubuntu**: Fully tested and supported +- ✅ **Arch**: Fully tested and supported +- ✅ **Fedora**: Fully tested and supported - ✅ **macOS**: Fully supported with launchd service management > [!IMPORTANT]