Files
claude-mem/scripts/sync-marketplace.cjs
T
Alex Newman 56db06811e Add native Codex hooks integration (#2319)
* Add native Codex hooks integration

* Address Codex review feedback

* Use durable Codex marketplace root

* Address Codex file context review feedback

* Harden Codex installer review paths

* Report Codex legacy cleanup failures

* fix: keep MCP manifests in marketplace sync

* fix: bundle zod in MCP server

* fix: warn on Codex legacy cleanup failure

* Fix hook observation readiness timeouts

* Address Codex hook review notes

* Tighten Codex MCP file context matching

* Resolve final Codex review nits

* Add Codex marketplace version guidance

* Reset worker failure counter on API fallback

* Fix Codex cat flag file extraction
2026-05-06 01:55:27 -07:00

224 lines
8.0 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env node
const { execSync } = require('child_process');
const { existsSync, readFileSync } = require('fs');
const path = require('path');
const os = require('os');
const INSTALLED_PATH = path.join(os.homedir(), '.claude', 'plugins', 'marketplaces', 'thedotmack');
const CACHE_BASE_PATH = path.join(os.homedir(), '.claude', 'plugins', 'cache', 'thedotmack', 'claude-mem');
// Reject obviously invalid ports before they reach http.request, which would
// throw with a confusing error like "RangeError: Port should be > 0 and < 65536".
function parseWorkerPort(value) {
const port = Number.parseInt(String(value ?? ''), 10);
return Number.isInteger(port) && port >= 1 && port <= 65535 ? port : null;
}
function getCurrentBranch() {
try {
if (!existsSync(path.join(INSTALLED_PATH, '.git'))) {
return null;
}
return execSync('git rev-parse --abbrev-ref HEAD', {
cwd: INSTALLED_PATH,
encoding: 'utf-8',
stdio: ['pipe', 'pipe', 'pipe']
}).trim();
} catch {
return null;
}
}
function getGitignoreExcludes(basePath) {
const gitignorePath = path.join(basePath, '.gitignore');
if (!existsSync(gitignorePath)) return '';
const syncManagedFiles = new Set([
'.mcp.json',
]);
const lines = readFileSync(gitignorePath, 'utf-8').split('\n');
return lines
.map(line => line.trim())
.filter(line =>
line &&
!line.startsWith('#') &&
!line.startsWith('!') &&
!syncManagedFiles.has(line)
)
.map(pattern => `--exclude=${JSON.stringify(pattern)}`)
.join(' ');
}
const branch = getCurrentBranch();
const isForce = process.argv.includes('--force');
if (branch && branch !== 'main' && !isForce) {
console.log('');
console.log('\x1b[33m%s\x1b[0m', `WARNING: Installed plugin is on beta branch: ${branch}`);
console.log('\x1b[33m%s\x1b[0m', 'Running rsync would overwrite beta code.');
console.log('');
console.log('Options:');
console.log(' 1. Use UI at http://localhost:37777 to update beta');
console.log(' 2. Switch to stable in UI first, then run sync');
console.log(' 3. Force rsync: npm run sync-marketplace:force');
console.log('');
process.exit(1);
}
function getPluginVersion() {
try {
const pluginJsonPath = path.join(__dirname, '..', 'plugin', '.claude-plugin', 'plugin.json');
const pluginJson = JSON.parse(readFileSync(pluginJsonPath, 'utf-8'));
return pluginJson.version;
} catch (error) {
console.error('\x1b[31m%s\x1b[0m', 'Failed to read plugin version:', error.message);
process.exit(1);
}
}
function detectInstalledVersion(buildVersion) {
const dataDir = process.env.CLAUDE_MEM_DATA_DIR || path.join(os.homedir(), '.claude-mem');
const settingsPath = path.join(dataDir, 'settings.json');
let port = parseWorkerPort(process.env.CLAUDE_MEM_WORKER_PORT);
if (!port && existsSync(settingsPath)) {
try {
const s = JSON.parse(readFileSync(settingsPath, 'utf8'));
const settingsPort = parseWorkerPort(s.CLAUDE_MEM_WORKER_PORT);
if (settingsPort) port = settingsPort;
} catch {}
}
if (!port) {
const uid = typeof process.getuid === 'function' ? process.getuid() : 77;
port = 37700 + (uid % 100);
}
let healthBody;
try {
healthBody = execSync(`curl -s --max-time 2 http://127.0.0.1:${port}/api/health`, {
stdio: ['ignore', 'pipe', 'ignore'],
}).toString().trim();
} catch {
return null;
}
if (!healthBody) return null;
let installedVersion;
let installedPath;
try {
const j = JSON.parse(healthBody);
installedVersion = j.version;
installedPath = j.workerPath;
} catch {
return null;
}
if (!installedVersion || installedVersion === buildVersion) return null;
return { installedVersion, installedPath };
}
const installedMismatch = detectInstalledVersion(getPluginVersion());
if (installedMismatch) {
console.log('');
console.log('\x1b[33m%s\x1b[0m', 'Version mismatch detected:');
console.log(` Building: ${getPluginVersion()}`);
console.log(` Installed: ${installedMismatch.installedVersion}`);
if (installedMismatch.installedPath) console.log(` Worker path: ${installedMismatch.installedPath}`);
console.log('');
console.log('Claude Code is pinned to the installed version, so the worker loads from');
console.log(`its cache dir. Mirroring this build into the installed-version cache so the`);
console.log('worker restart picks up new code without a Claude Code session restart.');
console.log('');
console.log('\x1b[36m%s\x1b[0m', `For a formal version bump, run \`claude plugin update thedotmack/claude-mem\``);
console.log('\x1b[36m%s\x1b[0m', `and restart Claude Code so it loads the ${getPluginVersion()} cache dir.`);
console.log('');
}
console.log('Syncing to marketplace...');
try {
const rootDir = path.join(__dirname, '..');
const gitignoreExcludes = getGitignoreExcludes(rootDir);
execSync(
`rsync -av --delete --exclude=.git --exclude=bun.lock --exclude=package-lock.json --exclude=scripts/package.json --exclude=scripts/node_modules ${gitignoreExcludes} ./ ~/.claude/plugins/marketplaces/thedotmack/`,
{ stdio: 'inherit' }
);
console.log('Running bun install in marketplace...');
execSync(
'cd ~/.claude/plugins/marketplaces/thedotmack/ && bun install',
{ stdio: 'inherit' }
);
const version = getPluginVersion();
const CACHE_VERSION_PATH = path.join(CACHE_BASE_PATH, version);
const pluginDir = path.join(rootDir, 'plugin');
const pluginGitignoreExcludes = getGitignoreExcludes(pluginDir);
console.log(`Syncing to cache folder (version ${version})...`);
execSync(
`rsync -av --delete --exclude=.git ${pluginGitignoreExcludes} plugin/ "${CACHE_VERSION_PATH}/"`,
{ stdio: 'inherit' }
);
console.log(`Running bun install in cache folder (version ${version})...`);
execSync(`bun install`, { cwd: CACHE_VERSION_PATH, stdio: 'inherit' });
if (installedMismatch && installedMismatch.installedVersion !== version) {
const INSTALLED_CACHE_PATH = path.join(CACHE_BASE_PATH, installedMismatch.installedVersion);
console.log(`Mirroring to installed-version cache (${installedMismatch.installedVersion}) for hot reload...`);
execSync(
`rsync -av --delete --exclude=.git ${pluginGitignoreExcludes} plugin/ "${INSTALLED_CACHE_PATH}/"`,
{ stdio: 'inherit' }
);
console.log(`Running bun install in installed-version cache (${installedMismatch.installedVersion})...`);
execSync(`bun install`, { cwd: INSTALLED_CACHE_PATH, stdio: 'inherit' });
}
console.log('\x1b[32m%s\x1b[0m', 'Sync complete!');
console.log('\n🔄 Triggering worker restart...');
const http = require('http');
const dataDir = process.env.CLAUDE_MEM_DATA_DIR || path.join(os.homedir(), '.claude-mem');
const settingsPath = path.join(dataDir, 'settings.json');
let settingsPort = null;
if (existsSync(settingsPath)) {
try {
const settings = JSON.parse(readFileSync(settingsPath, 'utf8'));
settingsPort = parseWorkerPort(settings.CLAUDE_MEM_WORKER_PORT);
} catch {
// fall through to env / default
}
}
const uid = typeof process.getuid === 'function' ? process.getuid() : 77;
const defaultPort = 37700 + (uid % 100);
const workerPort =
parseWorkerPort(process.env.CLAUDE_MEM_WORKER_PORT) ??
settingsPort ??
defaultPort;
const req = http.request({
hostname: '127.0.0.1',
port: workerPort,
path: '/api/admin/restart',
method: 'POST',
timeout: 2000
}, (res) => {
if (res.statusCode === 200) {
console.log('\x1b[32m%s\x1b[0m', `✓ Worker restart triggered on port ${workerPort}`);
} else {
console.log('\x1b[33m%s\x1b[0m', ` Worker restart on port ${workerPort} returned status ${res.statusCode}`);
}
});
req.on('error', () => {
console.log('\x1b[33m%s\x1b[0m', ` No worker reachable on port ${workerPort}; the next worker:restart step will start one.`);
});
req.on('timeout', () => {
req.destroy();
console.log('\x1b[33m%s\x1b[0m', ` Worker restart on port ${workerPort} timed out`);
});
req.end();
} catch (error) {
console.error('\x1b[31m%s\x1b[0m', 'Sync failed:', error.message);
process.exit(1);
}