From 929295c949dbf427b278feabffbd894475993a1d Mon Sep 17 00:00:00 2001 From: guoli Date: Fri, 9 Oct 2026 09:11:33 +0800 Subject: [PATCH] use-family 0.10.0: install every missing CLI with one command A new user had to copy a dozen installer lines one by one, and on an Intel Mac two of them (wechat-use, discord-use) just stopped with an error. - install-clis.sh (new): installs each *-use CLI this machine is missing through its own install.sh. Skips what doesn't run here (wechat-use on Intel) and what's installed, carries on past a failure and reports it, and lists the one-time setup (memory-use init, a mailbox, Full Disk Access, DISCORD_TOKEN). `sh -s -- ` installs just those. - doctor.sh and install-use-family.sh lead with that one line. discord-use (Intel binary since v0.2.1) and image-use (install.sh since #49) now use the standard installer line, so the special cases are gone. - README and the routing skill point to it. - tests/install-clis.test.sh (in CI) with a fake curl and uname; the other two tests follow the new hints. Checked end to end with real installers in a fresh HOME on an Intel Mac: 12 of 13 installed; image-use needed a fix for macOS's Python 3.9 (leeguooooo/image-use, fix/install-old-python). Co-Authored-By: Claude Opus 5.5 --- .claude-plugin/marketplace.json | 2 +- .github/workflows/test.yml | 1 + README.md | 14 +++- install-clis.sh | 61 +++++++++++++++++ install-use-family.sh | 15 ++--- plugins/use-family/.claude-plugin/plugin.json | 2 +- plugins/use-family/scripts/doctor.sh | 15 ++--- plugins/use-family/skills/use-family/SKILL.md | 2 +- tests/doctor.test.sh | 9 ++- tests/install-clis.test.sh | 66 +++++++++++++++++++ tests/install-use-family.test.sh | 10 +-- 11 files changed, 159 insertions(+), 38 deletions(-) create mode 100644 install-clis.sh create mode 100644 tests/install-clis.test.sh diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index d850164..6a90ac5 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -34,7 +34,7 @@ { "name": "use-family", "description": "The whole *-use family in one install: every *-use plugin below as a dependency, a routing skill that tells the agent which one to use and how they combine, and a session-start check for missing CLIs, and CLIs kept at their plugin's version.", - "version": "0.9.1", + "version": "0.10.0", "category": "productivity", "source": "./plugins/use-family", "homepage": "https://github.com/leeguooooo/plugins/tree/main/plugins/use-family" diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 222c7ba..2f55f8f 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -12,6 +12,7 @@ jobs: - uses: actions/checkout@v4 - run: sh tests/install-use-family.test.sh - run: sh tests/doctor.test.sh + - run: sh tests/install-clis.test.sh - run: sh tests/readme-codex-list.test.sh - name: install-use-family.ps1 parses shell: pwsh diff --git a/README.md b/README.md index 7d68a75..9df169a 100644 --- a/README.md +++ b/README.md @@ -46,7 +46,15 @@ Agent-native CLIs that let coding agents drive real, logged-in surfaces — a br /plugin install use-family@leeguooooo-plugins ``` -`use-family` installs every `*-use` plugin below as a dependency, adds a `use-family` skill that tells the agent which one to reach for and how they combine, and checks at session start for any missing CLI (it only names the one-line installer; nothing runs without asking). +`use-family` installs every `*-use` plugin below as a dependency, adds a `use-family` skill that tells the agent which one to reach for and how they combine, and checks at session start for any missing CLI (it only names the installers; nothing runs without asking). + +Then install every CLI this machine is missing in one go. It runs each use's own installer, skips the ones that don't run here (wechat-use on Intel Macs, for example) and the ones already installed, and lists the one-time setup some of them need: + +```sh +curl -fsSL https://raw.githubusercontent.com/leeguooooo/plugins/main/install-clis.sh | sh +``` + +`… | sh -s -- mail-use ocs` installs just those, including iphone-use, which isn't in the default set because it needs an iPhone. **Codex and the ChatGPT desktop app.** Codex doesn't install plugin dependencies, so `use-family` there brings in the routing skill alone and every `*-use` has to be added by name. To add the whole family: @@ -57,7 +65,7 @@ for plugin in use-family chrome-use cookie-use mail-use message-use paste-use we done ``` -The list is `use-family`'s `dependencies` in [plugins/use-family/.claude-plugin/plugin.json](plugins/use-family/.claude-plugin/plugin.json); drop the ones you don't want. This installs the plugins (skills) only. The CLIs still install themselves on first use or with the one-line installers below, and a few uses need a one-time setup of their own (for example `memory-use init`, adding a mailbox to `mail-use`). +The list is `use-family`'s `dependencies` in [plugins/use-family/.claude-plugin/plugin.json](plugins/use-family/.claude-plugin/plugin.json); drop the ones you don't want. This installs the plugins (skills) only. The CLIs still install themselves on first use, or all at once with `install-clis.sh` (above), and a few uses need a one-time setup of their own (for example `memory-use init`, adding a mailbox to `mail-use`). The marketplace also shows up in the ChatGPT desktop app's Plugins Directory. `codex plugin marketplace upgrade leeguooooo-plugins` refreshes it. @@ -73,7 +81,7 @@ Windows (PowerShell; skips the macOS-only uses): irm https://raw.githubusercontent.com/leeguooooo/plugins/main/install-use-family.ps1 | iex ``` -A skill that already exists as a real folder is left alone and reported. Some uses (wechat-use, iphone-use) have installers that keep their skill folder in step with the CLI they install; leave those. A copy from `npx skills add` can be deleted so the installer links an updatable checkout instead. A link you made yourself (to your own checkout) is kept. The installer doesn't install CLIs; it lists the missing ones with their one-line installers. memory-use also needs `memory-use init --repo /` once (`--create` for a new private notes repo). +A skill that already exists as a real folder is left alone and reported. Some uses (wechat-use, iphone-use) have installers that keep their skill folder in step with the CLI they install; leave those. A copy from `npx skills add` can be deleted so the installer links an updatable checkout instead. A link you made yourself (to your own checkout) is kept. The installer doesn't install CLIs; it lists the missing ones, and `install-clis.sh` (above) installs them all. memory-use also needs `memory-use init --repo /` once (`--create` for a new private notes repo). **Upgrading.** Every CLI in the family has ` upgrade` (updates the CLI and its skill, `--check` to only look) and prints one line on stderr when a newer release exists, at most once a day; see [docs/upgrade.md](docs/upgrade.md). To upgrade everything at once: diff --git a/install-clis.sh b/install-clis.sh new file mode 100644 index 0000000..586c9a9 --- /dev/null +++ b/install-clis.sh @@ -0,0 +1,61 @@ +#!/bin/sh +# Install every *-use CLI this machine is missing, each through its own install.sh, in one go. +# curl -fsSL https://raw.githubusercontent.com/leeguooooo/plugins/main/install-clis.sh | sh +# curl -fsSL https://raw.githubusercontent.com/leeguooooo/plugins/main/install-clis.sh | sh -s -- mail-use ocs +# With names, installs just those (iphone-use too, which needs an iPhone and isn't in the default set). +# Installed CLIs are left alone: ` upgrade`, or upgrade-use-family.sh, updates them. +# The plugins / skills come from `/plugin install use-family@leeguooooo-plugins` or +# install-use-family.sh; this only puts the CLIs on PATH. One failure doesn't stop the rest. +set -u +PATH="$PATH:$HOME/.local/bin" +RAW="${USE_FAMILY_RAW:-https://raw.githubusercontent.com/leeguooooo}" + +# Keep in step with plugins/use-family/scripts/doctor.sh. +os=$(uname -s); arch=$(uname -m) +names="chrome-use cookie-use mail-use discord-use bitwarden-use chatgpt-use memory-use ocs image-use profile-use" +case "$os" in MINGW*|MSYS*|CYGWIN*) ;; *) names="$names motion-use" ;; esac # motion-use: macOS and Linux only +[ "$os" = Darwin ] && names="$names message-use paste-use" +# wechat-use's macOS installer stops with "Apple Silicon only" on Intel. +[ "$os-$arch" = Darwin-arm64 ] && names="$names wechat-use" +[ $# -gt 0 ] && names="$*" + +repo_of() { case "$1" in ocs) echo open-cross-session ;; *) echo "$1" ;; esac; } +installed() { # installed : already set up here + case "$1" in + # profile-use is a skill with a script, not a CLI; iphone-use is a daemon with an app. + profile-use) [ -f "${USE_FAMILY_DIR:-$HOME/.agents/use-family}/profile-use/SKILL.md" ] ;; + iphone-use) [ -d "$HOME/Applications/iPhoneUse.app" ] || [ -d /Applications/iPhoneUse.app ] ;; + *) command -v "$1" >/dev/null 2>&1 ;; + esac +} + +tmp=$(mktemp -d) || exit 1 +trap 'rm -rf "$tmp"' EXIT +done_list=""; skipped=""; failed="" +for name in $names; do + if installed "$name"; then skipped="$skipped $name"; continue; fi + echo "==> $name" + url="$RAW/$(repo_of "$name")/main/install.sh" + # Download first: in `curl | sh` a failed download is an empty script that "succeeds". + if ! curl -fsSL --retry 2 -o "$tmp/$name.sh" "$url"; then + echo " could not download $url"; failed="$failed $name"; continue + fi + if sh "$tmp/$name.sh" < /dev/null; then done_list="$done_list $name"; else failed="$failed $name"; fi +done + +echo +[ -n "$done_list" ] && echo "installed:$done_list" +[ -n "$skipped" ] && echo "already installed:$skipped" +[ -n "$failed" ] && echo "failed:$failed (re-run to retry; each one's output is above)" +case ":$PATH:" in *":$HOME/.local/bin:"*) ;; *) + echo "note: most CLIs live in ~/.local/bin; add it to PATH" ;; +esac +# One-time setup some of them need before they're useful. +case " $done_list $skipped " in *" memory-use "*) + [ -f "$HOME/.config/memory-use/config.json" ] || + echo "next: memory-use init --repo / (--create for a new private notes repo)" ;; +esac +case " $done_list " in *" mail-use "*) echo "next: add a mailbox: \`mail-use apple-mail import\` (takes Apple Mail's accounts) or \`mail-use account --help\`" ;; esac +case " $done_list " in *" message-use "*) echo "next: give your terminal Full Disk Access so message-use can read Messages" ;; esac +case " $done_list " in *" discord-use "*) echo "next: set DISCORD_TOKEN (a bot token) for discord-use" ;; esac +[ -z "$failed" ] diff --git a/install-use-family.sh b/install-use-family.sh index 3ebd5fc..735dbb4 100644 --- a/install-use-family.sh +++ b/install-use-family.sh @@ -66,17 +66,10 @@ echo echo "Skills are in $SKILLS. Start a new Codex session to pick them up." if [ -n "$missing" ]; then echo "CLIs not installed yet:$missing" + echo "Install them all at once:" + echo " curl -fsSL https://raw.githubusercontent.com/leeguooooo/plugins/main/install-clis.sh | sh" + echo "Or one at a time:" for name in $missing; do - case "$name" in - # image-use has no install.sh: the CLI is the single file at the root of the checkout above. - image-use) echo " sudo install \"$BASE/image-use/image-use\" /usr/local/bin/image-use" ;; - # No prebuilt binary for Intel Macs; the installer only says to build from source. - discord-use) if [ "$(uname -s)-$(uname -m)" = Darwin-x86_64 ]; then - echo " cargo install --git https://github.com/leeguooooo/discord-use --root ~/.local # Intel Mac: builds from source, needs Rust (rustup.rs)" - else - echo " curl -fsSL https://raw.githubusercontent.com/leeguooooo/discord-use/main/install.sh | sh" - fi ;; - *) echo " curl -fsSL https://raw.githubusercontent.com/leeguooooo/$(repo_of "$name")/main/install.sh | sh" ;; - esac + echo " curl -fsSL https://raw.githubusercontent.com/leeguooooo/$(repo_of "$name")/main/install.sh | sh" done fi diff --git a/plugins/use-family/.claude-plugin/plugin.json b/plugins/use-family/.claude-plugin/plugin.json index 9272fc7..de624ab 100644 --- a/plugins/use-family/.claude-plugin/plugin.json +++ b/plugins/use-family/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "use-family", - "version": "0.9.1", + "version": "0.10.0", "description": "The whole *-use family in one install: browser, email, texts (iMessage/SMS), chat, phone, identity, passwords, sessions, notes, clipboard history, promo and explainer videos, and the other agents on your machines. Adds a routing skill that tells the agent which use to reach for and how they combine, plus a session-start check for missing CLIs and one that upgrades a CLI older than its plugin.", "author": { "name": "leeguooooo" diff --git a/plugins/use-family/scripts/doctor.sh b/plugins/use-family/scripts/doctor.sh index bd41ecb..f28f710 100755 --- a/plugins/use-family/scripts/doctor.sh +++ b/plugins/use-family/scripts/doctor.sh @@ -1,11 +1,12 @@ #!/bin/sh # use-family session-start check: name any missing *-use CLI and its one-line installer. # Silent when everything is installed, so the common case adds nothing to the context. -# Only names the installer; the agent asks the user before running it. +# Only names the installers; the agent asks the user before running them. PATH="$PATH:$HOME/.local/bin:/opt/homebrew/bin:/usr/local/bin" # iphone-use runs as a daemon and profile-use ships its script inside the skill, # so neither has a CLI on PATH to look for. +# Keep the list in step with install-clis.sh at the repo root. os=$(uname -s); arch=$(uname -m) names="chrome-use cookie-use mail-use discord-use bitwarden-use chatgpt-use memory-use ocs image-use" case "$os" in MINGW*|MSYS*|CYGWIN*) ;; *) names="$names motion-use" ;; esac # motion-use: macOS and Linux only @@ -25,14 +26,10 @@ fi [ -z "$missing" ] && exit 0 echo "use-family: these CLIs are not installed:$missing" -echo "Install one when a task needs it, after asking the user:" +echo "Install them when a task needs one, after asking the user. All at once:" +echo " curl -fsSL https://raw.githubusercontent.com/leeguooooo/plugins/main/install-clis.sh | sh" +echo "Or one at a time:" for name in $missing; do repo=$name; [ "$name" = ocs ] && repo=open-cross-session - case "$name-$os-$arch" in - # image-use has no install.sh: the CLI is the single file at the root of its repo. - image-use-*) echo " git clone -q --depth 1 https://github.com/leeguooooo/image-use ~/.agents/use-family/image-use && ln -sf ~/.agents/use-family/image-use/image-use ~/.local/bin/image-use" ;; - # No prebuilt binary for Intel Macs; the installer only says to build from source. - discord-use-Darwin-x86_64) echo " cargo install --git https://github.com/leeguooooo/discord-use --root ~/.local # Intel Mac: builds from source, needs Rust (rustup.rs)" ;; - *) echo " curl -fsSL https://raw.githubusercontent.com/leeguooooo/$repo/main/install.sh | sh" ;; - esac + echo " curl -fsSL https://raw.githubusercontent.com/leeguooooo/$repo/main/install.sh | sh" done diff --git a/plugins/use-family/skills/use-family/SKILL.md b/plugins/use-family/skills/use-family/SKILL.md index 31c3193..0b54f01 100644 --- a/plugins/use-family/skills/use-family/SKILL.md +++ b/plugins/use-family/skills/use-family/SKILL.md @@ -71,4 +71,4 @@ Each CLI prints ` X is available` on stderr when a newer release exists (a 2. Redacted by default. Raw values only at the moment of use, never in the final reply. 3. Never bypass a Touch ID or unlock prompt, and never handle the master password. 4. Before committing to any repo that could echo personal data, run `profile-use leak-scan --staged --all-profiles`. -5. A CLI is missing → this plugin's session-start check names the one-line installer; ask the user before running it. +5. A CLI is missing → this plugin's session-start check names its installer, plus `install-clis.sh` for all missing ones at once; ask the user before running either. diff --git a/tests/doctor.test.sh b/tests/doctor.test.sh index 766862e..1073431 100644 --- a/tests/doctor.test.sh +++ b/tests/doctor.test.sh @@ -21,14 +21,13 @@ fail() { echo "FAIL: $1"; echo "$out"; exit 1; } out=$(run x86_64) echo "$out" | grep -q 'not installed:.* wechat-use' && fail "Intel: suggests wechat-use, whose installer is Apple Silicon only" -echo "$out" | grep -qF 'cargo install --git https://github.com/leeguooooo/discord-use' || fail "Intel: no build-from-source hint for discord-use" -echo "$out" | grep -q 'discord-use/main/install.sh' && fail "Intel: points at discord-use's install.sh, which has no Intel binary" +echo "$out" | grep -qF 'leeguooooo/plugins/main/install-clis.sh | sh' || fail "no all-at-once install-clis.sh line" +echo "$out" | grep -qF 'leeguooooo/discord-use/main/install.sh | sh' || fail "Intel: no discord-use installer (it has an Intel binary since v0.2.1)" echo "$out" | grep -q 'not installed:.* image-use' || fail "image-use not checked" -echo "$out" | grep -q 'image-use/main/install.sh' && fail "points at an install.sh image-use does not have" -echo "$out" | grep -qF 'ln -sf ~/.agents/use-family/image-use/image-use ~/.local/bin/image-use' || fail "no single-file install hint for image-use" +echo "$out" | grep -qF 'leeguooooo/image-use/main/install.sh | sh' || fail "no image-use installer line" +echo "$out" | grep -qF 'leeguooooo/open-cross-session/main/install.sh | sh' || fail "ocs not mapped to its repo" out=$(run arm64) echo "$out" | grep -q 'not installed:.* wechat-use' || fail "Apple Silicon: wechat-use not checked" -echo "$out" | grep -qF 'leeguooooo/discord-use/main/install.sh | sh' || fail "Apple Silicon: lost discord-use's install.sh hint" echo "ok doctor.sh" diff --git a/tests/install-clis.test.sh b/tests/install-clis.test.sh new file mode 100644 index 0000000..81b1c44 --- /dev/null +++ b/tests/install-clis.test.sh @@ -0,0 +1,66 @@ +#!/bin/sh +# Runs install-clis.sh with a fake curl (each "installer" just drops a CLI into ~/.local/bin), +# a fake uname and an isolated HOME. Checks which CLIs it installs per platform, that it skips +# installed ones, keeps going past a failure and reports it. Nothing outside a temp dir is touched. +# sh tests/install-clis.test.sh +set -eu +ROOT=$(cd "$(dirname "$0")/.." && pwd) +TMP=$(mktemp -d); trap 'rm -rf "$TMP"' EXIT +mkdir -p "$TMP/bin" +cat > "$TMP/bin/uname" <<'UNAME' +#!/bin/sh +case "$1" in -m) echo "$FAKE_ARCH" ;; *) echo Darwin ;; esac +UNAME +# Fake curl: -o ; the "installer" for creates the CLI. $FAIL_REPO 404s. +cat > "$TMP/bin/curl" <<'CURL' +#!/bin/sh +out=""; url="" +while [ $# -gt 0 ]; do case "$1" in -o) out=$2; shift 2 ;; -*) shift ;; *) url=$1; shift ;; esac; done +repo=${url#*/leeguooooo/}; repo=${repo%%/*} +[ "$repo" = "${FAIL_REPO:-}" ] && exit 22 +name=$repo; [ "$repo" = open-cross-session ] && name=ocs +if [ "$name" = profile-use ]; then + printf 'mkdir -p "$HOME/.agents/use-family/profile-use"; touch "$HOME/.agents/use-family/profile-use/SKILL.md"\n' > "$out" +else + printf 'mkdir -p "$HOME/.local/bin"; printf "#!/bin/sh\\n" > "$HOME/.local/bin/%s"; chmod +x "$HOME/.local/bin/%s"\n' "$name" "$name" > "$out" +fi +CURL +chmod +x "$TMP/bin/uname" "$TMP/bin/curl" +for tool in sh mkdir mktemp rm chmod touch printf cat; do + p=$(command -v "$tool") && case "$p" in /*) ln -sf "$p" "$TMP/bin/$tool" ;; esac +done + +fresh() { rm -rf "$TMP/home"; mkdir -p "$TMP/home"; } +run() { # run [args]: install-clis.sh's output; status in $rc + arch=$1; shift + set +e; out=$(FAKE_ARCH=$arch HOME="$TMP/home" PATH="$TMP/bin" sh "$ROOT/install-clis.sh" "$@" 2>&1); rc=$?; set -e +} +fail() { echo "FAIL: $1"; echo "$out"; exit 1; } +has() { [ -x "$TMP/home/.local/bin/$1" ]; } + +fresh; run x86_64 +[ "$rc" = 0 ] || fail "Intel: exit $rc" +for n in chrome-use cookie-use mail-use discord-use bitwarden-use chatgpt-use memory-use ocs image-use motion-use message-use paste-use; do + has "$n" || fail "Intel: $n not installed" +done +[ -f "$TMP/home/.agents/use-family/profile-use/SKILL.md" ] || fail "Intel: profile-use not installed" +has wechat-use && fail "Intel: ran wechat-use's Apple Silicon-only installer" +echo "$out" | grep -q '^next: memory-use init' || fail "no memory-use init hint" + +run x86_64 +echo "$out" | grep -q '^installed:' && fail "re-run installed something again" +echo "$out" | grep -q '^already installed:.* ocs' || fail "re-run: ocs not reported as already installed" + +fresh; run arm64 +has wechat-use || fail "Apple Silicon: wechat-use not installed" + +fresh; FAIL_REPO=mail-use run arm64 +[ "$rc" != 0 ] || fail "a failed install still exited 0" +echo "$out" | grep -q '^failed: mail-use' || fail "failure not reported" +has ocs || fail "stopped after a failure instead of carrying on" + +fresh; run arm64 ocs iphone-use +has ocs || fail "named: ocs not installed" +has chrome-use && fail "named: installed more than it was asked" + +echo "ok install-clis.sh" diff --git a/tests/install-use-family.test.sh b/tests/install-use-family.test.sh index aae6586..46b1590 100644 --- a/tests/install-use-family.test.sh +++ b/tests/install-use-family.test.sh @@ -35,20 +35,16 @@ fail() { echo "FAIL: $1"; echo "$out"; exit 1; } out=$(run) echo "$out" | grep -q '^CLIs not installed yet:.* image-use' || fail "image-use missing from the not-installed list" -echo "$out" | grep -qF "sudo install \"$TMP/home/.agents/use-family/image-use/image-use\" /usr/local/bin/image-use" \ - || fail "no single-file install hint for image-use" -echo "$out" | grep -q 'leeguooooo/image-use/main/install.sh' && fail "points at an install.sh image-use does not have" +echo "$out" | grep -qF 'leeguooooo/image-use/main/install.sh | sh' || fail "no image-use installer line" echo "$out" | grep -qF 'leeguooooo/mail-use/main/install.sh | sh' || fail "generic install.sh hint lost" - +echo "$out" | grep -qF 'leeguooooo/plugins/main/install-clis.sh | sh' || fail "no all-at-once install-clis.sh line" echo "$out" | grep -q '^CLIs not installed yet:.* wechat-use' || fail "Apple Silicon: wechat-use missing from the not-installed list" -echo "$out" | grep -qF 'leeguooooo/discord-use/main/install.sh | sh' || fail "Apple Silicon: lost discord-use's install.sh hint" out=$(FAKE_ARCH=x86_64 run) echo "$out" | grep -q '^CLIs not installed yet:.* wechat-use' && fail "Intel: suggests wechat-use, whose installer is Apple Silicon only" -echo "$out" | grep -qF 'cargo install --git https://github.com/leeguooooo/discord-use' || fail "Intel: no build-from-source hint for discord-use" printf '#!/bin/sh\n' > "$TMP/bin/image-use"; chmod +x "$TMP/bin/image-use" out=$(run) -echo "$out" | grep -q 'image-use/image-use\|not installed yet:.* image-use' && fail "image-use reported although it is on PATH" +echo "$out" | grep -q 'not installed yet:.* image-use' && fail "image-use reported although it is on PATH" echo "ok install-use-family.sh"