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