Skip to content

Commit 2fe23d0

Browse files
authored
feat(release): add make package-desktop for claude.ai plugin upload (#149)
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.
1 parent f73a8ef commit 2fe23d0

3 files changed

Lines changed: 140 additions & 1 deletion

File tree

DEVELOPER.md

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -390,6 +390,40 @@ restart Claude Code to apply. Codex users rerun
390390
`npx claude-smart install --host codex`, then restart Codex after `/plugins` has
391391
upgraded the installed plugin.
392392

393+
### Desktop app (claude.ai) — manual plugin upload
394+
395+
The Claude **desktop app** (Cowork / "local agent mode") installs plugins from a
396+
claude.ai marketplace, and a marketplace can only sync from **git**. This repo is
397+
deliberately non-installable from git — the vendored Reflexio runtime
398+
(`plugin/vendor/reflexio`) and the marketplace manifest are gitignored and produced
399+
only at pack time (see [Why the marketplace entry is generated, not
400+
committed](#why-the-marketplace-entry-is-generated-not-committed)). So the desktop
401+
app **cannot track the npm release automatically**; it has its own plugin store,
402+
separate from the CLI (`npx claude-smart`) and from npm. Update it by uploading a
403+
built plugin bundle:
404+
405+
```bash
406+
# Builds a fresh npm tarball, then zips its plugin payload for upload.
407+
make package-desktop
408+
# → dist/claude-smart-desktop-<version>.zip
409+
```
410+
411+
`make package-desktop` derives the zip from the npm tarball's file set, so it
412+
matches what npm ships (no `.venv` / `node_modules` / build caches). The underlying
413+
`scripts/build-desktop-plugin.sh` also runs standalone (`--skip-build` reuses the
414+
newest existing tarball, `--output PATH` overrides the destination).
415+
416+
Then, in the Claude desktop app:
417+
418+
1. **Settings → Customize → Plugins → Add ▾ → Upload plugin**
419+
2. Drop `dist/claude-smart-desktop-<version>.zip`**Upload**
420+
3. Uninstall any older **Claude smart** plugin (and, under **Directory → Personal**,
421+
``**Remove** a stale git-synced `claude-smart` marketplace), then **restart
422+
the desktop app**.
423+
424+
This step is manual per release — there is no auto-update path for the desktop app
425+
as long as the git repo stays npm-only by design.
426+
393427
## Pre-release checklist
394428

395429
Before running `make release`:
@@ -403,6 +437,7 @@ Before running `make release`:
403437
- [ ] `python scripts/check-reflexio-lock.py` passes.
404438
- [ ] `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.
405439
- [ ] If you touched `pyproject.toml` dependencies, `uv build` succeeds locally and the wheel's `METADATA` does not carry any local path dependency.
440+
- [ ] 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)).
406441

407442
## Common failures and fixes
408443

Makefile

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,8 @@
1818
# - uv (for standalone lockfile resolution + the plugin venv)
1919
# - git (for the release flow)
2020

21-
.PHONY: help bump release release-npm publish publish-npm publish-dry package vendor-release \
21+
.PHONY: help bump release release-npm publish publish-npm publish-dry package package-desktop \
22+
vendor-release \
2223
check-version check-clean check-npm-auth check-reflexio-pin check-reflexio-lock \
2324
check-vendor-reflexio \
2425
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
149150
echo " npx --package=$$abs -- claude-smart install --host codex"; \
150151
echo " npx --package=$$abs -- claude-smart install --host opencode"
151152

153+
package-desktop: package ## Build the claude.ai desktop-uploadable plugin zip (Settings -> Plugins -> Upload plugin)
154+
@bash scripts/build-desktop-plugin.sh --skip-build
155+
152156
publish: publish-npm ## Publish the current version to npm (claude-smart is npm-only)
153157

154158
release: ## Alias for release-npm — claude-smart is distributed via npm only

scripts/build-desktop-plugin.sh

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
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

Comments
 (0)