Merge branch 'main' into codex/repo-audit-remediation-package-2

# Conflicts:
#	README.md
#	setup/scripts/install-packages
This commit is contained in:
Gabriel Brown
2026-08-27 16:51:53 -04:00
24 changed files with 605 additions and 114 deletions
+27
View File
@@ -89,6 +89,11 @@ source "$PANAMA_PATH/setup/lib/extras-catalog"
source "$PANAMA_PATH/setup/lib/machine-role"
ROLE="$(panama_role)"
# Establishing the verified ChatGPT repository, shared with the migration that
# replaces the community build, so neither can install it a less careful way.
# shellcheck source=../lib/chatgpt-package
source "$PANAMA_PATH/setup/lib/chatgpt-package"
# One list, installed the way every list is installed: --skip-unavailable so a
# single rotted name cannot cost the transaction, then report_missing so a
# skipped name is a warning somebody reads.
@@ -1299,6 +1304,28 @@ if ! install_claude_desktop_if_trusted; then
softly_failed+=("Claude Desktop")
fi
# ChatGPT Desktop: OpenAI ships an official Linux RPM now. Panama used to build
# a community wrapper from the macOS disk image -- it was `panama app
# chatgpt-desktop` -- because no packaged form existed; that build froze often
# and carried its own local rebuild daemon. The official package is strictly
# better: it comes from a repository, so it upgrades with every other package
# from then on.
#
# The repository and its signing key are established first, from the copy
# pinned in setup/keys/, so dnf verifies the metadata and the package before
# either reaches root. Upstream's own instructions do not allow that -- see
# setup/lib/chatgpt-package for why they are not followed here.
if rpm -q chatgpt >/dev/null 2>&1; then
log "ChatGPT Desktop already installed"
elif ! chatgpt_install_repository sudo; then
log "Could not establish the verified ChatGPT repository; skipping"
softly_failed+=("ChatGPT Desktop")
else
log "Installing ChatGPT Desktop..."
sudo dnf install -y chatgpt > /dev/null \
|| { log "ChatGPT Desktop install failed; skipping"; softly_failed+=("ChatGPT Desktop"); }
fi
# The RPM ships rustdesk.service already enabled, which is what provides
# unattended access; Panama deliberately does not start it a second time.
install_rustdesk || true
+25 -23
View File
@@ -11,12 +11,11 @@
# this repository, not anybody's personal content, so a stranger who clones
# Panama wants it for exactly the same reason its author does.
#
# ~/.claude/skills was a single symlink into user/agents/skills until now, and
# a directory cannot be two things at once. So the destination becomes a real
# directory and every skill -- shipped here, personal from user/ -- is linked
# into it one at a time. link-user runs after this stage on purpose: it links
# last, so a personal skill named like a shipped one wins, which is the
# precedence Claude Code itself uses.
# ~/.agents/skills and ~/.claude/skills may each start as a single symlink into
# user/agents/skills, but a directory cannot point at personal and shipped
# skills at once. Both destinations become real directories with one link per
# skill. link-user runs after this stage on purpose, so a personal skill named
# like a shipped one wins in every agent runtime.
set -euo pipefail
@@ -25,7 +24,7 @@ log() { echo -e "\033[1;34m[INFO]\033[0m $*"; }
PANAMA_PATH="${PANAMA_PATH:-$HOME/.local/share/Panama}"
SKILLS_DIR="$PANAMA_PATH/skills"
PANAMA_OLD="$PANAMA_PATH/config/old"
DESTINATION="$HOME/.claude/skills"
DESTINATIONS=("$HOME/.agents/skills" "$HOME/.claude/skills")
[[ -d "$SKILLS_DIR" ]] || { log "No skills/ in this checkout; nothing to link."; exit 0; }
@@ -51,27 +50,30 @@ displace() {
log "Moved existing $destination to $backup"
}
# The destination itself has to be a real directory before anything can be
# linked into it. An old whole-directory symlink is removed; a regular file
# somebody left at this path is kept, in config/old/.
mkdir -p "$(dirname "$DESTINATION")"
if [[ -L "$DESTINATION" ]]; then
rm -f "$DESTINATION"
log "Removed the old $DESTINATION symlink; skills are linked one by one now"
elif [[ -e "$DESTINATION" && ! -d "$DESTINATION" ]]; then
displace "$DESTINATION"
fi
mkdir -p "$DESTINATION"
# Each destination has to be a real directory before anything can be linked
# into it. An old whole-directory symlink is removed; a regular file somebody
# left at the path is kept in config/old/.
for destination in "${DESTINATIONS[@]}"; do
mkdir -p "$(dirname "$destination")"
if [[ -L "$destination" ]]; then
rm -f "$destination"
log "Removed the old $destination symlink; skills are linked one by one now"
elif [[ -e "$destination" && ! -d "$destination" ]]; then
displace "$destination"
fi
mkdir -p "$destination"
done
linked=0
for skill in "$SKILLS_DIR"/*; do
[[ -e "$skill" ]] || continue
name="$(basename "$skill")"
target="$DESTINATION/$name"
displace "$target"
ln -s "$skill" "$target"
log "Linked skills/$name → $target"
for destination in "${DESTINATIONS[@]}"; do
target="$destination/$name"
displace "$target"
ln -s "$skill" "$target"
log "Linked skills/$name → $target"
done
linked=$(( linked + 1 ))
done