From 086726d4d2aa6f1a5fa813a21a1065aeae380fb8 Mon Sep 17 00:00:00 2001 From: Yi Lu Date: Fri, 31 Jul 2026 12:06:01 -0700 Subject: [PATCH] feat(release): add make package-desktop for claude.ai plugin upload The Claude desktop app installs plugins from a claude.ai marketplace, which can only sync from git. This repo is intentionally non-installable from git (npm-only: the vendored Reflexio runtime and marketplace manifest are gitignored), so the desktop app cannot track npm releases. Add scripts/build-desktop-plugin.sh and a package-desktop make target that build an uploadable plugin zip from the npm tarball's vetted file set, plus a DEVELOPER.md release-flow section documenting the manual upload path. --- DEVELOPER.md | 35 +++++++++++ Makefile | 6 +- scripts/build-desktop-plugin.sh | 100 ++++++++++++++++++++++++++++++++ 3 files changed, 140 insertions(+), 1 deletion(-) create mode 100755 scripts/build-desktop-plugin.sh diff --git a/DEVELOPER.md b/DEVELOPER.md index 3952fe3..9c9e7e8 100644 --- a/DEVELOPER.md +++ b/DEVELOPER.md @@ -390,6 +390,40 @@ restart Claude Code to apply. Codex users rerun `npx claude-smart install --host codex`, then restart Codex after `/plugins` has upgraded the installed plugin. +### Desktop app (claude.ai) — manual plugin upload + +The Claude **desktop app** (Cowork / "local agent mode") installs plugins from a +claude.ai marketplace, and a marketplace can only sync from **git**. This repo is +deliberately non-installable from git — the vendored Reflexio runtime +(`plugin/vendor/reflexio`) and the marketplace manifest are gitignored and produced +only at pack time (see [Why the marketplace entry is generated, not +committed](#why-the-marketplace-entry-is-generated-not-committed)). So the desktop +app **cannot track the npm release automatically**; it has its own plugin store, +separate from the CLI (`npx claude-smart`) and from npm. Update it by uploading a +built plugin bundle: + +```bash +# Builds a fresh npm tarball, then zips its plugin payload for upload. +make package-desktop +# → dist/claude-smart-desktop-.zip +``` + +`make package-desktop` derives the zip from the npm tarball's file set, so it +matches what npm ships (no `.venv` / `node_modules` / build caches). The underlying +`scripts/build-desktop-plugin.sh` also runs standalone (`--skip-build` reuses the +newest existing tarball, `--output PATH` overrides the destination). + +Then, in the Claude desktop app: + +1. **Settings → Customize → Plugins → Add ▾ → Upload plugin** +2. Drop `dist/claude-smart-desktop-.zip` → **Upload** +3. Uninstall any older **Claude smart** plugin (and, under **Directory → Personal**, + `⋯` → **Remove** a stale git-synced `claude-smart` marketplace), then **restart + the desktop app**. + +This step is manual per release — there is no auto-update path for the desktop app +as long as the git repo stays npm-only by design. + ## Pre-release checklist Before running `make release`: @@ -403,6 +437,7 @@ Before running `make release`: - [ ] `python scripts/check-reflexio-lock.py` passes. - [ ] `npm pack --dry-run --json` includes the root wrapper, marketplace metadata, plugin payload, dashboard sources, README, and LICENSE, and excludes `.venv`, `node_modules`, `.next/cache`, and Python caches. - [ ] If you touched `pyproject.toml` dependencies, `uv build` succeeds locally and the wheel's `METADATA` does not carry any local path dependency. +- [ ] If the Claude desktop app needs this release, run `make package-desktop` and re-upload the zip (see [Desktop app (claude.ai) — manual plugin upload](#desktop-app-claudeai--manual-plugin-upload)). ## Common failures and fixes diff --git a/Makefile b/Makefile index 62eab8a..5dc307b 100644 --- a/Makefile +++ b/Makefile @@ -18,7 +18,8 @@ # - uv (for standalone lockfile resolution + the plugin venv) # - git (for the release flow) -.PHONY: help bump release release-npm publish publish-npm publish-dry package vendor-release \ +.PHONY: help bump release release-npm publish publish-npm publish-dry package package-desktop \ + vendor-release \ check-version check-clean check-npm-auth check-reflexio-pin check-reflexio-lock \ check-vendor-reflexio \ check-locked-project-version check-standalone-lock relock unskip-worktree @@ -149,6 +150,9 @@ package: check-locked-project-version check-standalone-lock ## Build the npm tar echo " npx --package=$$abs -- claude-smart install --host codex"; \ echo " npx --package=$$abs -- claude-smart install --host opencode" +package-desktop: package ## Build the claude.ai desktop-uploadable plugin zip (Settings -> Plugins -> Upload plugin) + @bash scripts/build-desktop-plugin.sh --skip-build + publish: publish-npm ## Publish the current version to npm (claude-smart is npm-only) release: ## Alias for release-npm — claude-smart is distributed via npm only diff --git a/scripts/build-desktop-plugin.sh b/scripts/build-desktop-plugin.sh new file mode 100755 index 0000000..360fadf --- /dev/null +++ b/scripts/build-desktop-plugin.sh @@ -0,0 +1,100 @@ +#!/usr/bin/env bash +# Build the claude.ai desktop-uploadable plugin zip. +# +# The Claude desktop app (Cowork / "local agent mode") installs plugins from a +# claude.ai marketplace, and a marketplace can only sync from git. claude-smart's +# git repo is deliberately non-installable (npm-only): the vendored Reflexio +# runtime (plugin/vendor/reflexio) and the marketplace manifest are gitignored and +# generated only at pack time. So the desktop app cannot track the npm release +# automatically — you update it by uploading a built plugin bundle via +# Settings -> Customize -> Plugins -> Add -> Upload plugin. +# +# This script produces that bundle from the npm tarball's vetted file set, so the +# zip always matches what npm ships (no .venv/node_modules/.next-cache leakage). +# +# Usage: +# scripts/build-desktop-plugin.sh # build a fresh tarball, then zip +# scripts/build-desktop-plugin.sh --skip-build # reuse the newest existing tarball +# scripts/build-desktop-plugin.sh --output PATH +# +# Prints the absolute path of the resulting zip on stdout; all logs go to stderr. + +set -euo pipefail + +HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "$HERE/.." && pwd)" + +log() { printf '[build-desktop-plugin] %s\n' "$*" >&2; } +die() { printf '[build-desktop-plugin] error: %s\n' "$*" >&2; exit 1; } + +usage() { sed -n '15,20p' "$0" >&2; } + +SKIP_BUILD=0 +OUTPUT="" +while [ $# -gt 0 ]; do + case "$1" in + --skip-build) SKIP_BUILD=1 ;; + --output) OUTPUT="${2:-}"; shift ;; + --output=*) OUTPUT="${1#--output=}" ;; + -h|--help) usage; exit 0 ;; + *) die "unknown argument: $1 (try --help)" ;; + esac + shift +done + +command -v zip >/dev/null 2>&1 || die "zip is required but not found on PATH" +command -v node >/dev/null 2>&1 || die "node is required but not found on PATH" + +cd "$REPO_ROOT" + +VERSION="$(node -p "require('./package.json').version")" +[ -n "$VERSION" ] || die "could not read version from package.json" + +if [ "$SKIP_BUILD" -eq 0 ]; then + log "building npm tarball (make package)..." + make package >&2 +fi + +TARBALL="$(ls -t claude-smart-*.tgz 2>/dev/null | head -1 || true)" +[ -n "$TARBALL" ] || die "no claude-smart-*.tgz found; run without --skip-build to build one" +log "using tarball: $TARBALL" + +WORK="$(mktemp -d "${TMPDIR:-/tmp}/claude-smart-desktop.XXXXXX")" +trap 'rm -rf "$WORK"' EXIT +tar xf "$TARBALL" -C "$WORK" + +PLUGIN_DIR="$WORK/package/plugin" +[ -f "$PLUGIN_DIR/.claude-plugin/plugin.json" ] \ + || die "tarball is missing plugin/.claude-plugin/plugin.json" +# Stray test artifact that occasionally rides along in the pack; not part of the plugin. +rm -f "$PLUGIN_DIR/.coverage" + +OUT="${OUTPUT:-$REPO_ROOT/dist/claude-smart-desktop-$VERSION.zip}" +mkdir -p "$(dirname "$OUT")" +rm -f "$OUT" +# Zip the CONTENTS of plugin/ so .claude-plugin/plugin.json sits at the archive +# root, which is where Claude looks for the plugin manifest. +( cd "$PLUGIN_DIR" && zip -rq "$OUT" . ) + +# Guard against the exact failure this script exists to prevent: shipping the +# machine-local runtime caches instead of the vendored release bundle. Capture the +# listing first — piping unzip straight into `grep -q` trips `set -o pipefail`, +# because grep exits on the first match and SIGPIPEs unzip. +listing="$(unzip -l "$OUT")" +grep -q '\.claude-plugin/plugin\.json' <<<"$listing" \ + || die "built zip is missing the plugin manifest at its root" +if grep -Eq '(^|/)(\.venv|node_modules)/' <<<"$listing"; then + die "built zip contains runtime caches (.venv/node_modules) — aborting" +fi + +SIZE="$(du -h "$OUT" | cut -f1 | tr -d ' ')" +{ + printf '\n' + printf '✓ built %s (%s)\n\n' "$OUT" "$SIZE" + printf 'Upload to the Claude desktop app:\n' + printf ' 1. claude.ai -> Settings -> Customize -> Plugins -> Add -> Upload plugin\n' + printf ' 2. Drop %s -> Upload\n' "$(basename "$OUT")" + printf ' 3. Uninstall any older "Claude smart" plugin, then restart the app\n' +} >&2 + +echo "$OUT"