Add sibling command for installing and updating sibling projects

`sibling` asks for a git URL, clones the repo into
~/.local/share/siblings/<name>, runs its install.sh and confirms the
install. `sibling list` and `sibling remove` manage installed siblings.

updates.d/71-siblings.sh pulls every sibling during `update` and runs
its update.sh, reporting failures in the update summary.

Document the sibling contract in skills/sibling-project-format.md and
update the custom-hook guide to match.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-19 16:23:58 -06:00
co-authored by Claude Opus 5.5
parent 78f88fcefd
commit 76fceee115
7 changed files with 445 additions and 9 deletions
+219
View File
@@ -0,0 +1,219 @@
# Skill: Formatting a Repository as a CoreDesktop Sibling Project
## Overview
A sibling project is a git repository that builds on CoreDesktop. Users install
one with CoreDesktop's `sibling` command, and from then on CoreDesktop's
`update` command keeps it up to date.
Follow this document when you are working in a sibling repository and need it
to install and update correctly under CoreDesktop. A repository needs no
special files to be installable. It becomes useful by adding two optional
scripts at its top level:
| File | When it runs |
|--------------|--------------------------------------------------------------|
| `install.sh` | Once, when the user runs `sibling add`, and again if they re-run it |
| `update.sh` | On every `update`, right after the repo has been pulled |
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 CoreDesktop Does
### `sibling add <url>` (or `sibling`, which asks for the URL)
1. Works out the project name from the last part of the URL, dropping `.git`:
`git@gitea.federated.computer:david/my-tools.git` → `my-tools`.
2. Clones the repo into `~/.local/share/siblings/<name>`, or, if that folder
already holds a clone of the same URL, runs `git pull --ff-only` there.
3. If `install.sh` exists, runs `bash install.sh` with the repo folder as the
working directory. Its input and output are the user's terminal.
4. Reports success only if every step above succeeded.
### `update`
CoreDesktop 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.
2. If the pull succeeded and `update.sh` exists, runs `bash update.sh` with the
repo folder as the working directory.
A failed pull or a non-zero exit from `update.sh` is printed and listed in
`update`'s "Done, with errors in: …" summary. Other siblings still update.
CoreDesktop's end-of-run hooks (GNOME settings, firmware, cleanup, reboot
check) run after all siblings.
### `sibling remove <name>`
This only deletes `~/.local/share/siblings/<name>`. CoreDesktop has no uninstall
step for siblings, so anything `install.sh` set up elsewhere stays.
## Rules for the Repository
### Layout
- Put `install.sh` and `update.sh` at the top level of the repo. Other names or
locations are ignored.
- Both scripts are run with `bash`, so they don't need to be executable, but a
`#!/bin/bash` line keeps them runnable by hand.
- Locate files relative to the script, not to a hard-coded path. The user may
also clone the repo somewhere else and run `install.sh` by hand.
```bash
SELF="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
```
### Environment
- The scripts run as separate `bash` processes, so they inherit **no**
variables or functions from CoreDesktop. In particular `$COREDESKTOP` is
**not** set. Use the path directly:
```bash
COREDESKTOP="$HOME/.local/share/coredesktop"
```
- CoreDesktop's helpers are available by sourcing `utils.sh`:
| Function | What it does |
|--------------------------|-----------------------------------------------------|
| `install_packages ...` | Installs pacman/AUR packages via `yay`, skipping installed ones |
| `remove_packages ...` | Removes installed pacman/AUR packages |
| `remove_flatpaks ...` | Uninstalls flatpaks that are installed |
| `enable_services ...` | Enables systemd services not yet enabled |
| `install_script src dst` | Copies a file atomically and makes it executable |
| `is_installed pkg` | True if a pacman package is installed |
- `sudo` prompts for a password as normal. CoreDesktop does not grant
passwordless sudo to siblings.
### `install.sh`
- **Exit non-zero on failure.** `set -e` near the top is the simplest way.
`sibling` then tells the user it failed and how to retry.
- **Make it safe to re-run.** Re-running `sibling add` is how users repair an
install, so skip work that is already done instead of failing on it.
- It may prompt the user, since it runs in their terminal.
- Install commands into `~/.local/bin` (already on `PATH` under CoreDesktop)
with `install_script`.
### `update.sh`
- **Make it idempotent.** It runs on every `update`, usually with nothing to do.
- **Don't prompt.** Users expect `update` to run through without questions,
apart from `sudo` passwords.
- **Exit non-zero only for real failures**, so that `update`'s error summary
stays meaningful.
- **Print what it's doing.** CoreDesktop prints only the project's name before
it runs.
- The repo has already been pulled, so `update.sh` always runs its newest
version. Use it to apply changes made since the last update: new packages,
updated commands in `~/.local/bin`, config migrations.
- It's fine to omit `update.sh` if pulling the repo is enough, for example when
`~/.local/bin` holds symlinks into the repo.
### Keep pulls working
`update` pulls with `--ff-only` and never merges or resets. A pull fails if:
- **History on the default branch is rewritten** (force-pushed). Never do this.
- **Tracked files are edited in place** in the clone and a new commit touches
them. Scripts must not write into tracked files. Write generated files,
caches and per-machine state outside the repo (e.g. `~/.config/<name>/`), or
into paths listed in `.gitignore`.
### Authentication
`sibling` clones with plain `git`. A private repo works only if the user can
already clone it (SSH key, credential helper, or a password typed at the
prompt). Every later `update` needs the same access.
## Templates
Share the install logic between the two scripts so that packages or commands
added later reach existing machines through `update`, not just new installs.
`install.sh`:
```bash
#!/bin/bash
#
# Purpose : Install <project> (run by CoreDesktop's `sibling add`)
set -e
SELF="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
COREDESKTOP="$HOME/.local/share/coredesktop"
source "$COREDESKTOP/utils.sh"
# One-time setup: prompts, accounts, initial config...
mkdir -p "$HOME/.config/<project>"
# Everything that update.sh also needs to keep current
bash "$SELF/update.sh"
echo "<project> installed."
```
`update.sh`:
```bash
#!/bin/bash
#
# Purpose : Keep <project> current (run by CoreDesktop's `update`)
set -e
SELF="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
COREDESKTOP="$HOME/.local/share/coredesktop"
source "$COREDESKTOP/utils.sh"
PACKAGES=(
jq
ripgrep
)
install_packages "${PACKAGES[@]}"
mkdir -p "$HOME/.local/bin"
install_script "$SELF/bin/<project>" "$HOME/.local/bin/<project>"
```
## Testing
Test in a throwaway home folder so nothing touches the real one. Point
`sibling` at a local clone; it accepts any URL or path that `git clone` does.
```bash
export HOME="$(mktemp -d)"
mkdir -p "$HOME/.local/share"
ln -s /path/to/real/coredesktop "$HOME/.local/share/coredesktop"
# Install
bash "$HOME/.local/share/coredesktop/sibling" add /path/to/sibling-repo
# Commit a change in /path/to/sibling-repo, then run the update hook the way
# `update` does
( source "$HOME/.local/share/coredesktop/updates.d/71-siblings.sh"; is_installed && run_update )
# Re-run the install to check it is safe to repeat
bash "$HOME/.local/share/coredesktop/sibling" add /path/to/sibling-repo
```
Scripts that call `sudo`, `yay` or `systemctl` still change the real system
even with a temporary `HOME`. Test those on a machine or VM you don't mind
changing.
## Checklist
- [ ] The repo can be cloned by URL by the people who will install it
- [ ] `install.sh` (optional) is at the top level, exits non-zero on failure,
and is safe to re-run
- [ ] `update.sh` (optional) is at the top level, idempotent, doesn't prompt,
and prints what it's doing
- [ ] Scripts find their own files relative to the script and set
`COREDESKTOP="$HOME/.local/share/coredesktop"` themselves
- [ ] Nothing writes to tracked files in the clone, and the default branch is
never force-pushed
- [ ] The project doesn't also copy a hook into CoreDesktop's `updates.d/`
- [ ] The README tells users to install with
`sibling add <url>`
+60 -8
View File
@@ -2,10 +2,41 @@
## Overview
CoreDesktop's `update` script sources every `*.sh` file found in
`~/.local/share/coredesktop/updates.d/` in filename order. A sibling project
hooks into this system by installing a numbered shell script into that directory
during its own install step.
There are two ways for a sibling project to plug into CoreDesktop:
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.** CoreDesktop's `update` script sources every `*.sh` file
found in `~/.local/share/coredesktop/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
@@ -41,20 +72,21 @@ Existing coredesktop hooks use a numeric prefix to control execution order:
| `60-pipx.sh` | pipx packages |
| `65-migrate.sh` | Migrate backup tool |
| `70-claude.sh` | Claude Code setup |
| `71-siblings.sh` | Projects installed with `sibling` |
| `80-gnome-extensions.sh` | GNOME extensions |
| `85-gnome-settings.sh` | GNOME extension settings |
| `90-firmware.sh` | Firmware update check |
| `95-cleanup.sh` | Flatpak/pacman cache cleanup |
| `99-reboot-check.sh` | Reboot-needed notice |
Sibling projects should use an unused number from **71–79** so they run after
Sibling projects should use an unused number from **72–79** so they run after
the core updates but before coredesktop's end-of-run hooks (80+).
## What to Add to a Sibling Project
### 1. Create the hook script
Add a file like `updates.d/70-myproject.sh` to the sibling repo:
Add a file like `updates.d/72-myproject.sh` to the sibling repo:
```bash
#!/bin/bash
@@ -73,12 +105,32 @@ In the sibling's `install.sh`, copy the hook into coredesktop's `updates.d/`:
```bash
echo "Registering MyProject update hook..."
cp updates.d/70-myproject.sh ~/.local/share/coredesktop/updates.d/
cp updates.d/72-myproject.sh ~/.local/share/coredesktop/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
@@ -100,5 +152,5 @@ remove_packages "${MY_REMOVE_PACKAGES[@]}"
- [ ] 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/coredesktop/updates.d/`
- [ ] Numeric prefix is an unused number in 71–79
- [ ] Numeric prefix is an unused number in 72–79
- [ ] Script is idempotent — safe to run multiple times without side effects