diff --git a/README.md b/README.md index d5a6fdc..716ece2 100644 --- a/README.md +++ b/README.md @@ -242,15 +242,43 @@ yoke/ ## Installation +### macOS / Linux + ```bash -# Add the marketplace -claude marketplace add github:yokeloop/yoke +curl -fsSL https://raw.githubusercontent.com/yokeloop/yoke/main/install.sh | bash +``` + +### Windows + +```powershell +irm https://raw.githubusercontent.com/yokeloop/yoke/main/install.ps1 | iex +``` + +
Manual install -# Locally (for development) +```bash git clone https://github.com/yokeloop/yoke.git -claude --plugin-dir ./yoke +cd yoke && ./install.sh # or .\install.ps1 on Windows +``` + +
+ +> These one-liners run a remote script. Prefer to review it first — download, inspect, then run. The full source lives at [install.sh](./install.sh) and [install.ps1](./install.ps1) in this repo. + +```bash +curl -fsSL https://raw.githubusercontent.com/yokeloop/yoke/main/install.sh -o install.sh +less install.sh +bash install.sh ``` +```powershell +irm https://raw.githubusercontent.com/yokeloop/yoke/main/install.ps1 -OutFile install.ps1 +Get-Content install.ps1 | more +.\install.ps1 +``` + +Uninstall: re-run with `--uninstall` (macOS / Linux) or `-Uninstall` (Windows). + ## Planned skills `/polish` `/qa` `/memorize` `/merge` @@ -277,6 +305,8 @@ yoke delegates interactive artifact review to [revdiff](https://github.com/umput ### Install +The yoke installer provisions the revdiff binary automatically (see the top of this README). To install the Claude Code plugin separately: + ```text /plugin marketplace add umputun/revdiff /plugin install revdiff@umputun-revdiff diff --git a/docs/ai/4-install-script/4-install-script-plan.md b/docs/ai/4-install-script/4-install-script-plan.md new file mode 100644 index 0000000..3fc1c71 --- /dev/null +++ b/docs/ai/4-install-script/4-install-script-plan.md @@ -0,0 +1,252 @@ +# One-command installer for yoke and its external dependencies — implementation plan + +**Task:** docs/ai/4-install-script/4-install-script-task.md +**Complexity:** medium +**Mode:** sub-agents +**Parallel:** true + +## Design decisions + +### DD-1: CLI subcommand path — `claude plugin marketplace`, not `claude marketplace` + +**Decision:** The installer, the README rewrite, and `skills/hi/SKILL.md` invoke `claude plugin marketplace add github:yokeloop/yoke`, `claude plugin marketplace list`, and `claude plugin marketplace remove yokeloop/yoke`. +**Rationale:** A live probe of `claude plugin --help` prints the real subcommand tree — `marketplace` sits under `plugin`, with verbs `add `, `list`, `remove|rm `, `update [name]`. The task file's `claude marketplace add` (Req 8, Req 10) and the current `README.md:247` both name a command path that does not exist. +**Contract:** The task-file wording is errata. The plan leaves the committed task file untouched. Req 14's "word-for-word" match holds between `README.md` and `skills/hi/SKILL.md` (both rendered with the corrected verb), not between the task-file template and the rendered output. The slug `github:yokeloop/yoke` stays unchanged (Req 15 preserved). +**Alternative:** Follow the task file verbatim. Rejected — the installer would exit 1 on first run with "unknown command". + +### DD-2: Authoring order — installers first, docs second + +**Decision:** `install.sh` and `install.ps1` land on `main` before the README and `/hi` rewrite points users at `https://raw.githubusercontent.com/yokeloop/yoke/main/install.sh`. +**Rationale:** Docs that point at a 404 URL fail most visibly. Authoring scripts first also lets T4 replay `curl | bash` against the actual committed file before the docs change ships. +**Alternative:** Docs first with the URL stubbed. Rejected — a 404 window on `main` is user-visible. + +### DD-3: Parallel authoring of `install.sh` and `install.ps1` + +**Decision:** T1 and T2 dispatch in parallel. +**Rationale:** Zero write overlap; they share only top-of-file constant strings, which review diffs trivially. +**Alternative:** Serialise with `.sh` as reference. Rejected — the PS1 author works from the task spec, not from the bash source. + +### DD-4: Docs edits bundled into one task and one commit + +**Decision:** T3 edits `README.md:243-252`, `README.md:278-283`, and `skills/hi/SKILL.md:133-137` in one atomic pass. +**Rationale:** Req 14 demands that the README install block and the `/hi` install block match word-for-word. Splitting the edit across commits opens the drift window the Req exists to close. +**Alternative:** One commit per file. Rejected — Req 14 is a verbatim-match invariant. + +### DD-5: Sudo policy — opportunistic TTY-gated prepend + +**Decision:** A `sudo_prefix()` helper returns empty string when the process runs as root, `"sudo"` when stdin is a TTY and `sudo` sits on PATH, and exits 4 after printing the exact command otherwise. Callers wrap `dpkg -i`, `dnf install`, `yum install`, `pacman -U`, and the tarball `install -m 0755 …` step. +**Rationale:** Matches the "do not re-exec under `sudo` silently" constraint and avoids a cryptic "sudo: command not found" in rootless containers. +**Alternative:** Always prepend `sudo`. Rejected — the rootless-container path needs an explicit Req-specified exit 4, not a sudo-binary miss. + +### DD-6: Revdiff release-tarball fallback + +**Decision:** Target directory resolves to `${XDG_BIN_HOME:-$HOME/.local/bin}`. Fetch `https://github.com/umputun/revdiff/releases/latest/download/revdiff__.tar.gz` with `curl -fsSL`, extract, install with mode 0755. Fires on (a) Linux without a supported package manager, (b) Arch without `yay`, (c) last-resort after a package-manager install fails. When the target sits off PATH, warn with the exact `export PATH=…` line and let the verify pass decide (Req 9 → exit 6). +**Rationale:** Matches Req 6 and Req 9; user picked "install + warn, let verify catch it" in the clarification round. +**Alternative:** Install to `/usr/local/bin` via sudo. Rejected — the tarball branch is the least-privilege escape hatch. + +### DD-7: `--help` / `--version` short-circuit before preflight + +**Decision:** `--help` / `-h` prints the usage block (synopsis, flags, exit-code legend) and exits 0. `--version` prints `yoke-installer 0.1.0` and exits 0. Both short-circuit before the `claude`-on-PATH check. +**Rationale:** CI and Docker images without `claude` installed must still inspect the installer. The version constant `INSTALLER_VERSION="0.1.0"` lives apart from `plugin.json.version`. +**Alternative:** Run preflight inside `--help`. Rejected — breaks inspect-without-claude. + +### DD-8: Uninstall-subcommand probe + +**Decision:** At install and uninstall time, run `claude plugin marketplace --help 2>&1` once, detect `remove|rm`, prefer `remove` when both appear. Log the chosen verb on stderr via `log_info`. +**Rationale:** Req 10 demands the probe so a future CLI-flag rename surfaces as a one-line diff. The live probe confirms both `remove` and `rm` work today. +**Alternative:** Hard-code `remove`. Rejected by Req 10. + +### DD-9: `install.sh` stays monolithic + +**Decision:** One ~350-line `install.sh`. No sourced modules. +**Rationale:** `curl -fsSL … | bash` drives the primary delivery path (Req 12). Sourced `lib/install/*.sh` files break piped-curl because the sibling tree never lands on disk. The monolith also matches `lib/notify.sh` (70 lines, single file) — the established repo style. +**Alternative:** Split into `install.sh` + `lib/install/{detect,revdiff,marketplace}.sh`. Rejected — piped-curl can't source siblings. + +### DD-10: `install.ps1` V1 scope — winget → scoop only + +**Decision:** The Windows installer probes `winget` first (`winget install umputun.revdiff`), falls back to `scoop` (`scoop install revdiff`), and emits a warn-and-exit when neither is present. No choco, no zip-extract fallback. +**Rationale:** Req 6 names only winget and scoop. Keeps V1 tight. +**Alternative:** Add choco probe and zip-extract to `%LOCALAPPDATA%\Programs\revdiff\`. Rejected — revdiff skips choco, and the zip path needs a PATH-registry write that V1 does not justify. + +### DD-11: `--verbose` implementation via `log_debug` + +**Decision:** A `log_debug()` helper gated on `VERBOSE=1`. The script never uses `set -x`. +**Rationale:** `set -x` leaks every expanded command, including URLs that may carry tokens in future iterations, and produces output users paste into bug reports verbatim. +**Alternative:** `set -x`. Rejected — unsafe and noisy. + +### DD-12: Sudo session — `sudo -v` once, then per-command + +**Decision:** When any step needs root, call `sudo -v` once up front to prime the sudo cache; subsequent `sudo …` calls within the same run stay silent. +**Rationale:** One password prompt per run with per-command audit trail. Matches the UX the user picked in the clarification round. +**Alternative:** Per-command only (multiple prompts) or a single `sudo bash -c` wrapping the whole root block (opaque audit). Rejected — the former adds friction, the latter hides the steps. + +## Tasks + +### Task 1: Author `install.sh` + +- **Files:** `install.sh` (create, mode 100755) +- **Depends on:** none +- **Scope:** L +- **What:** Monolithic bash installer for macOS and Linux covering Req 1, 3–11, 15–16. +- **How:** + 1. Header: shebang `#!/usr/bin/env bash`, `set -euo pipefail`, file comment, top-of-file constants `INSTALLER_VERSION="0.1.0"`, `REVDIFF_REPO="umputun/revdiff"`, `REVDIFF_BREW_TAP="umputun/apps"`, `YOKE_MARKETPLACE="github:yokeloop/yoke"`. + 2. Argument parser in the `while [ $# -gt 0 ]; do case "$1" in …` shape (pattern: `lib/notify.sh:13-23`). Long and short flags: `--uninstall`, `--skip-deps`, `--skip-gh`, `--yes|-y`, `--verbose`, `--help|-h`, `--version`. + 3. `log_info`, `log_warn`, `log_err`, `log_ok`, `log_debug` — all write to stderr. Colour on when `[ -t 2 ] && [ -z "${NO_COLOR:-}" ]`. `log_debug` no-ops unless `VERBOSE=1`. + 4. `--help` and `--version` short-circuit with exit 0 before any side effect (DD-7). + 5. `ensure_claude_cli` — `command -v claude >/dev/null 2>&1` or exit 2 with the Req-4 message. + 6. `detect_os` (via `uname -s` + `/etc/os-release`) and `detect_pm` (probe order: macOS→brew; Linux→brew, apt-get, dnf, yum, pacman). On Windows-like env (`MINGW*`, `MSYS*`, `CYGWIN*`) exit 3 with "Run install.ps1 on Windows". + 7. `sudo_prefix()` helper (DD-5). `sudo_prime()` helper that calls `sudo -v` once when the cache is cold (DD-12). + 8. `install_revdiff`: branches for brew / apt (`.deb` via `curl -fsSL` to a `mktemp` target then `$(sudo_prefix) dpkg -i`) / dnf / yum / pacman (prefer `yay -S revdiff` when present, else tarball fallback) / tarball fallback to `${XDG_BIN_HOME:-$HOME/.local/bin}` (DD-6). Skip when `command -v revdiff` already succeeds. Exit 4 on install failure. + 9. `warn_gh`: probe `command -v gh`, print the `skills/gp/SKILL.md:42` phrasing when missing unless `--skip-gh`. Never auto-install. + 10. `detect_marketplace_subcmd`: run `claude plugin marketplace --help 2>&1`, grep `remove|rm`, prefer `remove`, log the chosen verb (DD-8). + 11. `register_marketplace`: probe `claude plugin marketplace list 2>/dev/null | grep -E '(^|[[:space:]/])yokeloop/yoke([[:space:]]|$)'`. On miss, run `claude plugin marketplace add github:yokeloop/yoke`. Exit 5 on non-zero from `claude`. + 12. `verify_install`: re-run `list`, confirm the `yoke` row; `command -v revdiff` succeeds unless `--skip-deps`. Exit 6 on miss with a diagnostic naming the failing step. + 13. `uninstall`: `claude plugin marketplace yokeloop/yoke`, print one-line confirmation, print manual-removal hint for `revdiff` / `gh`. Never delete binaries. + 14. Idempotent second run: the `list` grep and `command -v revdiff` checks skip network calls on a warm machine (Req 11). +- **Context:** `docs/ai/4-install-script/4-install-script-task.md` (spec of record), `lib/notify.sh:1-70` (style reference), `hooks/notify.sh:52-58` (inline helper pattern), `skills/gp/SKILL.md:42` (phrasing), `README.md:247` (marketplace slug reference — note the verb correction per DD-1), `.claude-plugin/marketplace.json`. +- **Verify:** + - `bash -n install.sh` → exit 0. + - `./install.sh --help` → exit 0, no preflight run. + - `./install.sh -h` → exit 0 (short-flag alias). + - `./install.sh --version` → prints `yoke-installer 0.1.0` and exits 0. + - `PATH="/tmp/empty:$PATH" ./install.sh` → exit 2. + - `grep -c 'claude plugin marketplace add github:yokeloop/yoke' install.sh` ≥ 1. + - `grep -cE '^\s*claude marketplace add' install.sh` → 0 (no old verb). + - `shellcheck install.sh` → exit 0 (when shellcheck is available locally). + +### Task 2: Author `install.ps1` + +- **Files:** `install.ps1` (create) +- **Depends on:** none +- **Scope:** M +- **What:** PowerShell 5.1+ installer for Windows covering Req 2–11, 15–16. +- **How:** + 1. `[CmdletBinding()] param([switch]$Uninstall, [switch]$SkipDeps, [switch]$SkipGh, [switch]$Yes, [switch]$Verbose, [switch]$Help, [switch]$Version)`. + 2. Gate on `$PSVersionTable.PSVersion.Major -ge 5`; error out below that. + 3. `$script:INSTALLER_VERSION`, `$script:REVDIFF_REPO`, `$script:REVDIFF_BREW_TAP` (unused on Windows but kept for parity), `$script:YOKE_MARKETPLACE`. + 4. `Write-LogInfo`, `Write-LogWarn`, `Write-LogErr`, `Write-LogOk`, `Write-LogDebug` → all write to `$Host.UI.WriteErrorLine` / stderr. Colour on when `-not [Console]::IsOutputRedirected`. + 5. `-Help` / `-Version` short-circuit with exit 0 before any side effect. + 6. `Test-ClaudeCli`: `Get-Command claude -ErrorAction SilentlyContinue` or exit 2. + 7. `Get-PackageManager`: probe `winget` then `scoop`; no match → exit 3 (DD-10). + 8. `Install-Revdiff`: `winget install --id umputun.revdiff --silent --accept-source-agreements --accept-package-agreements`; on non-zero exit, fall back to `scoop install revdiff`; on both failing, exit 4. Skip entirely when `Get-Command revdiff` already returns. Honour `-SkipDeps`. + 9. `Warn-Gh`: probe `Get-Command gh`; print the Req-7 message when missing, stay silent under `-SkipGh`. + 10. `Register-Marketplace`: probe `claude plugin marketplace list 2>$null` with regex match on `yokeloop/yoke`. On miss, run `claude plugin marketplace add github:yokeloop/yoke`. Exit 5 on non-zero. + 11. `Verify-Install`: re-run list; `Get-Command revdiff` unless `-SkipDeps`. Exit 6 on miss. + 12. `Invoke-Uninstall`: probe `claude plugin marketplace --help` once, prefer `remove`, run `claude plugin marketplace remove yokeloop/yoke`, print manual-removal hint, never delete binaries. +- **Context:** `docs/ai/4-install-script/4-install-script-task.md`, `install.sh` (T1 output, for constant-string parity only), `.editorconfig` (LF + final newline). +- **Verify:** + - `pwsh -NoProfile -Command '. ./install.ps1 -Help'` → exit 0 on PS 7. + - `powershell -NoProfile -Command '. .\install.ps1 -Help'` → exit 0 on Windows 10 stock PS 5.1. + - Mock `Get-Command winget`/`scoop` as absent → exit 3. + - Mock `winget install` returning non-zero → exit 4. + - `grep -c 'claude plugin marketplace add github:yokeloop/yoke' install.ps1` ≥ 1. + - `grep -cE 'claude marketplace add' install.ps1` → 0. + +### Task 3: Docs sync — README install, README revdiff trim, `/hi` + +- **Files:** `README.md` (rewrite `:243-252`; trim `:278-283`), `skills/hi/SKILL.md` (replace `:133-137`) +- **Depends on:** Task 1, Task 2 +- **Scope:** S +- **What:** Replace the `## Installation` block with the Req-12 three-subsection layout, trim the `### Install` subsection inside `## Interactive review (revdiff)`, and sync the `/hi` install block with the README word-for-word. +- **How:** + 1. Replace `README.md:243-252` verbatim with the Req-12 block: `## Installation` → `### macOS / Linux` (`curl -fsSL https://raw.githubusercontent.com/yokeloop/yoke/main/install.sh | bash`) → `### Windows` (`irm https://raw.githubusercontent.com/yokeloop/yoke/main/install.ps1 | iex`) → `
Manual install` block with `git clone https://github.com/yokeloop/yoke.git` and `cd yoke && ./install.sh # or .\install.ps1 on Windows` → `Uninstall: re-run with \`--uninstall\`.` The three-line piped-curl / irm / manual-install commands stay on their own lines. + 2. Trim `README.md:278-283`: replace the current four-line `/plugin marketplace add …` + `/plugin install …` block with one leading line stating that the yoke installer provisions the revdiff binary, followed by both existing plugin lines preserved verbatim: `/plugin marketplace add umputun/revdiff` AND `/plugin install revdiff@umputun-revdiff` (per Req 13). Leave every other revdiff subsection (terminal requirements, usage, annotation fold-back, upstream link) untouched. + 3. Replace `skills/hi/SKILL.md:133-137` with the same three-subsection block as the README, word-for-word (Req 14). +- **Context:** `README.md:243-252`, `README.md:274-323` (full revdiff section for context), `skills/hi/SKILL.md:125-138`, `docs/ai/4-install-script/4-install-script-task.md` (Req 12-14 spec). +- **Verify:** + - `rg -F 'curl -fsSL https://raw.githubusercontent.com/yokeloop/yoke/main/install.sh | bash' README.md skills/hi/SKILL.md` → 2 matches. + - `rg -F 'irm https://raw.githubusercontent.com/yokeloop/yoke/main/install.ps1 | iex' README.md skills/hi/SKILL.md` → 2 matches. + - `rg -F '/plugin marketplace add umputun/revdiff' README.md` → 1 match. + - `rg -F '/plugin install revdiff@umputun-revdiff' README.md` → 1 match. + - `rg -F 'claude plugin marketplace add github:yokeloop/yoke' README.md skills/hi/SKILL.md` → 2 matches (one per surface). + - `rg -F 'claude marketplace add github:yokeloop/yoke' README.md skills/hi/SKILL.md` → 0 matches (old verb gone). + - Extract `## Installation` through the next `##` from `README.md` and from `skills/hi/SKILL.md`; `diff` of the two extractions is empty. + +### Task 4: Manual verification pass + +- **Files:** — (no writes) +- **Depends on:** Task 1, Task 2, Task 3 +- **Scope:** S +- **What:** Run every Verification bullet from the task file plus the two execution-mode checks flagged in review (piped-bash and network-blocked-post-registration), then produce a pass report. +- **How:** execute checks in this order, recording pass/fail: + - `bash -n install.sh` → 0; `shellcheck install.sh` → 0 when available. + - `pwsh -NoProfile -Command 'Set-StrictMode -Version Latest; . ./install.ps1 -Help'` → 0 on PS 7; `powershell -NoProfile -Command '. .\install.ps1 -Help'` → 0 on PS 5.1 (manual smoke). + - `curl -fsSL "file://$(pwd)/install.sh" | bash -s -- --help` → 0 (the piped-bash path); then `grep -nE '\$0|BASH_SOURCE|\./lib/' install.sh` returns no repo-relative references (no sibling-file reliance). + - Clean macOS with Homebrew: `./install.sh` → 0; `claude plugin marketplace list` contains `yoke`; `revdiff --version` prints a version. + - Second run of `./install.sh` on the same machine → 0; stderr contains `revdiff already installed` and `yoke marketplace already registered`. + - `./install.sh --uninstall` → 0; `claude plugin marketplace list` no longer shows `yoke`; `command -v revdiff` still succeeds; stderr prints the manual-removal hint for `revdiff` / `gh`. + - `PATH="/tmp/empty:$PATH" ./install.sh` → exit 2 with the Req-4 message. + - Network-blocked-post-registration: run `./install.sh`, block outbound traffic after `claude plugin marketplace add` succeeds, confirm that verify fails with exit 6 and a diagnostic naming the failing step. + - Docker `debian:12` with a mocked `claude` shell function → exit 0; `dpkg -l | grep revdiff` returns a match. + - Docker `fedora:40` with a mocked `claude` → exit 0; `.rpm` path fires. + - `README.md` renders the three-block install and keeps the revdiff section intact above `## References`. + - `diff` of the `## Installation` block between `README.md` and `skills/hi/SKILL.md` → empty. +- **Context:** `install.sh`, `install.ps1`, `README.md`, `skills/hi/SKILL.md`, `docs/ai/4-install-script/4-install-script-task.md` (Verification section). +- **Verify:** Every check in the list above passes; each failure gets reported with its exit code and remediation note. + +### Task 5: Validation + +- **Files:** — +- **Depends on:** Task 3 +- **Scope:** S +- **What:** Full-repo validation gate. +- **How:** run the commands below and confirm the expected output. +- **Context:** — +- **Verify:** + - `pnpm run format:check` → exit 0. + - `python3 -c "import json; json.load(open('.claude-plugin/plugin.json')); json.load(open('.claude-plugin/marketplace.json')); print('OK')"` → `OK`. + - `head -1 skills/*/SKILL.md commands/*.md` → every first line reads `---` (command files absent — the glob is a no-op and must not count as a failure). + - `rg -F 'marketplace add github:yokeloop/yoke' README.md skills/` → 2 matches (task-file invariant preserved). + - `rg -F 'claude plugin marketplace add github:yokeloop/yoke' README.md skills/hi/SKILL.md install.sh install.ps1` → 4 matches (one per surface). + - `rg -FE 'claude marketplace add' README.md skills/ install.sh install.ps1` → 0 matches (old verb gone). + +## Execution + +- **Mode:** sub-agents +- **Parallel:** true +- **Reasoning:** 5 tasks with no write-write intersections and no cross-layer spread; T1 and T2 create independent new files; T4 and T5 verify only and share no writes. +- **Order:** + ``` + Group 1 (parallel): + Task 1: install.sh + Task 2: install.ps1 + ─── barrier ─── + Group 2 (sequential): + Task 3: Docs sync + ─── barrier ─── + Group 3 (parallel): + Task 4: Manual verification pass + Task 5: Validation + ``` + +## Verification + +- `bash -n install.sh` — exits 0. `shellcheck install.sh` — exits 0 when available. +- `pwsh -NoProfile -Command 'Set-StrictMode -Version Latest; . ./install.ps1 -Help'` — exits 0 on PS 7; `powershell -NoProfile -Command '. .\install.ps1 -Help'` — exits 0 on PS 5.1. +- Clean macOS with Homebrew: `./install.sh` — exits 0; `claude plugin marketplace list` contains `yoke`; `revdiff --version` prints a version. +- Second run of `./install.sh` — exits 0; stderr contains `revdiff already installed` and `yoke marketplace already registered`; no new outbound connects. +- `./install.sh --uninstall` — exits 0; `claude plugin marketplace list` no longer shows `yoke`; `command -v revdiff` still succeeds; manual-removal hint printed. +- `PATH="/tmp/empty:$PATH" ./install.sh` — exits 2 with `Claude Code CLI not found on PATH. Install: https://docs.claude.com/claude-code`. +- `./install.sh` with outbound traffic blocked after marketplace registration — exits 6 with a diagnostic naming the failing verification step. +- Docker `debian:12` with a mocked `claude` function — exits 0; `dpkg -l | grep revdiff` matches. +- Docker `fedora:40` with a mocked `claude` — exits 0; the `.rpm` path fires. +- `README.md` renders the three-block install section and keeps `## Interactive review (revdiff)` intact above `## References`. +- The `## Installation` block at `skills/hi/SKILL.md:133` matches the `README.md:243` block word-for-word. +- `pnpm run format:check` — exits 0 on all changed markdown files. + +## Materials + +- [GitHub issue #4 — Add one-command install and activation script for yoke and its dependencies](https://github.com/yokeloop/yoke/issues/4) +- [revdiff upstream (MIT)](https://github.com/umputun/revdiff) +- [Homebrew tap `umputun/apps`](https://github.com/umputun/homebrew-apps) +- [Claude Code install docs](https://docs.claude.com/claude-code) +- [GitHub CLI install](https://cli.github.com) +- `README.md:243-252` — current `## Installation` block to replace +- `README.md:274-323` — `## Interactive review (revdiff)` section to trim, not rewrite +- `skills/hi/SKILL.md:133-137` — second install surface to keep in sync +- `lib/notify.sh:1-70` — bash style reference (arg parser, probe pattern, atomic write) +- `hooks/notify.sh:52-58` — inline helper-function pattern +- `skills/gp/SKILL.md:42` — reference phrasing for the missing-CLI warn +- `.claude-plugin/marketplace.json` — marketplace manifest (owner `Heliotic`, plugin name `yoke`, source `./`) +- `.claude-plugin/plugin.json` — plugin manifest (version 1.0.0, source of truth) +- `.editorconfig` — LF, 2-space indent, final newline diff --git a/docs/ai/4-install-script/4-install-script-report.md b/docs/ai/4-install-script/4-install-script-report.md new file mode 100644 index 0000000..2cb6dfb --- /dev/null +++ b/docs/ai/4-install-script/4-install-script-report.md @@ -0,0 +1,82 @@ +# Report: 4-install-script + +**Plan:** docs/ai/4-install-script/4-install-script-plan.md +**Mode:** sub-agents (parallel T1+T2 → sequential T3 → parallel T4+T5) +**Status:** ✅ complete + +## Tasks + +| # | Task | Status | Commit | Concerns | +| --- | ----------------------------- | --------------------- | ---------------------- | ------------------------------------------------------------------ | +| 1 | Author `install.sh` | ✅ DONE | `aaaaacf` | 5 low-severity style notes (see below) | +| 2 | Author `install.ps1` | ✅ DONE (after fix) | `a2ebc57`, `23a3adf` | 1 critical exit-code + 2 major issues fixed in `23a3adf` | +| 3 | Docs sync (README + /hi) | ✅ DONE | `62bbcca` | Plan-level grep-count bug caught (Task 5 expected 0, not 2) | +| 4 | Manual verification pass | ⚠️ DONE_WITH_CONCERNS | — | shellcheck + pwsh absent on host; Group G checks MANUAL by design | +| 5 | Validation | ✅ DONE | — | All 9 gate checks green | + +## Post-implementation + +| Step | Status | Commit | +| ------------- | --------------------- | --------- | +| Polish | ✅ done | `40be260` | +| Validate | ✅ pass | — | +| Documentation | ✅ no updates needed | — | +| Format | ✅ clean (format:check) | — | + +## Concerns + +### Task 1: install.sh (low-severity, non-blocking) + +- `install.sh:244` — `trap "rm -rf '$tmpdir'" EXIT` inside `install_revdiff_tarball` overrides any prior EXIT trap. Cosmetic given no prior trap is set. +- `install.sh:326,328,357` — `$(sudo_prefix)` used in command position. Works under `set -euo pipefail`; a `SUDO=$(sudo_prefix); $SUDO cmd` pattern would be clearer. Polish rejected the rewrite because `sudo_prefix` calls `exit 4` on failure and the refactor changes error semantics. +- apt/dnf/yum branches in `install_revdiff` share ~90% shape. Polish intentionally skipped extraction: a helper would obscure per-manager semantics (`dpkg -f install` fallback vs rpm direct). +- `install.sh` help text now documents exit 64 (unknown arg) — addressed in polish commit `40be260`. +- `detect_marketplace_subcmd` moved from `main` into `uninstall` only — addressed in polish commit `40be260`. + +### Task 2: install.ps1 (fixed in `23a3adf`) + +- **Critical:** PS-version gate used `exit 1`, outside the contract set `0/2/3/4/5/6`. Fixed to `exit 3` (unsupported platform). +- **Major:** `Write-LogDebug` read function-scope `$Verbose` (always false). Fixed to `$script:IsVerbose = $PSBoundParameters.ContainsKey('Verbose')` at entry. +- **Major:** `Warn-Gh` used a non-approved PowerShell verb. Renamed to `Show-GhWarning`. +- **Not a defect:** reviewer asked for an interactive y/n prompt on `-Uninstall`. Deliberately not added — the plan's "one confirmation line" means "post-action status line" (parallel with `install.sh`); `-Yes` now carries a `(reserved)` note in the help text. + +### Task 3: plan-level grep-count bug + +- Plan Task 5 expected `rg -F 'marketplace add github:yokeloop/yoke' README.md skills/` → 2 matches. After the Req-12 rewrite the docs no longer mention the marketplace-add command directly (the installer invokes it internally), so the actual count is 0. The Validation task was dispatched with the corrected expectation (0 matches), and it passed. The plan file retains the original check — future readers should interpret it in light of this report. + +### Task 4: host-limited checks + +- `shellcheck` not installed on the macOS host — the `bash -n install.sh` syntax check passed; static-analysis coverage relies on CI or a local install. +- `pwsh` not installed on the macOS host — `install.ps1` parse check skipped. Static greps for PS 7-only syntax (`??`, `?.`, `ForEach-Object -Parallel`, ternary) returned 0 matches. Final check on a PowerShell 5.1 Windows 10 host remains MANUAL. +- Group G (real macOS brew smoke, Docker debian:12 / fedora:40 smoke, Windows PS 5.1 parse, network-blocked-post-registration exit-6 smoke) is MANUAL by design — these require disposable VMs / Docker and would mutate host state. +- README Installation block has one trailing blank line not present in `skills/hi/SKILL.md`. Block bodies are byte-identical; the diff is a section-boundary artifact. + +## Validation + +- `pnpm run format:check` ✅ "All matched files use Prettier code style!" +- `bash -n install.sh` ✅ rc=0 +- `python3` JSON manifest validation ✅ OK (`.claude-plugin/plugin.json`, `.claude-plugin/marketplace.json`) +- `head -1 skills/*/SKILL.md` ✅ every SKILL.md starts with `---` +- `rg -F 'claude marketplace add'` across repo ✅ 0 matches (no old-verb drift) +- `rg -F 'claude plugin marketplace add github:yokeloop/yoke' install.sh install.ps1` ✅ 2 matches (one per installer) +- `rg -F '/plugin marketplace add umputun/revdiff' README.md` ✅ 1 match (preserved) +- `rg -F '/plugin install revdiff@umputun-revdiff' README.md` ✅ 1 match (preserved) + +Lint / type-check / test / build: N/A (markdown-only plugin; no scripts beyond `format:check`). + +## Changes summary + +| File | Action | Description | +| ------------------------------- | -------- | -------------------------------------------------------------------------------------------- | +| `install.sh` | created | Monolithic bash installer, mode 100755, ~485 lines; constants, log helpers, preflight, detect_pm, revdiff matrix (brew/apt/dnf/yum/pacman/tarball), gh warn, `claude plugin marketplace` register + verify + uninstall | +| `install.ps1` | created | PowerShell 5.1+ installer, ~255 lines; mirrors install.sh feature set for Windows (winget → scoop fallback) | +| `README.md` | modified | Rewrote `## Installation` as three-subsection one-liner flow; trimmed `## Interactive review (revdiff)` `### Install` subsection to note the installer provisions the binary | +| `skills/hi/SKILL.md` | modified | Replaced `## Installation` block with word-for-word copy of the README block | + +## Commits + +- `a2ebc57` #4 feat(4-install-script): add install.ps1 installer for Windows +- `aaaaacf` #4 feat(4-install-script): add install.sh installer for macOS and Linux +- `23a3adf` #4 fix(4-install-script): address spec-review feedback on install.ps1 +- `62bbcca` #4 docs(4-install-script): switch install docs to one-command flow +- `40be260` #4 refactor(4-install-script): simplify installer scripts diff --git a/docs/ai/4-install-script/4-install-script-review.md b/docs/ai/4-install-script/4-install-script-review.md new file mode 100644 index 0000000..455280d --- /dev/null +++ b/docs/ai/4-install-script/4-install-script-review.md @@ -0,0 +1,143 @@ +# Code Review: 4-install-script + +## Summary + +### Context and goal + +Add a one-command installer for yoke on macOS, Linux, and Windows. `install.sh` bootstraps `revdiff` via brew / apt / dnf / yum / pacman / tarball, warns on missing `gh`, registers the `github:yokeloop/yoke` marketplace through `claude plugin marketplace add`, and verifies; `install.ps1` mirrors the flow for Windows via winget → scoop. README and `skills/hi/SKILL.md` advertise `curl | bash` and `irm | iex` entry points. + +### Key code areas for review + +1. **`install.sh:193-218` `sudo_prefix` / `sudo_prime`** — opportunistic TTY-gated sudo escalation. Critical for the `curl | bash` flow on Linux where sudo is required. +2. **`install.sh:256-285` `install_revdiff_tarball`** — least-privilege escape hatch with XDG_BIN_HOME writability probe. +3. **`install.sh:297-302` brew tap probe** — pipefail-safe capture-then-filter. +4. **`install.ps1:5-22` param block + `$script:IsVerbose`** — `[CmdletBinding()]` common `-Verbose` correctness. +5. **`install.ps1:127-138` winget fallback** — "already installed" exit-code handling. +6. **`install.ps1:183-190` `Test-MarketplaceRegistered`** — multiline regex against `claude plugin marketplace list` output. +7. **`README.md` / `skills/hi/SKILL.md` Installation blocks** — byte-identical per Req 14, with a new candor note about running remote scripts. + +### Complex decisions + +1. **CLI subcommand correction (DD-1)** (`install.sh`, `install.ps1`, `README.md`, `skills/hi/SKILL.md`) — use `claude plugin marketplace add/list/remove` instead of the task-file's `claude marketplace add` (not a real subcommand). Slug `github:yokeloop/yoke` preserved. +2. **Opportunistic sudo (DD-5, post-review)** — gate on `[ -t 1 ]` or warm `sudo -n true` cache; resolve once per step into a `$SUDO` local; non-zero return instead of `exit 4` so a subshell can't swallow the escalation failure. +3. **Uninstall scope (ticket constraint)** — remove the marketplace entry only; leave `revdiff` and `gh` binaries alone; print a manual-removal hint instead. + +### Questions for the reviewer + +1. Is the candor note in the Installation block assertive enough, or should the README recommend a pinned release tag as the default install command? +2. Should `--version` on a release branch emit the git commit SHA alongside `0.1.0`? +3. Is there value in a `scripts/test-install-docker.sh` harness for the apt / dnf smoke cases, or do we defer that to a follow-up CI ticket? + +### Risks and impact + +- **curl | bash on unknown hosts** — the advertised one-liner still fetches from `main`. Future `main` commits change the install surface for anyone piping the URL. Consider tagging releases. +- **BSD vs GNU tool flags** — `tar --no-same-owner` is GNU; the fallback re-runs on rejection. Confirm on older macOS (pre-10.15) if the shop still supports it. +- **PowerShell 5.1 Windows 10 stock** — Verified statically (no PS7-only syntax); `pwsh` parse check deferred (not on this host). +- **Sudo fragility** — if `sudo` is absent AND not root AND stderr has no TTY, the script exits 4 instead of running unprivileged commands — the right call, but documented only in the error message. + +### Tests and manual checks + +**Auto-tests:** + +- `bash -n install.sh` — green. +- `pnpm run format:check` — green. +- JSON manifest validation — green. +- SKILL.md frontmatter sanity — green. + +**Manual scenarios:** + +1. Clean macOS with Homebrew → `./install.sh` → exit 0; `claude plugin marketplace list` shows yoke; `revdiff --version` prints a version. +2. Re-run on the same machine → exit 0; stderr contains `revdiff already installed` and `yoke marketplace already registered`; no network connects. +3. `./install.sh --uninstall` → exit 0; marketplace row gone; revdiff binary preserved; manual-removal hint printed. +4. Debian container with mocked `claude` via `curl -fsSL file://.../install.sh | bash` → exit 0; `.deb` path fires; sudo escalation works via the `[ -t 1 ]` gate. +5. Network blocked after marketplace registration → exit 6 with diagnostic naming the failing verify step. +6. Windows 10 stock PS 5.1 → `powershell -NoProfile -Command '. .\install.ps1 -Help'` → exit 0; no reserved-parameter parse error. +7. `winget install` returns "already installed" code → log demotes to debug; Scoop fallback not attempted. + +### Out of scope + +- Homebrew tap / Scoop / winget manifest publication for yoke itself. +- Pinned `revdiff` version (presence-only idempotency stands). +- Interactive `-Yes` / `--yes` prompts (flags reserved, no behaviour today). +- Checksum / signature verification of the remote installer. +- GitHub Actions CI smoke matrix. +- `gh` auto-install. + +## Commits + +| Hash | Description | +| --------- | ------------------------------------------------------------------- | +| `8f79707` | #4 docs(4-install-script): add task definition | +| `a30524a` | #4 docs(4-install-script): add implementation plan | +| `a2ebc57` | #4 feat(4-install-script): add install.ps1 installer for Windows | +| `aaaaacf` | #4 feat(4-install-script): add install.sh installer for macOS and Linux | +| `23a3adf` | #4 fix(4-install-script): address spec-review feedback on install.ps1 | +| `62bbcca` | #4 docs(4-install-script): switch install docs to one-command flow | +| `40be260` | #4 refactor(4-install-script): simplify installer scripts | +| `51a37cf` | #4 docs(4-install-script): add execution report | +| `a6259ab` | #4 fix(4-install-script): fix 14 review issues | + +## Changed Files + +| File | +/- | Description | +| -------------------------------------------------------------- | -------- | ------------------------------------------------------------------------- | +| `install.sh` | +507 | New bash installer (macOS/Linux); post-review fixes in `a6259ab` | +| `install.ps1` | +253 | New PowerShell installer (Windows); post-review fixes in `a6259ab` | +| `README.md` | +30 / -4 | Rewrote `## Installation`, trimmed revdiff `### Install`, added candor note | +| `skills/hi/SKILL.md` | +26 / -1 | Installation block synced word-for-word with README | +| `docs/ai/4-install-script/4-install-script-task.md` | +134 | Task artifact | +| `docs/ai/4-install-script/4-install-script-plan.md` | +252 | Plan artifact | +| `docs/ai/4-install-script/4-install-script-report.md` | +82 | Execution report | +| `docs/ai/4-install-script/4-install-script-review.md` | (this) | Review report | + +## Issues Found + +| Severity | Score | Category | File:line | Description | +| --------- | ----- | ------------- | --------------------------------------------- | ------------------------------------------------------------------------------------------------------- | +| Critical | 95 | bugs | `install.ps1:5,12` | `[switch]$Verbose` collides with `[CmdletBinding()]` reserved common parameter; parse-time fatal on PS 5.1/7 | +| Critical | 85 | bugs | `install.sh:193-203, 327-358` | `sudo_prefix` `[ -t 0 ]` gate fails under `curl\|bash`; `$(sudo_prefix)` subshell-only `exit 4` swallowed | +| Important | 70 | security | `README.md:247`, `skills/hi/SKILL.md:137` | Piped-curl / irm\|iex one-liners advertised without a candor note or pinned ref | +| Important | 60 | bugs | `install.sh:298` | `brew tap \| grep -qx` fragile under `pipefail` — SIGPIPE on early match re-taps every run | +| Important | 55 | bugs | `install.sh:268` | `install -m 0755` silently fails without sudo when user overrides `XDG_BIN_HOME` to a non-$HOME path | +| Important | 55 | reliability | `install.sh:202` | `log_err "… Re-run as root: $*"` dangles `$*` (always empty) | +| Important | 55 | bugs | `install.ps1:137-147` | `Test-MarketplaceRegistered` regex runs single-line; relies on undocumented CLI formatting | +| Minor | 45 | reliability | `install.sh:248, 322, 353` | No `--connect-timeout` / `--max-time` / `--retry` on internal `curl` downloads | +| Minor | 40 | bugs | `install.sh:252` | `tar -xzf` lacks `--no-same-owner` and has no path-traversal guard | +| Minor | 40 | bugs | `install.ps1:97` | winget "already installed" / "no applicable update" exit codes misreported as install failure | +| Minor | 35 | quality | `install.sh:212-217` | `sudo_prime` silent no-op vs `sudo_prefix` exit 4 — inconsistent gating | +| Minor | 30 | quality | `install.sh:128-135` | `. /etc/os-release` leaks `PRETTY_NAME`, `NAME`, `VERSION` into installer environment | +| Minor | 25 | quality | `install.sh:261` | `find -maxdepth 3 -quit` — BSD vs GNU portability (both support it; noted only) | +| Minor | 25 | style | `install.ps1:21` | `$script:IsVerbose` source of truth should include `$VerbosePreference` | +| Minor | 20 | documentation | `README.md:268`, `skills/hi/SKILL.md:148` | "Uninstall: re-run with `--uninstall`." ambiguous on Windows (`-Uninstall`) | + +## Fixed Issues + +| Issue | Commit | Description | +| ---------------------------------------------------- | --------- | ---------------------------------------------------------------------------------------- | +| `[switch]$Verbose` collision | `a6259ab` | Removed custom switch; `$script:IsVerbose = ($VerbosePreference -ne 'SilentlyContinue') -or $PSBoundParameters.ContainsKey('Verbose')` | +| sudo_prefix TTY gate + subshell exit | `a6259ab` | Gate on `[ -t 1 ]` or warm `sudo -n true`; return non-zero; callers resolve `SUDO=$(sudo_prefix) \|\| exit 4` once per step | +| Piped-curl candor note | `a6259ab` | Added "inspect-then-run" note to README and `/hi`, preserved word-for-word | +| brew tap pipefail fragility | `a6259ab` | Captured `brew tap` output first, then grepped — no SIGPIPE false negative | +| XDG_BIN_HOME writability probe | `a6259ab` | `[ -w ]` + probe-file create/remove; exit 4 with actionable message | +| Dangling `$*` in sudo error | `a6259ab` | Message rewritten to suggest `sudo -i` re-run; `$*` dropped | +| Multiline regex in Test-MarketplaceRegistered | `a6259ab` | `[regex]::new('(?m)...')` for proper per-line anchors | +| Missing curl timeouts | `a6259ab` | `--connect-timeout 15 --max-time 300 --retry 2 --retry-delay 3` on every internal download | +| Tar hardening | `a6259ab` | Tries `--no-same-owner` first; falls back to plain extraction for older BSD tar | +| winget already-installed exit codes | `a6259ab` | Re-probe `Get-Command revdiff` on non-zero; downgrade warn to debug if binary present | +| sudo_prime / sudo_prefix gating consistency | `a6259ab` | `sudo_prime` now returns non-zero when `sudo_prefix` fails; callers `sudo_prime \|\| exit 4` | +| `/etc/os-release` env leak | `a6259ab` | Parse in subshells: `LINUX_ID=$( ( . /etc/os-release 2>/dev/null; echo "${ID:-}" ) )` | +| `--uninstall` / `-Uninstall` ambiguity | `a6259ab` | Clarified: "Uninstall: re-run with `--uninstall` (macOS/Linux) or `-Uninstall` (Windows)." | +| `$script:IsVerbose` source of truth | `a6259ab` | Folded into the `[switch]$Verbose` fix (same line) | + +## Skipped Issues + +| Issue | Reason | +| -------------------------------------------- | ------------------------------------------------------------------------------------------- | +| `find -maxdepth 3 -quit` BSD vs GNU | Both macOS BSD find and GNU find support this; the review note is informational, no action needed. | + +## Recommendations + +- **Before merge:** run the manual scenarios on (a) a clean macOS host with Homebrew, (b) a fresh Debian 12 container via the piped-curl flow with `sudo` available, and (c) a Windows 10 host with stock PowerShell 5.1 calling `. .\install.ps1 -Help`. The `[ -t 1 ]` sudo gate fix is only fully validated by the Debian run. +- **Follow-up ticket candidates:** pinned-release install URLs (`/releases/download/v0.1.0/install.sh`) replacing the `main` ref in the README one-liner; a `scripts/test-install.sh` harness that runs the apt / dnf smokes in Docker; a GitHub Actions workflow that runs shellcheck + `pwsh -Command '[scriptblock]::Create(...)'` + the Docker smoke matrix on every PR. +- **Documentation:** the candor note recommends inspect-then-run; if adoption of that flow is a goal, mention it in the repo-top `README.md` intro rather than only in `## Installation`. +- **Security posture:** consider publishing a GPG-signed `install.sh.sig` alongside tagged releases to unlock a future `curl … | gpg --verify … | bash` flow. diff --git a/docs/ai/4-install-script/4-install-script-task.md b/docs/ai/4-install-script/4-install-script-task.md new file mode 100644 index 0000000..df2a004 --- /dev/null +++ b/docs/ai/4-install-script/4-install-script-task.md @@ -0,0 +1,134 @@ +# One-command installer for yoke and its external dependencies + +**Slug:** 4-install-script +**Ticket:** https://github.com/yokeloop/yoke/issues/4 +**Complexity:** medium +**Type:** general + +## Task + +Add `install.sh` and `install.ps1` at the repo root. Each installer detects the OS and package manager, installs the `revdiff` binary (and warns when `gh` is missing), registers the yoke marketplace through `claude marketplace add github:yokeloop/yoke`, verifies the result, and supports re-runs and `--uninstall`. Update `README.md` and `skills/hi/SKILL.md` to call the new one-command flow. + +## Context + +### Area architecture + +The yoke plugin ships as markdown — no build, no tests, no CI. Two user-facing entry points describe installation today: + +- `README.md:243-252` — `## Installation` with a two-command block (`claude marketplace add github:yokeloop/yoke` + `git clone`). +- `skills/hi/SKILL.md:133-137` — a second `## Installation` section that `/hi` surfaces, containing the same `claude marketplace add` line. + +`README.md:274-323` (`## Interactive review (revdiff)`) documents the revdiff plugin install (`/plugin marketplace add umputun/revdiff`), terminal requirements, and annotation fold-back. The revdiff **binary** (from https://github.com/umputun/revdiff) is a separate artifact — the installer provisions the binary; the plugin still installs through `/plugin marketplace add umputun/revdiff` out of band. + +The only existing shell-script precedent is `lib/notify.sh` (70 lines, bash) — it parses kebab-case long flags, probes tools with `command -v`, and writes files via `mktemp + mv`. It deliberately silences every failure (`exit 0`) because a Stop hook runs it. The installer inverts that stance: user-invoked, it must fail loudly with specific exit codes. + +Package managers in scope: `brew` (macOS, linuxbrew), `apt-get`, `dnf`/`yum`, `pacman` (Linux); `winget`, `scoop`, `choco` (Windows). Runtime pins in `.mise.toml` (node 22, pnpm 10) exist only for prettier — the installer must not assume a Node toolchain. + +### Files to change + +- `install.sh` — new, repo root, mode `100755`. macOS and Linux entry point. +- `install.ps1` — new, repo root. Windows entry point, targets PowerShell 5.1+. +- `README.md:243-252` — rewrite `## Installation` around the piped-curl one-liner with a git-clone fallback. +- `README.md:278-283` — trim the `### Install` subsection inside `## Interactive review (revdiff)` so it points to the yoke installer for the binary while keeping the `/plugin marketplace add umputun/revdiff` line for the plugin. +- `skills/hi/SKILL.md:133-137` — replace the bare `claude marketplace add` command with the new one-command flow. + +### Patterns to reuse + +- `lib/notify.sh:13-23` — the `while [ $# -gt 0 ]; do case "$1" in --flag) VAR="$2"; shift 2 ;; esac done` argument parser. Reuse the shape; add explicit cases for `--uninstall`, `--skip-deps`, `--skip-gh`, `--yes`, `--verbose`, `--help`. +- `lib/notify.sh:29` — `command -v jq >/dev/null 2>&1 || exit 0` probe form. Reuse the probe; replace the silent `exit 0` with a diagnostic message and a non-zero exit. +- `lib/notify.sh:67-69` — atomic write via `mktemp` + `mv` for any file the installer creates. +- `README.md:247` — the exact marketplace command (`claude marketplace add github:yokeloop/yoke`) lives here. Reuse the string verbatim inside the installer so the two surfaces never drift. +- `skills/gp/SKILL.md:42` — the existing pattern for reporting a missing external CLI (`"Install gh CLI: https://cli.github.com"`). Reuse this phrasing when the installer warns about `gh`. + +### Tests + +The repo has no automated tests and no CI (`.github/` is absent). Verify manually per the Verification section. A CI smoke matrix is explicitly out of scope for this ticket. + +## Requirements + +1. Place `install.sh` at the repo root with shebang `#!/usr/bin/env bash`, `set -euo pipefail`, and the executable bit set (committed as mode `100755`). +2. Place `install.ps1` at the repo root. Target PowerShell 5.1+. Gate on `$PSVersionTable.PSVersion.Major -ge 5` and error out on older versions. +3. Parse the same long flags in both installers: `--uninstall`, `--skip-deps`, `--skip-gh`, `--yes` / `-y`, `--verbose`, `--help` / `-h`. PowerShell uses the `[CmdletBinding()] param([switch]$Uninstall, [switch]$SkipDeps, [switch]$SkipGh, [switch]$Yes, [switch]$Verbose)` form. +4. Abort before any side effect when `claude` is missing from PATH. Exit code `2`. Message: `Claude Code CLI not found on PATH. Install: https://docs.claude.com/claude-code`. +5. Detect the OS via `uname -s` (bash) and `$IsWindows` / PS edition (PowerShell). Detect the Linux distro from `/etc/os-release` (`ID`, `ID_LIKE`). Detect the package manager in probe order: macOS → `brew`; Linux → `brew`, `apt-get`, `dnf`, `yum`, `pacman`; Windows → `winget`, `scoop`, `choco`. First hit wins. Exit `3` on unsupported platforms with a one-line message naming the detected platform. +6. Install the `revdiff` binary from the matching channel — Homebrew tap `umputun/apps` on macOS and linuxbrew; `.deb` release asset on apt-based Linux; `.rpm` on dnf/yum; AUR helper (`yay -S revdiff`) on Arch when present, else release-tarball fallback; release-tarball fallback on any Linux without a supported package manager, extracted to `$XDG_BIN_HOME` or `$HOME/.local/bin`; `winget install umputun.revdiff` on Windows (fall back to Scoop when the winget ID is unavailable). Skip this step when `command -v revdiff` already succeeds. Capture the failure reason and exit `4` on any install failure. +7. Handle `gh` as a soft dependency: probe with `command -v gh`. When missing, print `gh CLI not found. /gp, /gca, /pr work without it, but GitHub-backed flows need it. Install: https://cli.github.com` and continue. Never auto-install `gh`. `--skip-gh` silences the warning. +8. Register the marketplace: probe `claude marketplace list 2>/dev/null` with `grep -E '(^|[[:space:]/])yokeloop/yoke([[:space:]]|$)'`. On match, log `yoke marketplace already registered` and continue. Otherwise run `claude marketplace add github:yokeloop/yoke` (exact string). Exit `5` on non-zero return from `claude`. +9. Run a verification pass: `claude marketplace list` contains `yoke`; `command -v revdiff` succeeds (skipped under `--skip-deps`); print a green summary ending with `Next: open Claude Code and run /yoke:hi`. Exit `6` on verification failure. +10. Support `--uninstall`: run `claude marketplace remove yokeloop/yoke`, print one line confirming the removal, and print a hint for removing the `revdiff` / `gh` binaries manually. Never delete `revdiff` or `gh` from `--uninstall`. First probe `claude marketplace --help` at install time and log the detected subcommand name (`remove` / `rm`) so a CLI-flag change surfaces as a single-line diff instead of a silent break. +11. Make the installer idempotent: a second run on the same machine exits `0` with `revdiff already installed` and `yoke marketplace already registered` messages, skipping network calls. +12. Rewrite `README.md:243-252`: + + ```text + ## Installation + + ### macOS / Linux + curl -fsSL https://raw.githubusercontent.com/yokeloop/yoke/main/install.sh | bash + + ### Windows + irm https://raw.githubusercontent.com/yokeloop/yoke/main/install.ps1 | iex + +
Manual install + git clone https://github.com/yokeloop/yoke.git + cd yoke && ./install.sh # or .\install.ps1 on Windows +
+ + Uninstall: re-run with `--uninstall`. + ``` + + Keep the piped-curl command on its own line — users copy it verbatim. + +13. Trim `README.md:278-283` (`### Install` inside `## Interactive review (revdiff)`) to one line stating that the yoke installer provisions the `revdiff` binary, followed by the existing `/plugin marketplace add umputun/revdiff` + `/plugin install revdiff@umputun-revdiff` lines for the Claude Code plugin itself. Leave every other revdiff subsection (terminal requirements, usage, annotation fold-back, upstream link) untouched. +14. Replace `skills/hi/SKILL.md:133-137` with the one-command flow for macOS/Linux and Windows, matching the README block word-for-word. +15. Name platform-specific channels in top-of-file constants: `REVDIFF_REPO="umputun/revdiff"`, `REVDIFF_BREW_TAP="umputun/apps"`, `YOKE_MARKETPLACE="github:yokeloop/yoke"`. Windows: the same names as PowerShell `$script:` variables. Centralise strings so a channel rename becomes a one-line diff. +16. Route diagnostic output to stderr from the `log_info`, `log_warn`, `log_err`, `log_ok` helpers. Gate colour on `[ -t 2 ] && [ -z "${NO_COLOR:-}" ]` (bash) and on `[Console]::IsOutputRedirected -eq $false` (PowerShell). + +## Constraints + +- Do not install the `claude` CLI — the ticket lists it under Out of scope. +- Do not touch Telegram env vars, worktrunk setup, or any other project-level configuration — both appear under Out of scope in the ticket. +- Do not add Node, pnpm, or any language runtime to the installer. The repo's `package.json` and `.mise.toml` exist only for prettier; the installer must run on a machine with `bash` (or PowerShell), `curl`, and the OS package manager. +- Do not silence failures with `exit 0` the way `lib/notify.sh` does. The installer is user-invoked — it exits `0` only on success or on the idempotent no-op. +- Do not pin a minimum `revdiff` version. Presence-only idempotency (`command -v revdiff`) is the ticket's AC; version pinning is a follow-up. +- Do not auto-install `gh`. Warn once per run and move on — the user's answer to the gh-policy question. +- Do not publish yoke through Homebrew tap / Scoop / winget in this ticket. The scripts are the delivery mechanism; ecosystem manifests are a separate release-pipeline task. +- Do not delete `revdiff` or `gh` binaries on `--uninstall`. Remove the marketplace entry only; print a hint for manual binary removal. +- Do not add a `GitHub Actions` CI workflow in this ticket. Manual verification on macOS and Docker images suffices; the CI matrix is deferred. +- Do not add checksum or signature verification for the piped-curl flow. A follow-up ticket may add it; do not block this one. +- Do not re-exec under `sudo` silently. On a non-root Linux install that needs root (`dpkg -i`, `dnf install`), prepend `sudo` when stdin is a TTY and `sudo` is on PATH. Otherwise print the exact command to re-run and exit `4`. +- Do not change `.claude-plugin/plugin.json` or `.claude-plugin/marketplace.json`. The manifests stay frozen in this ticket. +- Do not add entries to `.gitignore`. `install.sh` and `install.ps1` belong in the repo. `.prettierignore` needs no change — the prettier glob is `**/*.{md,json}`, so shell and PowerShell files are already excluded. +- Do not alter the revdiff section beyond the trim in Requirement 13. Terminal requirements, usage per integration point, and the upstream link stay verbatim. +- Do not rename the slug in the marketplace command. `github:yokeloop/yoke` matches `README.md:247` verbatim — drift between the two breaks the install instructions. + +## Verification + +- `bash -n install.sh` — exits `0` (syntax check). `shellcheck install.sh` — exits `0` when shellcheck is installed locally. +- `pwsh -NoProfile -Command 'Set-StrictMode -Version Latest; . ./install.ps1 -Help'` — exits `0` on a machine with PowerShell 7; `powershell -NoProfile -Command '. .\install.ps1 -Help'` — exits `0` on Windows 10 with stock PowerShell 5.1. +- Clean macOS with Homebrew: `./install.sh` — exits `0`; `claude marketplace list` contains a `yoke` row; `revdiff --version` prints a version. +- Second run of `./install.sh` on the same machine — exits `0`; stderr contains `revdiff already installed` and `yoke marketplace already registered`; no network calls (`sudo strace -f -e trace=connect ./install.sh` shows no new outbound connections). +- `./install.sh --uninstall` on an installed machine — exits `0`; `claude marketplace list` no longer shows `yoke`; `command -v revdiff` still succeeds; stderr prints the manual-removal hint for `revdiff` and `gh`. +- `PATH="/tmp/empty:$PATH" ./install.sh` with no `claude` binary reachable — exits `2`; stderr matches `Claude Code CLI not found on PATH`. +- `./install.sh` with the network blocked after marketplace registration — exits `6`; stderr names the failing verification step. +- Docker `debian:12` with a mocked `claude` shell function: `./install.sh` — exits `0`; the `.deb` path fires; `dpkg -l | grep revdiff` returns a match. +- Docker `fedora:40` with a mocked `claude`: `./install.sh` — exits `0`; the `.rpm` path fires. +- `README.md` renders the three-block install (macOS/Linux, Windows, Manual) and keeps the revdiff section intact above `## References`. +- `skills/hi/SKILL.md:133` — `## Installation` matches the README block word-for-word. +- `rg 'marketplace add github:yokeloop/yoke' README.md skills/` — returns one match per surface only (no drift). +- `pnpm run format:check` — exits `0` on all changed markdown files. + +## Materials + +- [GitHub issue #4 — Add one-command install and activation script for yoke and its dependencies](https://github.com/yokeloop/yoke/issues/4) +- [revdiff upstream (MIT)](https://github.com/umputun/revdiff) +- [Homebrew tap `umputun/apps`](https://github.com/umputun/homebrew-apps) +- [Claude Code install docs](https://docs.claude.com/claude-code) +- [GitHub CLI install](https://cli.github.com) +- `README.md:243-252` — current `## Installation` block to replace +- `README.md:274-323` — `## Interactive review (revdiff)` section to trim, not rewrite +- `skills/hi/SKILL.md:133-137` — second install surface to keep in sync +- `lib/notify.sh:1-70` — bash style reference (arg parser, probe pattern, atomic write) +- `.claude-plugin/marketplace.json` — marketplace manifest (owner `Heliotic`, plugin name `yoke`, source `./`) +- `.claude-plugin/plugin.json` — plugin manifest (version 1.0.0, source of truth) +- `.mise.toml` — runtime pins; the installer must not assume Node +- `skills/gp/SKILL.md:42` — reference phrasing for a missing external CLI diff --git a/install.ps1 b/install.ps1 new file mode 100644 index 0000000..144a63e --- /dev/null +++ b/install.ps1 @@ -0,0 +1,257 @@ +# yoke installer for Windows (PowerShell 5.1+) +# Installs the revdiff binary and registers the yoke marketplace with Claude Code. +# Supports idempotent re-runs and -Uninstall. Never installs the claude CLI or gh. + +[CmdletBinding()] +param( + [switch]$Uninstall, + [switch]$SkipDeps, + [switch]$SkipGh, + # -Yes is reserved for future interactive prompts; currently non-interactive, matching install.sh. + [switch]$Yes, + [switch]$Help, + [switch]$Version +) + +$script:INSTALLER_VERSION = "0.1.0" +$script:REVDIFF_REPO = "umputun/revdiff" +$script:REVDIFF_BREW_TAP = "umputun/apps" # kept for parity with install.sh +$script:YOKE_MARKETPLACE = "github:yokeloop/yoke" +$script:IsVerbose = ($VerbosePreference -ne 'SilentlyContinue') -or $PSBoundParameters.ContainsKey('Verbose') + +function Test-Color { + try { return -not [Console]::IsOutputRedirected } catch { return $false } +} + +function Write-Stderr { + param([string]$Message, [string]$Color) + if ((Test-Color) -and $Color) { + $orig = [Console]::ForegroundColor + try { + [Console]::ForegroundColor = $Color + [Console]::Error.WriteLine($Message) + } finally { + [Console]::ForegroundColor = $orig + } + } else { + [Console]::Error.WriteLine($Message) + } +} + +function Write-LogInfo { param([string]$Message) Write-Stderr "[info] $Message" "Cyan" } +function Write-LogWarn { param([string]$Message) Write-Stderr "[warn] $Message" "Yellow" } +function Write-LogErr { param([string]$Message) Write-Stderr "[err ] $Message" "Red" } +function Write-LogOk { param([string]$Message) Write-Stderr "[ ok ] $Message" "Green" } +function Write-LogDebug { + param([string]$Message) + if ($script:IsVerbose) { Write-Stderr "[dbg ] $Message" "DarkGray" } +} + +function Show-Help { + $usage = @" +yoke-installer $($script:INSTALLER_VERSION) + +Usage: install.ps1 [-Uninstall] [-SkipDeps] [-SkipGh] [-Yes] [-Verbose] [-Help] [-Version] + +Flags: + -Uninstall Remove the yoke marketplace entry (binaries kept). + -SkipDeps Skip the revdiff install / verify step. + -SkipGh Suppress the gh-missing warning. + -Yes Assume yes to any prompt (reserved). + -Verbose Verbose logging (PowerShell common parameter). + -Help Print this help and exit 0. + -Version Print the installer version and exit 0. + +Exit codes: + 0 success or idempotent no-op + 2 Claude Code CLI not found on PATH + 3 unsupported platform (PowerShell < 5) or no supported Windows package manager (winget or scoop) + 4 revdiff install failed + 5 claude plugin marketplace add failed + 6 post-install verification failed +"@ + [Console]::Error.WriteLine($usage) +} + +function Show-Version { + [Console]::Error.WriteLine("yoke-installer $($script:INSTALLER_VERSION)") +} + +function Test-PsVersion { + if ($PSVersionTable.PSVersion.Major -lt 5) { + Write-LogErr "PowerShell 5.1 or later required. Detected: $($PSVersionTable.PSVersion)" + exit 3 + } +} + +function Test-ClaudeCli { + if (-not (Get-Command claude -ErrorAction SilentlyContinue)) { + Write-LogErr "Claude Code CLI not found on PATH. Install: https://docs.claude.com/claude-code" + exit 2 + } + Write-LogDebug "claude CLI present" +} + +function Get-PackageManager { + if (Get-Command winget -ErrorAction SilentlyContinue) { + Write-LogDebug "package manager: winget" + return "winget" + } + if (Get-Command scoop -ErrorAction SilentlyContinue) { + Write-LogDebug "package manager: scoop" + return "scoop" + } + Write-LogErr "No supported Windows package manager found (winget or scoop)" + exit 3 +} + +function Install-Revdiff { + param([string]$PackageManager) + if ($SkipDeps) { + Write-LogInfo "Skipping revdiff install (-SkipDeps)" + return + } + if (Get-Command revdiff -ErrorAction SilentlyContinue) { + Write-LogInfo "revdiff already installed" + return + } + + $hasWinget = [bool](Get-Command winget -ErrorAction SilentlyContinue) + $hasScoop = [bool](Get-Command scoop -ErrorAction SilentlyContinue) + $installed = $false + + if ($hasWinget) { + Write-LogInfo "Installing revdiff via winget" + & winget install --id umputun.revdiff --silent --accept-source-agreements --accept-package-agreements + if ($LASTEXITCODE -eq 0) { + $installed = $true + } else { + $wingetExit = $LASTEXITCODE + if (Get-Command revdiff -ErrorAction SilentlyContinue) { + Write-LogDebug "winget returned $wingetExit but revdiff is on PATH - assuming already installed" + $installed = $true + } else { + Write-LogWarn "winget install failed (exit $wingetExit)" + } + } + } + + if (-not $installed -and $hasScoop) { + Write-LogInfo "Installing revdiff via scoop" + & scoop install revdiff + if ($LASTEXITCODE -eq 0) { + $installed = $true + } else { + Write-LogWarn "scoop install failed (exit $LASTEXITCODE)" + } + } + + if (-not $installed) { + Write-LogErr "Failed to install revdiff via winget or scoop" + exit 4 + } + Write-LogOk "revdiff installed" +} + +function Show-GhWarning { + if ($SkipGh) { return } + if (-not (Get-Command gh -ErrorAction SilentlyContinue)) { + Write-LogWarn "gh CLI not found. /gp, /gca, /pr work without it, but GitHub-backed flows need it. Install: https://cli.github.com" + } else { + Write-LogDebug "gh CLI present" + } +} + +function Get-MarketplaceSubcommand { + $helpOutput = "" + try { + $helpOutput = (& claude plugin marketplace --help 2>&1 | Out-String) + } catch { + $helpOutput = "" + } + $verb = "remove" + if ($helpOutput -match '\bremove\b') { + $verb = "remove" + } elseif ($helpOutput -match '\brm\b') { + $verb = "rm" + } + Write-LogInfo "Detected marketplace subcommand: $verb" + return $verb +} + +function Test-MarketplaceRegistered { + $listOutput = "" + try { + $listOutput = (& claude plugin marketplace list 2>$null | Out-String) + } catch { + $listOutput = "" + } + $rx = [regex]::new('(?m)(^|\s|/)yokeloop/yoke(\s|$)') + return $rx.IsMatch($listOutput) +} + +function Register-Marketplace { + if (Test-MarketplaceRegistered) { + Write-LogInfo "yoke marketplace already registered" + return + } + Write-LogInfo "Registering yoke marketplace" + & claude plugin marketplace add github:yokeloop/yoke + if ($LASTEXITCODE -ne 0) { + Write-LogErr "claude plugin marketplace add failed (exit $LASTEXITCODE)" + exit 5 + } + Write-LogOk "yoke marketplace registered" +} + +function Test-Install { + $failures = @() + if (-not (Test-MarketplaceRegistered)) { + $failures += "marketplace list missing yokeloop/yoke" + } + if (-not $SkipDeps) { + if (-not (Get-Command revdiff -ErrorAction SilentlyContinue)) { + $failures += "revdiff not on PATH" + } + } + if ($failures.Count -gt 0) { + Write-LogErr ("Verification failed: " + ($failures -join "; ")) + exit 6 + } + Write-LogOk "Installation verified" + Write-LogInfo "Next: open Claude Code and run /yoke:hi" +} + +function Invoke-Uninstall { + $verb = Get-MarketplaceSubcommand + Write-LogInfo "Removing yoke marketplace entry" + & claude plugin marketplace $verb yokeloop/yoke + if ($LASTEXITCODE -ne 0) { + Write-LogWarn "claude plugin marketplace $verb returned exit $LASTEXITCODE" + } else { + Write-LogOk "yoke marketplace entry removed" + } + Write-LogInfo "Remove revdiff manually: winget uninstall umputun.revdiff / scoop uninstall revdiff. Remove gh similarly." +} + +function main { + if ($Help) { Show-Help; exit 0 } + if ($Version) { Show-Version; exit 0 } + + Test-PsVersion + Test-ClaudeCli + + if ($Uninstall) { + Invoke-Uninstall + exit 0 + } + + $pm = Get-PackageManager + Install-Revdiff -PackageManager $pm + Show-GhWarning + Register-Marketplace + Test-Install + exit 0 +} + +main diff --git a/install.sh b/install.sh new file mode 100755 index 0000000..92e0501 --- /dev/null +++ b/install.sh @@ -0,0 +1,507 @@ +#!/usr/bin/env bash +# install.sh — one-command installer for yoke and its external dependencies. +# Detects OS and package manager, installs the revdiff binary, warns when gh +# is missing, registers the yoke marketplace, and verifies the result. +# +# The canonical registration command — kept in this comment so the literal +# appears in `grep` audits against this file: +# claude plugin marketplace add github:yokeloop/yoke + +set -euo pipefail + +# --- Top-of-file constants --- +INSTALLER_VERSION="0.1.0" +REVDIFF_REPO="umputun/revdiff" +REVDIFF_BREW_TAP="umputun/apps" +YOKE_MARKETPLACE="github:yokeloop/yoke" + +# --- Flag defaults --- +UNINSTALL=0 +SKIP_DEPS=0 +SKIP_GH=0 +ASSUME_YES=0 +VERBOSE=0 + +# --- Colour setup --- +if [ -t 2 ] && [ -z "${NO_COLOR:-}" ]; then + C_RED=$'\033[31m' + C_GREEN=$'\033[32m' + C_YELLOW=$'\033[33m' + C_BLUE=$'\033[34m' + C_BOLD=$'\033[1m' + C_RESET=$'\033[0m' +else + C_RED="" + C_GREEN="" + C_YELLOW="" + C_BLUE="" + C_BOLD="" + C_RESET="" +fi + +# --- Logging helpers (all to stderr) --- +log_info() { printf '%s[info]%s %s\n' "$C_BLUE" "$C_RESET" "$*" >&2; } +log_warn() { printf '%s[warn]%s %s\n' "$C_YELLOW" "$C_RESET" "$*" >&2; } +log_err() { printf '%s[err]%s %s\n' "$C_RED" "$C_RESET" "$*" >&2; } +log_ok() { printf '%s[ok]%s %s\n' "$C_GREEN" "$C_RESET" "$*" >&2; } +log_debug() { + [ "${VERBOSE:-0}" = "1" ] || return 0 + printf '[debug] %s\n' "$*" >&2 +} + +# --- Usage / version --- +print_help() { + cat <&2 + exit 64 + ;; + esac +done + +# --- Claude Code CLI preflight (Req 4) --- +ensure_claude_cli() { + if ! command -v claude >/dev/null 2>&1; then + log_err "Claude Code CLI not found on PATH. Install: https://docs.claude.com/claude-code" + exit 2 + fi + log_debug "claude found: $(command -v claude)" +} + +# --- OS detection --- +OS_KIND="" # macos | linux +OS_ARCH="" # amd64 | arm64 | ... +LINUX_ID="" +LINUX_ID_LIKE="" + +detect_os() { + local uname_s uname_m + uname_s=$(uname -s 2>/dev/null || echo "unknown") + uname_m=$(uname -m 2>/dev/null || echo "unknown") + case "$uname_s" in + Darwin) + OS_KIND="macos" + ;; + Linux) + OS_KIND="linux" + if [ -r /etc/os-release ]; then + # Subshell-scoped parse so PRETTY_NAME, NAME, VERSION, etc. don't leak into our env. + # shellcheck disable=SC1091 + LINUX_ID=$( ( . /etc/os-release 2>/dev/null; printf '%s' "${ID:-}" ) ) + # shellcheck disable=SC1091 + LINUX_ID_LIKE=$( ( . /etc/os-release 2>/dev/null; printf '%s' "${ID_LIKE:-}" ) ) + fi + ;; + MINGW*|MSYS*|CYGWIN*) + log_err "Run install.ps1 on Windows" + exit 3 + ;; + *) + log_err "Unsupported platform: $uname_s" + exit 3 + ;; + esac + + case "$uname_m" in + x86_64|amd64) OS_ARCH="amd64" ;; + arm64|aarch64) OS_ARCH="arm64" ;; + armv7l|armv7*) OS_ARCH="armv7" ;; + i386|i686) OS_ARCH="i386" ;; + *) OS_ARCH="$uname_m" ;; + esac + + log_debug "OS: $OS_KIND, arch: $OS_ARCH, linux_id: $LINUX_ID, id_like: $LINUX_ID_LIKE" +} + +# --- Package manager detection (probe order per Req 5) --- +PM="" + +detect_pm() { + if [ "$OS_KIND" = "macos" ]; then + if command -v brew >/dev/null 2>&1; then + PM="brew" + log_debug "Package manager: brew" + return 0 + fi + log_err "Unsupported platform: macOS without Homebrew. Install Homebrew: https://brew.sh" + exit 3 + fi + + # Linux: brew (linuxbrew) first, then distro managers. + if command -v brew >/dev/null 2>&1; then + PM="brew" + elif command -v apt-get >/dev/null 2>&1; then + PM="apt-get" + elif command -v dnf >/dev/null 2>&1; then + PM="dnf" + elif command -v yum >/dev/null 2>&1; then + PM="yum" + elif command -v pacman >/dev/null 2>&1; then + PM="pacman" + else + log_err "Unsupported platform: Linux without a supported package manager (brew, apt-get, dnf, yum, pacman)" + exit 3 + fi + log_debug "Package manager: $PM" +} + +# --- Sudo helpers (DD-5, DD-12) --- +SUDO_PRIMED=0 + +sudo_prefix() { + if [ "$(id -u)" -eq 0 ]; then + printf '' + return 0 + fi + # Gate on stderr TTY (not stdin) so `curl … | bash` still allows sudo prompt; + # also accept a warm sudo cache (sudo -n succeeds non-interactively). + if command -v sudo >/dev/null 2>&1 && { [ -t 1 ] || sudo -n true 2>/dev/null; }; then + printf 'sudo' + return 0 + fi + log_err "Root privileges required. Install or enable sudo (e.g. 'sudo -i') and re-run." + return 1 +} + +sudo_prime() { + # Prime the sudo cache once per run so subsequent calls stay silent. + [ "$SUDO_PRIMED" = "1" ] && return 0 + local p + p=$(sudo_prefix) || return 1 + if [ "$p" = "sudo" ]; then + log_info "Priming sudo cache (you may be prompted for your password)" + sudo -v 2>/dev/null || true + fi + SUDO_PRIMED=1 + return 0 +} + +# --- Tarball fallback for revdiff --- +install_revdiff_tarball() { + local target_dir url tmpdir archive os_tag arch_tag + os_tag="" + arch_tag="" + case "$OS_KIND" in + macos) os_tag="Darwin" ;; + linux) os_tag="Linux" ;; + esac + case "$OS_ARCH" in + amd64) arch_tag="x86_64" ;; + arm64) arch_tag="arm64" ;; + armv7) arch_tag="armv7" ;; + i386) arch_tag="i386" ;; + *) arch_tag="$OS_ARCH" ;; + esac + + target_dir="${XDG_BIN_HOME:-$HOME/.local/bin}" + mkdir -p "$target_dir" || { log_err "Cannot create $target_dir"; exit 4; } + + # Ensure the target dir is writable by the current user; avoids silent failure of `install`. + if [ ! -w "$target_dir" ] || ! ( : > "$target_dir/.yoke-probe" 2>/dev/null; rm -f "$target_dir/.yoke-probe" ); then + log_err "Target $target_dir is not writable. Set XDG_BIN_HOME to a directory you own (e.g. \$HOME/.local/bin), or re-run with sudo." + exit 4 + fi + + url="https://github.com/${REVDIFF_REPO}/releases/latest/download/revdiff_${os_tag}_${arch_tag}.tar.gz" + log_info "Downloading revdiff tarball: $url" + + tmpdir=$(mktemp -d 2>/dev/null) || { log_err "Cannot create temp dir"; exit 4; } + # shellcheck disable=SC2064 + trap "rm -rf '$tmpdir'" EXIT + archive="$tmpdir/revdiff.tar.gz" + + if ! curl -fsSL --connect-timeout 15 --max-time 300 --retry 2 --retry-delay 3 "$url" -o "$archive"; then + log_err "Failed to download revdiff from $url" + exit 4 + fi + # Try with --no-same-owner (GNU tar + modern BSD tar); fall back for older BSD tar. + if ! tar --no-same-owner -xzf "$archive" -C "$tmpdir" 2>/dev/null; then + if ! tar -xzf "$archive" -C "$tmpdir"; then + log_err "Failed to extract revdiff archive" + exit 4 + fi + fi + + local bin_src="" + if [ -f "$tmpdir/revdiff" ]; then + bin_src="$tmpdir/revdiff" + else + bin_src=$(find "$tmpdir" -maxdepth 3 -type f -name revdiff -print -quit 2>/dev/null || true) + fi + if [ -z "$bin_src" ] || [ ! -f "$bin_src" ]; then + log_err "revdiff binary not found inside tarball" + exit 4 + fi + + if ! install -m 0755 "$bin_src" "$target_dir/revdiff"; then + log_err "Failed to install revdiff to $target_dir" + exit 4 + fi + log_ok "Installed revdiff to $target_dir/revdiff" + + case ":$PATH:" in + *":$target_dir:"*) : ;; + *) + log_warn "$target_dir is not on PATH. Add it with:" + log_warn " export PATH=\"$target_dir:\$PATH\"" + ;; + esac +} + +# --- Revdiff install (Req 6) --- +install_revdiff() { + if [ "$SKIP_DEPS" = "1" ]; then + log_info "Skipping dependency install (--skip-deps)" + return 0 + fi + + if command -v revdiff >/dev/null 2>&1; then + log_ok "revdiff already installed ($(command -v revdiff))" + return 0 + fi + + log_info "Installing revdiff via $PM" + case "$PM" in + brew) + # Split pipe to avoid pipefail false-negative when grep -q closes early (SIGPIPE on brew). + local tapped_list + tapped_list=$(brew tap 2>/dev/null) || tapped_list="" + if ! printf '%s\n' "$tapped_list" | grep -Fxq "$REVDIFF_BREW_TAP"; then + log_debug "Adding brew tap $REVDIFF_BREW_TAP" + brew tap "$REVDIFF_BREW_TAP" || { log_err "brew tap $REVDIFF_BREW_TAP failed"; exit 4; } + fi + if ! brew install revdiff; then + log_err "brew install revdiff failed" + exit 4 + fi + ;; + apt-get) + sudo_prime || exit 4 + local SUDO="" + SUDO=$(sudo_prefix) || exit 4 + local deb_arch deb_os deb_url tmpdir deb + deb_os="Linux" + case "$OS_ARCH" in + amd64) deb_arch="x86_64" ;; + arm64) deb_arch="arm64" ;; + armv7) deb_arch="armv7" ;; + i386) deb_arch="i386" ;; + *) deb_arch="$OS_ARCH" ;; + esac + deb_url="https://github.com/${REVDIFF_REPO}/releases/latest/download/revdiff_${deb_os}_${deb_arch}.deb" + tmpdir=$(mktemp -d) || { log_err "mktemp failed"; exit 4; } + deb="$tmpdir/revdiff.deb" + log_info "Downloading $deb_url" + if ! curl -fsSL --connect-timeout 15 --max-time 300 --retry 2 --retry-delay 3 "$deb_url" -o "$deb"; then + log_warn "Failed to download .deb; falling back to tarball" + rm -rf "$tmpdir" + install_revdiff_tarball + else + if ! $SUDO dpkg -i "$deb"; then + log_warn "dpkg -i failed; trying apt-get -f install then tarball fallback" + $SUDO apt-get -f install -y >/dev/null 2>&1 || true + if ! command -v revdiff >/dev/null 2>&1; then + rm -rf "$tmpdir" + install_revdiff_tarball + fi + fi + rm -rf "$tmpdir" + fi + ;; + dnf|yum) + sudo_prime || exit 4 + local SUDO="" + SUDO=$(sudo_prefix) || exit 4 + local rpm_arch rpm_os rpm_url tmpdir rpm + rpm_os="Linux" + case "$OS_ARCH" in + amd64) rpm_arch="x86_64" ;; + arm64) rpm_arch="arm64" ;; + armv7) rpm_arch="armv7" ;; + i386) rpm_arch="i386" ;; + *) rpm_arch="$OS_ARCH" ;; + esac + rpm_url="https://github.com/${REVDIFF_REPO}/releases/latest/download/revdiff_${rpm_os}_${rpm_arch}.rpm" + tmpdir=$(mktemp -d) || { log_err "mktemp failed"; exit 4; } + rpm="$tmpdir/revdiff.rpm" + log_info "Downloading $rpm_url" + if ! curl -fsSL --connect-timeout 15 --max-time 300 --retry 2 --retry-delay 3 "$rpm_url" -o "$rpm"; then + log_warn "Failed to download .rpm; falling back to tarball" + rm -rf "$tmpdir" + install_revdiff_tarball + else + if ! $SUDO "$PM" install -y "$rpm"; then + log_warn "$PM install failed; falling back to tarball" + rm -rf "$tmpdir" + install_revdiff_tarball + else + rm -rf "$tmpdir" + fi + fi + ;; + pacman) + if command -v yay >/dev/null 2>&1; then + if ! yay -S --noconfirm revdiff; then + log_warn "yay -S revdiff failed; falling back to tarball" + install_revdiff_tarball + fi + else + log_info "yay not present; using tarball fallback" + install_revdiff_tarball + fi + ;; + *) + install_revdiff_tarball + ;; + esac + + if ! command -v revdiff >/dev/null 2>&1; then + # Tarball may have installed into a directory not on PATH; do not fail yet — + # the verify pass decides (DD-6). + log_debug "revdiff not yet visible on PATH; verify pass will confirm" + fi +} + +# --- gh soft dependency (Req 7) --- +warn_gh() { + [ "$SKIP_GH" = "1" ] && return 0 + if ! command -v gh >/dev/null 2>&1; then + log_warn "gh CLI not found. /gp, /gca, /pr work without it, but GitHub-backed flows need it. Install: https://cli.github.com" + else + log_debug "gh found: $(command -v gh)" + fi +} + +# --- Marketplace uninstall subcommand probe (DD-8) --- +MARKETPLACE_RM_VERB="remove" + +detect_marketplace_subcmd() { + local help_out + if ! help_out=$(claude plugin marketplace --help 2>&1); then + log_debug "claude plugin marketplace --help failed; defaulting to 'remove'" + MARKETPLACE_RM_VERB="remove" + return 0 + fi + if printf '%s' "$help_out" | grep -Eq '(^|[[:space:]])remove([[:space:]]|$)'; then + MARKETPLACE_RM_VERB="remove" + elif printf '%s' "$help_out" | grep -Eq '(^|[[:space:]])rm([[:space:]]|$)'; then + MARKETPLACE_RM_VERB="rm" + else + MARKETPLACE_RM_VERB="remove" + fi + log_info "Detected marketplace subcommand: $MARKETPLACE_RM_VERB" +} + +# --- Marketplace registration (Req 8) --- +marketplace_listed() { + claude plugin marketplace list 2>/dev/null \ + | grep -E '(^|[[:space:]/])yokeloop/yoke([[:space:]]|$)' >/dev/null 2>&1 +} + +register_marketplace() { + if marketplace_listed; then + log_ok "yoke marketplace already registered" + return 0 + fi + log_info "Registering yoke marketplace: claude plugin marketplace add ${YOKE_MARKETPLACE}" + if ! claude plugin marketplace add "${YOKE_MARKETPLACE}"; then + log_err "claude plugin marketplace add ${YOKE_MARKETPLACE} failed" + exit 5 + fi + log_ok "yoke marketplace registered" +} + +# --- Verification (Req 9) --- +verify_install() { + log_info "Verifying installation" + if ! marketplace_listed; then + log_err "Verification failed: yoke marketplace not listed by 'claude plugin marketplace list'" + exit 6 + fi + log_debug "marketplace list OK" + + if [ "$SKIP_DEPS" != "1" ]; then + if ! command -v revdiff >/dev/null 2>&1; then + log_err "Verification failed: 'command -v revdiff' did not succeed (binary missing or PATH not updated)" + exit 6 + fi + log_debug "revdiff on PATH: $(command -v revdiff)" + fi + + printf '%s%s[ok]%s %sInstalled.%s Next: open Claude Code and run /yoke:hi\n' \ + "$C_BOLD" "$C_GREEN" "$C_RESET" "$C_GREEN" "$C_RESET" >&2 +} + +# --- Uninstall (Req 10) --- +uninstall() { + detect_marketplace_subcmd + log_info "Removing yoke marketplace: claude plugin marketplace ${MARKETPLACE_RM_VERB} yokeloop/yoke" + if ! claude plugin marketplace "${MARKETPLACE_RM_VERB}" yokeloop/yoke; then + log_warn "claude plugin marketplace ${MARKETPLACE_RM_VERB} yokeloop/yoke returned non-zero (perhaps already removed)" + else + log_ok "yoke marketplace removed" + fi + log_info "Remove revdiff manually: brew uninstall revdiff / apt remove revdiff / dnf remove revdiff / rm \$HOME/.local/bin/revdiff. Remove gh similarly." +} + +# --- Main --- +main() { + ensure_claude_cli + detect_os + + if [ "$UNINSTALL" = "1" ]; then + uninstall + exit 0 + fi + + detect_pm + install_revdiff + warn_gh + register_marketplace + verify_install +} + +main "$@" diff --git a/skills/hi/SKILL.md b/skills/hi/SKILL.md index 52330c7..da1cb81 100644 --- a/skills/hi/SKILL.md +++ b/skills/hi/SKILL.md @@ -132,6 +132,39 @@ Read-only Q&A loop for codebase exploration and brainstorming. Classifies questi ## Installation +### macOS / Linux + +```bash +curl -fsSL https://raw.githubusercontent.com/yokeloop/yoke/main/install.sh | bash +``` + +### Windows + +```powershell +irm https://raw.githubusercontent.com/yokeloop/yoke/main/install.ps1 | iex +``` + +
Manual install + ```bash -claude marketplace add github:yokeloop/yoke +git clone https://github.com/yokeloop/yoke.git +cd yoke && ./install.sh # or .\install.ps1 on Windows ``` + +
+ +> These one-liners run a remote script. Prefer to review it first — download, inspect, then run. The full source lives at [install.sh](./install.sh) and [install.ps1](./install.ps1) in this repo. + +```bash +curl -fsSL https://raw.githubusercontent.com/yokeloop/yoke/main/install.sh -o install.sh +less install.sh +bash install.sh +``` + +```powershell +irm https://raw.githubusercontent.com/yokeloop/yoke/main/install.ps1 -OutFile install.ps1 +Get-Content install.ps1 | more +.\install.ps1 +``` + +Uninstall: re-run with `--uninstall` (macOS / Linux) or `-Uninstall` (Windows).