|
| 1 | +#!/usr/bin/env bash |
| 2 | +# Build the claude.ai desktop-uploadable plugin zip. |
| 3 | +# |
| 4 | +# The Claude desktop app (Cowork / "local agent mode") installs plugins from a |
| 5 | +# claude.ai marketplace, and a marketplace can only sync from git. claude-smart's |
| 6 | +# git repo is deliberately non-installable (npm-only): the vendored Reflexio |
| 7 | +# runtime (plugin/vendor/reflexio) and the marketplace manifest are gitignored and |
| 8 | +# generated only at pack time. So the desktop app cannot track the npm release |
| 9 | +# automatically — you update it by uploading a built plugin bundle via |
| 10 | +# Settings -> Customize -> Plugins -> Add -> Upload plugin. |
| 11 | +# |
| 12 | +# This script produces that bundle from the npm tarball's vetted file set, so the |
| 13 | +# zip always matches what npm ships (no .venv/node_modules/.next-cache leakage). |
| 14 | +# |
| 15 | +# Usage: |
| 16 | +# scripts/build-desktop-plugin.sh # build a fresh tarball, then zip |
| 17 | +# scripts/build-desktop-plugin.sh --skip-build # reuse the newest existing tarball |
| 18 | +# scripts/build-desktop-plugin.sh --output PATH |
| 19 | +# |
| 20 | +# Prints the absolute path of the resulting zip on stdout; all logs go to stderr. |
| 21 | + |
| 22 | +set -euo pipefail |
| 23 | + |
| 24 | +HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" |
| 25 | +REPO_ROOT="$(cd "$HERE/.." && pwd)" |
| 26 | + |
| 27 | +log() { printf '[build-desktop-plugin] %s\n' "$*" >&2; } |
| 28 | +die() { printf '[build-desktop-plugin] error: %s\n' "$*" >&2; exit 1; } |
| 29 | + |
| 30 | +usage() { sed -n '15,20p' "$0" >&2; } |
| 31 | + |
| 32 | +SKIP_BUILD=0 |
| 33 | +OUTPUT="" |
| 34 | +while [ $# -gt 0 ]; do |
| 35 | + case "$1" in |
| 36 | + --skip-build) SKIP_BUILD=1 ;; |
| 37 | + --output) OUTPUT="${2:-}"; shift ;; |
| 38 | + --output=*) OUTPUT="${1#--output=}" ;; |
| 39 | + -h|--help) usage; exit 0 ;; |
| 40 | + *) die "unknown argument: $1 (try --help)" ;; |
| 41 | + esac |
| 42 | + shift |
| 43 | +done |
| 44 | + |
| 45 | +command -v zip >/dev/null 2>&1 || die "zip is required but not found on PATH" |
| 46 | +command -v node >/dev/null 2>&1 || die "node is required but not found on PATH" |
| 47 | + |
| 48 | +cd "$REPO_ROOT" |
| 49 | + |
| 50 | +VERSION="$(node -p "require('./package.json').version")" |
| 51 | +[ -n "$VERSION" ] || die "could not read version from package.json" |
| 52 | + |
| 53 | +if [ "$SKIP_BUILD" -eq 0 ]; then |
| 54 | + log "building npm tarball (make package)..." |
| 55 | + make package >&2 |
| 56 | +fi |
| 57 | + |
| 58 | +TARBALL="$(ls -t claude-smart-*.tgz 2>/dev/null | head -1 || true)" |
| 59 | +[ -n "$TARBALL" ] || die "no claude-smart-*.tgz found; run without --skip-build to build one" |
| 60 | +log "using tarball: $TARBALL" |
| 61 | + |
| 62 | +WORK="$(mktemp -d "${TMPDIR:-/tmp}/claude-smart-desktop.XXXXXX")" |
| 63 | +trap 'rm -rf "$WORK"' EXIT |
| 64 | +tar xf "$TARBALL" -C "$WORK" |
| 65 | + |
| 66 | +PLUGIN_DIR="$WORK/package/plugin" |
| 67 | +[ -f "$PLUGIN_DIR/.claude-plugin/plugin.json" ] \ |
| 68 | + || die "tarball is missing plugin/.claude-plugin/plugin.json" |
| 69 | +# Stray test artifact that occasionally rides along in the pack; not part of the plugin. |
| 70 | +rm -f "$PLUGIN_DIR/.coverage" |
| 71 | + |
| 72 | +OUT="${OUTPUT:-$REPO_ROOT/dist/claude-smart-desktop-$VERSION.zip}" |
| 73 | +mkdir -p "$(dirname "$OUT")" |
| 74 | +rm -f "$OUT" |
| 75 | +# Zip the CONTENTS of plugin/ so .claude-plugin/plugin.json sits at the archive |
| 76 | +# root, which is where Claude looks for the plugin manifest. |
| 77 | +( cd "$PLUGIN_DIR" && zip -rq "$OUT" . ) |
| 78 | + |
| 79 | +# Guard against the exact failure this script exists to prevent: shipping the |
| 80 | +# machine-local runtime caches instead of the vendored release bundle. Capture the |
| 81 | +# listing first — piping unzip straight into `grep -q` trips `set -o pipefail`, |
| 82 | +# because grep exits on the first match and SIGPIPEs unzip. |
| 83 | +listing="$(unzip -l "$OUT")" |
| 84 | +grep -q '\.claude-plugin/plugin\.json' <<<"$listing" \ |
| 85 | + || die "built zip is missing the plugin manifest at its root" |
| 86 | +if grep -Eq '(^|/)(\.venv|node_modules)/' <<<"$listing"; then |
| 87 | + die "built zip contains runtime caches (.venv/node_modules) — aborting" |
| 88 | +fi |
| 89 | + |
| 90 | +SIZE="$(du -h "$OUT" | cut -f1 | tr -d ' ')" |
| 91 | +{ |
| 92 | + printf '\n' |
| 93 | + printf '✓ built %s (%s)\n\n' "$OUT" "$SIZE" |
| 94 | + printf 'Upload to the Claude desktop app:\n' |
| 95 | + printf ' 1. claude.ai -> Settings -> Customize -> Plugins -> Add -> Upload plugin\n' |
| 96 | + printf ' 2. Drop %s -> Upload\n' "$(basename "$OUT")" |
| 97 | + printf ' 3. Uninstall any older "Claude smart" plugin, then restart the app\n' |
| 98 | +} >&2 |
| 99 | + |
| 100 | +echo "$OUT" |
0 commit comments