Files
oh-my-claudecode/scripts/plugin-setup.mjs
T

259 lines
9.7 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
/**
* Plugin Post-Install Setup
*
* Configures HUD statusline when plugin is installed.
*/
import { existsSync, mkdirSync, writeFileSync, readFileSync, readdirSync, chmodSync } from 'node:fs';
import { execSync } from 'node:child_process';
import { homedir } from 'node:os';
import { join, dirname } from 'node:path';
import { fileURLToPath, pathToFileURL } from 'node:url';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const CLAUDE_DIR = process.env.CLAUDE_CONFIG_DIR || join(homedir(), '.claude');
const HUD_DIR = join(CLAUDE_DIR, 'hud');
const SETTINGS_FILE = join(CLAUDE_DIR, 'settings.json');
console.log('[OMC] Running post-install setup...');
// 1. Create HUD directory
if (!existsSync(HUD_DIR)) {
mkdirSync(HUD_DIR, { recursive: true });
}
// 2. Create HUD wrapper script
const hudScriptPath = join(HUD_DIR, 'omc-hud.mjs').replace(/\\/g, '/');
const hudScript = `#!/usr/bin/env node
/**
* OMC HUD - Statusline Script
* Wrapper that imports from plugin cache or development paths
*/
import { existsSync, readdirSync } from "node:fs";
import { homedir } from "node:os";
import { join } from "node:path";
import { pathToFileURL } from "node:url";
// Semantic version comparison: returns negative if a < b, positive if a > b, 0 if equal
function semverCompare(a, b) {
// Use parseInt to handle pre-release suffixes (e.g. "0-beta" -> 0)
const pa = a.replace(/^v/, "").split(".").map(s => parseInt(s, 10) || 0);
const pb = b.replace(/^v/, "").split(".").map(s => parseInt(s, 10) || 0);
for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
const na = pa[i] || 0;
const nb = pb[i] || 0;
if (na !== nb) return na - nb;
}
// If numeric parts equal, non-pre-release > pre-release
const aHasPre = /-/.test(a);
const bHasPre = /-/.test(b);
if (aHasPre && !bHasPre) return -1;
if (!aHasPre && bHasPre) return 1;
return 0;
}
async function main() {
const home = homedir();
let pluginCacheDir = null;
// 1. Try plugin cache first (marketplace: omc, plugin: oh-my-claudecode)
// Respect CLAUDE_CONFIG_DIR so installs under a custom config dir are found
const configDir = process.env.CLAUDE_CONFIG_DIR || join(home, ".claude");
const pluginCacheBase = join(configDir, "plugins", "cache", "omc", "oh-my-claudecode");
if (existsSync(pluginCacheBase)) {
try {
const versions = readdirSync(pluginCacheBase);
if (versions.length > 0) {
const sortedVersions = versions.sort(semverCompare).reverse();
pluginCacheDir = join(pluginCacheBase, sortedVersions[0]);
// Filter to only versions with built dist/hud/index.js
const builtVersions = sortedVersions.filter(v => {
const hudPath = join(pluginCacheBase, v, "dist/hud/index.js");
return existsSync(hudPath);
});
if (builtVersions.length > 0) {
const latestBuilt = builtVersions[0];
pluginCacheDir = join(pluginCacheBase, latestBuilt);
const pluginPath = join(pluginCacheBase, latestBuilt, "dist/hud/index.js");
await import(pathToFileURL(pluginPath).href);
return;
}
}
} catch { /* continue */ }
}
// 2. Development paths
const devPaths = [
join(home, "Workspace/oh-my-claudecode/dist/hud/index.js"),
join(home, "workspace/oh-my-claudecode/dist/hud/index.js"),
join(home, "Workspace/oh-my-claudecode/dist/hud/index.js"),
join(home, "workspace/oh-my-claudecode/dist/hud/index.js"),
];
for (const devPath of devPaths) {
if (existsSync(devPath)) {
try {
await import(pathToFileURL(devPath).href);
return;
} catch { /* continue */ }
}
}
// 3. Marketplace clone (for marketplace installs without a populated cache)
const marketplaceHudPath = join(configDir, "plugins", "marketplaces", "omc", "dist/hud/index.js");
if (existsSync(marketplaceHudPath)) {
try {
await import(pathToFileURL(marketplaceHudPath).href);
return;
} catch { /* continue */ }
}
// 4. Fallback: provide targeted repair guidance
if (pluginCacheDir && existsSync(pluginCacheDir)) {
const distDir = join(pluginCacheDir, "dist");
if (!existsSync(distDir)) {
console.log(\`[OMC HUD] Plugin installed but not built. Run: cd "\${pluginCacheDir}" && npm install && npm run build\`);
} else {
console.log(\`[OMC HUD] Plugin HUD load failed. Run: cd "\${pluginCacheDir}" && npm install && npm run build\`);
}
} else if (existsSync(pluginCacheBase)) {
console.log("[OMC HUD] Plugin cache found but no versions installed. Run: /oh-my-claudecode:omc-setup");
} else {
console.log("[OMC HUD] Plugin not installed. Run: /oh-my-claudecode:omc-setup");
}
}
main();
`;
writeFileSync(hudScriptPath, hudScript);
try {
chmodSync(hudScriptPath, 0o755);
} catch { /* Windows doesn't need this */ }
console.log('[OMC] Installed HUD wrapper script');
// 3. Configure settings.json
try {
let settings = {};
if (existsSync(SETTINGS_FILE)) {
settings = JSON.parse(readFileSync(SETTINGS_FILE, 'utf-8'));
}
// Use the absolute node binary path so nvm/fnm users don't get
// "node not found" errors in non-interactive shells (issue #892).
const nodeBin = process.execPath || 'node';
settings.statusLine = {
type: 'command',
command: `"${nodeBin}" "${hudScriptPath.replace(/\\/g, "/")}"`
};
writeFileSync(SETTINGS_FILE, JSON.stringify(settings, null, 2));
console.log('[OMC] Configured HUD statusLine in settings.json');
// Persist the node binary path to .omc-config.json for use by find-node.sh
try {
const configPath = join(CLAUDE_DIR, '.omc-config.json');
let omcConfig = {};
if (existsSync(configPath)) {
omcConfig = JSON.parse(readFileSync(configPath, 'utf-8'));
}
if (nodeBin !== 'node') {
omcConfig.nodeBinary = nodeBin;
writeFileSync(configPath, JSON.stringify(omcConfig, null, 2));
console.log(`[OMC] Saved node binary path: ${nodeBin}`);
}
} catch (e) {
console.log('[OMC] Warning: Could not save node binary path (non-fatal):', e.message);
}
} catch (e) {
console.log('[OMC] Warning: Could not configure settings.json:', e.message);
}
// Patch hooks.json to use the absolute node binary path so hooks work on all
// platforms: Windows (no `sh`), nvm/fnm users (node not on PATH in hooks), etc.
//
// The source hooks.json uses shell-expanded `$CLAUDE_PLUGIN_ROOT` path segments
// so bash preserves spaces in Windows profile paths; this step only substitutes
// the real process.execPath so Claude Code always invokes the same Node binary
// that ran this setup script.
//
// Two patterns are handled:
// 1. New format – node "$CLAUDE_PLUGIN_ROOT"/scripts/run.cjs ... (all platforms)
// 2. Old format – sh "${CLAUDE_PLUGIN_ROOT}/scripts/find-node.sh" ... (Windows
// backward-compat: migrates old installs to the new run.cjs chain)
//
// Fixes issues #909, #899, #892, #869.
try {
const hooksJsonPath = join(__dirname, '..', 'hooks', 'hooks.json');
if (existsSync(hooksJsonPath)) {
const data = JSON.parse(readFileSync(hooksJsonPath, 'utf-8'));
let patched = false;
// Pattern 2 (old, Windows backward-compat): sh find-node.sh <target> [args]
const findNodePattern =
/^sh "\$\{CLAUDE_PLUGIN_ROOT\}\/scripts\/find-node\.sh" "\$\{CLAUDE_PLUGIN_ROOT\}\/scripts\/([^"]+)"(.*)$/;
for (const groups of Object.values(data.hooks ?? {})) {
for (const group of groups) {
for (const hook of (group.hooks ?? [])) {
if (typeof hook.command !== 'string') continue;
// New run.cjs format — replace bare `node` with absolute path (all platforms)
if (hook.command.startsWith('node ') && hook.command.includes('/scripts/run.cjs')) {
hook.command = hook.command.replace(/^node\b/, `"${nodeBin}"`);
patched = true;
continue;
}
// Old find-node.sh format — migrate to run.cjs + absolute path (Windows only)
if (process.platform === 'win32') {
const m2 = hook.command.match(findNodePattern);
if (m2) {
hook.command = `"${nodeBin}" "$CLAUDE_PLUGIN_ROOT"/scripts/run.cjs "$CLAUDE_PLUGIN_ROOT"/scripts/${m2[1]}${m2[2]}`;
patched = true;
}
}
}
}
}
if (patched) {
writeFileSync(hooksJsonPath, JSON.stringify(data, null, 2) + '\n');
console.log(`[OMC] Patched hooks.json with absolute node path (${nodeBin}), fixes issues #909, #899, #892`);
}
}
} catch (e) {
console.log('[OMC] Warning: Could not patch hooks.json:', e.message);
}
// 5. Ensure runtime dependencies are installed in the plugin cache directory.
// The npm-published tarball includes only the files listed in "files" (package.json),
// which does NOT include node_modules. When Claude Code extracts the plugin into its
// cache the dependencies are therefore missing, causing ERR_MODULE_NOT_FOUND at runtime.
// We detect this by probing for a known production dependency (commander) and running a
// production-only install when it is absent. --ignore-scripts avoids re-triggering this
// very setup script (and any other lifecycle hooks). Fixes #1113.
const packageDir = join(__dirname, '..');
const commanderCheck = join(packageDir, 'node_modules', 'commander');
if (!existsSync(commanderCheck)) {
console.log('[OMC] Installing runtime dependencies...');
try {
execSync('npm install --omit=dev --ignore-scripts', {
cwd: packageDir,
stdio: 'pipe',
timeout: 60000,
});
console.log('[OMC] Runtime dependencies installed successfully');
} catch (e) {
console.log('[OMC] Warning: Could not install dependencies:', e.message);
}
} else {
console.log('[OMC] Runtime dependencies already present');
}
console.log('[OMC] Setup complete! Restart Claude Code to activate HUD.');