@pcircle/memesh 4.8.2 → 4.8.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +2 -1
- package/.codex-plugin/mcp.json +7 -0
- package/.codex-plugin/plugin.json +6 -0
- package/AGENTS.md +18 -8
- package/README.de.md +13 -11
- package/README.md +25 -12
- package/README.zh-TW.md +25 -12
- package/dashboard/dist/index.html +3 -3
- package/dist/core/agent-message-inbox.d.ts +3 -2
- package/dist/core/agent-message-inbox.d.ts.map +1 -1
- package/dist/core/agent-message-inbox.js +30 -6
- package/dist/core/agent-message-inbox.js.map +1 -1
- package/dist/core/agent-messaging.d.ts +7 -0
- package/dist/core/agent-messaging.d.ts.map +1 -1
- package/dist/core/agent-messaging.js +51 -24
- package/dist/core/agent-messaging.js.map +1 -1
- package/dist/core/agent-router.d.ts +27 -20
- package/dist/core/agent-router.d.ts.map +1 -1
- package/dist/core/agent-router.js +114 -43
- package/dist/core/agent-router.js.map +1 -1
- package/dist/core/agent-scope-id.d.ts +11 -0
- package/dist/core/agent-scope-id.d.ts.map +1 -0
- package/dist/core/agent-scope-id.js +40 -0
- package/dist/core/agent-scope-id.js.map +1 -0
- package/dist/core/briefing.d.ts +1 -1
- package/dist/core/briefing.d.ts.map +1 -1
- package/dist/core/briefing.js +25 -7
- package/dist/core/briefing.js.map +1 -1
- package/dist/core/citation-rule.d.ts.map +1 -1
- package/dist/core/citation-rule.js +5 -2
- package/dist/core/citation-rule.js.map +1 -1
- package/dist/core/doctor.d.ts +9 -0
- package/dist/core/doctor.d.ts.map +1 -1
- package/dist/core/doctor.js +461 -24
- package/dist/core/doctor.js.map +1 -1
- package/dist/core/dreamer.d.ts.map +1 -1
- package/dist/core/dreamer.js +3 -1
- package/dist/core/dreamer.js.map +1 -1
- package/dist/core/embedder.d.ts.map +1 -1
- package/dist/core/embedder.js +4 -1
- package/dist/core/embedder.js.map +1 -1
- package/dist/core/install-channel.d.ts +3 -0
- package/dist/core/install-channel.d.ts.map +1 -1
- package/dist/core/install-channel.js +79 -2
- package/dist/core/install-channel.js.map +1 -1
- package/dist/core/install-hooks.d.ts.map +1 -1
- package/dist/core/install-hooks.js +10 -6
- package/dist/core/install-hooks.js.map +1 -1
- package/dist/core/lesson-slug.d.ts.map +1 -1
- package/dist/core/lesson-slug.js +6 -4
- package/dist/core/lesson-slug.js.map +1 -1
- package/dist/core/lifecycle.d.ts.map +1 -1
- package/dist/core/lifecycle.js +8 -5
- package/dist/core/lifecycle.js.map +1 -1
- package/dist/core/llm-client.d.ts.map +1 -1
- package/dist/core/llm-client.js +4 -1
- package/dist/core/llm-client.js.map +1 -1
- package/dist/core/llm-validator.d.ts.map +1 -1
- package/dist/core/llm-validator.js +6 -15
- package/dist/core/llm-validator.js.map +1 -1
- package/dist/core/memory-tool.d.ts.map +1 -1
- package/dist/core/memory-tool.js +3 -1
- package/dist/core/memory-tool.js.map +1 -1
- package/dist/core/ollama-host.d.ts +6 -0
- package/dist/core/ollama-host.d.ts.map +1 -0
- package/dist/core/ollama-host.js +62 -0
- package/dist/core/ollama-host.js.map +1 -0
- package/dist/core/operations.d.ts +1 -1
- package/dist/core/operations.d.ts.map +1 -1
- package/dist/core/operations.js +1 -1
- package/dist/core/operations.js.map +1 -1
- package/dist/core/project-tags.d.ts +2 -0
- package/dist/core/project-tags.d.ts.map +1 -1
- package/dist/core/project-tags.js +29 -1
- package/dist/core/project-tags.js.map +1 -1
- package/dist/core/schema-export.d.ts.map +1 -1
- package/dist/core/schema-export.js +10 -8
- package/dist/core/schema-export.js.map +1 -1
- package/dist/core/version-check.d.ts +1 -0
- package/dist/core/version-check.d.ts.map +1 -1
- package/dist/core/version-check.js +35 -0
- package/dist/core/version-check.js.map +1 -1
- package/dist/core/work-topology.d.ts +3 -0
- package/dist/core/work-topology.d.ts.map +1 -1
- package/dist/core/work-topology.js +24 -2
- package/dist/core/work-topology.js.map +1 -1
- package/dist/db.d.ts.map +1 -1
- package/dist/db.js +5 -2
- package/dist/db.js.map +1 -1
- package/dist/host-adapters/claude-channel.d.ts +0 -58
- package/dist/host-adapters/claude-channel.d.ts.map +1 -1
- package/dist/host-adapters/claude-channel.js +0 -242
- package/dist/host-adapters/claude-channel.js.map +1 -1
- package/dist/host-adapters/codex-cli-queue.d.ts.map +1 -1
- package/dist/host-adapters/codex-cli-queue.js +15 -12
- package/dist/host-adapters/codex-cli-queue.js.map +1 -1
- package/dist/host-runtime/acp.d.ts +2 -0
- package/dist/host-runtime/acp.d.ts.map +1 -1
- package/dist/host-runtime/acp.js +6 -1
- package/dist/host-runtime/acp.js.map +1 -1
- package/dist/host-runtime/claude.d.ts +2 -0
- package/dist/host-runtime/claude.d.ts.map +1 -1
- package/dist/host-runtime/claude.js +12 -13
- package/dist/host-runtime/claude.js.map +1 -1
- package/dist/host-runtime/codex-session.d.ts +3 -1
- package/dist/host-runtime/codex-session.d.ts.map +1 -1
- package/dist/host-runtime/codex-session.js +7 -5
- package/dist/host-runtime/codex-session.js.map +1 -1
- package/dist/host-runtime/codex.d.ts +2 -0
- package/dist/host-runtime/codex.d.ts.map +1 -1
- package/dist/host-runtime/codex.js +6 -1
- package/dist/host-runtime/codex.js.map +1 -1
- package/dist/host-runtime/entry.d.ts +5 -0
- package/dist/host-runtime/entry.d.ts.map +1 -0
- package/dist/host-runtime/entry.js +11 -0
- package/dist/host-runtime/entry.js.map +1 -0
- package/dist/host-runtime/router-client.d.ts +2 -0
- package/dist/host-runtime/router-client.d.ts.map +1 -1
- package/dist/host-runtime/router-client.js.map +1 -1
- package/dist/knowledge-graph.d.ts +0 -1
- package/dist/knowledge-graph.d.ts.map +1 -1
- package/dist/knowledge-graph.js +27 -19
- package/dist/knowledge-graph.js.map +1 -1
- package/dist/skills-manifest.json +40 -25
- package/dist/storage/entity-index.d.ts +4 -0
- package/dist/storage/entity-index.d.ts.map +1 -0
- package/dist/storage/entity-index.js +15 -0
- package/dist/storage/entity-index.js.map +1 -0
- package/dist/storage/fts-index.d.ts.map +1 -1
- package/dist/storage/fts-index.js +5 -0
- package/dist/storage/fts-index.js.map +1 -1
- package/dist/storage/graph-repairs.d.ts +11 -1
- package/dist/storage/graph-repairs.d.ts.map +1 -1
- package/dist/storage/graph-repairs.js +241 -30
- package/dist/storage/graph-repairs.js.map +1 -1
- package/dist/transports/agent-messaging.d.ts +10 -1
- package/dist/transports/agent-messaging.d.ts.map +1 -1
- package/dist/transports/agent-messaging.js +92 -9
- package/dist/transports/agent-messaging.js.map +1 -1
- package/dist/transports/cli/cli.d.ts +4 -0
- package/dist/transports/cli/cli.d.ts.map +1 -1
- package/dist/transports/cli/cli.js +88 -38
- package/dist/transports/cli/cli.js.map +1 -1
- package/dist/transports/mcp/handlers.d.ts +12 -8
- package/dist/transports/mcp/handlers.d.ts.map +1 -1
- package/dist/transports/mcp/handlers.js +23 -11
- package/dist/transports/mcp/handlers.js.map +1 -1
- package/dist/transports/schemas.d.ts +22 -17
- package/dist/transports/schemas.d.ts.map +1 -1
- package/dist/transports/schemas.js +25 -11
- package/dist/transports/schemas.js.map +1 -1
- package/docs/platforms/README.md +2 -2
- package/docs/platforms/agent-messaging.md +246 -36
- package/hooks/hooks.json +10 -0
- package/llms-install.md +29 -6
- package/package.json +13 -5
- package/scripts/hooks/_generated/agent-message-inbox.js +30 -6
- package/scripts/hooks/_generated/citation-rule.js +5 -2
- package/scripts/hooks/_generated/fts-index.js +5 -0
- package/scripts/hooks/_generated/work-topology.js +24 -2
- package/scripts/hooks/_shared.js +52 -4
- package/scripts/hooks/decision-nudge.js +152 -0
- package/scripts/hooks/post-commit.js +11 -0
- package/scripts/hooks/pre-compact.js +12 -4
- package/scripts/hooks/session-start.js +61 -26
- package/scripts/upgrade-plugin.sh +600 -81
- package/skills/memesh/SKILL.md +24 -5
- /package/{.mcp.json → .claude-plugin/mcp.json} +0 -0
|
@@ -5,14 +5,22 @@
|
|
|
5
5
|
# bash scripts/upgrade-plugin.sh
|
|
6
6
|
#
|
|
7
7
|
# What it does:
|
|
8
|
-
# 1. Fast-forwards the marketplace cache
|
|
8
|
+
# 1. Fast-forwards the marketplace cache under
|
|
9
|
+
# ${CLAUDE_CONFIG_DIR:-~/.claude}/plugins/marketplaces/pcircle-memesh
|
|
9
10
|
# against origin.
|
|
10
|
-
# 2. Reads the new version from .claude-plugin/marketplace.json
|
|
11
|
-
#
|
|
11
|
+
# 2. Reads the new version from .claude-plugin/marketplace.json at the exact
|
|
12
|
+
# marketplace commit that will be installed.
|
|
13
|
+
# 3. Stages a new install cache under the same Claude config root at
|
|
14
|
+
# plugins/cache/pcircle-memesh/memesh/<new-version>/,
|
|
15
|
+
# from `git archive` of the exact recorded commit (never the marketplace
|
|
16
|
+
# checkout's working tree, which can hold uncommitted or tampered files).
|
|
12
17
|
# 4. Installs runtime deps inside that cache (npm install --omit=dev).
|
|
13
|
-
# 5. Patches
|
|
14
|
-
# new version + path
|
|
15
|
-
#
|
|
18
|
+
# 5. Patches plugins/installed_plugins.json under that config root to point at the
|
|
19
|
+
# new version + path, rolling the cache swap back if the write fails,
|
|
20
|
+
# the registry changed underneath it, or a signal interrupts the swap.
|
|
21
|
+
# A same-version refresh (marketplace moved, version did not) is staged next
|
|
22
|
+
# to the live cache and swapped in only after npm install succeeded.
|
|
23
|
+
# The whole run holds a lock directory so two invocations cannot race.
|
|
16
24
|
#
|
|
17
25
|
# Why this exists:
|
|
18
26
|
# Claude Code's plugin marketplace pins versions at install time and
|
|
@@ -22,9 +30,67 @@
|
|
|
22
30
|
|
|
23
31
|
set -uo pipefail
|
|
24
32
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
33
|
+
# D9: removes every OTHER stale version directory under $root, not just the
|
|
34
|
+
# one an upgrade just swapped out. Before the atomic-swap rename elsewhere in
|
|
35
|
+
# this script, an interrupted or pre-this-mechanism upgrade could leave a
|
|
36
|
+
# version directory behind with nothing left to remove it later — measured
|
|
37
|
+
# on a real machine: 9 old version directories, 1.2 GB, accumulated with no
|
|
38
|
+
# bound. Only entries whose full name is exactly `<major>.<minor>.<patch>`
|
|
39
|
+
# are touched, so a `.staging-*`/`.previous-*` marker from a genuinely
|
|
40
|
+
# concurrent run (should be impossible under $LOCK_DIR, but this check does
|
|
41
|
+
# not rely on that) or anything else unexpected under $root is left alone.
|
|
42
|
+
# $keep_version is always excluded; an optional $also_keep protects a second
|
|
43
|
+
# name — the registry's OWN recorded install path (however it is spelled),
|
|
44
|
+
# so the noncanonical-path repair in section 2 below can keep leaving that
|
|
45
|
+
# one directory alone for a human to clean up, exactly as it already did
|
|
46
|
+
# before this function existed.
|
|
47
|
+
#
|
|
48
|
+
# Defined this early, and callable on its own, so
|
|
49
|
+
# `tests/upgrade-plugin-cache-sweep.test.ts` can source this file with
|
|
50
|
+
# MEMESH_UPGRADE_PLUGIN_SOURCE_ONLY=1 (below) and call it directly against a
|
|
51
|
+
# throwaway root — testing the shipped function, not a hand-copied
|
|
52
|
+
# reimplementation of it — without running the rest of this script, which
|
|
53
|
+
# talks to a real Claude Code marketplace checkout.
|
|
54
|
+
sweep_stale_cache_versions() {
|
|
55
|
+
local root="$1" keep_version="$2" also_keep="${3:-}" entry name
|
|
56
|
+
[ -d "$root" ] || return 0
|
|
57
|
+
for entry in "$root"/*; do
|
|
58
|
+
[ -d "$entry" ] || continue
|
|
59
|
+
name="$(basename "$entry")"
|
|
60
|
+
[ "$name" = "$keep_version" ] && continue
|
|
61
|
+
[ -n "$also_keep" ] && [ "$name" = "$also_keep" ] && continue
|
|
62
|
+
if printf '%s' "$name" | grep -qE '^[0-9]+\.[0-9]+\.[0-9]+$'; then
|
|
63
|
+
rm -rf "$entry" 2>/dev/null
|
|
64
|
+
if [ -e "$entry" ]; then
|
|
65
|
+
echo "WARNING: could not remove stale cached version at $entry — remove it manually." >&2
|
|
66
|
+
fi
|
|
67
|
+
fi
|
|
68
|
+
done
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
# Test-only escape hatch: source this file with this variable set to load
|
|
72
|
+
# `sweep_stale_cache_versions` (and any other function defined above this
|
|
73
|
+
# guard) without running the rest of the script. `return` exits a sourced
|
|
74
|
+
# file without killing the parent shell; `|| exit 0` is the fallback for the
|
|
75
|
+
# (unsupported, but harmless to guard) case of someone executing the script
|
|
76
|
+
# directly with the variable set.
|
|
77
|
+
if [ "${MEMESH_UPGRADE_PLUGIN_SOURCE_ONLY:-0}" = "1" ]; then
|
|
78
|
+
return 0 2>/dev/null || exit 0
|
|
79
|
+
fi
|
|
80
|
+
|
|
81
|
+
if [ -n "${CLAUDE_CONFIG_DIR:-}" ]; then
|
|
82
|
+
CLAUDE_CONFIG_ROOT="$CLAUDE_CONFIG_DIR"
|
|
83
|
+
elif [ -n "${HOME:-}" ]; then
|
|
84
|
+
CLAUDE_CONFIG_ROOT="$HOME/.claude"
|
|
85
|
+
else
|
|
86
|
+
echo "ERROR: neither CLAUDE_CONFIG_DIR nor HOME is set, so the Claude Code config root cannot be resolved safely." >&2
|
|
87
|
+
echo " Set CLAUDE_CONFIG_DIR to the active Claude Code config directory, then retry." >&2
|
|
88
|
+
exit 1
|
|
89
|
+
fi
|
|
90
|
+
MARKETPLACE_DIR="$CLAUDE_CONFIG_ROOT/plugins/marketplaces/pcircle-memesh"
|
|
91
|
+
INSTALL_REGISTRY="$CLAUDE_CONFIG_ROOT/plugins/installed_plugins.json"
|
|
92
|
+
CACHE_ROOT="$CLAUDE_CONFIG_ROOT/plugins/cache/pcircle-memesh/memesh"
|
|
93
|
+
LOCK_DIR="$CACHE_ROOT.lock"
|
|
28
94
|
|
|
29
95
|
# ─── Pre-flight ────────────────────────────────────────────────────────────
|
|
30
96
|
if [ ! -d "$MARKETPLACE_DIR" ]; then
|
|
@@ -32,6 +98,11 @@ if [ ! -d "$MARKETPLACE_DIR" ]; then
|
|
|
32
98
|
echo " Install the plugin from the Claude Code /plugin UI first." >&2
|
|
33
99
|
exit 1
|
|
34
100
|
fi
|
|
101
|
+
if [ -L "$INSTALL_REGISTRY" ]; then
|
|
102
|
+
echo "ERROR: installed_plugins.json is a symlink at $INSTALL_REGISTRY — refusing to replace a host-owned link." >&2
|
|
103
|
+
echo " Restore a regular Claude Code registry file, then retry." >&2
|
|
104
|
+
exit 1
|
|
105
|
+
fi
|
|
35
106
|
if [ ! -f "$INSTALL_REGISTRY" ]; then
|
|
36
107
|
echo "ERROR: installed_plugins.json not found at $INSTALL_REGISTRY" >&2
|
|
37
108
|
echo " Install the plugin from the Claude Code /plugin UI first." >&2
|
|
@@ -45,134 +116,582 @@ if ! command -v npm >/dev/null 2>&1; then
|
|
|
45
116
|
echo "ERROR: npm is not on PATH (needed to install runtime deps)." >&2
|
|
46
117
|
exit 1
|
|
47
118
|
fi
|
|
48
|
-
if ! command -v
|
|
49
|
-
echo "ERROR:
|
|
119
|
+
if ! command -v git >/dev/null 2>&1; then
|
|
120
|
+
echo "ERROR: git is not on PATH (needed to fetch and archive the marketplace commit)." >&2
|
|
121
|
+
exit 1
|
|
122
|
+
fi
|
|
123
|
+
if ! command -v tar >/dev/null 2>&1; then
|
|
124
|
+
echo "ERROR: tar is not on PATH (needed to unpack the exact marketplace commit)." >&2
|
|
125
|
+
exit 1
|
|
126
|
+
fi
|
|
127
|
+
|
|
128
|
+
# ─── Lock: one upgrade at a time ───────────────────────────────────────────
|
|
129
|
+
# `mkdir` is atomic on POSIX. Refuse immediately instead of waiting on an
|
|
130
|
+
# ownerless directory whose process identity cannot be proven.
|
|
131
|
+
STAGE_PATH=""
|
|
132
|
+
PREVIOUS_PATH=""
|
|
133
|
+
NEW_INSTALL_PATH=""
|
|
134
|
+
HAD_LIVE_CACHE=0
|
|
135
|
+
SWAP_PENDING=0
|
|
136
|
+
LOCK_HELD=0
|
|
137
|
+
cleanup_on_exit() {
|
|
138
|
+
exit_status=$?
|
|
139
|
+
trap - EXIT INT TERM HUP
|
|
140
|
+
if [ "$SWAP_PENDING" = 1 ]; then
|
|
141
|
+
should_rollback=0
|
|
142
|
+
if [ -e "$PREVIOUS_PATH" ] || [ -L "$PREVIOUS_PATH" ]; then
|
|
143
|
+
should_rollback=1
|
|
144
|
+
elif [ "$HAD_LIVE_CACHE" = 0 ] && { [ -e "$NEW_INSTALL_PATH" ] || [ -L "$NEW_INSTALL_PATH" ]; }; then
|
|
145
|
+
should_rollback=1
|
|
146
|
+
fi
|
|
147
|
+
if [ "$should_rollback" = 1 ]; then
|
|
148
|
+
echo "ERROR: upgrade interrupted during the cache swap — restoring the previous state." >&2
|
|
149
|
+
SWAP_PENDING=0
|
|
150
|
+
if ! rollback_swap; then
|
|
151
|
+
echo "ERROR: automatic rollback failed; follow the recovery instructions above." >&2
|
|
152
|
+
fi
|
|
153
|
+
fi
|
|
154
|
+
fi
|
|
155
|
+
[ -n "$STAGE_PATH" ] && rm -rf "$STAGE_PATH"
|
|
156
|
+
[ "$LOCK_HELD" = 1 ] && rmdir "$LOCK_DIR" 2>/dev/null
|
|
157
|
+
exit "$exit_status"
|
|
158
|
+
}
|
|
159
|
+
handle_signal() { exit "$1"; }
|
|
160
|
+
install_signal_traps() {
|
|
161
|
+
trap 'handle_signal 130' INT
|
|
162
|
+
trap 'handle_signal 143' TERM
|
|
163
|
+
trap 'handle_signal 129' HUP
|
|
164
|
+
}
|
|
165
|
+
trap cleanup_on_exit EXIT
|
|
166
|
+
install_signal_traps
|
|
167
|
+
|
|
168
|
+
if ! mkdir "$LOCK_DIR" 2>/dev/null; then
|
|
169
|
+
if [ -d "$LOCK_DIR" ]; then
|
|
170
|
+
echo "ERROR: could not acquire the upgrade lock at $LOCK_DIR — another upgrade may be running." >&2
|
|
171
|
+
echo " If nothing else is actually running (a previous run crashed and left the lock behind): rmdir \"$LOCK_DIR\"" >&2
|
|
172
|
+
else
|
|
173
|
+
echo "ERROR: could not create the upgrade lock at $LOCK_DIR." >&2
|
|
174
|
+
echo " Its parent must exist and be writable: ${LOCK_DIR%/*}" >&2
|
|
175
|
+
fi
|
|
50
176
|
exit 1
|
|
51
177
|
fi
|
|
178
|
+
LOCK_HELD=1
|
|
52
179
|
|
|
53
180
|
# ─── 1. Refresh marketplace cache ─────────────────────────────────────────
|
|
54
181
|
echo "==> Fetching latest from marketplace origin..."
|
|
55
182
|
(
|
|
56
183
|
cd "$MARKETPLACE_DIR" || exit 1
|
|
57
|
-
CURRENT_BRANCH="$(git
|
|
58
|
-
|
|
184
|
+
CURRENT_BRANCH="$(git symbolic-ref --quiet --short HEAD 2>/dev/null)" || {
|
|
185
|
+
echo "ERROR: marketplace cache is on a detached HEAD — refusing to guess which branch to update." >&2
|
|
186
|
+
exit 1
|
|
187
|
+
}
|
|
188
|
+
git fetch --quiet origin "refs/heads/$CURRENT_BRANCH:refs/remotes/origin/$CURRENT_BRANCH" || {
|
|
59
189
|
echo "ERROR: git fetch failed in $MARKETPLACE_DIR" >&2
|
|
60
190
|
exit 1
|
|
61
191
|
}
|
|
62
|
-
git merge --ff-only "origin/$CURRENT_BRANCH" --quiet || {
|
|
63
|
-
|
|
64
|
-
|
|
192
|
+
MERGE_ERROR="$(git merge --ff-only "origin/$CURRENT_BRANCH" --quiet 2>&1)" || {
|
|
193
|
+
AHEAD_COUNT="$(git rev-list --count "origin/$CURRENT_BRANCH..HEAD" 2>/dev/null || echo unknown)"
|
|
194
|
+
if [[ "$AHEAD_COUNT" =~ ^[0-9]+$ ]] && [ "$AHEAD_COUNT" -gt 0 ]; then
|
|
195
|
+
echo "ERROR: marketplace cache has $AHEAD_COUNT local commit(s), so it cannot fast-forward." >&2
|
|
196
|
+
echo " Reconcile those commits with origin/$CURRENT_BRANCH, then retry." >&2
|
|
197
|
+
else
|
|
198
|
+
echo "ERROR: marketplace fast-forward failed; the checkout may be dirty or its history may not match origin/$CURRENT_BRANCH." >&2
|
|
199
|
+
[ -n "$MERGE_ERROR" ] && echo " $MERGE_ERROR" >&2
|
|
200
|
+
echo " Inspect it without discarding work: git -C \"$MARKETPLACE_DIR\" status --short" >&2
|
|
201
|
+
fi
|
|
65
202
|
exit 1
|
|
66
203
|
}
|
|
67
204
|
) || exit 1
|
|
68
205
|
|
|
69
|
-
# ─── 2.
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
206
|
+
# ─── 2. Bind the exact marketplace commit and read its version ────────────
|
|
207
|
+
MARKETPLACE_SHA="$(git -C "$MARKETPLACE_DIR" rev-parse HEAD 2>/dev/null)" || {
|
|
208
|
+
echo "ERROR: could not read the marketplace checkout's commit ($MARKETPLACE_DIR)" >&2
|
|
209
|
+
exit 1
|
|
210
|
+
}
|
|
211
|
+
if ! [[ "$MARKETPLACE_SHA" =~ ^[0-9a-f]{40}$ ]]; then
|
|
212
|
+
echo "ERROR: marketplace HEAD is not a full 40-hex commit: $MARKETPLACE_SHA" >&2
|
|
213
|
+
exit 1
|
|
214
|
+
fi
|
|
215
|
+
|
|
216
|
+
# Read from the same immutable commit that section 3 archives. A dirty tracked
|
|
217
|
+
# marketplace.json must never redirect committed bytes into a different
|
|
218
|
+
# version directory or produce a version/commit pair that never coexisted.
|
|
219
|
+
NEW_VERSION="$(
|
|
220
|
+
git -C "$MARKETPLACE_DIR" show "$MARKETPLACE_SHA:.claude-plugin/marketplace.json" |
|
|
221
|
+
node -e "
|
|
73
222
|
const fs = require('fs');
|
|
74
|
-
const
|
|
75
|
-
const j = JSON.parse(fs.readFileSync(p, 'utf8'));
|
|
223
|
+
const j = JSON.parse(fs.readFileSync(0, 'utf8'));
|
|
76
224
|
const e = (j.plugins || []).find(x => x && x.name === 'memesh');
|
|
77
225
|
if (!e || typeof e.version !== 'string') { process.exit(2); }
|
|
226
|
+
const match = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/.exec(e.version);
|
|
227
|
+
const prerelease = match && match[4] ? match[4].split('.') : [];
|
|
228
|
+
if (!match || prerelease.some(x => /^\d+$/.test(x) && x.length > 1 && x.startsWith('0'))) {
|
|
229
|
+
console.error('ERROR: marketplace.json reported an unsafe version string: ' + e.version);
|
|
230
|
+
process.exit(3);
|
|
231
|
+
}
|
|
78
232
|
process.stdout.write(e.version);
|
|
79
|
-
"
|
|
80
|
-
|
|
233
|
+
"
|
|
234
|
+
)" || {
|
|
235
|
+
echo "ERROR: could not read memesh version from marketplace.json at commit $MARKETPLACE_SHA" >&2
|
|
81
236
|
exit 1
|
|
82
237
|
}
|
|
83
238
|
|
|
84
|
-
if ! [[ "$NEW_VERSION" =~ ^[0-9A-Za-z.+-]+$ ]]; then
|
|
85
|
-
echo "ERROR: marketplace.json reported an unsafe version string: $NEW_VERSION" >&2
|
|
86
|
-
exit 1
|
|
87
|
-
fi
|
|
88
|
-
|
|
89
239
|
echo "==> Target version: $NEW_VERSION"
|
|
240
|
+
NEW_INSTALL_PATH="$CACHE_ROOT/$NEW_VERSION"
|
|
90
241
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
242
|
+
# Which registry entry is THIS install? Claude Code keeps one entry per scope
|
|
243
|
+
# (user / project / local); reading entries[0] on a machine with two of them
|
|
244
|
+
# reports another cache's version and sha, and section 5 would then rewrite
|
|
245
|
+
# the wrong entry. Pick the entry whose installPath sits under this cache
|
|
246
|
+
# root; fall back only to a sole legacy entry with no installPath; refuse when
|
|
247
|
+
# a path points elsewhere or the registry is ambiguous. Resolve authority and
|
|
248
|
+
# capture its index/version/sha and the complete registry digest in this ONE
|
|
249
|
+
# read. The index is used later only after the registry's exact bytes still
|
|
250
|
+
# match this digest, so a concurrent reorder cannot redirect the write.
|
|
251
|
+
ENTRY_SNAPSHOT="$(INSTALL_REGISTRY="$INSTALL_REGISTRY" CACHE_ROOT="$CACHE_ROOT" NEW_VERSION="$NEW_VERSION" node -e "
|
|
252
|
+
const fs = require('fs'), path = require('path'), crypto = require('crypto');
|
|
253
|
+
const registryPath = process.env.INSTALL_REGISTRY;
|
|
254
|
+
let before, fd;
|
|
255
|
+
try {
|
|
256
|
+
before = fs.lstatSync(registryPath, { bigint: true });
|
|
257
|
+
if (!before.isFile() || before.isSymbolicLink()) {
|
|
258
|
+
process.stdout.write('identity-changed');
|
|
259
|
+
process.exit(0);
|
|
260
|
+
}
|
|
261
|
+
fd = fs.openSync(registryPath, fs.constants.O_RDONLY | (fs.constants.O_NOFOLLOW || 0));
|
|
262
|
+
} catch (error) {
|
|
263
|
+
const raceCodes = new Set(['ELOOP', 'ENOENT', 'ENOTDIR']);
|
|
264
|
+
process.stdout.write(raceCodes.has(error && error.code) ? 'identity-changed' : 'unreadable');
|
|
265
|
+
process.exit(0);
|
|
266
|
+
}
|
|
267
|
+
let opened, registryText;
|
|
268
|
+
try {
|
|
269
|
+
opened = fs.fstatSync(fd, { bigint: true });
|
|
270
|
+
registryText = fs.readFileSync(fd, 'utf8');
|
|
271
|
+
} finally {
|
|
272
|
+
fs.closeSync(fd);
|
|
273
|
+
}
|
|
274
|
+
const after = fs.lstatSync(registryPath, { bigint: true });
|
|
275
|
+
const sameFile = (a, b) => a.dev === b.dev && a.ino === b.ino;
|
|
276
|
+
if (!before.isFile() || before.isSymbolicLink() || !sameFile(before, opened) || !sameFile(opened, after)) {
|
|
277
|
+
process.stdout.write('identity-changed');
|
|
278
|
+
process.exit(0);
|
|
279
|
+
}
|
|
280
|
+
const registrySha256 = crypto.createHash('sha256').update(registryText).digest('hex');
|
|
281
|
+
const j = JSON.parse(registryText);
|
|
282
|
+
const raw = j && j.plugins ? j.plugins['memesh@pcircle-memesh'] : undefined;
|
|
283
|
+
const entries = raw === undefined ? [] : raw;
|
|
284
|
+
const isRecord = value => value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
285
|
+
if (!Array.isArray(entries) || entries.some(entry => !isRecord(entry))) {
|
|
286
|
+
process.stdout.write('malformed');
|
|
287
|
+
process.exit(0);
|
|
288
|
+
}
|
|
95
289
|
if (entries.length === 0) { process.stdout.write('none'); process.exit(0); }
|
|
96
|
-
process.
|
|
290
|
+
const root = path.resolve(process.env.CACHE_ROOT) + path.sep;
|
|
291
|
+
const underRoot = entries
|
|
292
|
+
.map((e, i) => i)
|
|
293
|
+
.filter(i => {
|
|
294
|
+
const e = entries[i];
|
|
295
|
+
return typeof e.installPath === 'string'
|
|
296
|
+
&& path.isAbsolute(e.installPath)
|
|
297
|
+
&& (path.resolve(e.installPath) + path.sep).startsWith(root);
|
|
298
|
+
});
|
|
299
|
+
// Exactly one entry lives under this cache root: use it. A sole legacy
|
|
300
|
+
// entry with no usable installPath is also identifiable. Anything else — none
|
|
301
|
+
// matching with several entries present, or more than one matching — is
|
|
302
|
+
// ambiguous; guessing which scope to touch is how the wrong one gets
|
|
303
|
+
// rewritten.
|
|
304
|
+
let index;
|
|
305
|
+
if (underRoot.length === 1) index = underRoot[0];
|
|
306
|
+
const sole = entries.length === 1 ? entries[0] : null;
|
|
307
|
+
const soleHasPath = sole && typeof sole.installPath === 'string' && sole.installPath.length > 0;
|
|
308
|
+
if (underRoot.length === 0 && sole && !soleHasPath) index = 0;
|
|
309
|
+
if (underRoot.length === 0 && soleHasPath) { process.stdout.write('outside-root'); process.exit(0); }
|
|
310
|
+
// Two different, accurate refusals: several entries live under this root
|
|
311
|
+
// (which one is THIS install?) vs. none do and there is more than one
|
|
312
|
+
// entry (nothing to disambiguate by at all). Collapsing both into one
|
|
313
|
+
// 'ambiguous' value produced a message that said 'none of them lives
|
|
314
|
+
// under \$CACHE_ROOT' even when the real problem was the opposite —
|
|
315
|
+
// several of them did.
|
|
316
|
+
if (index === undefined) {
|
|
317
|
+
process.stdout.write(underRoot.length > 1 ? 'ambiguous-multiple' : 'ambiguous-none');
|
|
318
|
+
process.exit(0);
|
|
319
|
+
}
|
|
320
|
+
const entry = entries[index];
|
|
321
|
+
const version = typeof entry.version === 'string' && !/[\\r\\n\\t]/.test(entry.version)
|
|
322
|
+
? entry.version
|
|
323
|
+
: 'unknown';
|
|
324
|
+
const sha = typeof entry.gitCommitSha === 'string' && /^[0-9a-f]{40}$/.test(entry.gitCommitSha)
|
|
325
|
+
? entry.gitCommitSha
|
|
326
|
+
: 'unknown';
|
|
327
|
+
const expectedPath = path.join(path.resolve(process.env.CACHE_ROOT), process.env.NEW_VERSION);
|
|
328
|
+
const installPathState = typeof entry.installPath === 'string'
|
|
329
|
+
&& path.isAbsolute(entry.installPath)
|
|
330
|
+
&& path.resolve(entry.installPath) === expectedPath
|
|
331
|
+
? 'canonical'
|
|
332
|
+
: 'noncanonical';
|
|
333
|
+
// D9's cache sweep must never remove a directory the registry ITSELF
|
|
334
|
+
// still points to, canonical or not — this is the one directory the
|
|
335
|
+
// 'noncanonical … repairing it' path above (section 2) deliberately
|
|
336
|
+
// leaves in place. Reported as a bare basename, and only when that
|
|
337
|
+
// basename resolves back under this same cache root with no '..'
|
|
338
|
+
// segment, so a crafted absolute installPath from a tampered registry
|
|
339
|
+
// cannot smuggle an arbitrary path into the sweep's exclusion list.
|
|
340
|
+
const rootDir = path.resolve(process.env.CACHE_ROOT);
|
|
341
|
+
let recordedBasename = '';
|
|
342
|
+
if (typeof entry.installPath === 'string' && path.isAbsolute(entry.installPath)) {
|
|
343
|
+
const resolved = path.resolve(entry.installPath);
|
|
344
|
+
const base = path.basename(resolved);
|
|
345
|
+
if (path.join(rootDir, base) === resolved && !/[\\r\\n\\t]/.test(base)) {
|
|
346
|
+
recordedBasename = base;
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
process.stdout.write(['selected', index, version, sha, registrySha256, installPathState, String(opened.dev), String(opened.ino), recordedBasename].join('\\t'));
|
|
97
350
|
")" || {
|
|
98
|
-
|
|
99
|
-
# unreadable or malformed installed_plugins.json made CURRENT_VERSION the
|
|
100
|
-
# empty string. That compares unequal to every target, so the script
|
|
101
|
-
# reported an upgrade from "" and carried on — on a registry it had just
|
|
102
|
-
# failed to parse.
|
|
103
|
-
echo "ERROR: could not read the installed memesh version from $INSTALL_REGISTRY" >&2
|
|
351
|
+
echo "ERROR: could not read the installed memesh entries from $INSTALL_REGISTRY" >&2
|
|
104
352
|
exit 1
|
|
105
353
|
}
|
|
354
|
+
IFS=$'\t' read -r ENTRY_STATE ENTRY_INDEX CURRENT_VERSION INSTALLED_SHA ORIGINAL_REGISTRY_SHA256 INSTALL_PATH_STATE ORIGINAL_REGISTRY_DEV ORIGINAL_REGISTRY_INO RECORDED_INSTALL_BASENAME <<< "$ENTRY_SNAPSHOT"
|
|
355
|
+
case "$ENTRY_STATE" in
|
|
356
|
+
identity-changed)
|
|
357
|
+
echo "ERROR: installed_plugins.json changed file identity while this upgrade was reading it — refusing to continue." >&2
|
|
358
|
+
exit 1
|
|
359
|
+
;;
|
|
360
|
+
unreadable)
|
|
361
|
+
echo "ERROR: installed_plugins.json could not be opened safely at $INSTALL_REGISTRY — refusing to continue." >&2
|
|
362
|
+
exit 1
|
|
363
|
+
;;
|
|
364
|
+
ambiguous-multiple)
|
|
365
|
+
echo "ERROR: installed_plugins.json lists several memesh entries under $CACHE_ROOT — refusing to guess which one to upgrade." >&2
|
|
366
|
+
exit 1
|
|
367
|
+
;;
|
|
368
|
+
ambiguous-none)
|
|
369
|
+
echo "ERROR: installed_plugins.json lists several memesh entries and none of them lives under $CACHE_ROOT — refusing to guess which one to upgrade." >&2
|
|
370
|
+
exit 1
|
|
371
|
+
;;
|
|
372
|
+
outside-root)
|
|
373
|
+
echo "ERROR: the only memesh entry in installed_plugins.json lives outside $CACHE_ROOT — refusing to guess which scope to upgrade." >&2
|
|
374
|
+
exit 1
|
|
375
|
+
;;
|
|
376
|
+
malformed)
|
|
377
|
+
echo "ERROR: installed_plugins.json has a malformed memesh@pcircle-memesh entry — refusing to guess which scope to upgrade." >&2
|
|
378
|
+
exit 1
|
|
379
|
+
;;
|
|
380
|
+
none)
|
|
381
|
+
# Refuse before touching the filesystem: section 6 has no entry to update.
|
|
382
|
+
echo "ERROR: installed_plugins.json has no memesh@pcircle-memesh entry — nothing to upgrade in place." >&2
|
|
383
|
+
echo " Reinstall the plugin from the Claude Code /plugin UI." >&2
|
|
384
|
+
exit 1
|
|
385
|
+
;;
|
|
386
|
+
esac
|
|
387
|
+
if [ "$ENTRY_STATE" != "selected" ] \
|
|
388
|
+
|| ! [[ "$ENTRY_INDEX" =~ ^[0-9]+$ ]] \
|
|
389
|
+
|| ! [[ "$ORIGINAL_REGISTRY_SHA256" =~ ^[0-9a-f]{64}$ ]] \
|
|
390
|
+
|| ! [[ "$ORIGINAL_REGISTRY_DEV" =~ ^[0-9]+$ ]] \
|
|
391
|
+
|| ! [[ "$ORIGINAL_REGISTRY_INO" =~ ^[0-9]+$ ]] \
|
|
392
|
+
|| { [ "$INSTALL_PATH_STATE" != "canonical" ] && [ "$INSTALL_PATH_STATE" != "noncanonical" ]; }; then
|
|
393
|
+
echo "ERROR: could not select one installed memesh entry from $INSTALL_REGISTRY" >&2
|
|
394
|
+
exit 1
|
|
395
|
+
fi
|
|
106
396
|
|
|
107
397
|
echo "==> Currently installed: $CURRENT_VERSION"
|
|
108
398
|
|
|
399
|
+
# Same version is NOT "same code". Claude Code keys the plugin cache by
|
|
400
|
+
# version, and so did this script — which is exactly how a machine that
|
|
401
|
+
# auto-updated between the `release: prepare v4.8.2` commit and the two fix
|
|
402
|
+
# PRs that merged under the same version kept serving a 4.8.2 that lacked
|
|
403
|
+
# them, and was told "Already at 4.8.2 — nothing to do." The registry
|
|
404
|
+
# records the commit the cache was staged from (section 5 below); compare
|
|
405
|
+
# that, not the version string. CURRENT_VERSION and INSTALLED_SHA came from
|
|
406
|
+
# the same authority-bound registry snapshot above.
|
|
407
|
+
|
|
109
408
|
if [ "$CURRENT_VERSION" = "$NEW_VERSION" ]; then
|
|
110
|
-
|
|
111
|
-
|
|
409
|
+
if [ "$INSTALLED_SHA" = "$MARKETPLACE_SHA" ] \
|
|
410
|
+
&& [ "$INSTALL_PATH_STATE" = "canonical" ] \
|
|
411
|
+
&& [ -d "$NEW_INSTALL_PATH" ] \
|
|
412
|
+
&& [ ! -L "$NEW_INSTALL_PATH" ]; then
|
|
413
|
+
echo "==> Already at $NEW_VERSION (commit ${MARKETPLACE_SHA:0:8}) — nothing to do."
|
|
414
|
+
exit 0
|
|
415
|
+
fi
|
|
416
|
+
if [ "$INSTALLED_SHA" != "$MARKETPLACE_SHA" ]; then
|
|
417
|
+
echo "==> $NEW_VERSION is installed, but the plugin cache was built from commit ${INSTALLED_SHA:0:8}"
|
|
418
|
+
echo " and the marketplace is at ${MARKETPLACE_SHA:0:8} — refreshing the cache in place."
|
|
419
|
+
elif [ "$INSTALL_PATH_STATE" != "canonical" ]; then
|
|
420
|
+
echo "==> $NEW_VERSION is recorded at a noncanonical cache path — repairing it at $NEW_INSTALL_PATH."
|
|
421
|
+
else
|
|
422
|
+
echo "==> $NEW_VERSION is recorded at $NEW_INSTALL_PATH, but that path is missing or not a real directory — repairing it."
|
|
423
|
+
fi
|
|
112
424
|
fi
|
|
113
425
|
|
|
114
|
-
# ─── 3. Stage
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
426
|
+
# ─── 3. Stage into a sibling, never into the live cache ───────────────────
|
|
427
|
+
# A same-version refresh targets the directory Claude Code is running from.
|
|
428
|
+
# Staging straight into it and then a failed npm install would leave new
|
|
429
|
+
# code with old or missing dependencies and the old registry sha — a broken
|
|
430
|
+
# install that the next restart loads. So: build the whole thing next to the
|
|
431
|
+
# live cache, and only swap once every step has succeeded.
|
|
432
|
+
STAGE_PATH="$CACHE_ROOT/.staging-$NEW_VERSION-$$"
|
|
433
|
+
PREVIOUS_PATH="$CACHE_ROOT/.previous-$NEW_VERSION-$$"
|
|
434
|
+
|
|
435
|
+
# Remove the failed candidate before restoring the previous cache. Never move
|
|
436
|
+
# the previous cache while the failed live path still exists: `mv` would nest
|
|
437
|
+
# it below that directory and make the postcondition look successful.
|
|
438
|
+
rollback_swap() {
|
|
439
|
+
# Cache removal + restoration is one short recovery transaction. A second
|
|
440
|
+
# signal after the failed candidate is removed but before the previous cache
|
|
441
|
+
# is restored must not strand the install with only .previous-* left behind.
|
|
442
|
+
trap '' INT TERM HUP
|
|
443
|
+
rm -rf "$NEW_INSTALL_PATH" 2>/dev/null
|
|
444
|
+
if [ -e "$NEW_INSTALL_PATH" ] || [ -L "$NEW_INSTALL_PATH" ]; then
|
|
445
|
+
echo "ERROR: could not remove the broken cache at $NEW_INSTALL_PATH." >&2
|
|
446
|
+
if [ -e "$PREVIOUS_PATH" ] || [ -L "$PREVIOUS_PATH" ]; then
|
|
447
|
+
echo " The previous cache is still intact at $PREVIOUS_PATH; installed_plugins.json remains unchanged." >&2
|
|
448
|
+
echo " Remove the broken cache, then restore the previous cache manually:" >&2
|
|
449
|
+
echo " rm -rf \"$NEW_INSTALL_PATH\"" >&2
|
|
450
|
+
echo " mv \"$PREVIOUS_PATH\" \"$NEW_INSTALL_PATH\"" >&2
|
|
451
|
+
else
|
|
452
|
+
echo " installed_plugins.json was not updated to point at it, so it is orphaned. Remove it manually:" >&2
|
|
453
|
+
echo " rm -rf \"$NEW_INSTALL_PATH\"" >&2
|
|
454
|
+
fi
|
|
455
|
+
return 1
|
|
456
|
+
fi
|
|
457
|
+
if [ -e "$PREVIOUS_PATH" ] || [ -L "$PREVIOUS_PATH" ]; then
|
|
458
|
+
mv "$PREVIOUS_PATH" "$NEW_INSTALL_PATH" 2>/dev/null
|
|
459
|
+
if { [ -e "$NEW_INSTALL_PATH" ] || [ -L "$NEW_INSTALL_PATH" ]; } \
|
|
460
|
+
&& [ ! -e "$PREVIOUS_PATH" ] \
|
|
461
|
+
&& [ ! -L "$PREVIOUS_PATH" ]; then
|
|
462
|
+
echo "Rollback succeeded: restored the previous cache; installed_plugins.json remains unchanged." >&2
|
|
463
|
+
return 0
|
|
464
|
+
fi
|
|
465
|
+
echo "ERROR: could not restore the previous cache." >&2
|
|
466
|
+
if [ -e "$PREVIOUS_PATH" ] || [ -L "$PREVIOUS_PATH" ]; then
|
|
467
|
+
echo " It is still intact at $PREVIOUS_PATH — move it back manually:" >&2
|
|
468
|
+
echo " mv \"$PREVIOUS_PATH\" \"$NEW_INSTALL_PATH\"" >&2
|
|
469
|
+
else
|
|
470
|
+
echo " It is gone from $PREVIOUS_PATH and $NEW_INSTALL_PATH is also missing. This should not happen — check $CACHE_ROOT by hand." >&2
|
|
471
|
+
fi
|
|
472
|
+
return 1
|
|
473
|
+
fi
|
|
474
|
+
echo "Cleanup succeeded: no failed cache remains; installed_plugins.json remains unchanged." >&2
|
|
475
|
+
return 0
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
echo "==> Staging $NEW_VERSION next to the cache..."
|
|
479
|
+
if ! rm -rf "$STAGE_PATH"; then
|
|
480
|
+
echo "ERROR: could not clear the staging path at $STAGE_PATH — the live cache was not touched." >&2
|
|
481
|
+
exit 1
|
|
482
|
+
fi
|
|
483
|
+
if ! mkdir -p "$STAGE_PATH"; then
|
|
484
|
+
echo "ERROR: could not create the staging directory at $STAGE_PATH — the live cache was not touched." >&2
|
|
485
|
+
exit 1
|
|
486
|
+
fi
|
|
487
|
+
# `git archive`, not `rsync` of the working tree: rsync would copy whatever
|
|
488
|
+
# is sitting in $MARKETPLACE_DIR right now, including files nobody committed
|
|
489
|
+
# — so a machine with a tampered or half-merged marketplace checkout could
|
|
490
|
+
# have those bytes installed while MARKETPLACE_SHA (recorded above, and
|
|
491
|
+
# written into the registry in section 6) truthfully names a clean commit
|
|
492
|
+
# that never contained them. `git archive` extracts exactly the tree at
|
|
493
|
+
# $MARKETPLACE_SHA; nothing untracked or uncommitted can reach the cache.
|
|
494
|
+
# node_modules and .git are never tracked, so they never appear in the
|
|
495
|
+
# archive; tests/benchmarks/docs/plans are tracked (real source) and are
|
|
496
|
+
# removed after extraction to keep the shipped cache the same shape as
|
|
497
|
+
# before.
|
|
498
|
+
git -C "$MARKETPLACE_DIR" archive "$MARKETPLACE_SHA" | tar -x -C "$STAGE_PATH" || {
|
|
499
|
+
echo "ERROR: git archive failed — the live cache at $NEW_INSTALL_PATH was not touched" >&2
|
|
126
500
|
exit 1
|
|
127
501
|
}
|
|
502
|
+
if ! rm -rf "$STAGE_PATH/tests" "$STAGE_PATH/benchmarks" "$STAGE_PATH/docs/plans"; then
|
|
503
|
+
echo "ERROR: could not remove development-only files from the staging copy — the live cache was not touched." >&2
|
|
504
|
+
exit 1
|
|
505
|
+
fi
|
|
128
506
|
|
|
129
|
-
# ─── 4. Install runtime deps
|
|
507
|
+
# ─── 4. Install runtime deps in the staging copy ──────────────────────────
|
|
130
508
|
echo "==> Installing runtime deps (this may take a minute)..."
|
|
131
509
|
(
|
|
132
|
-
cd "$
|
|
510
|
+
cd "$STAGE_PATH" || exit 1
|
|
133
511
|
npm install --omit=dev --no-audit --no-fund --silent
|
|
134
512
|
) || {
|
|
135
|
-
echo "ERROR: npm install failed in $NEW_INSTALL_PATH" >&2
|
|
513
|
+
echo "ERROR: npm install failed in the staging copy — the live cache at $NEW_INSTALL_PATH was not touched" >&2
|
|
514
|
+
exit 1
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
# ─── 5. Swap the staged copy in ───────────────────────────────────────────
|
|
518
|
+
if [ -e "$NEW_INSTALL_PATH" ] || [ -L "$NEW_INSTALL_PATH" ]; then
|
|
519
|
+
HAD_LIVE_CACHE=1
|
|
520
|
+
fi
|
|
521
|
+
SWAP_PENDING=1
|
|
522
|
+
if [ "$HAD_LIVE_CACHE" = 1 ]; then
|
|
523
|
+
mv "$NEW_INSTALL_PATH" "$PREVIOUS_PATH" || {
|
|
524
|
+
SWAP_PENDING=0
|
|
525
|
+
echo "ERROR: could not move the live cache aside — nothing was changed" >&2
|
|
526
|
+
exit 1
|
|
527
|
+
}
|
|
528
|
+
fi
|
|
529
|
+
mv "$STAGE_PATH" "$NEW_INSTALL_PATH" || {
|
|
530
|
+
echo "ERROR: could not move the staged copy into place." >&2
|
|
531
|
+
SWAP_PENDING=0
|
|
532
|
+
rollback_swap
|
|
136
533
|
exit 1
|
|
137
534
|
}
|
|
138
535
|
|
|
139
|
-
# ───
|
|
536
|
+
# ─── 6. Patch installed_plugins.json (last: the registry names a cache that exists) ──
|
|
537
|
+
# Compare the complete registry snapshot captured during authority selection,
|
|
538
|
+
# then write and rename in one Node process. Re-check the snapshot immediately
|
|
539
|
+
# before rename to detect host writes observed up to that point. Ignore signals
|
|
540
|
+
# only for this short registry critical section;
|
|
541
|
+
# before it, the signal trap restores the cache, and after it the registry and
|
|
542
|
+
# live path already agree.
|
|
140
543
|
echo "==> Updating installed_plugins.json..."
|
|
544
|
+
trap '' INT TERM HUP
|
|
141
545
|
NEW_INSTALL_PATH="$NEW_INSTALL_PATH" \
|
|
142
546
|
NEW_VERSION="$NEW_VERSION" \
|
|
143
547
|
INSTALL_REGISTRY="$INSTALL_REGISTRY" \
|
|
144
|
-
|
|
548
|
+
MARKETPLACE_SHA="$MARKETPLACE_SHA" \
|
|
549
|
+
ORIGINAL_REGISTRY_SHA256="$ORIGINAL_REGISTRY_SHA256" \
|
|
550
|
+
ORIGINAL_REGISTRY_DEV="$ORIGINAL_REGISTRY_DEV" \
|
|
551
|
+
ORIGINAL_REGISTRY_INO="$ORIGINAL_REGISTRY_INO" \
|
|
552
|
+
ENTRY_INDEX="$ENTRY_INDEX" \
|
|
145
553
|
node -e "
|
|
146
|
-
const fs = require('fs');
|
|
147
|
-
const
|
|
148
|
-
const
|
|
149
|
-
|
|
150
|
-
|
|
554
|
+
const fs = require('fs'), path = require('path'), crypto = require('crypto');
|
|
555
|
+
const registryPath = process.env.INSTALL_REGISTRY;
|
|
556
|
+
const sha256 = text => crypto.createHash('sha256').update(text).digest('hex');
|
|
557
|
+
const expectedDev = BigInt(process.env.ORIGINAL_REGISTRY_DEV);
|
|
558
|
+
const expectedIno = BigInt(process.env.ORIGINAL_REGISTRY_INO);
|
|
559
|
+
const assertOriginalFile = stat => {
|
|
560
|
+
if (!stat.isFile() || stat.isSymbolicLink() || stat.dev !== expectedDev || stat.ino !== expectedIno) {
|
|
561
|
+
throw new Error('registry file identity changed since this upgrade started');
|
|
562
|
+
}
|
|
563
|
+
};
|
|
564
|
+
const readOriginalFile = () => {
|
|
565
|
+
const before = fs.lstatSync(registryPath, { bigint: true });
|
|
566
|
+
assertOriginalFile(before);
|
|
567
|
+
const fd = fs.openSync(registryPath, fs.constants.O_RDONLY | (fs.constants.O_NOFOLLOW || 0));
|
|
568
|
+
try {
|
|
569
|
+
const opened = fs.fstatSync(fd, { bigint: true });
|
|
570
|
+
assertOriginalFile(opened);
|
|
571
|
+
const text = fs.readFileSync(fd, 'utf8');
|
|
572
|
+
assertOriginalFile(fs.lstatSync(registryPath, { bigint: true }));
|
|
573
|
+
return { text, mode: Number(opened.mode & 0o777n) };
|
|
574
|
+
} finally {
|
|
575
|
+
fs.closeSync(fd);
|
|
576
|
+
}
|
|
577
|
+
};
|
|
578
|
+
const original = readOriginalFile();
|
|
579
|
+
const registryText = original.text;
|
|
580
|
+
if (sha256(registryText) !== process.env.ORIGINAL_REGISTRY_SHA256) {
|
|
581
|
+
console.error('the complete registry changed since this upgrade started');
|
|
582
|
+
process.exit(2);
|
|
583
|
+
}
|
|
584
|
+
const j = JSON.parse(registryText);
|
|
585
|
+
const registryMode = original.mode;
|
|
586
|
+
const entries = j.plugins && j.plugins['memesh@pcircle-memesh'];
|
|
587
|
+
const idx = Number(process.env.ENTRY_INDEX);
|
|
588
|
+
const entry = Array.isArray(entries) ? entries[idx] : undefined;
|
|
589
|
+
if (!Number.isSafeInteger(idx) || idx < 0 || entry === null || typeof entry !== 'object' || Array.isArray(entry)) {
|
|
590
|
+
console.error('the authority-selected registry entry is no longer structurally valid');
|
|
151
591
|
process.exit(2);
|
|
152
592
|
}
|
|
153
|
-
// Read the actual commit sha so future doctor calls match the install.
|
|
154
|
-
let sha = 'unknown';
|
|
155
|
-
try {
|
|
156
|
-
sha = require('child_process')
|
|
157
|
-
.execFileSync('git', ['-C', process.env.MARKETPLACE_DIR, 'rev-parse', 'HEAD'], { encoding: 'utf8' })
|
|
158
|
-
.trim();
|
|
159
|
-
} catch {}
|
|
160
|
-
const entry = j.plugins['memesh@pcircle-memesh'][0] || {};
|
|
161
593
|
entry.installPath = process.env.NEW_INSTALL_PATH;
|
|
162
594
|
entry.version = process.env.NEW_VERSION;
|
|
163
595
|
entry.lastUpdated = new Date().toISOString();
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
596
|
+
// Read once above (MARKETPLACE_SHA); the staleness check compares against it next run.
|
|
597
|
+
entry.gitCommitSha = process.env.MARKETPLACE_SHA;
|
|
598
|
+
entries[idx] = entry;
|
|
599
|
+
const sameFile = (a, b) => a.dev === b.dev && a.ino === b.ino;
|
|
600
|
+
let tmpDir, tmpDirIdentity, tmpPath, tmpFd, tmpIdentity;
|
|
601
|
+
const cleanupTemp = () => {
|
|
602
|
+
if (tmpFd !== undefined) {
|
|
603
|
+
try { fs.closeSync(tmpFd); } catch {}
|
|
604
|
+
tmpFd = undefined;
|
|
605
|
+
}
|
|
606
|
+
if (tmpPath && tmpIdentity) {
|
|
607
|
+
try {
|
|
608
|
+
const named = fs.lstatSync(tmpPath, { bigint: true });
|
|
609
|
+
if (named.isFile() && !named.isSymbolicLink() && sameFile(named, tmpIdentity)) {
|
|
610
|
+
fs.unlinkSync(tmpPath);
|
|
611
|
+
}
|
|
612
|
+
} catch {}
|
|
613
|
+
}
|
|
614
|
+
if (tmpDir && tmpDirIdentity) {
|
|
615
|
+
try {
|
|
616
|
+
const named = fs.lstatSync(tmpDir, { bigint: true });
|
|
617
|
+
if (named.isDirectory() && !named.isSymbolicLink() && sameFile(named, tmpDirIdentity)) {
|
|
618
|
+
fs.rmdirSync(tmpDir);
|
|
619
|
+
}
|
|
620
|
+
} catch {}
|
|
621
|
+
}
|
|
622
|
+
};
|
|
623
|
+
const assertTempDir = () => {
|
|
624
|
+
const named = fs.lstatSync(tmpDir, { bigint: true });
|
|
625
|
+
if (!named.isDirectory() || named.isSymbolicLink() || !sameFile(named, tmpDirIdentity)) {
|
|
626
|
+
throw new Error('registry temporary directory identity changed before the atomic write');
|
|
627
|
+
}
|
|
628
|
+
};
|
|
629
|
+
const assertTempFile = stat => {
|
|
630
|
+
if (!stat.isFile() || stat.isSymbolicLink() || !sameFile(stat, tmpIdentity)) {
|
|
631
|
+
throw new Error('registry temporary file identity changed before the atomic write');
|
|
632
|
+
}
|
|
633
|
+
};
|
|
634
|
+
try {
|
|
635
|
+
const registryDir = path.dirname(registryPath);
|
|
636
|
+
tmpDir = fs.mkdtempSync(path.join(registryDir, '.' + path.basename(registryPath) + '.tmp-'));
|
|
637
|
+
tmpDirIdentity = fs.lstatSync(tmpDir, { bigint: true });
|
|
638
|
+
if (!tmpDirIdentity.isDirectory() || tmpDirIdentity.isSymbolicLink()) {
|
|
639
|
+
throw new Error('registry temporary location is not a real directory');
|
|
640
|
+
}
|
|
641
|
+
tmpPath = path.join(tmpDir, 'registry.json');
|
|
642
|
+
const flags = fs.constants.O_WRONLY | fs.constants.O_CREAT | fs.constants.O_EXCL
|
|
643
|
+
| (fs.constants.O_NOFOLLOW || 0);
|
|
644
|
+
tmpFd = fs.openSync(tmpPath, flags, registryMode);
|
|
645
|
+
tmpIdentity = fs.fstatSync(tmpFd, { bigint: true });
|
|
646
|
+
assertTempDir();
|
|
647
|
+
assertTempFile(fs.lstatSync(tmpPath, { bigint: true }));
|
|
648
|
+
fs.writeFileSync(tmpFd, JSON.stringify(j, null, 4) + '\n', 'utf8');
|
|
649
|
+
fs.fchmodSync(tmpFd, registryMode);
|
|
650
|
+
fs.fsyncSync(tmpFd);
|
|
651
|
+
assertTempFile(fs.fstatSync(tmpFd, { bigint: true }));
|
|
652
|
+
assertTempFile(fs.lstatSync(tmpPath, { bigint: true }));
|
|
653
|
+
fs.closeSync(tmpFd);
|
|
654
|
+
tmpFd = undefined;
|
|
655
|
+
if (sha256(readOriginalFile().text) !== process.env.ORIGINAL_REGISTRY_SHA256) {
|
|
656
|
+
throw new Error('the complete registry changed immediately before the atomic write');
|
|
657
|
+
}
|
|
658
|
+
assertOriginalFile(fs.lstatSync(registryPath, { bigint: true }));
|
|
659
|
+
assertTempDir();
|
|
660
|
+
assertTempFile(fs.lstatSync(tmpPath, { bigint: true }));
|
|
661
|
+
fs.renameSync(tmpPath, registryPath);
|
|
662
|
+
// rename is the commit point. Cleanup after it must never turn an applied
|
|
663
|
+
// registry update into a reported failure and trigger a cache rollback.
|
|
664
|
+
try { fs.rmdirSync(tmpDir); } catch {}
|
|
665
|
+
} catch (err) {
|
|
666
|
+
cleanupTemp();
|
|
667
|
+
throw err;
|
|
668
|
+
}
|
|
669
|
+
"
|
|
670
|
+
REGISTRY_STATUS=$?
|
|
671
|
+
if [ "$REGISTRY_STATUS" = 0 ]; then
|
|
672
|
+
SWAP_PENDING=0
|
|
673
|
+
fi
|
|
674
|
+
install_signal_traps
|
|
675
|
+
if [ "$REGISTRY_STATUS" != 0 ]; then
|
|
676
|
+
echo "ERROR: failed to update installed_plugins.json." >&2
|
|
677
|
+
SWAP_PENDING=0
|
|
678
|
+
rollback_swap
|
|
169
679
|
exit 1
|
|
170
|
-
|
|
680
|
+
fi
|
|
681
|
+
rm -rf "$PREVIOUS_PATH" 2>/dev/null
|
|
682
|
+
if [ -e "$PREVIOUS_PATH" ] || [ -L "$PREVIOUS_PATH" ]; then
|
|
683
|
+
echo "WARNING: upgrade succeeded, but the previous cache could not be removed at $PREVIOUS_PATH." >&2
|
|
684
|
+
echo " Remove it manually when no Claude Code process is using it: rm -rf \"$PREVIOUS_PATH\"" >&2
|
|
685
|
+
fi
|
|
686
|
+
|
|
687
|
+
# D9: sweep every OTHER stale version directory under $CACHE_ROOT, not just
|
|
688
|
+
# the one this run just swapped out — see `sweep_stale_cache_versions`'s own
|
|
689
|
+
# definition near the top of this file for why.
|
|
690
|
+
sweep_stale_cache_versions "$CACHE_ROOT" "$NEW_VERSION" "$RECORDED_INSTALL_BASENAME"
|
|
171
691
|
|
|
172
|
-
# ───
|
|
692
|
+
# ─── 7. Done ─────────────────────────────────────────────────────────────
|
|
173
693
|
echo ""
|
|
174
|
-
echo "✓ MeMesh upgraded: $CURRENT_VERSION -> $NEW_VERSION"
|
|
694
|
+
echo "✓ MeMesh upgraded: $CURRENT_VERSION (${INSTALLED_SHA:0:8}) -> $NEW_VERSION (${MARKETPLACE_SHA:0:8})"
|
|
175
695
|
echo " Install path: $NEW_INSTALL_PATH"
|
|
176
696
|
echo ""
|
|
177
697
|
echo "Next step: restart Claude Code so the new MCP server picks up."
|
|
178
|
-
echo "Old version still on disk at $CACHE_ROOT/$CURRENT_VERSION (safe to delete once verified)."
|