harmonize with coredesktop
This commit is contained in:
@@ -10,7 +10,13 @@ wget -qO- https://gitea.young.computer/david/MiniServer/raw/branch/main/install.
|
||||
update
|
||||
```
|
||||
|
||||
This pulls the latest MiniServer into `~/.local/share/MiniServer`, then runs each step in `updates.d/` in order: the Arch keyring and system packages (plus any packages newly added to `packages.conf`), AUR packages, pm2, Cockpit, sibling projects, the firewall, cache cleanup, and a reboot check. If a step fails, the others still run and the failed ones are listed at the end.
|
||||
This pulls the latest MiniServer into `~/.local/share/MiniServer`, then runs each step in `updates.d/` in order: the Arch keyring and system packages (plus any packages newly added to `packages.conf`), one-time migrations, AUR packages, pm2, Cockpit, Claude Code, sibling projects, the agent sandbox and Hermes Agent, the firewall, cache cleanup, and a reboot check. If a step fails, the others still run and the failed ones are listed at the end.
|
||||
|
||||
`update` won't start with less than 10 GiB free on `/`, since running out part way through can leave the system half-upgraded. `UPDATE_FORCE=1 update` skips that check.
|
||||
|
||||
A change that existing servers need to catch up on (something `install.sh` now does differently) goes in `migrations/$(date +%s).sh`. Each migration runs once per server, on the next `update`.
|
||||
|
||||
`version` shows which MiniServer commit this server is on, whether a newer one is available, and when it last updated.
|
||||
|
||||
## Refresh
|
||||
|
||||
@@ -20,6 +26,30 @@ refresh
|
||||
|
||||
Re-runs `install.sh` to pick up anything new in the installer (services, config). Run `update` afterwards.
|
||||
|
||||
## Shell
|
||||
|
||||
MiniServer hooks [shell/bashrc](shell/bashrc) into `~/.bashrc`: a starship prompt, `z` to jump to directories you've visited (zoxide), fzf key bindings (Ctrl-R history, Ctrl-T files, Alt-C directories), larger shared history, and the aliases and functions in [shell/aliases](shell/aliases) and [shell/functions](shell/functions). Anything in `~/.bashrc` after that line overrides it, and your own `~/.config/starship.toml` replaces MiniServer's prompt.
|
||||
|
||||
## AI agents
|
||||
|
||||
MiniServer installs Claude Code, opencode and Hermes Agent. To sign in to them, and to mount your Federated Computer Core's Nextcloud files at `/mnt/<domain>`:
|
||||
|
||||
```
|
||||
configure
|
||||
```
|
||||
|
||||
It lists what still needs setting up. `configure <item>` sets up one item, and `configure --list` shows their names.
|
||||
|
||||
The Nextcloud mount is made by root at the first use after each boot, owned by you. It uses a Nextcloud app password (revocable under Settings > Security in Nextcloud), kept in `/etc/davfs2/secrets`, which only root can read.
|
||||
|
||||
### Sandbox
|
||||
|
||||
opencode and Hermes run inside Anthropic's sandbox runtime (`srt`), which uses bubblewrap. An agent can write only to the directory it was started in (plus `/tmp` and package caches), can't read credentials such as `~/.ssh`, `~/.gnupg` or other agents' sign-ins, and can reach only the domains in [sandbox/config/](sandbox/config/). Start an agent from a project directory, not from `~`. Claude Code can't run inside `srt`, because bubblewrap sandboxes can't be nested, so MiniServer turns on Claude Code's own sandbox with the same domain list instead.
|
||||
|
||||
This works because `shell/bashrc` puts `sandbox/bin` first on `PATH`, so typing `opencode` or `hermes` runs the sandboxed version. `unsandboxed <agent>` runs the agent without the sandbox. To allow more, add files in the same format as the ones in `sandbox/config/` to `~/.config/miniserver/sandbox/`: `base.json` for every agent or `<agent>.json` for one. Lists in your files add to MiniServer's.
|
||||
|
||||
If Hermes' gateway service (`hermes-gateway.service`) is installed, it runs in the sandbox too. MiniServer turns on lingering for your user so the gateway keeps running when nobody is logged in.
|
||||
|
||||
## Sibling projects
|
||||
|
||||
A sibling project is another git repository that builds on MiniServer. To install one:
|
||||
@@ -32,7 +62,7 @@ It asks for the repository's git URL, clones it into `~/.local/share/siblings/`,
|
||||
|
||||
Use `sibling list` to see what's installed and `sibling remove {name}` to uninstall one.
|
||||
|
||||
To make your own repository work as a sibling project, see [skills/sibling-project-format.md](skills/sibling-project-format.md).
|
||||
To make your own repository work as a sibling project, see [skills/sibling-project-format.md](skills/sibling-project-format.md). For full control over how it updates, see [skills/sibling-project-updates.md](skills/sibling-project-updates.md).
|
||||
|
||||
## Firewall
|
||||
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# Purpose : Install Claude Code using the official native installer
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# Check if Claude Code is already installed
|
||||
if command -v claude &>/dev/null; then
|
||||
echo "Claude Code is already installed: $(claude --version)"
|
||||
else
|
||||
echo "Installing Claude Code..."
|
||||
curl -fsSL https://claude.ai/install.sh | bash
|
||||
fi
|
||||
|
||||
# The native installer places the binary in ~/.local/bin
|
||||
LINE='export PATH="$HOME/.local/bin:$PATH"'
|
||||
if grep -qF "$LINE" ~/.bashrc; then
|
||||
echo "~/.local/bin already on PATH in .bashrc"
|
||||
else
|
||||
echo "$LINE" >> ~/.bashrc
|
||||
echo "~/.local/bin added to PATH in .bashrc"
|
||||
fi
|
||||
|
||||
# sandbox/setup.sh turns on Claude Code's sandbox in this file, so it must exist
|
||||
SETTINGS_FILE="$HOME/.claude/settings.json"
|
||||
if [ -f "$SETTINGS_FILE" ]; then
|
||||
echo "Claude Code settings already exist at $SETTINGS_FILE"
|
||||
else
|
||||
echo "Writing Claude Code settings to $SETTINGS_FILE..."
|
||||
mkdir -p "$(dirname "$SETTINGS_FILE")"
|
||||
echo '{}' > "$SETTINGS_FILE"
|
||||
fi
|
||||
|
||||
echo "Done. Run 'configure claude' to log in."
|
||||
@@ -0,0 +1,156 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# Purpose : Set up the things MiniServer installs that need your input:
|
||||
# mounting your Nextcloud files and signing in to the AI agents.
|
||||
#
|
||||
# Each file in configure.d/ is one item. It sets TITLE and DESCRIPTION and
|
||||
# defines is_configured and run_configure. It can also define is_installed;
|
||||
# the item is left out when that fails. To add an item, add a file there.
|
||||
|
||||
# Wrapped in { ...; exit; } so bash reads the whole script up front; update
|
||||
# replaces this file while it runs.
|
||||
{
|
||||
|
||||
MINISERVER="$HOME/.local/share/MiniServer"
|
||||
PATH="$HOME/.local/bin:$PATH"
|
||||
|
||||
# Run an agent's real command rather than its sandboxed one in sandbox/bin:
|
||||
# setting an agent up writes outside its sandbox
|
||||
unsandboxed() {
|
||||
"$MINISERVER/sandbox/bin/unsandboxed" "$@"
|
||||
}
|
||||
|
||||
# Ask a yes/no question; the default is no
|
||||
confirm() {
|
||||
local answer
|
||||
read -rp "$1 [y/N] " answer
|
||||
[[ "$answer" == [yY]* ]]
|
||||
}
|
||||
|
||||
# Ask a yes/no question; the default is yes
|
||||
confirm_yes() {
|
||||
local answer
|
||||
read -rp "$1 [Y/n] " answer
|
||||
[[ "$answer" != [nN]* ]]
|
||||
}
|
||||
|
||||
usage() {
|
||||
cat << EOF
|
||||
Usage: configure [item]
|
||||
|
||||
configure Show what's set up and choose what to set up
|
||||
configure <item> Set up one item
|
||||
configure --list Show what's set up, and the item names
|
||||
|
||||
EOF
|
||||
}
|
||||
|
||||
# configure.d/30-claude.sh -> claude
|
||||
item_name() {
|
||||
local name
|
||||
name="$(basename "$1" .sh)"
|
||||
echo "${name#[0-9][0-9]-}"
|
||||
}
|
||||
|
||||
# Prints "<state>|<title>|<description>"; state is done, todo or absent
|
||||
item_info() {
|
||||
(
|
||||
source "$1"
|
||||
if declare -f is_installed > /dev/null && ! is_installed &> /dev/null; then
|
||||
echo "absent||"
|
||||
elif is_configured &> /dev/null; then
|
||||
echo "done|$TITLE|$DESCRIPTION"
|
||||
else
|
||||
echo "todo|$TITLE|$DESCRIPTION"
|
||||
fi
|
||||
)
|
||||
}
|
||||
|
||||
configure_item() {
|
||||
(
|
||||
source "$1"
|
||||
echo
|
||||
echo -e "\e[1m$TITLE\e[0m"
|
||||
run_configure
|
||||
)
|
||||
local status=$?
|
||||
if [ $status -ne 0 ]; then
|
||||
echo -e "\e[31m$(item_name "$1") was not set up.\e[0m"
|
||||
fi
|
||||
return $status
|
||||
}
|
||||
|
||||
# Fill ITEMS, STATES, TITLES and DESCRIPTIONS with the installed items
|
||||
load_items() {
|
||||
local file state title description
|
||||
ITEMS=() STATES=() TITLES=() DESCRIPTIONS=()
|
||||
for file in "$MINISERVER"/configure.d/*.sh; do
|
||||
[ -f "$file" ] || continue
|
||||
IFS='|' read -r state title description < <(item_info "$file")
|
||||
[ "$state" = absent ] && continue
|
||||
ITEMS+=("$file") STATES+=("$state") TITLES+=("$title") DESCRIPTIONS+=("$description")
|
||||
done
|
||||
}
|
||||
|
||||
show_items() {
|
||||
local i mark
|
||||
for i in "${!ITEMS[@]}"; do
|
||||
if [ "${STATES[$i]}" = done ]; then
|
||||
mark="\e[32mdone\e[0m "
|
||||
else
|
||||
mark="\e[33mto do\e[0m"
|
||||
fi
|
||||
printf " %2d) $mark %-22s %s\n" $((i + 1)) "${TITLES[$i]}" "${DESCRIPTIONS[$i]}"
|
||||
done
|
||||
}
|
||||
|
||||
case "$1" in
|
||||
"") ;;
|
||||
-h|--help)
|
||||
usage
|
||||
exit 0 ;;
|
||||
-l|--list)
|
||||
load_items
|
||||
for i in "${!ITEMS[@]}"; do
|
||||
printf '%-6s %-14s %s\n' "${STATES[$i]/todo/to do}" "$(item_name "${ITEMS[$i]}")" "${TITLES[$i]}"
|
||||
done
|
||||
exit 0 ;;
|
||||
*)
|
||||
for file in "$MINISERVER"/configure.d/*.sh; do
|
||||
if [ "$(item_name "$file")" = "$1" ]; then
|
||||
configure_item "$file"
|
||||
exit
|
||||
fi
|
||||
done
|
||||
echo "No item named $1. These are: configure --list"
|
||||
exit 2 ;;
|
||||
esac
|
||||
|
||||
while true; do
|
||||
load_items
|
||||
echo
|
||||
echo "MiniServer configuration"
|
||||
echo
|
||||
show_items
|
||||
echo
|
||||
echo " a) Set up everything still to do"
|
||||
echo " q) Quit"
|
||||
echo
|
||||
read -rp "Choose: " choice || exit 0
|
||||
|
||||
case "$choice" in
|
||||
q|Q|"")
|
||||
exit 0 ;;
|
||||
a|A)
|
||||
for i in "${!ITEMS[@]}"; do
|
||||
[ "${STATES[$i]}" = todo ] && configure_item "${ITEMS[$i]}"
|
||||
done ;;
|
||||
*)
|
||||
if [[ "$choice" =~ ^[0-9]+$ ]] && (( choice >= 1 && choice <= ${#ITEMS[@]} )); then
|
||||
configure_item "${ITEMS[$((choice - 1))]}"
|
||||
else
|
||||
echo "Choose a number from the list, a or q."
|
||||
fi ;;
|
||||
esac
|
||||
done
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
#!/bin/bash
|
||||
|
||||
TITLE="Nextcloud files"
|
||||
DESCRIPTION="Mount your Core's Nextcloud files"
|
||||
|
||||
# What you entered last time, so running this again starts from it
|
||||
FILES_CONF="$HOME/.config/miniserver/files.conf"
|
||||
DAVFS_SECRETS="/etc/davfs2/secrets"
|
||||
source "$MINISERVER/configure.d/lib/nextcloud.sh"
|
||||
[ -f "$FILES_CONF" ] && source "$FILES_CONF"
|
||||
|
||||
is_installed() {
|
||||
command -v mount.davfs
|
||||
}
|
||||
|
||||
# CORE_DOMAIN and CORE_EMAIL come from files.conf; the rest follow
|
||||
core_names() {
|
||||
CORE_USER="${CORE_EMAIL%@*}"
|
||||
NEXTCLOUD="https://nextcloud.$CORE_DOMAIN"
|
||||
WEBDAV="$NEXTCLOUD/remote.php/dav/files/$CORE_USER"
|
||||
MOUNT="/mnt/$CORE_DOMAIN"
|
||||
}
|
||||
|
||||
# The fstab line is added last, after the password is saved
|
||||
is_configured() {
|
||||
[ -n "$CORE_DOMAIN" ] || return 1
|
||||
core_names
|
||||
grep -qs " $MOUNT davfs " /etc/fstab
|
||||
}
|
||||
|
||||
save_conf() {
|
||||
mkdir -p "$(dirname "$FILES_CONF")"
|
||||
{
|
||||
printf 'CORE_DOMAIN=%q\n' "$CORE_DOMAIN"
|
||||
printf 'CORE_EMAIL=%q\n' "$CORE_EMAIL"
|
||||
} > "$FILES_CONF"
|
||||
}
|
||||
|
||||
# Mount your Nextcloud files at /mnt/<domain> with davfs2. On a server nobody
|
||||
# may be logged in, so root mounts it, owned by you, the first time something
|
||||
# uses it after boot (x-systemd.automount); nofail keeps boot going if
|
||||
# Nextcloud can't be reached. The password is in davfs2's own secrets file,
|
||||
# which only root can read, so sandboxed agents can't either.
|
||||
setup_files() {
|
||||
local secret unit
|
||||
|
||||
secret="${APP_PASSWORD//\\/\\\\}"
|
||||
secret="${secret//\"/\\\"}"
|
||||
sudo sed -i "\\#^$MOUNT #d" "$DAVFS_SECRETS" || return 1
|
||||
printf '%s %s "%s"\n' "$MOUNT" "$CORE_USER" "$secret" | sudo tee -a "$DAVFS_SECRETS" > /dev/null || return 1
|
||||
sudo chmod 600 "$DAVFS_SECRETS" || return 1
|
||||
|
||||
sudo install -d "$MOUNT" || return 1
|
||||
if ! grep -qs " $MOUNT davfs " /etc/fstab; then
|
||||
echo "$WEBDAV $MOUNT davfs rw,uid=$(id -u),gid=$(id -g),_netdev,nofail,x-systemd.automount 0 0" \
|
||||
| sudo tee -a /etc/fstab > /dev/null || return 1
|
||||
fi
|
||||
sudo systemctl daemon-reload || return 1
|
||||
|
||||
# Start the automount now rather than at the next boot, then use it once so
|
||||
# a wrong address or password shows up here. Unmount first when running
|
||||
# again, so a new password is used.
|
||||
unit="$(systemd-escape --path "$MOUNT")"
|
||||
if ! sudo systemctl stop "$unit.mount"; then
|
||||
echo "$MOUNT is in use; close what's using it and run: configure files"
|
||||
return 1
|
||||
fi
|
||||
sudo systemctl restart "$unit.automount" || return 1
|
||||
if ! ls "$MOUNT" > /dev/null; then
|
||||
echo "Couldn't mount $WEBDAV; see: journalctl -u $unit.mount"
|
||||
return 1
|
||||
fi
|
||||
echo "Your Nextcloud files are at $MOUNT."
|
||||
}
|
||||
|
||||
run_configure() {
|
||||
local url tries
|
||||
|
||||
read -erp "Your Superpanel's address (e.g. https://superpanel.example.com): " \
|
||||
-i "${CORE_DOMAIN:+https://superpanel.$CORE_DOMAIN}" url
|
||||
url="${url#*://}"
|
||||
url="${url%%/*}"
|
||||
CORE_DOMAIN="${url#superpanel.}"
|
||||
[ -n "$CORE_DOMAIN" ] || { echo "That isn't an address."; return 1; }
|
||||
|
||||
read -erp "Your Core email: " -i "${CORE_EMAIL:-$USER@$CORE_DOMAIN}" CORE_EMAIL
|
||||
[[ "$CORE_EMAIL" == *@* ]] || { echo "That isn't an email address."; return 1; }
|
||||
core_names
|
||||
save_conf
|
||||
|
||||
# The mount gets an app password, not your Core password
|
||||
for tries in 1 2 3; do
|
||||
read -rsp "Your Core password: " PASSWORD
|
||||
echo
|
||||
nextcloud_app_password "MiniServer on $HOSTNAME"
|
||||
case $? in
|
||||
0) break ;;
|
||||
2) echo "Nextcloud says that password is wrong." ;;
|
||||
*) echo "Couldn't reach Nextcloud at $NEXTCLOUD."; unset PASSWORD; return 1 ;;
|
||||
esac
|
||||
done
|
||||
unset PASSWORD
|
||||
[ -n "$APP_PASSWORD" ] || return 1
|
||||
|
||||
setup_files
|
||||
local status=$?
|
||||
unset APP_PASSWORD
|
||||
return $status
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
#!/bin/bash
|
||||
|
||||
TITLE="Claude Code"
|
||||
DESCRIPTION="Sign in to Anthropic"
|
||||
|
||||
is_installed() {
|
||||
command -v claude
|
||||
}
|
||||
|
||||
is_configured() {
|
||||
claude auth status
|
||||
}
|
||||
|
||||
run_configure() {
|
||||
echo " 1) Claude subscription (Pro, Max, Team or Enterprise)"
|
||||
echo " 2) Anthropic Console (pay for API usage)"
|
||||
local choice
|
||||
read -rp "How do you pay for Claude? [1] " choice
|
||||
case "$choice" in
|
||||
""|1) claude auth login ;;
|
||||
2) claude auth login --console ;;
|
||||
*) echo "Choose 1 or 2."; return 1 ;;
|
||||
esac
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
#!/bin/bash
|
||||
|
||||
TITLE="opencode"
|
||||
DESCRIPTION="Sign in to AI providers"
|
||||
|
||||
is_installed() {
|
||||
[ -x /usr/bin/opencode ]
|
||||
}
|
||||
|
||||
# Signed in to at least one provider
|
||||
is_configured() {
|
||||
jq -e 'length > 0' "$HOME/.local/share/opencode/auth.json"
|
||||
}
|
||||
|
||||
run_configure() {
|
||||
echo "Choose a provider and sign in or paste its API key. You can pick the model inside opencode with /models."
|
||||
unsandboxed opencode auth login || return 1
|
||||
while confirm "Sign in to another provider?"; do
|
||||
unsandboxed opencode auth login || return 1
|
||||
done
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
#!/bin/bash
|
||||
|
||||
TITLE="Hermes Agent"
|
||||
DESCRIPTION="Choose a model provider and tools"
|
||||
|
||||
is_installed() {
|
||||
[ -x "$HOME/.local/bin/hermes" ]
|
||||
}
|
||||
|
||||
# Signed in with OAuth (e.g. Nous Portal), or an API key is set
|
||||
is_configured() {
|
||||
[ -s "$HOME/.hermes/auth.json" ] || grep -qE '^[A-Z_]*API_KEY=.+' "$HOME/.hermes/.env"
|
||||
}
|
||||
|
||||
run_configure() {
|
||||
echo " 1) Nous Portal (one sign-in covers the model, web search, images and more)"
|
||||
echo " 2) Another provider (OpenRouter, OpenAI, Anthropic or your own endpoint)"
|
||||
local choice
|
||||
read -rp "Which provider? [1] " choice
|
||||
case "$choice" in
|
||||
""|1) unsandboxed hermes setup --portal ;;
|
||||
2) unsandboxed hermes setup ;;
|
||||
*) echo "Choose 1 or 2."; return 1 ;;
|
||||
esac
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# Purpose : Nextcloud app passwords for configure.d items. Source this file.
|
||||
|
||||
# Basic auth header for curl, read from stdin (-H @-) so the password isn't
|
||||
# in the process list
|
||||
auth_header() {
|
||||
printf 'Authorization: Basic %s\n' "$(printf '%s:%s' "$1" "$2" | base64 -w0)"
|
||||
}
|
||||
|
||||
# Create a Nextcloud app password, so what MiniServer sets up doesn't keep
|
||||
# your Core password. It shows in Nextcloud under Settings > Security with the
|
||||
# name given, where it can be revoked. Uses CORE_USER, PASSWORD and NEXTCLOUD;
|
||||
# sets APP_PASSWORD. Returns 2 if Nextcloud says the password is wrong.
|
||||
nextcloud_app_password() {
|
||||
local name="$1" response code
|
||||
response="$(auth_header "$CORE_USER" "$PASSWORD" | curl -s -w '\n%{http_code}' -H @- \
|
||||
-H 'OCS-APIRequest: true' -A "$name" \
|
||||
"$NEXTCLOUD/ocs/v2.php/core/getapppassword?format=json")"
|
||||
code="${response##*$'\n'}"
|
||||
case "$code" in
|
||||
200) APP_PASSWORD="$(jq -r '.ocs.data.apppassword // empty' <<< "${response%$'\n'*}")"
|
||||
[ -n "$APP_PASSWORD" ] ;;
|
||||
401) return 2 ;;
|
||||
*) return 1 ;;
|
||||
esac
|
||||
}
|
||||
+39
-1
@@ -76,11 +76,13 @@ cd ~/.local/share/MiniServer
|
||||
# Source utility functions (provides install_packages, install_script and other helpers)
|
||||
source utils.sh
|
||||
|
||||
# Install the refresh, update and sibling management scripts to the user's bin
|
||||
# Install the refresh, update, sibling, version and configure scripts to the user's bin
|
||||
mkdir -p "$HOME/.local/bin"
|
||||
install_script "$HOME/.local/share/MiniServer/refresh" "$HOME/.local/bin/refresh"
|
||||
install_script "$HOME/.local/share/MiniServer/update" "$HOME/.local/bin/update"
|
||||
install_script "$HOME/.local/share/MiniServer/sibling" "$HOME/.local/bin/sibling"
|
||||
install_script "$HOME/.local/share/MiniServer/version" "$HOME/.local/bin/version"
|
||||
install_script "$HOME/.local/share/MiniServer/configure" "$HOME/.local/bin/configure"
|
||||
|
||||
# Replaced by refresh and update
|
||||
rm -f "$HOME/.local/bin/miniserverrefresh" "$HOME/.local/bin/miniserverupdate"
|
||||
@@ -122,6 +124,12 @@ install_packages "${SERVERS[@]}"
|
||||
echo "Installing utilities..."
|
||||
install_packages "${UTILITIES[@]}"
|
||||
|
||||
echo "Installing shell tools..."
|
||||
install_packages "${SHELL_TOOLS[@]}"
|
||||
|
||||
echo "Installing AI agents and their sandbox..."
|
||||
install_packages "${AGENTS[@]}"
|
||||
|
||||
if ! command -v pm2 &> /dev/null; then
|
||||
npm install pm2 -g --prefix "$HOME/.local"
|
||||
fi
|
||||
@@ -150,6 +158,36 @@ if ! systemctl is-enabled --quiet sddm; then
|
||||
sudo systemctl enable sddm
|
||||
fi
|
||||
|
||||
# Install Claude Code
|
||||
echo "Installing Claude Code..."
|
||||
bash ~/.local/share/MiniServer/claude.sh
|
||||
|
||||
# Run AI agents (opencode, hermes) in a sandbox, and turn on Claude Code's own
|
||||
echo "Setting up sandboxed AI agents..."
|
||||
bash ~/.local/share/MiniServer/sandbox/setup.sh
|
||||
|
||||
# Prompt, zoxide, fzf key bindings, aliases and functions
|
||||
echo "Configuring the shell..."
|
||||
bash ~/.local/share/MiniServer/shell/setup.sh
|
||||
|
||||
# Run one-time migrations not yet recorded on this machine. On a fresh install
|
||||
# they repeat setup done above, which is safe because each one is idempotent.
|
||||
echo "Running migrations..."
|
||||
run_migrations
|
||||
|
||||
# Offer configure only if something is still to do, as refresh runs this too.
|
||||
# This script is usually piped into bash, so ask on the terminal, not stdin.
|
||||
if ~/.local/bin/configure --list | grep -q '^to do'; then
|
||||
echo
|
||||
echo "configure mounts your Nextcloud files and signs you in to the AI agents."
|
||||
if { : < /dev/tty; } 2> /dev/null && read -rp "Run configure now? [Y/n] " answer < /dev/tty \
|
||||
&& [[ "$answer" != [nN]* ]]; then
|
||||
~/.local/bin/configure < /dev/tty || true
|
||||
else
|
||||
echo "To do it later, run: configure"
|
||||
fi
|
||||
fi
|
||||
|
||||
echo ""
|
||||
read -rp "A reboot is required to complete setup. Reboot now? [y/N] " response </dev/tty
|
||||
case "$response" in
|
||||
|
||||
Executable
+4
@@ -0,0 +1,4 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Hook MiniServer's shell setup into ~/.bashrc
|
||||
bash "$HOME/.local/share/MiniServer/shell/setup.sh"
|
||||
@@ -32,3 +32,28 @@ UTILITIES=(
|
||||
unzip
|
||||
)
|
||||
|
||||
# Shell: prompt, completion, directory jumping, fuzzy finding (see shell/)
|
||||
SHELL_TOOLS=(
|
||||
bash-completion
|
||||
bat
|
||||
eza
|
||||
fzf
|
||||
starship
|
||||
zoxide
|
||||
)
|
||||
|
||||
# AI agents and their sandbox (see sandbox/). Claude Code and Hermes Agent
|
||||
# install themselves; srt, the sandbox runtime, comes from npm.
|
||||
AGENTS=(
|
||||
# srt confines agents with bubblewrap, relays their network with socat and
|
||||
# scans for files to protect with ripgrep
|
||||
bubblewrap
|
||||
socat
|
||||
ripgrep
|
||||
jq
|
||||
# Hermes' Matrix support builds python-olm, which needs clang++ and cmake
|
||||
clang
|
||||
opencode
|
||||
# pipx apps installed with its uv backend, as Hermes does, need uv to upgrade
|
||||
uv
|
||||
)
|
||||
|
||||
Executable
+127
@@ -0,0 +1,127 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# Purpose : Run an AI agent inside Anthropic's sandbox runtime (srt). srt uses
|
||||
# bubblewrap to limit which files the agent can read and write, and
|
||||
# removes its network; socat links it to a proxy outside the sandbox
|
||||
# that only lets through the domains in its config.
|
||||
#
|
||||
# sandbox/bin/<agent> links here and shell/bashrc puts sandbox/bin first on
|
||||
# PATH, so typing an agent's name runs it sandboxed. `unsandboxed <agent>` runs
|
||||
# the agent's real command instead.
|
||||
#
|
||||
# The config is sandbox/config/base.json, then sandbox/config/<agent>.json,
|
||||
# then your own ~/.config/miniserver/sandbox/base.json and <agent>.json, in
|
||||
# the same format. Later files add to the lists in earlier ones. Domains in
|
||||
# MINISERVER_SANDBOX_DOMAINS (space-separated) are allowed too; setup.sh uses
|
||||
# it for Hermes' gateway, whose messaging server differs on each machine.
|
||||
|
||||
# Wrapped in { ...; exit; } so bash reads the whole script up front; update
|
||||
# replaces this file (git pull) while agents are running.
|
||||
{
|
||||
|
||||
SANDBOX="$HOME/.local/share/MiniServer/sandbox"
|
||||
USER_CONFIG="$HOME/.config/miniserver/sandbox"
|
||||
name="$(basename "$0")"
|
||||
|
||||
# The agent's real command: the first one on PATH outside sandbox/bin
|
||||
real_command() {
|
||||
local dir bin_dir IFS=:
|
||||
bin_dir="$(realpath "$SANDBOX/bin")"
|
||||
for dir in $PATH; do
|
||||
[ -n "$dir" ] && [ "$(realpath "$dir" 2> /dev/null)" != "$bin_dir" ] || continue
|
||||
if [ -x "$dir/$1" ] && [ ! -d "$dir/$1" ]; then
|
||||
echo "$dir/$1"
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
if [ "$name" = "unsandboxed" ]; then
|
||||
if [ $# -eq 0 ]; then
|
||||
echo "Usage: unsandboxed <agent> [arguments]"
|
||||
exit 2
|
||||
fi
|
||||
agent="$1"
|
||||
shift
|
||||
real="$(real_command "$agent")" || { echo "$agent isn't installed."; exit 127; }
|
||||
exec "$real" "$@"
|
||||
fi
|
||||
|
||||
agent="$name"
|
||||
real="$(real_command "$agent")" || { echo "$agent isn't installed."; exit 127; }
|
||||
|
||||
# Commands that share another agent's sandbox config
|
||||
case "$agent" in
|
||||
hermes-agent) profile=hermes ;;
|
||||
*) profile="$agent" ;;
|
||||
esac
|
||||
|
||||
srt="$(command -v srt || echo "$HOME/.local/bin/srt")"
|
||||
if [ ! -x "$srt" ]; then
|
||||
echo "The sandbox (srt) isn't installed; run update. To run $agent without it: unsandboxed $agent"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# The agent can change anything in the directory it starts in
|
||||
case "$PWD" in
|
||||
"$HOME"|/)
|
||||
echo "Start $agent from a project directory. In the sandbox it can change anything in the directory it starts in, and this is $PWD."
|
||||
exit 1 ;;
|
||||
esac
|
||||
|
||||
configs=("$SANDBOX/config/base.json")
|
||||
[ -f "$SANDBOX/config/$profile.json" ] && configs+=("$SANDBOX/config/$profile.json")
|
||||
ours=("${configs[@]}")
|
||||
for file in "$USER_CONFIG/base.json" "$USER_CONFIG/$profile.json"; do
|
||||
[ -f "$file" ] && configs+=("$file")
|
||||
done
|
||||
|
||||
# Objects are merged and lists joined, so later files only add to earlier ones
|
||||
MERGE='
|
||||
def merge(a; b):
|
||||
reduce (b | to_entries[]) as $e (a;
|
||||
.[$e.key] |= (
|
||||
if type == "object" and ($e.value | type) == "object" then merge(.; $e.value)
|
||||
elif type == "array" and ($e.value | type) == "array" then . + $e.value | unique
|
||||
else $e.value end));
|
||||
reduce .[] as $config ({}; merge(.; $config))'
|
||||
|
||||
# srt runs in place of this script (exec below), so a stop signal, e.g. from
|
||||
# systemd, reaches it and it passes it on to the agent. That leaves nothing to
|
||||
# remove the settings file afterwards, so it's named after this process, and
|
||||
# files whose process has ended are removed here.
|
||||
SETTINGS_DIR="${XDG_RUNTIME_DIR:-/tmp}/miniserver-sandbox"
|
||||
mkdir -p -m 700 "$SETTINGS_DIR" || exit 1
|
||||
for old in "$SETTINGS_DIR"/*.json; do
|
||||
pid="${old%.json}"
|
||||
pid="${pid##*.}"
|
||||
[[ "$pid" =~ ^[0-9]+$ ]] && ! kill -0 "$pid" 2> /dev/null && rm -f "$old"
|
||||
done
|
||||
|
||||
# Outside the sandbox's writable paths, so the agent can't change its own rules
|
||||
settings="$SETTINGS_DIR/$agent.$$.json"
|
||||
if ! jq -s --arg domains "$MINISERVER_SANDBOX_DOMAINS" \
|
||||
"$MERGE"' | .network.allowedDomains += ($domains | split(" ") | map(select(. != "")))' \
|
||||
"${configs[@]}" > "$settings"; then
|
||||
echo "Couldn't read the sandbox config: ${configs[*]}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Bubblewrap can only make writable a directory that exists, and the agent
|
||||
# can't create one itself in a parent it may not write to
|
||||
while IFS= read -r path; do
|
||||
path="${path/#\~/$HOME}"
|
||||
case "$path" in "$HOME"/*) mkdir -p "$path" ;; esac
|
||||
done < <(jq -rs "$MERGE | .filesystem.allowWrite[]? | strings" "${ours[@]}")
|
||||
|
||||
# srt sets TMPDIR to /tmp/claude, which nothing creates, unless told otherwise;
|
||||
# temp files then fail, and GTK aborts. /tmp is writable in every profile.
|
||||
export CLAUDE_CODE_TMPDIR=/tmp
|
||||
|
||||
# Hermes' terminal reaches its gateway, in another sandbox, through a relay
|
||||
case "$profile" in
|
||||
hermes) exec "$srt" --settings "$settings" -- bash "$SANDBOX/hermes-bridge" "$real" "$@" ;;
|
||||
esac
|
||||
exec "$srt" --settings "$settings" -- "$real" "$@"
|
||||
}
|
||||
Symlink
+1
@@ -0,0 +1 @@
|
||||
../agent
|
||||
Symlink
+1
@@ -0,0 +1 @@
|
||||
../agent
|
||||
Symlink
+1
@@ -0,0 +1 @@
|
||||
../agent
|
||||
Symlink
+1
@@ -0,0 +1 @@
|
||||
../agent
|
||||
@@ -0,0 +1,85 @@
|
||||
{
|
||||
"network": {
|
||||
"allowedDomains": [
|
||||
"anthropic.com",
|
||||
"*.anthropic.com",
|
||||
"openai.com",
|
||||
"*.openai.com",
|
||||
"chatgpt.com",
|
||||
"*.chatgpt.com",
|
||||
"openrouter.ai",
|
||||
"*.openrouter.ai",
|
||||
"nousresearch.com",
|
||||
"*.nousresearch.com",
|
||||
"generativelanguage.googleapis.com",
|
||||
"api.mistral.ai",
|
||||
"api.deepseek.com",
|
||||
"api.x.ai",
|
||||
"api.groq.com",
|
||||
"api.together.xyz",
|
||||
"*.githubcopilot.com",
|
||||
|
||||
"github.com",
|
||||
"*.github.com",
|
||||
"*.githubusercontent.com",
|
||||
"gitea.federated.computer",
|
||||
"gitea.young.computer",
|
||||
"registry.npmjs.org",
|
||||
"registry.yarnpkg.com",
|
||||
"pypi.org",
|
||||
"files.pythonhosted.org",
|
||||
"crates.io",
|
||||
"*.crates.io",
|
||||
"rubygems.org",
|
||||
"*.rubygems.org",
|
||||
"proxy.golang.org",
|
||||
"sum.golang.org",
|
||||
"archlinux.org",
|
||||
"*.archlinux.org"
|
||||
],
|
||||
"deniedDomains": []
|
||||
},
|
||||
"filesystem": {
|
||||
"denyRead": [
|
||||
"~/.ssh",
|
||||
"~/.gnupg",
|
||||
"~/.password-store",
|
||||
"~/.local/share/keyrings",
|
||||
"~/.netrc",
|
||||
"~/.git-credentials",
|
||||
"~/.aws",
|
||||
"~/.azure",
|
||||
"~/.config/gcloud",
|
||||
"~/.kube",
|
||||
"~/.docker",
|
||||
"~/.config/gh",
|
||||
"~/.config/tea",
|
||||
"~/.config/borg",
|
||||
"~/.config/Bitwarden",
|
||||
"~/.config/Element",
|
||||
"~/.mozilla",
|
||||
"~/.config/chromium",
|
||||
"~/.config/google-chrome",
|
||||
"~/.config/BraveSoftware",
|
||||
"~/.config/net.imput.helium",
|
||||
"~/.var/app",
|
||||
"~/.davfs2",
|
||||
"~/.claude",
|
||||
"~/.claude.json",
|
||||
"~/.hermes",
|
||||
"~/.config/opencode",
|
||||
"~/.local/share/opencode"
|
||||
],
|
||||
"allowRead": [],
|
||||
"allowWrite": [
|
||||
".",
|
||||
"/tmp",
|
||||
"~/.npm",
|
||||
"~/.cache/pip",
|
||||
"~/.cargo/registry",
|
||||
"~/.cargo/git",
|
||||
"~/go/pkg/mod"
|
||||
],
|
||||
"denyWrite": []
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"network": {
|
||||
"allowedDomains": [],
|
||||
"allowAllUnixSockets": true
|
||||
},
|
||||
"filesystem": {
|
||||
"denyRead": [
|
||||
"/run/user",
|
||||
"/run/dbus",
|
||||
"/run/docker.sock",
|
||||
"/run/podman",
|
||||
"/run/tailscale",
|
||||
"/run/ssh-unix-local",
|
||||
"/tmp/.X11-unix",
|
||||
"/tmp/tmux-*",
|
||||
"/tmp/ssh-*",
|
||||
"/tmp/*/SingletonSocket"
|
||||
],
|
||||
"allowRead": [
|
||||
"~/.hermes"
|
||||
],
|
||||
"allowWrite": [
|
||||
"~/.hermes"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"network": {
|
||||
"allowedDomains": [
|
||||
"opencode.ai",
|
||||
"*.opencode.ai",
|
||||
"models.dev"
|
||||
]
|
||||
},
|
||||
"filesystem": {
|
||||
"allowRead": [
|
||||
"~/.config/opencode",
|
||||
"~/.local/share/opencode"
|
||||
],
|
||||
"allowWrite": [
|
||||
"~/.config/opencode",
|
||||
"~/.local/share/opencode",
|
||||
"~/.local/state/opencode",
|
||||
"~/.cache/opencode"
|
||||
]
|
||||
}
|
||||
}
|
||||
Executable
+81
@@ -0,0 +1,81 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# Purpose : Run Hermes inside its sandbox (sandbox/agent runs this there) and
|
||||
# connect its terminal to its gateway.
|
||||
#
|
||||
# Hermes' gateway runs the agent; `hermes` in a terminal only talks to it, over
|
||||
# the gateway's control socket and then a WebSocket on 127.0.0.1:<port>. Each
|
||||
# sandbox has its own network, so that port can't be reached from another one.
|
||||
# The gateway's sandbox relays the port to API_SOCKET, and a terminal's sandbox
|
||||
# listens on the same port and relays to API_SOCKET, so `hermes` reaches the
|
||||
# gateway as if they shared a network.
|
||||
#
|
||||
# Usage: hermes-bridge <hermes command> [arguments]
|
||||
|
||||
real="$1"
|
||||
shift
|
||||
HERMES_HOME="${HERMES_HOME:-$HOME/.hermes}"
|
||||
API_SOCKET="$HERMES_HOME/miniserver-api.sock"
|
||||
|
||||
# The gateway's answer to `identify` on its control socket, or nothing if no
|
||||
# gateway is running
|
||||
identify() {
|
||||
local control="$HERMES_HOME/gateway.sock"
|
||||
[ -S "$control" ] || control="$(cat "$HERMES_HOME/gateway.sock.path" 2> /dev/null)"
|
||||
[ -S "$control" ] || return 1
|
||||
printf '{"protocol":1,"verb":"identify","id":1}\n' \
|
||||
| socat -t 5 - "UNIX-CONNECT:$control" 2> /dev/null | grep .
|
||||
}
|
||||
|
||||
# The port the gateway's API listens on, once the gateway is ready. Only an
|
||||
# IPv4 loopback origin is relayed, which is what Hermes uses by default.
|
||||
api_port() {
|
||||
local origin
|
||||
origin="$(identify | jq -r 'select(.result.state == "ready") | .result.api_origin // empty' 2> /dev/null)"
|
||||
[[ "$origin" =~ ^http://127\.0\.0\.1:([0-9]+)$ ]] || return 1
|
||||
echo "${BASH_REMATCH[1]}"
|
||||
}
|
||||
|
||||
if [ "$1" = "gateway" ] && [ "$2" = "run" ]; then
|
||||
# A terminal finds no relay, rather than the last gateway's, until this one
|
||||
# is up
|
||||
rm -f "$API_SOCKET"
|
||||
# The relay waits for the gateway, which runs in this process once exec'd;
|
||||
# the sandbox ends both together
|
||||
(
|
||||
for _ in $(seq 480); do
|
||||
if port="$(api_port)"; then
|
||||
exec socat "UNIX-LISTEN:$API_SOCKET,unlink-early,mode=600,fork" "TCP:127.0.0.1:$port"
|
||||
fi
|
||||
sleep 0.25
|
||||
done
|
||||
echo "hermes-bridge: the gateway's API didn't come up; terminals can't reach it" >&2
|
||||
) &
|
||||
exec "$real" "$@"
|
||||
fi
|
||||
|
||||
# A terminal: relay the gateway's port, if a gateway is running or its service
|
||||
# is turned on, waiting for one that is still starting. Without one, hermes
|
||||
# starts its own gateway inside this sandbox, which needs no relay.
|
||||
if [ -e "$HOME/.config/systemd/user/default.target.wants/hermes-gateway.service" ] \
|
||||
|| identify > /dev/null; then
|
||||
[ -S "$API_SOCKET" ] && api_port > /dev/null || echo "Waiting for Hermes' gateway..." >&2
|
||||
for _ in $(seq 240); do
|
||||
[ -S "$API_SOCKET" ] && port="$(api_port)" && break
|
||||
port=""
|
||||
sleep 0.25
|
||||
done
|
||||
if [ -n "$port" ]; then
|
||||
socat "TCP-LISTEN:$port,bind=127.0.0.1,reuseaddr,fork" "UNIX-CONNECT:$API_SOCKET" &
|
||||
# Wait for the listener (state 0A, LISTEN, in /proc/net/tcp) so hermes'
|
||||
# first connection doesn't fail
|
||||
hex="$(printf ':%04X ' "$port")"
|
||||
for _ in $(seq 50); do
|
||||
grep -q "$hex.* 0A " /proc/net/tcp && break
|
||||
sleep 0.05
|
||||
done
|
||||
else
|
||||
echo "hermes-bridge: Hermes' gateway is running but its API isn't reachable; try: systemctl --user restart hermes-gateway" >&2
|
||||
fi
|
||||
fi
|
||||
exec "$real" "$@"
|
||||
@@ -0,0 +1,84 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# Purpose : Set up sandboxed AI agents (see sandbox/agent): install or update
|
||||
# Anthropic's sandbox runtime (srt) and Hermes Agent, and turn on
|
||||
# Claude Code's built-in sandbox. Run by install.sh and on every
|
||||
# update (updates.d/72-sandbox.sh).
|
||||
|
||||
MINISERVER="$HOME/.local/share/MiniServer"
|
||||
STATUS=0
|
||||
|
||||
# npm's global prefix is under /usr, so install to ~/.local; srt lands in ~/.local/bin
|
||||
echo "Installing the sandbox runtime (srt)..."
|
||||
npm install --global --prefix "$HOME/.local" --no-fund --no-audit @anthropic-ai/sandbox-runtime || STATUS=1
|
||||
|
||||
# --non-interactive skips Hermes' setup questions and its background gateway
|
||||
# service; `configure hermes` chooses a model provider afterwards
|
||||
if [ ! -x "$HOME/.local/bin/hermes" ]; then
|
||||
echo "Installing Hermes Agent..."
|
||||
curl -fsSL https://hermes-agent.nousresearch.com/install.sh | bash -s -- --non-interactive || STATUS=1
|
||||
fi
|
||||
|
||||
# Claude Code runs every command it starts in its own sandbox, also built on
|
||||
# bubblewrap and socat; it can't run inside srt because bubblewrap can't be
|
||||
# nested. Anything already in the sandbox section of your settings wins over
|
||||
# these defaults.
|
||||
CLAUDE_SETTINGS="$HOME/.claude/settings.json"
|
||||
if [ -f "$CLAUDE_SETTINGS" ]; then
|
||||
domains="$(jq '.network.allowedDomains' "$MINISERVER/sandbox/config/base.json")"
|
||||
tmp="$(mktemp "$CLAUDE_SETTINGS.XXXXXX")"
|
||||
if jq --argjson domains "$domains" \
|
||||
'.sandbox = ({enabled: true, network: {allowedDomains: $domains}} * (.sandbox // {}))' \
|
||||
"$CLAUDE_SETTINGS" > "$tmp"; then
|
||||
if cmp -s "$tmp" "$CLAUDE_SETTINGS"; then
|
||||
rm -f "$tmp"
|
||||
else
|
||||
echo "Turning on Claude Code's sandbox..."
|
||||
mv -f "$tmp" "$CLAUDE_SETTINGS"
|
||||
fi
|
||||
else
|
||||
echo "Couldn't read $CLAUDE_SETTINGS; Claude Code's sandbox was not turned on."
|
||||
rm -f "$tmp"
|
||||
STATUS=1
|
||||
fi
|
||||
fi
|
||||
|
||||
# Hermes' gateway acts on messages from outside this machine, so it runs in
|
||||
# the sandbox too: a systemd drop-in starts it through sandbox/bin/hermes. It
|
||||
# also needs to reach the Matrix server in Hermes' settings, which differs on
|
||||
# each machine; for other messaging services, add their domains to
|
||||
# ~/.config/miniserver/sandbox/hermes.json.
|
||||
GATEWAY_UNIT="$HOME/.config/systemd/user/hermes-gateway.service"
|
||||
if [ -f "$GATEWAY_UNIT" ]; then
|
||||
if [ -x "$HOME/.local/bin/srt" ]; then
|
||||
homeserver="$(sed -nE "s#^MATRIX_HOMESERVER=[\"']?https?://([^/:\"']+).*#\1#p" "$HOME/.hermes/.env" 2> /dev/null | head -n 1)"
|
||||
dropin="$GATEWAY_UNIT.d/miniserver-sandbox.conf"
|
||||
tmp="$(mktemp)"
|
||||
cat > "$tmp" << EOF
|
||||
# Written by MiniServer (sandbox/setup.sh): run Hermes' gateway in the sandbox
|
||||
[Service]
|
||||
Environment="MINISERVER_SANDBOX_DOMAINS=$homeserver"
|
||||
ExecStart=
|
||||
ExecStart=%h/.local/share/MiniServer/sandbox/bin/hermes gateway run
|
||||
EOF
|
||||
if cmp -s "$tmp" "$dropin"; then
|
||||
rm -f "$tmp"
|
||||
else
|
||||
echo "Running Hermes' gateway in the sandbox..."
|
||||
mkdir -p "$(dirname "$dropin")" && mv -f "$tmp" "$dropin" && chmod 644 "$dropin" \
|
||||
&& systemctl --user daemon-reload || STATUS=1
|
||||
fi
|
||||
else
|
||||
echo "Hermes' gateway isn't sandboxed yet: the sandbox (srt) isn't installed."
|
||||
STATUS=1
|
||||
fi
|
||||
|
||||
# A server is rarely logged in to, and user services stop at logout unless
|
||||
# the user lingers
|
||||
if [ "$(loginctl show-user "$USER" --property=Linger --value 2> /dev/null)" != yes ]; then
|
||||
echo "Keeping Hermes' gateway running while you're logged out..."
|
||||
sudo loginctl enable-linger "$USER" || STATUS=1
|
||||
fi
|
||||
fi
|
||||
|
||||
exit $STATUS
|
||||
@@ -0,0 +1,31 @@
|
||||
# MiniServer aliases, sourced by shell/bashrc
|
||||
|
||||
# Files
|
||||
if command -v eza &> /dev/null; then
|
||||
alias ls='eza --group-directories-first --icons=auto'
|
||||
alias ll='eza -lh --group-directories-first --icons=auto'
|
||||
alias la='eza -lha --group-directories-first --icons=auto'
|
||||
alias lt='eza --tree --level=2 --long --icons=auto --git'
|
||||
fi
|
||||
|
||||
# Fuzzy-find a file with a preview, or open the one you pick in your editor
|
||||
alias ff="fzf --preview 'bat --style=numbers --color=always {}'"
|
||||
alias eff='${EDITOR:-nvim} "$(ff)"'
|
||||
|
||||
alias decompress='tar -xzf'
|
||||
|
||||
# Directories
|
||||
alias ..='cd ..'
|
||||
alias ...='cd ../..'
|
||||
alias ....='cd ../../..'
|
||||
|
||||
# Tools
|
||||
alias g='git'
|
||||
alias lg='lazygit'
|
||||
alias d='docker'
|
||||
alias ld='lazydocker'
|
||||
|
||||
# Git
|
||||
alias gcm='git commit -m'
|
||||
alias gcam='git commit -a -m'
|
||||
alias gcad='git commit -a --amend'
|
||||
@@ -0,0 +1,42 @@
|
||||
# MiniServer shell setup, sourced from ~/.bashrc (added by shell/setup.sh).
|
||||
# Anything in ~/.bashrc after the line that sources this file overrides it.
|
||||
|
||||
# Only for interactive shells
|
||||
[[ $- != *i* ]] && return
|
||||
|
||||
MINISERVER_SHELL="$HOME/.local/share/MiniServer/shell"
|
||||
|
||||
# AI agents such as opencode and hermes run sandboxed (see sandbox/agent), so
|
||||
# sandbox/bin must come before the directories holding their real commands
|
||||
SANDBOX_BIN="$HOME/.local/share/MiniServer/sandbox/bin"
|
||||
PATH="$SANDBOX_BIN:${PATH//$SANDBOX_BIN:/}"
|
||||
|
||||
# History: keep a lot of it, skip duplicates, and don't let shells overwrite
|
||||
# each other's history on exit
|
||||
shopt -s histappend
|
||||
HISTCONTROL=ignoreboth
|
||||
HISTSIZE=32768
|
||||
HISTFILESIZE="$HISTSIZE"
|
||||
|
||||
if [[ ! -v BASH_COMPLETION_VERSINFO && -f /usr/share/bash-completion/bash_completion ]]; then
|
||||
source /usr/share/bash-completion/bash_completion
|
||||
fi
|
||||
|
||||
source "$MINISERVER_SHELL/aliases"
|
||||
source "$MINISERVER_SHELL/functions"
|
||||
|
||||
# Prompt. Uses MiniServer's starship config unless you have your own.
|
||||
if [[ $TERM != "dumb" ]] && command -v starship &> /dev/null; then
|
||||
[[ -f "$HOME/.config/starship.toml" ]] || export STARSHIP_CONFIG="$MINISERVER_SHELL/starship.toml"
|
||||
eval "$(starship init bash)"
|
||||
fi
|
||||
|
||||
# z <part of a path> jumps to a directory you've visited before
|
||||
if command -v zoxide &> /dev/null; then
|
||||
eval "$(zoxide init bash)"
|
||||
fi
|
||||
|
||||
# Ctrl-R fuzzy history search, Ctrl-T to insert a file path, Alt-C to cd
|
||||
if [[ -f /usr/share/fzf/key-bindings.bash ]]; then
|
||||
source /usr/share/fzf/key-bindings.bash
|
||||
fi
|
||||
@@ -0,0 +1,78 @@
|
||||
# MiniServer shell functions, sourced by shell/bashrc
|
||||
|
||||
# n opens Neovim in the current directory, or on the files given
|
||||
n() { if [ "$#" -eq 0 ]; then command nvim . ; else command nvim "$@"; fi; }
|
||||
|
||||
# open a file or URL with its default app, detached from the terminal
|
||||
open() (
|
||||
xdg-open "$@" > /dev/null 2>&1 &
|
||||
)
|
||||
|
||||
# compress <dir> makes <dir>.tar.gz (decompress is an alias for tar -xzf)
|
||||
compress() { tar -czf "${1%/}.tar.gz" "${1%/}"; }
|
||||
|
||||
# SSH port forwarding
|
||||
# fip <host> <port>... forward each localhost port to the same port on host
|
||||
# dip <port>... stop forwarding those ports
|
||||
# lip list active forwards
|
||||
fip() {
|
||||
(( $# < 2 )) && echo "Usage: fip <host> <port1> [port2] ..." && return 1
|
||||
local host="$1"
|
||||
shift
|
||||
for port in "$@"; do
|
||||
ssh -f -N -L "${port}:localhost:${port}" "$host" && echo "Forwarding localhost:$port -> $host:$port"
|
||||
done
|
||||
}
|
||||
|
||||
dip() {
|
||||
(( $# == 0 )) && echo "Usage: dip <port1> [port2] ..." && return 1
|
||||
for port in "$@"; do
|
||||
pkill -f "ssh.*-L ${port}:localhost:${port}" && echo "Stopped forwarding port $port" || echo "No forwarding on port $port"
|
||||
done
|
||||
}
|
||||
|
||||
lip() {
|
||||
pgrep -af "ssh.*-L [0-9]+:localhost:[0-9]+" || echo "No active forwards"
|
||||
}
|
||||
|
||||
# Git worktrees
|
||||
# ga <branch> create ../<repo>--<branch> on a new branch and cd into it
|
||||
# gd from inside that worktree, remove it and its branch
|
||||
ga() {
|
||||
if [[ -z "$1" ]]; then
|
||||
echo "Usage: ga [branch name]"
|
||||
return 1
|
||||
fi
|
||||
|
||||
local branch="$1"
|
||||
local base="$(basename "$PWD")"
|
||||
local wt_path="../${base}--${branch}"
|
||||
|
||||
git worktree add -b "$branch" "$wt_path" || return 1
|
||||
command -v mise &> /dev/null && mise trust "$wt_path"
|
||||
cd "$wt_path"
|
||||
}
|
||||
|
||||
gd() {
|
||||
local cwd base branch root worktree answer
|
||||
|
||||
cwd="$(pwd)"
|
||||
worktree="$(basename "$cwd")"
|
||||
|
||||
# split on first `--`
|
||||
root="${worktree%%--*}"
|
||||
branch="${worktree#*--}"
|
||||
|
||||
# Protect against accidentally nuking a non-worktree directory
|
||||
if [[ "$root" == "$worktree" ]]; then
|
||||
echo "Not in a worktree made by ga (expected a <repo>--<branch> directory)."
|
||||
return 1
|
||||
fi
|
||||
|
||||
read -rp "Remove worktree $worktree and branch $branch? [y/N] " answer
|
||||
[[ $answer == [yY]* ]] || return 0
|
||||
|
||||
cd "../$root"
|
||||
git worktree remove "$cwd" --force || return 1
|
||||
git branch -D "$branch"
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# Purpose : Hook MiniServer's shell setup (prompt, zoxide, fzf key bindings,
|
||||
# aliases and functions in shell/) into ~/.bashrc
|
||||
|
||||
LINE='[[ -f "$HOME/.local/share/MiniServer/shell/bashrc" ]] && source "$HOME/.local/share/MiniServer/shell/bashrc"'
|
||||
|
||||
if grep -qF "$LINE" ~/.bashrc 2>/dev/null; then
|
||||
echo "MiniServer shell setup already in .bashrc"
|
||||
else
|
||||
echo "$LINE" >> ~/.bashrc
|
||||
echo "MiniServer shell setup added to .bashrc"
|
||||
fi
|
||||
@@ -0,0 +1,37 @@
|
||||
add_newline = true
|
||||
command_timeout = 200
|
||||
format = "[$hostname$directory$git_branch$git_status]($style)$character"
|
||||
|
||||
[character]
|
||||
error_symbol = "[✗](bold cyan)"
|
||||
success_symbol = "[❯](bold cyan)"
|
||||
|
||||
[hostname]
|
||||
ssh_only = false
|
||||
format = "[$hostname]($style):"
|
||||
style = "bold cyan"
|
||||
|
||||
[directory]
|
||||
truncation_length = 0
|
||||
truncate_to_repo = false
|
||||
repo_root_style = "bold cyan"
|
||||
repo_root_format = "[$repo_root]($repo_root_style)[$path]($style)[$read_only]($read_only_style) "
|
||||
|
||||
[git_branch]
|
||||
format = "[$branch]($style) "
|
||||
style = "italic cyan"
|
||||
|
||||
[git_status]
|
||||
format = '[$all_status]($style)'
|
||||
style = "cyan"
|
||||
ahead = "⇡${count} "
|
||||
diverged = "⇕⇡${ahead_count}⇣${behind_count} "
|
||||
behind = "⇣${count} "
|
||||
conflicted = " "
|
||||
up_to_date = " "
|
||||
untracked = "? "
|
||||
modified = " "
|
||||
stashed = ""
|
||||
staged = ""
|
||||
renamed = ""
|
||||
deleted = ""
|
||||
@@ -17,8 +17,9 @@ files at its top level:
|
||||
| `update.sh` | Runs on every `update`, right after the repo has been pulled |
|
||||
| `firewall.conf` | Read on `sibling add`, `sibling remove` and every `update` to open the project's ports |
|
||||
|
||||
Don't also copy a hook into MiniServer's `updates.d/` for the same project, or
|
||||
it updates twice.
|
||||
If you want full control over how the project updates, see
|
||||
`sibling-project-updates.md` (custom `updates.d` hooks) instead. Use one
|
||||
approach or the other, not both, or the project updates twice.
|
||||
|
||||
## What MiniServer Does
|
||||
|
||||
@@ -36,7 +37,7 @@ it updates twice.
|
||||
|
||||
### `update`
|
||||
|
||||
MiniServer runs its own update hooks in order (`10-pacman.sh` … `30-pm2.sh`).
|
||||
MiniServer runs its own update hooks in order (`10-pacman.sh` … `70-claude.sh`).
|
||||
Next, `updates.d/71-siblings.sh` takes each sibling in alphabetical order and:
|
||||
|
||||
1. Runs `git pull --ff-only --quiet` in its folder.
|
||||
|
||||
@@ -0,0 +1,149 @@
|
||||
# Skill: Supporting the updates.d Update Architecture in a Sibling Project
|
||||
|
||||
## Overview
|
||||
|
||||
There are two ways for a sibling project to plug into MiniServer:
|
||||
|
||||
1. **The `sibling` command (preferred).** The user runs `sibling` and gives it
|
||||
the project's git URL. The repo is cloned into
|
||||
`~/.local/share/siblings/<name>` and its `install.sh` (if any) is run once.
|
||||
On every `update`, `updates.d/71-siblings.sh` pulls each sibling and runs its
|
||||
`update.sh` (if any). The sibling only needs those two optional scripts.
|
||||
2. **A custom hook.** MiniServer's `update` script sources every `*.sh` file
|
||||
found in `~/.local/share/MiniServer/updates.d/` in filename order. A sibling
|
||||
project can install its own numbered shell script into that directory during
|
||||
its own install step, for full control over how and when it updates.
|
||||
|
||||
## Using the `sibling` Command
|
||||
|
||||
See `sibling-project-format.md` for the full rules, templates and testing.
|
||||
In short, give the repo these optional files at its top level:
|
||||
|
||||
| File | When it runs |
|
||||
|--------------|---------------------------------------------------|
|
||||
| `install.sh` | Once, from the repo folder, when the user runs `sibling add` (and again if they re-run it) |
|
||||
| `update.sh` | On every `update`, from the repo folder, after the repo has been pulled |
|
||||
|
||||
Both run with `bash`, and a non-zero exit is reported as a failure.
|
||||
`update.sh` should be idempotent. If you have no `update.sh`, `update` just pulls the repo.
|
||||
|
||||
Commands:
|
||||
|
||||
```bash
|
||||
sibling # asks for the git URL
|
||||
sibling add <url> # same, without the prompt
|
||||
sibling list # installed siblings, their URL and current commit
|
||||
sibling remove <name> # deletes ~/.local/share/siblings/<name>
|
||||
```
|
||||
|
||||
The rest of this document covers the custom-hook approach.
|
||||
|
||||
## How It Works
|
||||
|
||||
When a user runs `update`:
|
||||
|
||||
1. The MiniServer repo at `~/.local/share/MiniServer` is pulled.
|
||||
2. All `*.sh` files in `updates.d/` are **sourced**, each in its own subshell,
|
||||
in lexicographic order.
|
||||
3. Each script can see `$MINISERVER`, but nothing defined by earlier hooks —
|
||||
source `utils.sh` yourself if you need its helpers. A `cd`, `set -e` or `exit`
|
||||
in one hook can't affect the others.
|
||||
4. A hook that exits non-zero is listed in the summary at the end of `update`.
|
||||
|
||||
Because the scripts are sourced from within the cloned repo directory, any file
|
||||
placed there by a sibling project's installer will **survive `git pull`** — git
|
||||
does not delete untracked files on pull.
|
||||
|
||||
## Naming Convention
|
||||
|
||||
Existing MiniServer hooks use a numeric prefix to control execution order:
|
||||
|
||||
| File | Purpose |
|
||||
|-----------------------|---------------------------------------------|
|
||||
| `10-pacman.sh` | Arch system update |
|
||||
| `12-migrations.sh` | One-time migrations from `migrations/` |
|
||||
| `20-aur.sh` | AUR packages |
|
||||
| `30-pm2.sh` | pm2 |
|
||||
| `40-cockpit.sh` | Cockpit |
|
||||
| `70-claude.sh` | Claude Code setup |
|
||||
| `71-siblings.sh` | Projects installed with `sibling` |
|
||||
| `72-sandbox.sh` | Sandbox runtime and Hermes Agent |
|
||||
| `80-firewall.sh` | Ports from every `firewall.conf` |
|
||||
| `95-cleanup.sh` | pacman cache cleanup |
|
||||
| `99-reboot-check.sh` | Reboot-needed notice |
|
||||
|
||||
Sibling projects should use an unused number from **73–79** so they run after
|
||||
the core updates but before MiniServer's end-of-run hooks (80+).
|
||||
|
||||
## What to Add to a Sibling Project
|
||||
|
||||
### 1. Create the hook script
|
||||
|
||||
Add a file like `updates.d/73-myproject.sh` to the sibling repo:
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
|
||||
echo "Updating MyProject..."
|
||||
git -C "$HOME/.local/share/myproject" pull
|
||||
|
||||
# Optional: use MiniServer helpers if needed
|
||||
# source "$MINISERVER/utils.sh"
|
||||
# install_packages "${MY_PACKAGES[@]}"
|
||||
```
|
||||
|
||||
### 2. Install the hook during the sibling's install step
|
||||
|
||||
In the sibling's `install.sh`, copy the hook into MiniServer's `updates.d/`:
|
||||
|
||||
```bash
|
||||
echo "Registering MyProject update hook..."
|
||||
cp updates.d/73-myproject.sh ~/.local/share/MiniServer/updates.d/
|
||||
```
|
||||
|
||||
This is a one-time step that wires the sibling into every future `update`
|
||||
run on the machine.
|
||||
|
||||
### Optional: only update when installed
|
||||
|
||||
Instead of running code at the top level, a hook can define `run_update` and,
|
||||
optionally, `is_installed`. `update` calls `run_update` only when `is_installed`
|
||||
is missing or returns true, so the hook is skipped on machines where the project
|
||||
isn't installed. Return non-zero from `run_update` to report a failure:
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
|
||||
is_installed() {
|
||||
[ -d "$HOME/.local/share/myproject" ]
|
||||
}
|
||||
|
||||
run_update() {
|
||||
echo "Updating MyProject..."
|
||||
git -C "$HOME/.local/share/myproject" pull --ff-only
|
||||
}
|
||||
```
|
||||
|
||||
## Available Helpers
|
||||
|
||||
Hooks don't inherit functions from earlier hooks, so source `utils.sh` for its
|
||||
helpers. Do this before defining your own `is_installed`, because `utils.sh` has
|
||||
a pacman-specific function with the same name:
|
||||
|
||||
```bash
|
||||
source "$MINISERVER/utils.sh"
|
||||
|
||||
# Install pacman packages (skips already-installed ones)
|
||||
install_packages "${MY_PACMAN_PACKAGES[@]}"
|
||||
|
||||
# Remove packages
|
||||
remove_packages "${MY_REMOVE_PACKAGES[@]}"
|
||||
```
|
||||
|
||||
## Checklist for Sibling Project Authors
|
||||
|
||||
- [ ] Create `updates.d/7X-<project>.sh` in the sibling repo
|
||||
- [ ] Hook script echoes its name so progress is visible during `update`
|
||||
- [ ] Install step copies the hook to `~/.local/share/MiniServer/updates.d/`
|
||||
- [ ] Numeric prefix is an unused number in 73–79
|
||||
- [ ] Script is idempotent — safe to run multiple times without side effects
|
||||
@@ -49,12 +49,24 @@ mkdir -p "$HOME/.local/bin" "$MINISERVER/updates.d"
|
||||
install_script "$MINISERVER/update" ~/.local/bin/update
|
||||
install_script "$MINISERVER/refresh" ~/.local/bin/refresh
|
||||
install_script "$MINISERVER/sibling" ~/.local/bin/sibling
|
||||
install_script "$MINISERVER/version" ~/.local/bin/version
|
||||
install_script "$MINISERVER/configure" ~/.local/bin/configure
|
||||
|
||||
if [[ -z "$UPDATE_REEXECED" ]]; then
|
||||
export UPDATE_REEXECED=1
|
||||
exec ~/.local/bin/update
|
||||
fi
|
||||
|
||||
# An upgrade that runs out of disk space part way through can leave the system
|
||||
# half-upgraded. Set UPDATE_FORCE=1 to skip this check.
|
||||
MIN_FREE_GIB=10
|
||||
FREE_KIB="$(df -k --output=avail / | tail -n 1 | tr -d ' ')"
|
||||
if [[ -z "$UPDATE_FORCE" && "$FREE_KIB" =~ ^[0-9]+$ ]] && (( FREE_KIB < MIN_FREE_GIB * 1024 * 1024 )); then
|
||||
echo "Only $(( FREE_KIB / 1024 / 1024 )) GiB free on /; updating needs at least $MIN_FREE_GIB GiB."
|
||||
echo "Free up space (sudo paccache -rk1 clears old cached packages), or run: UPDATE_FORCE=1 update"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Ask for the password once, then keep sudo's cached credentials fresh until
|
||||
# update exits, so long-running hooks don't stop for a password again.
|
||||
sudo -v || exit 1
|
||||
|
||||
@@ -25,7 +25,9 @@ if initramfs_build_failed; then
|
||||
fi
|
||||
|
||||
echo "Checking for new packages..."
|
||||
install_packages "${SERVERS[@]}"
|
||||
install_packages "${UTILITIES[@]}"
|
||||
install_packages "${SERVERS[@]}" || STATUS=1
|
||||
install_packages "${UTILITIES[@]}" || STATUS=1
|
||||
install_packages "${SHELL_TOOLS[@]}" || STATUS=1
|
||||
install_packages "${AGENTS[@]}" || STATUS=1
|
||||
|
||||
exit $STATUS
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Run one-time changes from migrations/ that haven't run on this machine yet.
|
||||
# This is how machines installed before a change to install.sh catch up, so a
|
||||
# change that should reach existing machines needs a migration as well. To add
|
||||
# one, create migrations/$(date +%s).sh; it runs with bash, once, on success.
|
||||
#
|
||||
# Runs after 10-pacman.sh so migrations install packages against fresh
|
||||
# package databases.
|
||||
|
||||
MINISERVER="$HOME/.local/share/MiniServer"
|
||||
|
||||
source "$MINISERVER/utils.sh"
|
||||
|
||||
run_migrations
|
||||
Executable
+14
@@ -0,0 +1,14 @@
|
||||
#!/bin/bash
|
||||
|
||||
MINISERVER="$HOME/.local/share/MiniServer"
|
||||
|
||||
# claude.sh has run if Claude Code is installed, ~/.local/bin is on PATH in
|
||||
# .bashrc, and its settings file exists
|
||||
if command -v claude &>/dev/null \
|
||||
&& grep -qF 'export PATH="$HOME/.local/bin:$PATH"' ~/.bashrc 2>/dev/null \
|
||||
&& [ -f "$HOME/.claude/settings.json" ]; then
|
||||
echo "Claude Code already set up."
|
||||
else
|
||||
echo "Setting up Claude Code..."
|
||||
bash "$MINISERVER/claude.sh"
|
||||
fi
|
||||
@@ -0,0 +1,31 @@
|
||||
#!/bin/bash
|
||||
|
||||
MINISERVER="$HOME/.local/share/MiniServer"
|
||||
|
||||
run_update() {
|
||||
local status=0 gateway=0
|
||||
bash "$MINISERVER/sandbox/setup.sh" || status=1
|
||||
|
||||
# Hermes update asks a running gateway to finish its work and restart, but
|
||||
# that signal (SIGUSR1) reaches srt, not the gateway inside its sandbox, so
|
||||
# the update would wait half an hour. Stop the gateway for the update instead;
|
||||
# starting it afterwards also loads new sandbox rules and dependencies.
|
||||
if systemctl --user is-active --quiet hermes-gateway.service; then
|
||||
gateway=1
|
||||
systemctl --user stop hermes-gateway.service || status=1
|
||||
fi
|
||||
|
||||
# The real command, not the sandboxed one in sandbox/bin: updating Hermes
|
||||
# writes outside what its sandbox allows. python-olm, for Matrix, bundles a
|
||||
# CMake file too old for CMake 4 unless told which policies to use.
|
||||
if [ -x "$HOME/.local/bin/hermes" ]; then
|
||||
echo "Updating Hermes Agent..."
|
||||
CMAKE_POLICY_VERSION_MINIMUM=3.5 "$HOME/.local/bin/hermes" update < /dev/null || status=1
|
||||
fi
|
||||
|
||||
if [ "$gateway" = 1 ]; then
|
||||
echo "Starting Hermes' gateway..."
|
||||
systemctl --user start hermes-gateway.service || status=1
|
||||
fi
|
||||
return $status
|
||||
}
|
||||
@@ -56,6 +56,29 @@ enable_services() {
|
||||
done
|
||||
}
|
||||
|
||||
# Run each script in migrations/ that hasn't succeeded on this machine yet, in
|
||||
# order, and record it once it does. Stops at the first failure so later
|
||||
# migrations can rely on earlier ones; it runs again on the next update.
|
||||
run_migrations() {
|
||||
local dir="${MINISERVER:-$HOME/.local/share/MiniServer}/migrations"
|
||||
local state="$HOME/.local/state/miniserver/migrations"
|
||||
local file name
|
||||
|
||||
mkdir -p "$state"
|
||||
for file in "$dir"/*.sh; do
|
||||
[ -f "$file" ] || continue
|
||||
name="$(basename "$file")"
|
||||
[ -f "$state/$name" ] && continue
|
||||
|
||||
echo "Running migration $name..."
|
||||
if ! bash "$file"; then
|
||||
echo "Migration $name failed; it will run again on the next update."
|
||||
return 1
|
||||
fi
|
||||
touch "$state/$name"
|
||||
done
|
||||
}
|
||||
|
||||
# Output of the last update's pacman -Su, written by updates.d/10-pacman.sh
|
||||
PACMAN_UPDATE_LOG="$HOME/.local/state/miniserver/pacman-update.log"
|
||||
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# Purpose : Show which version of MiniServer this machine is running
|
||||
#
|
||||
# MiniServer is a rolling release, so its version is the date of the commit
|
||||
# the local checkout is on, plus the short hash to tell same-day commits apart,
|
||||
# e.g. 2026.10.22 (c5f0651). The kernel is shown separately because it comes
|
||||
# from Arch, not MiniServer.
|
||||
#
|
||||
# version --check prints the newer version and exits 0 if one is available,
|
||||
# exits 1 if this machine is up to date, or 2 if it couldn't check.
|
||||
|
||||
MINISERVER="$HOME/.local/share/MiniServer"
|
||||
|
||||
source "$MINISERVER/utils.sh"
|
||||
|
||||
if ! git -C "$MINISERVER" rev-parse --git-dir &> /dev/null; then
|
||||
echo "MiniServer isn't installed in $MINISERVER."
|
||||
exit 2
|
||||
fi
|
||||
|
||||
# Version string for a commit, e.g. 2026.10.22 (c5f0651)
|
||||
describe() {
|
||||
git -C "$MINISERVER" log -1 --format='%cd (%h)' --date=format:%Y.%m.%d "$1"
|
||||
}
|
||||
|
||||
# Print the newer version if the remote has commits this checkout doesn't.
|
||||
# Returns 1 if up to date, 2 if the remote couldn't be reached.
|
||||
newer_version() {
|
||||
GIT_TERMINAL_PROMPT=0 timeout 30 git -C "$MINISERVER" fetch --quiet &> /dev/null || return 2
|
||||
local behind
|
||||
behind="$(git -C "$MINISERVER" rev-list --count 'HEAD..@{upstream}' 2> /dev/null)" || return 2
|
||||
(( behind > 0 )) || return 1
|
||||
describe '@{upstream}'
|
||||
}
|
||||
|
||||
if [ "$1" = "--check" ]; then
|
||||
newer_version
|
||||
exit
|
||||
fi
|
||||
|
||||
version="$(describe HEAD)"
|
||||
|
||||
# Local edits to the checkout mean this isn't exactly the version above
|
||||
if [ -n "$(git -C "$MINISERVER" status --porcelain --untracked-files=no)" ]; then
|
||||
version="$version, modified"
|
||||
fi
|
||||
|
||||
if latest="$(newer_version)"; then
|
||||
version="$version, $latest is available (run update)"
|
||||
fi
|
||||
|
||||
kernel="$(uname -r)"
|
||||
# If the running kernel's modules are gone, a newer kernel was installed
|
||||
if [ ! -d "/usr/lib/modules/$kernel" ]; then
|
||||
kernel="$kernel (newer kernel installed, reboot to use it)"
|
||||
fi
|
||||
|
||||
# 10-pacman.sh rewrites this log on every update
|
||||
if [ -f "$PACMAN_UPDATE_LOG" ]; then
|
||||
last_update="$(date -r "$PACMAN_UPDATE_LOG" '+%Y-%m-%d %H:%M')"
|
||||
else
|
||||
last_update="never"
|
||||
fi
|
||||
|
||||
echo "MiniServer $version"
|
||||
echo "Kernel $kernel"
|
||||
echo "Last update $last_update"
|
||||
Reference in New Issue
Block a user