chore: import upstream snapshot with attribution
CI / typecheck · build · test · bundle-size (push) Failing after 1s
CI / clean-room dependency closure smoke (push) Failing after 1s
CI / server-runtime e2e (docker · pg + valkey) (push) Failing after 2s
Deploy Install Scripts / deploy (push) Failing after 2s
Windows / build (push) Has been cancelled
CI / typecheck · build · test · bundle-size (push) Failing after 1s
CI / clean-room dependency closure smoke (push) Failing after 1s
CI / server-runtime e2e (docker · pg + valkey) (push) Failing after 2s
Deploy Install Scripts / deploy (push) Failing after 2s
Windows / build (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,193 @@
|
||||
#!/usr/bin/env node
|
||||
import { spawnSync } from 'child_process';
|
||||
import { existsSync, readFileSync, rmSync } from 'fs';
|
||||
import { homedir } from 'os';
|
||||
import { join, dirname } from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
const IS_WINDOWS = process.platform === 'win32';
|
||||
const VERSION_CHECK_LOG_PREFIX = '[version-check]';
|
||||
const BUN_INSTALL_ARGS = Object.freeze(['install', '--production']);
|
||||
const BUN_INSTALL_TIMEOUT_MS = 120_000;
|
||||
const NODE_MODULES_DIRNAME = 'node_modules';
|
||||
|
||||
function findBun() {
|
||||
const pathCheck = IS_WINDOWS
|
||||
? spawnSync('where', ['bun'], { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'], windowsHide: true })
|
||||
: spawnSync('which', ['bun'], { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] });
|
||||
|
||||
if (pathCheck.status === 0 && pathCheck.stdout.trim()) {
|
||||
if (IS_WINDOWS) {
|
||||
const bunCmdPath = pathCheck.stdout.split('\n').find((line) => line.trim().endsWith('bun.cmd'));
|
||||
if (bunCmdPath) return bunCmdPath.trim();
|
||||
}
|
||||
return 'bun';
|
||||
}
|
||||
|
||||
const bunPaths = IS_WINDOWS
|
||||
? [join(homedir(), '.bun', 'bin', 'bun.exe')]
|
||||
: [
|
||||
join(homedir(), '.bun', 'bin', 'bun'),
|
||||
'/usr/local/bin/bun',
|
||||
'/opt/homebrew/bin/bun',
|
||||
'/home/linuxbrew/.linuxbrew/bin/bun',
|
||||
];
|
||||
|
||||
for (const bunPath of bunPaths) {
|
||||
if (existsSync(bunPath)) return bunPath;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// Setup-phase auto-install of plugin runtime dependencies.
|
||||
//
|
||||
// The plugin marketplace extracts files into ~/.claude/plugins/cache/...
|
||||
// but does not run `bun install`. On fresh installs the worker crashes
|
||||
// with `Cannot find module 'zod/v3'` on the very first hook invocation
|
||||
// (gh #2640, #2637). The previous defense-in-depth fix (gh #2644) ran
|
||||
// the install on the SessionStart / UserPromptSubmit hot path; review
|
||||
// (gh #2649 — YOMXXX) flagged that as the wrong architectural home
|
||||
// because it makes proxy / offline / OOM failures land on the user's
|
||||
// first prompt instead of at install time.
|
||||
//
|
||||
// Running it here at Setup keeps the install off the hot path: Setup
|
||||
// has a 300s timeout (vs 60s for SessionStart), runs once per Claude
|
||||
// Code launch, and is the only standalone hook script — the natural
|
||||
// place to materialise plugin runtime state.
|
||||
function ensurePluginDependencies(pluginRoot) {
|
||||
if (!existsSync(join(pluginRoot, 'package.json'))) return;
|
||||
|
||||
// Guard on node_modules (package-manager marker) rather than a specific
|
||||
// package, so the check stays correct if dependencies are later renamed.
|
||||
if (existsSync(join(pluginRoot, NODE_MODULES_DIRNAME))) return;
|
||||
|
||||
const bunPath = findBun();
|
||||
if (!bunPath) {
|
||||
console.error(`${VERSION_CHECK_LOG_PREFIX} bun not found on PATH; cannot auto-install plugin dependencies`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Progress diagnostic so users understand the (one-time) Setup hang.
|
||||
console.error(`${VERSION_CHECK_LOG_PREFIX} installing plugin dependencies (first run, one-time)...`);
|
||||
|
||||
let result;
|
||||
try {
|
||||
result = spawnSync(bunPath, BUN_INSTALL_ARGS, {
|
||||
cwd: pluginRoot,
|
||||
encoding: 'utf-8',
|
||||
stdio: ['pipe', 'pipe', 'pipe'],
|
||||
timeout: BUN_INSTALL_TIMEOUT_MS,
|
||||
windowsHide: true,
|
||||
});
|
||||
} catch (err) {
|
||||
const reason = err && err.message ? err.message : String(err);
|
||||
console.error(`${VERSION_CHECK_LOG_PREFIX} bun install threw (${reason}); worker may crash with missing module errors`);
|
||||
return;
|
||||
}
|
||||
|
||||
// spawnSync does NOT throw on a failed child. Three distinct failure
|
||||
// modes must be surfaced explicitly:
|
||||
// 1. result.error set (ENOENT / ETIMEDOUT / ...)
|
||||
// 2. non-zero exit code
|
||||
// 3. signal-killed (OOM SIGKILL, SIGTERM, ...) where result.status is
|
||||
// null AND result.error is undefined — only result.signal is set.
|
||||
const killedBySignal = result.status === null && !!result.signal;
|
||||
const nonZeroExit = result.status !== null && result.status !== 0;
|
||||
if (result.error || nonZeroExit || killedBySignal) {
|
||||
let reason;
|
||||
if (result.error) {
|
||||
reason = result.error.message;
|
||||
} else if (killedBySignal) {
|
||||
reason = `killed by ${result.signal}`;
|
||||
} else {
|
||||
reason = `exit ${result.status}`;
|
||||
}
|
||||
console.error(`${VERSION_CHECK_LOG_PREFIX} bun install failed (${reason}); worker may crash with missing module errors`);
|
||||
// `bun install` often creates `node_modules/` BEFORE the failure point
|
||||
// (network timeout mid-fetch, OOM kill, registry 5xx after partial
|
||||
// resolution). The existence guard above would then permanently skip
|
||||
// retry on every subsequent Setup run, leaving the plugin broken with
|
||||
// no recovery path short of manual `rm -rf node_modules`. Remove the
|
||||
// partial dir so the next Setup invocation can retry automatically
|
||||
// (gh #2650 review).
|
||||
try {
|
||||
rmSync(join(pluginRoot, NODE_MODULES_DIRNAME), { recursive: true, force: true });
|
||||
} catch (rmErr) {
|
||||
const rmReason = rmErr && rmErr.message ? rmErr.message : String(rmErr);
|
||||
console.error(`${VERSION_CHECK_LOG_PREFIX} failed to clean up partial node_modules (${rmReason}); next Setup run may skip retry`);
|
||||
}
|
||||
} else {
|
||||
// Close the diagnostic loop: a Setup hook that can block for up to
|
||||
// 120s needs an explicit completion line so users can distinguish a
|
||||
// hung install from one that finished silently (gh #2650 review).
|
||||
console.error(`${VERSION_CHECK_LOG_PREFIX} plugin dependencies installed successfully`);
|
||||
}
|
||||
}
|
||||
|
||||
function resolveRoot() {
|
||||
if (process.env.CLAUDE_PLUGIN_ROOT) {
|
||||
const root = process.env.CLAUDE_PLUGIN_ROOT;
|
||||
if (existsSync(join(root, 'package.json'))) return root;
|
||||
}
|
||||
try {
|
||||
const scriptDir = dirname(fileURLToPath(import.meta.url));
|
||||
const candidate = dirname(scriptDir);
|
||||
if (existsSync(join(candidate, 'package.json'))) return candidate;
|
||||
} catch {}
|
||||
return null;
|
||||
}
|
||||
|
||||
const ROOT = resolveRoot();
|
||||
if (!ROOT) process.exit(0);
|
||||
|
||||
ensurePluginDependencies(ROOT);
|
||||
|
||||
function emitUpgradeHint(message) {
|
||||
if (process.env.CLAUDE_MEM_CODEX_HOOK === '1') {
|
||||
console.log(JSON.stringify({
|
||||
hookSpecificOutput: {
|
||||
hookEventName: 'SessionStart',
|
||||
additionalContext: message,
|
||||
},
|
||||
}));
|
||||
} else {
|
||||
console.error(message);
|
||||
}
|
||||
}
|
||||
|
||||
const LEGACY_VERSION_MARKER_RE =
|
||||
/^v?\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/;
|
||||
|
||||
function readInstallMarkerVersion(markerPath) {
|
||||
const content = readFileSync(markerPath, 'utf-8');
|
||||
try {
|
||||
const marker = JSON.parse(content);
|
||||
return marker && typeof marker === 'object' && typeof marker.version === 'string'
|
||||
? marker.version
|
||||
: null;
|
||||
} catch {
|
||||
const legacyVersion = content.trim();
|
||||
return LEGACY_VERSION_MARKER_RE.test(legacyVersion)
|
||||
? legacyVersion.replace(/^v/i, '')
|
||||
: null;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const pkg = JSON.parse(readFileSync(join(ROOT, 'package.json'), 'utf-8'));
|
||||
const markerPath = join(ROOT, '.install-version');
|
||||
if (!existsSync(markerPath)) {
|
||||
emitUpgradeHint('claude-mem: runtime not yet set up - run: npx claude-mem@latest install');
|
||||
process.exit(0);
|
||||
}
|
||||
const markerVersion = readInstallMarkerVersion(markerPath);
|
||||
if (!markerVersion) {
|
||||
emitUpgradeHint('claude-mem: install marker unreadable - run: npx claude-mem@latest install');
|
||||
} else if (markerVersion !== pkg.version) {
|
||||
emitUpgradeHint(`claude-mem: upgraded to v${pkg.version} - run: npx claude-mem@latest install`);
|
||||
}
|
||||
} catch {
|
||||
emitUpgradeHint('claude-mem: install marker unreadable - run: npx claude-mem@latest install');
|
||||
}
|
||||
process.exit(0);
|
||||
Reference in New Issue
Block a user