diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json new file mode 100644 index 0000000..7154c84 --- /dev/null +++ b/.claude-plugin/marketplace.json @@ -0,0 +1,18 @@ +{ + "name": "llbbl-upkeep", + "owner": { + "name": "Logan Lindquist Land", + "email": "logan.lindquist@gmail.com" + }, + "metadata": { + "description": "Marketplace for the upkeep Claude Code plugin", + "version": "0.2.0" + }, + "plugins": [ + { + "name": "upkeep", + "source": "./", + "description": "Security audits, dependency upgrades, and quality scoring for JS/TS repos. Bundles the /upkeep:audit, /upkeep:deps, and /upkeep:quality skills." + } + ] +} diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json new file mode 100644 index 0000000..48601ab --- /dev/null +++ b/.claude-plugin/plugin.json @@ -0,0 +1,13 @@ +{ + "name": "upkeep", + "description": "AI-powered maintenance workflows for JS/TS repos: security audits, dependency upgrades, and quality scoring. Requires the `upkeep` CLI on PATH (brew install llbbl/tap/upkeep).", + "version": "0.2.0", + "author": { + "name": "Logan Lindquist Land", + "email": "logan.lindquist@gmail.com" + }, + "homepage": "https://github.com/llbbl/upkeep", + "repository": "https://github.com/llbbl/upkeep", + "license": "MIT", + "keywords": ["maintenance", "security", "dependencies", "audit", "quality", "typescript"] +} diff --git a/.github/workflows/auto-release.yml b/.github/workflows/auto-release.yml index 109c281..bb235e3 100644 --- a/.github/workflows/auto-release.yml +++ b/.github/workflows/auto-release.yml @@ -74,7 +74,7 @@ jobs: run: | git config user.name "github-actions[bot]" git config user.email "github-actions[bot]@users.noreply.github.com" - git add package.json src/cli/index.ts skills/*/SKILL.md + git add package.json src/cli/index.ts skills/*/SKILL.md .claude-plugin/plugin.json git commit -m "chore(release): bump version to v$VERSION" git tag -a "$TAG" -m "Release $TAG" git push origin main --follow-tags diff --git a/README.md b/README.md index 5afb2fb..a27eb2f 100644 --- a/README.md +++ b/README.md @@ -12,34 +12,44 @@ A comprehensive maintenance toolkit for JavaScript and TypeScript repositories, - **Risk Assessment** - Evaluate upgrade risk before making changes - **Dependabot Integration** - Manage Dependabot PRs from the command line +upkeep has two parts that install independently: + +- **The `upkeep` CLI binary** — via Homebrew or the install script (below). +- **The Claude Code skills** — via the [plugin marketplace](#claude-code-skills) (`/plugin install upkeep@llbbl-upkeep`). + ## Installation -### Quick Install (Recommended) +### Homebrew (Recommended) + +```bash +brew install llbbl/tap/upkeep +``` + +### Install Script ```bash curl -fsSL https://raw.githubusercontent.com/llbbl/upkeep/main/scripts/install.sh | bash ``` -This installs: -- The `upkeep` CLI binary to `~/.local/bin/` (or `~/.upkeep/bin/` if that doesn't exist) -- Claude Code skills to `~/.claude/skills/` for AI-powered workflows +This installs the `upkeep` CLI binary to `~/.local/bin/` (or `~/.upkeep/bin/` if that doesn't exist). It no longer installs the skills — those come from the plugin marketplace (see [Claude Code Skills](#claude-code-skills)). To install a specific version: ```bash -UPKEEP_VERSION=v0.1.3 curl -fsSL https://raw.githubusercontent.com/llbbl/upkeep/main/scripts/install.sh | bash +UPKEEP_VERSION=v0.2.0 curl -fsSL https://raw.githubusercontent.com/llbbl/upkeep/main/scripts/install.sh | bash ``` ### Manual Installation -Download the appropriate binary from [releases](https://github.com/llbbl/upkeep/releases): +Download the appropriate archive from [releases](https://github.com/llbbl/upkeep/releases) and extract the `upkeep` binary (verify against `checksums.txt`): -| Platform | Binary | -|----------|--------| -| Linux x64 | `upkeep-linux-x64` | -| macOS ARM64 (Apple Silicon) | `upkeep-darwin-arm64` | -| macOS x64 (Intel) | `upkeep-darwin-x64` | -| Windows x64 | `upkeep-windows-x64.exe` | +| Platform | Asset | +|----------|-------| +| Linux x64 | `upkeep__linux_amd64.tar.gz` | +| Linux ARM64 | `upkeep__linux_arm64.tar.gz` | +| macOS ARM64 (Apple Silicon) | `upkeep__darwin_arm64.tar.gz` | +| macOS x64 (Intel) | `upkeep__darwin_amd64.tar.gz` | +| Windows x64 | `upkeep__windows_amd64.exe` | ### From Source @@ -122,23 +132,30 @@ upkeep --log-level=debug audit ## Claude Code Skills -upkeep includes skills for Claude Code that provide AI-powered maintenance workflows. Each skill has access to the upkeep binary: +upkeep ships its Claude Code skills as a plugin distributed through its own marketplace. Install them with: + +```text +/plugin marketplace add llbbl/upkeep +/plugin install upkeep@llbbl-upkeep +``` -### `/upkeep-deps` +This installs all three skills, namespaced under the `upkeep` plugin. The skills shell out to the `upkeep` CLI, so make sure the binary is installed and on your `PATH` first (see [Installation](#installation)). + +### `/upkeep:deps` Upgrade dependencies with intelligent risk assessment: - Prioritizes Dependabot PRs and security fixes - Assesses risk before each upgrade - Runs tests and rolls back on failure -### `/upkeep-audit` +### `/upkeep:audit` Security audit with fix recommendations: - Explains each vulnerability - Shows dependency paths - Guides through safe fixes -### `/upkeep-quality` +### `/upkeep:quality` Improve project health: - Explains quality metrics @@ -203,9 +220,13 @@ src/ └── logger.ts # Pino logging skills/ -├── upkeep-deps/ # Dependency upgrade skill -├── upkeep-audit/ # Security audit skill -└── upkeep-quality/ # Quality improvement skill +├── deps/ # Dependency upgrade skill (/upkeep:deps) +├── audit/ # Security audit skill (/upkeep:audit) +└── quality/ # Quality improvement skill (/upkeep:quality) + +.claude-plugin/ +├── plugin.json # Plugin manifest (the `upkeep` plugin) +└── marketplace.json # Marketplace manifest (`llbbl-upkeep`) tests/ ├── cli/ # CLI integration tests diff --git a/justfile b/justfile index 52a38ff..a4dcb3a 100644 --- a/justfile +++ b/justfile @@ -69,14 +69,15 @@ bump-version bump: update-all-versions: @VERSION=$(jq -r '.version' package.json); \ sed -i '' 's/const VERSION = "[^"]*";/const VERSION = "'"$VERSION"'";/' src/cli/index.ts; \ - sed -i '' 's/^version: .*/version: '"$VERSION"'/' skills/upkeep-deps/SKILL.md; \ - sed -i '' 's/^version: .*/version: '"$VERSION"'/' skills/upkeep-audit/SKILL.md; \ - sed -i '' 's/^version: .*/version: '"$VERSION"'/' skills/upkeep-quality/SKILL.md; \ + sed -i '' 's/^version: .*/version: '"$VERSION"'/' skills/deps/SKILL.md; \ + sed -i '' 's/^version: .*/version: '"$VERSION"'/' skills/audit/SKILL.md; \ + sed -i '' 's/^version: .*/version: '"$VERSION"'/' skills/quality/SKILL.md; \ + jq --arg v "$VERSION" '.version = $v' .claude-plugin/plugin.json > .claude-plugin/plugin.json.tmp && mv .claude-plugin/plugin.json.tmp .claude-plugin/plugin.json; \ echo "Updated versions to $VERSION" commit-version: @VERSION=$(jq -r '.version' package.json); \ - git add package.json src/cli/index.ts skills/*/SKILL.md; \ + git add package.json src/cli/index.ts skills/*/SKILL.md .claude-plugin/plugin.json; \ git commit -m "chore: bump version to v$VERSION"; \ git tag v$VERSION; \ echo "Created tag v$VERSION"; \ @@ -86,9 +87,10 @@ set-version version: jq --arg v "{{version}}" '.version = $v' package.json > package.json.tmp && mv package.json.tmp package.json @VERSION="{{version}}"; \ sed -i 's/const VERSION = "[^"]*";/const VERSION = "'"$VERSION"'";/' src/cli/index.ts; \ - sed -i 's/^version: .*/version: '"$VERSION"'/' skills/upkeep-deps/SKILL.md; \ - sed -i 's/^version: .*/version: '"$VERSION"'/' skills/upkeep-audit/SKILL.md; \ - sed -i 's/^version: .*/version: '"$VERSION"'/' skills/upkeep-quality/SKILL.md; \ + sed -i 's/^version: .*/version: '"$VERSION"'/' skills/deps/SKILL.md; \ + sed -i 's/^version: .*/version: '"$VERSION"'/' skills/audit/SKILL.md; \ + sed -i 's/^version: .*/version: '"$VERSION"'/' skills/quality/SKILL.md; \ + jq --arg v "$VERSION" '.version = $v' .claude-plugin/plugin.json > .claude-plugin/plugin.json.tmp && mv .claude-plugin/plugin.json.tmp .claude-plugin/plugin.json; \ echo "Set all versions to $VERSION" version-sync: @@ -98,6 +100,7 @@ show-versions: echo "=== Current Versions ===" echo "package.json: $(jq -r '.version' package.json)" echo "src/cli/index.ts: $(grep 'const VERSION' src/cli/index.ts | sed 's/.*"\(.*\)".*/\1/')" - echo "upkeep-deps/SKILL.md: $(grep '^version:' skills/upkeep-deps/SKILL.md | sed 's/version: //')" - echo "upkeep-audit/SKILL.md: $(grep '^version:' skills/upkeep-audit/SKILL.md | sed 's/version: //')" - echo "upkeep-quality/SKILL.md: $(grep '^version:' skills/upkeep-quality/SKILL.md | sed 's/version: //')" + echo "skills/deps/SKILL.md: $(grep '^version:' skills/deps/SKILL.md | sed 's/version: //')" + echo "skills/audit/SKILL.md: $(grep '^version:' skills/audit/SKILL.md | sed 's/version: //')" + echo "skills/quality/SKILL.md: $(grep '^version:' skills/quality/SKILL.md | sed 's/version: //')" + echo ".claude-plugin/plugin.json: $(jq -r '.version' .claude-plugin/plugin.json)" diff --git a/scripts/install.sh b/scripts/install.sh index 539c77d..3cb8d94 100755 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -5,7 +5,6 @@ set -euo pipefail # Usage: curl -fsSL https://raw.githubusercontent.com/llbbl/upkeep/main/scripts/install.sh | bash VERSION="${UPKEEP_VERSION:-latest}" -SKILLS_DIR="${CLAUDE_SKILLS_DIR:-$HOME/.claude/skills}" # Colors for output RED='\033[0;31m' @@ -38,7 +37,7 @@ detect_platform() { esac case "$(uname -m)" in - x86_64|amd64) arch="x64" ;; + x86_64|amd64) arch="amd64" ;; arm64|aarch64) arch="arm64" ;; *) error "Unsupported architecture: $(uname -m)" ;; esac @@ -46,17 +45,26 @@ detect_platform() { echo "${os}-${arch}" } -# Get download URL for the binary -get_download_url() { - local platform="$1" - local version="$2" - local base_url="https://github.com/llbbl/upkeep/releases" +# Resolve "latest" to a concrete version tag (e.g. v0.2.0). +# Release assets are versioned (upkeep___.tar.gz), so we +# need the actual tag even for "latest". +resolve_version() { + local version="$1" + if [[ "$version" != "latest" ]]; then + echo "$version" + return + fi - if [[ "$version" == "latest" ]]; then - echo "${base_url}/latest/download/upkeep-${platform}" - else - echo "${base_url}/download/${version}/upkeep-${platform}" + local api="https://api.github.com/repos/llbbl/upkeep/releases/latest" + local tag="" + if command -v curl &> /dev/null; then + tag=$(curl -fsSL "$api" | grep -m1 '"tag_name"' | sed -E 's/.*"tag_name": *"([^"]+)".*/\1/') + elif command -v wget &> /dev/null; then + tag=$(wget -qO- "$api" | grep -m1 '"tag_name"' | sed -E 's/.*"tag_name": *"([^"]+)".*/\1/') fi + + [[ -n "$tag" ]] || error "Could not resolve the latest version from GitHub. Set UPKEEP_VERSION explicitly (e.g. UPKEEP_VERSION=v0.2.0)." + echo "$tag" } # Determine install directory @@ -70,22 +78,20 @@ get_install_dir() { fi } -# Download binary -download_binary() { +# Download a file (no chmod — caller decides what to do with it) +download_file() { local url="$1" local dest="$2" - info "Downloading upkeep from $url" + info "Downloading $url" if command -v curl &> /dev/null; then - curl -fsSL "$url" -o "$dest" + curl -fsSL "$url" -o "$dest" || error "Download failed: $url" elif command -v wget &> /dev/null; then - wget -q "$url" -O "$dest" + wget -q "$url" -O "$dest" || error "Download failed: $url" else error "Neither curl nor wget found. Please install one of them." fi - - chmod +x "$dest" } # Install binary to global location @@ -100,72 +106,42 @@ install_binary() { # Create install directory if it doesn't exist mkdir -p "$install_dir" - local binary_name="upkeep" - local url - url=$(get_download_url "$platform" "$VERSION") - - # Handle Windows extension - if [[ "$platform" == windows-* ]]; then - binary_name="upkeep.exe" - url="${url}.exe" + local os="${platform%-*}" + local arch="${platform#*-}" + + local version + version=$(resolve_version "$VERSION") # e.g. v0.2.0 + local ver="${version#v}" # e.g. 0.2.0 + local base_url="https://github.com/llbbl/upkeep/releases/download/${version}" + + # Windows ships as a raw .exe; Unix platforms ship as gzipped tarballs + # containing a single binary named `upkeep`. + if [[ "$os" == "windows" ]]; then + local binary_path="$install_dir/upkeep.exe" + download_file "${base_url}/upkeep_${ver}_windows_${arch}.exe" "$binary_path" + chmod +x "$binary_path" + info "Installed upkeep to $binary_path" + echo "$binary_path" + return fi - local binary_path="$install_dir/$binary_name" - download_binary "$url" "$binary_path" + local asset="upkeep_${ver}_${os}_${arch}.tar.gz" + local tmp + tmp=$(mktemp -d) + download_file "${base_url}/${asset}" "$tmp/$asset" + tar -xzf "$tmp/$asset" -C "$tmp" || error "Failed to extract $asset" + + local binary_path="$install_dir/upkeep" + mv "$tmp/upkeep" "$binary_path" + chmod +x "$binary_path" + rm -rf "$tmp" info "Installed upkeep to $binary_path" - # Return the path for use by install_skills + # Return the path for the caller echo "$binary_path" } -# Install Claude Code skills with symlinks to the binary -install_skills() { - local binary_path="$1" - local platform - platform=$(detect_platform) - - info "Installing Claude Code skills to $SKILLS_DIR" - - local skills=("upkeep-deps" "upkeep-audit" "upkeep-quality") - local base_url="https://raw.githubusercontent.com/llbbl/upkeep/main/skills" - - local binary_name="upkeep" - if [[ "$platform" == windows-* ]]; then - binary_name="upkeep.exe" - fi - - for skill in "${skills[@]}"; do - local skill_dir="$SKILLS_DIR/$skill" - local bin_dir="$skill_dir/bin" - - mkdir -p "$bin_dir" - - # Download skill file - info "Installing skill: $skill" - if command -v curl &> /dev/null; then - curl -fsSL "$base_url/$skill/SKILL.md" -o "$skill_dir/SKILL.md" - else - wget -q "$base_url/$skill/SKILL.md" -O "$skill_dir/SKILL.md" - fi - - # Remove existing binary/symlink - rm -f "$bin_dir/$binary_name" - - # Try symlink first (saves disk space), fall back to copy - if [[ "$platform" != windows-* ]] && ln -s "$binary_path" "$bin_dir/$binary_name" 2>/dev/null; then - info " Linked to $binary_path" - else - # Copy on Windows or if symlink fails - cp "$binary_path" "$bin_dir/$binary_name" - chmod +x "$bin_dir/$binary_name" - info " Copied binary to $bin_dir" - fi - done - - info "Skills installed successfully" -} - # Show PATH instructions show_path_instructions() { local install_dir @@ -209,10 +185,11 @@ verify_installation() { echo " upkeep audit # Security audit" echo " upkeep quality # Quality report" echo "" - echo "Claude Code skills installed:" - echo " /upkeep-deps # Dependency upgrades" - echo " /upkeep-audit # Security fixes" - echo " /upkeep-quality # Quality improvements" + echo "Claude Code skills are distributed separately via the plugin marketplace:" + echo " /plugin marketplace add llbbl/upkeep" + echo " /plugin install upkeep@llbbl-upkeep" + echo "" + echo "Then use /upkeep:audit, /upkeep:deps, /upkeep:quality in Claude Code." } # Main @@ -229,7 +206,6 @@ main() { local binary_path binary_path=$(install_binary) - install_skills "$binary_path" verify_installation "$binary_path" } diff --git a/skills/upkeep-audit/SKILL.md b/skills/audit/SKILL.md similarity index 99% rename from skills/upkeep-audit/SKILL.md rename to skills/audit/SKILL.md index 1e7ca6f..b31e69c 100644 --- a/skills/upkeep-audit/SKILL.md +++ b/skills/audit/SKILL.md @@ -1,11 +1,11 @@ --- -name: upkeep-audit +name: audit version: 0.2.0 description: Security audit with fix recommendations for JS/TS projects allowed-tools: Bash, Read, Grep, Glob, Edit --- -# /upkeep-audit +# /upkeep:audit Security audit with intelligent fix recommendations for JavaScript/TypeScript projects. diff --git a/skills/upkeep-deps/SKILL.md b/skills/deps/SKILL.md similarity index 99% rename from skills/upkeep-deps/SKILL.md rename to skills/deps/SKILL.md index be91828..f33374b 100644 --- a/skills/upkeep-deps/SKILL.md +++ b/skills/deps/SKILL.md @@ -1,11 +1,11 @@ --- -name: upkeep-deps +name: deps version: 0.2.0 description: Upgrade JS/TS dependencies with risk assessment and Dependabot PR integration allowed-tools: Bash, Read, Grep, Glob, Edit --- -# /upkeep-deps +# /upkeep:deps Upgrade JavaScript/TypeScript dependencies with intelligent risk assessment. diff --git a/skills/upkeep-quality/SKILL.md b/skills/quality/SKILL.md similarity index 97% rename from skills/upkeep-quality/SKILL.md rename to skills/quality/SKILL.md index ff04f01..161cf4a 100644 --- a/skills/upkeep-quality/SKILL.md +++ b/skills/quality/SKILL.md @@ -1,11 +1,11 @@ --- -name: upkeep-quality +name: quality version: 0.2.0 description: Generate and improve code quality scores for JS/TS projects allowed-tools: Bash, Read, Grep, Glob, Edit --- -# /upkeep-quality +# /upkeep:quality Generate comprehensive quality reports and actionable improvement recommendations. @@ -114,10 +114,10 @@ For metrics scoring below 70, explain: Many issues can be fixed automatically: **Dependency Freshness:** -- Use `/upkeep-deps` skill to update packages +- Use `/upkeep:deps` skill to update packages **Security:** -- Use `/upkeep-audit` skill to fix vulnerabilities +- Use `/upkeep:audit` skill to fix vulnerabilities **TypeScript Strictness:** - Edit tsconfig.json to enable strict flags diff --git a/tests/scripts/install.test.ts b/tests/scripts/install.test.ts index 4d5d8b3..4743289 100644 --- a/tests/scripts/install.test.ts +++ b/tests/scripts/install.test.ts @@ -43,7 +43,7 @@ describe("install.sh", () => { const result = await runScriptFunction("detect_platform"); expect(result.exitCode).toBe(0); - expect(result.stdout.trim()).toMatch(/^(linux|darwin|windows)-(x64|arm64)$/); + expect(result.stdout.trim()).toMatch(/^(linux|darwin|windows)-(amd64|arm64)$/); }); test("detects darwin on macOS", async () => { @@ -54,36 +54,16 @@ describe("install.sh", () => { } const result = await runScriptFunction("detect_platform"); - expect(result.stdout.trim()).toMatch(/^darwin-(x64|arm64)$/); + expect(result.stdout.trim()).toMatch(/^darwin-(amd64|arm64)$/); }); }); - describe("get_download_url()", () => { - test("generates latest URL correctly", async () => { - const result = await runScriptFunction('get_download_url "darwin-arm64" "latest"'); + describe("resolve_version()", () => { + test("passes through an explicit version unchanged", async () => { + const result = await runScriptFunction('resolve_version "v1.2.3"'); expect(result.exitCode).toBe(0); - expect(result.stdout.trim()).toBe( - "https://github.com/llbbl/upkeep/releases/latest/download/upkeep-darwin-arm64" - ); - }); - - test("generates versioned URL correctly", async () => { - const result = await runScriptFunction('get_download_url "linux-x64" "v1.0.0"'); - - expect(result.exitCode).toBe(0); - expect(result.stdout.trim()).toBe( - "https://github.com/llbbl/upkeep/releases/download/v1.0.0/upkeep-linux-x64" - ); - }); - - test("generates Windows URL correctly", async () => { - const result = await runScriptFunction('get_download_url "windows-x64" "latest"'); - - expect(result.exitCode).toBe(0); - expect(result.stdout.trim()).toBe( - "https://github.com/llbbl/upkeep/releases/latest/download/upkeep-windows-x64" - ); + expect(result.stdout.trim()).toBe("v1.2.3"); }); }); @@ -153,22 +133,6 @@ describe("install.sh", () => { expect(result.exitCode).toBe(0); expect(result.stdout.trim()).toBe("v1.2.3"); }); - - test("CLAUDE_SKILLS_DIR defaults to ~/.claude/skills", async () => { - const result = await runScriptFunction('echo "$SKILLS_DIR"'); - - expect(result.exitCode).toBe(0); - expect(result.stdout.trim()).toBe(`${process.env.HOME}/.claude/skills`); - }); - - test("CLAUDE_SKILLS_DIR can be overridden", async () => { - const result = await runScriptFunction('echo "$SKILLS_DIR"', { - CLAUDE_SKILLS_DIR: "/custom/skills", - }); - - expect(result.exitCode).toBe(0); - expect(result.stdout.trim()).toBe("/custom/skills"); - }); }); describe("script syntax", () => { @@ -193,11 +157,10 @@ describe("install.sh", () => { test("has required functions", async () => { const functions = [ "detect_platform", - "get_download_url", + "resolve_version", "get_install_dir", - "download_binary", + "download_file", "install_binary", - "install_skills", "show_path_instructions", "verify_installation", "main", @@ -241,7 +204,7 @@ describe("install.sh", () => { const result = await runBash(script); expect(result.exitCode).toBe(0); - expect(result.stdout.trim()).toBe("linux-x64"); + expect(result.stdout.trim()).toBe("linux-amd64"); }); test("handles amd64 architecture", async () => { @@ -258,7 +221,7 @@ describe("install.sh", () => { const result = await runBash(script); expect(result.exitCode).toBe(0); - expect(result.stdout.trim()).toBe("linux-x64"); + expect(result.stdout.trim()).toBe("linux-amd64"); }); test("handles arm64 architecture", async () => { @@ -300,7 +263,7 @@ describe("install.sh", () => { test("prefers curl over wget", async () => { const script = ` source "${SCRIPT_PATH}" - type download_binary | grep -q 'curl' + type download_file | grep -q 'curl' `; const result = await runBash(script); @@ -310,7 +273,7 @@ describe("install.sh", () => { test("falls back to wget when curl unavailable", async () => { const script = ` source "${SCRIPT_PATH}" - type download_binary | grep -q 'wget' + type download_file | grep -q 'wget' `; const result = await runBash(script); @@ -318,17 +281,21 @@ describe("install.sh", () => { }); }); - describe("skill names", () => { - test("installs correct skills", async () => { + describe("binary-only installer", () => { + test("does not install skills (no install_skills function)", async () => { + const result = await runScriptFunction("type install_skills || true"); + + expect(result.stdout).not.toContain("install_skills is a function"); + }); + + test("points users at the plugin marketplace for skills", async () => { const script = ` source "${SCRIPT_PATH}" - type install_skills | grep -o 'upkeep-deps\\|upkeep-audit\\|upkeep-quality' | sort -u + type verify_installation | grep -q 'plugin install upkeep@llbbl-upkeep' `; const result = await runBash(script); - expect(result.stdout).toContain("upkeep-deps"); - expect(result.stdout).toContain("upkeep-audit"); - expect(result.stdout).toContain("upkeep-quality"); + expect(result.exitCode).toBe(0); }); });