Files
oh-my-claudecode/scripts/plugin-setup.mjs
Yeachan-Heo bf77e6e7f1 fix(installer): quote node/script paths in generated command strings
Wrap executable and script paths in quotes to prevent breakage on
installations where paths contain spaces.

Closes #931

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-24 02:30:33 +00:00

212 lines
7.4 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 { 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();
// 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) {
// Filter to only versions with built dist/hud/index.js
const builtVersions = versions.filter(v => {
const hudPath = join(pluginCacheBase, v, "dist/hud/index.js");
return existsSync(hudPath);
});
if (builtVersions.length > 0) {
const latestBuilt = builtVersions.sort(semverCompare).reverse()[0];
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. Fallback
console.log("[OMC] run /omc-setup to install properly");
}
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 `node run.cjs` as a portable template; this step
// 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 1 (new): node "${CLAUDE_PLUGIN_ROOT}/scripts/run.cjs" <rest>
const runCjsPattern =
/^node ("\$\{CLAUDE_PLUGIN_ROOT\}\/scripts\/run\.cjs".*)$/;
// 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)
const m1 = hook.command.match(runCjsPattern);
if (m1) {
hook.command = `"${nodeBin}" ${m1[1]}`;
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" "${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);
}
console.log('[OMC] Setup complete! Restart Claude Code to activate HUD.');