Files
JATMNandClaude Opus 5.5 e2ae21795a chore: rebrand user-facing gitlawb references to Twigpine (#520)
* chore: rebrand user-facing gitlawb references to Twigpine

Rename the product name in docs, README, CLI help/output, MCP tool
descriptions, macOS app strings, installers, npm metadata and license
headers from gitlawb/Gitlawb to Twigpine. Point GitHub links at the
renamed Twigpine org (and fix the release workflow's repository guard,
which still matched Gitlawb/node), and switch the public node hosts and
website/install URLs to twigpine.com.

Protocol and operator identifiers are intentionally unchanged
(did:gitlawb, gitlawb://, signing domains, GITLAWB_* env vars, crate
and binary names, X-Gitlawb-* headers, icaptcha and peer-network URLs);
they need a follow-up migration.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* docs: point image pulls at ghcr.io/twigpine/node and fix macOS DMG name

The release workflow publishes the container image to
ghcr.io/${GITHUB_REPOSITORY,,}, which is ghcr.io/twigpine/node since the
org rename; the old ghcr.io/gitlawb/node path no longer resolves for
anonymous pulls. Point the RUN-A-NODE docker run example, the OSS
readiness audit's claim about it, and the macOS app's generated compose
file at the published path.

The README listed the macOS DMG as "Twigpine Node.dmg", but
build-macos-app.sh writes GitlawbNode-macOS.dmg (kept on purpose), so
document the real file name.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* fix(infra): keep the AWS security group description to avoid replacement

`description` on aws_security_group is ForceNew, so renaming it forced a
replace of the in-use group on the next apply of an existing deployment.
Restore the original text and note why it must stay until a separate
infra migration.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* fix(macos): regenerate the app's compose file instead of treating it as a repo's

Config.repoPath fell back to the data directory when no repo clone was
found, and DockerCompose then picked up the compose file it had generated
there as if it belonged to a repo. The generator therefore only ran on
first launch, so upgraded installs kept their original compose, including
the old ghcr.io/gitlawb/node image, which no longer serves anonymous pulls.

repoPath now returns nil when there is no clone, and the app regenerates
its compose file whenever no repo compose is in use. Project directory and
named volumes are unchanged.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* fix(infra): default the AWS node image to ghcr.io/twigpine/node

release.yml publishes to ghcr.io/${GITHUB_REPOSITORY,,}, which is now
ghcr.io/twigpine/node; ghcr.io/gitlawb/node denies anonymous pulls, so the
default made new deployments fail to start and the upgrade command fail
on existing ones. On an existing deployment `terraform apply` only updates
the upgrade SSM document in place (user_data drift is ignored); the new
image is pulled on the next upgrade_command.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* fix(macos): keep operator edits to the generated compose, migrate only the image

e04efe4 regenerated the app's compose file on every access, overwriting
edits operators make to pass settings the generated environment block
omits (e.g. GITLAWB_OPERATOR_STRICT_MODE). Generate the file only when it
is missing; when it exists, keep it and rewrite only references to the
retired ghcr.io/gitlawb/node image, keeping tags and digests.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* fix(macos): only migrate complete ghcr.io/gitlawb/node references

Without a left boundary the migration also rewrote the retired path when
it appeared inside a longer reference, such as a registry mirror
(registry.example.com/ghcr.io/gitlawb/node), pointing it at a path the
mirror may not have.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

---------

Co-authored-by: Claude Opus 5.5 <[email protected]>
2026-11-04 21:58:29 +00:00

212 lines
5.8 KiB
Bash

#!/usr/bin/env bash
# Twigpine installer - downloads pre-built binaries from GitHub Releases.
#
# Usage:
# curl -fsSL https://twigpine.com/install.sh | sh
# curl -fsSL https://twigpine.com/install.sh | sh -s -- --version v0.3.9
set -euo pipefail
REPO="${GITLAWB_RELEASE_REPO:-Twigpine/node}"
INSTALL_DIR="${GITLAWB_INSTALL_DIR:-$HOME/.local/bin}"
VERSION_ARG="latest"
usage() {
cat <<EOF
Twigpine installer
Options:
--version <tag> Install a specific release tag, for example v0.3.9.
-h, --help Show this help.
Environment:
GITLAWB_INSTALL_DIR Install directory (default: $HOME/.local/bin)
GITLAWB_RELEASE_REPO GitHub repo to download from (default: Twigpine/node)
EOF
}
while [ "$#" -gt 0 ]; do
case "$1" in
--version)
if [ "$#" -lt 2 ]; then
echo "error: --version requires a value" >&2
exit 1
fi
VERSION_ARG="$2"
shift 2
;;
-h|--help)
usage
exit 0
;;
*)
VERSION_ARG="$1"
shift
;;
esac
done
OS=$(uname -s)
ARCH=$(uname -m)
case "$OS" in
Linux) OS_NAME="linux" ;;
Darwin) OS_NAME="darwin" ;;
*)
echo "error: unsupported OS: $OS" >&2
echo " please build from source: cargo install --git https://github.com/${REPO} gl" >&2
exit 1
;;
esac
case "$ARCH" in
x86_64) ARCH_NAME="x86_64" ;;
aarch64 | arm64) ARCH_NAME="aarch64" ;;
*)
echo "error: unsupported architecture: $ARCH" >&2
exit 1
;;
esac
case "${OS_NAME}-${ARCH_NAME}" in
linux-x86_64) TARGET="x86_64-unknown-linux-musl" ;;
linux-aarch64) TARGET="aarch64-unknown-linux-musl" ;;
darwin-x86_64) TARGET="x86_64-apple-darwin" ;;
darwin-aarch64) TARGET="aarch64-apple-darwin" ;;
esac
if [ "$VERSION_ARG" = "latest" ]; then
echo "Fetching latest release version..."
TAG=$(curl -fsSL "https://api.github.com/repos/${REPO}/releases/latest" \
| grep '"tag_name"' \
| sed -E 's/.*"tag_name": *"([^"]+)".*/\1/')
if [ -z "$TAG" ]; then
echo "error: could not determine latest release. Check https://github.com/${REPO}/releases" >&2
exit 1
fi
else
TAG="$VERSION_ARG"
fi
case "$TAG" in
v*) VERSION="${TAG#v}" ;;
*) VERSION="$TAG"; TAG="v$TAG" ;;
esac
PACKAGE_NAME="gitlawb-node-${VERSION}-${TARGET}"
ARCHIVE="${PACKAGE_NAME}.tar.gz"
DOWNLOAD_URL="https://github.com/${REPO}/releases/download/${TAG}/${ARCHIVE}"
CHECKSUM_URL="${DOWNLOAD_URL}.sha256"
echo "Installing Twigpine ${TAG} for ${OS_NAME}/${ARCH_NAME}"
echo " Archive: ${ARCHIVE}"
echo " Into: ${INSTALL_DIR}"
echo ""
TMP_DIR=$(mktemp -d)
trap 'rm -rf "$TMP_DIR"' EXIT
echo "Downloading..."
if ! curl -fSL --progress-bar -o "$TMP_DIR/$ARCHIVE" "$DOWNLOAD_URL"; then
echo ""
echo "error: download failed: $DOWNLOAD_URL" >&2
echo " Check https://github.com/${REPO}/releases for available builds." >&2
exit 1
fi
if curl -fsSL -o "$TMP_DIR/$ARCHIVE.sha256" "$CHECKSUM_URL" 2>/dev/null; then
echo "Verifying checksum..."
EXPECTED=$(awk '{print $1}' "$TMP_DIR/$ARCHIVE.sha256")
if command -v sha256sum >/dev/null 2>&1; then
ACTUAL=$(sha256sum "$TMP_DIR/$ARCHIVE" | awk '{print $1}')
elif command -v shasum >/dev/null 2>&1; then
ACTUAL=$(shasum -a 256 "$TMP_DIR/$ARCHIVE" | awk '{print $1}')
else
echo "warning: no sha256 tool found, skipping checksum verification"
ACTUAL="$EXPECTED"
fi
if [ "$EXPECTED" != "$ACTUAL" ]; then
echo "error: checksum mismatch!" >&2
echo " expected: $EXPECTED" >&2
echo " actual: $ACTUAL" >&2
exit 1
fi
echo " checksum OK"
fi
echo "Extracting..."
tar -xzf "$TMP_DIR/$ARCHIVE" -C "$TMP_DIR"
PACKAGE_DIR="$TMP_DIR/$PACKAGE_NAME"
if [ ! -d "$PACKAGE_DIR" ]; then
PACKAGE_DIR=$(find "$TMP_DIR" -mindepth 1 -maxdepth 1 -type d | head -n 1)
fi
if [ ! -d "$PACKAGE_DIR" ]; then
echo "error: could not find extracted package directory" >&2
exit 1
fi
mkdir -p "$INSTALL_DIR"
INSTALLED=()
for BIN in gl git-remote-gitlawb gitlawb-node; do
if [ -f "$PACKAGE_DIR/$BIN" ]; then
install -m 755 "$PACKAGE_DIR/$BIN" "$INSTALL_DIR/$BIN.new"
mv -f "$INSTALL_DIR/$BIN.new" "$INSTALL_DIR/$BIN"
INSTALLED+=("$BIN")
fi
done
if [ "${#INSTALLED[@]}" -eq 0 ]; then
echo "error: no installable binaries found in $ARCHIVE" >&2
exit 1
fi
echo ""
echo "Installed Twigpine ${TAG}"
for BIN in "${INSTALLED[@]}"; do
echo " ${BIN} -> ${INSTALL_DIR}/${BIN}"
done
echo ""
if echo ":$PATH:" | grep -q ":${INSTALL_DIR}:"; then
echo "Run:"
echo " gl doctor"
echo " gl quickstart"
else
SHELL_NAME=$(basename "${SHELL:-bash}")
case "$SHELL_NAME" in
zsh) RC="$HOME/.zshrc" ;;
fish) RC="$HOME/.config/fish/config.fish" ;;
*) RC="$HOME/.bashrc" ;;
esac
echo "Add ${INSTALL_DIR} to your PATH:"
echo ""
if [ "$SHELL_NAME" = "fish" ]; then
echo " fish_add_path ${INSTALL_DIR}"
else
echo " echo 'export PATH=\"${INSTALL_DIR}:\$PATH\"' >> ${RC}"
echo " source ${RC}"
fi
echo ""
echo "Then run:"
echo " gl doctor"
echo " gl quickstart"
fi
# oh-my-zsh's default git plugin aliases gl='git pull', which silently shadows
# the gl binary in every interactive zsh (aliases beat PATH). Detect the common
# setup and say so now, at install time, instead of letting the first `gl`
# print git's baffling "fatal: not a git repository".
if [ -f "$HOME/.zshrc" ] && [ -d "$HOME/.oh-my-zsh/plugins/git" ] \
&& grep -Eq '^[[:space:]]*plugins=\(([^)]*[[:space:]])?git([[:space:]][^)]*)?\)' "$HOME/.zshrc" \
&& ! grep -Eq '^([^#]*[[:space:];&|])?unalias[[:space:]]+([^;&|#]*[[:space:]])?gl([[:space:]]|$)' "$HOME/.zshrc"; then
echo ""
echo "NOTE: oh-my-zsh's git plugin aliases gl='git pull', which will shadow"
echo "the gl command in interactive shells. To use gl by name, run:"
echo ""
echo " echo 'unalias gl 2>/dev/null' >> ~/.zshrc && source ~/.zshrc"
fi
echo ""
echo "Docs: https://docs.gitlawb.com"