mirror of
https://github.com/Yeachan-Heo/oh-my-claudecode.git
synced 2026-10-21 18:52:21 -05:00
* fix: use ${CLAUDE_PLUGIN_ROOT} brace form in hook commands
Hook commands referenced $CLAUDE_PLUGIN_ROOT (bare form), which Claude
Code does not substitute on Windows — the variable is left for the shell
to expand and comes back empty, so node tried to load C:\scripts\run.cjs
and SessionStart hooks failed with a cjs/loader:1424 MODULE_NOT_FOUND
error. The braced ${CLAUDE_PLUGIN_ROOT} form is substituted by Claude Code
before the command reaches the shell, matching the pattern used by other
plugins (agentmemory, superpowers). Applies to all 25 hook command lines.
* fix(hooks): complete the braced placeholder migration across the normalizer
The manifest change alone did not hold. scripts/lib/hook-command-normalizer.mjs
emitted the bare $CLAUDE_PLUGIN_ROOT form and treated the braced spelling as
legacy to rewrite away, and both scripts/plugin-setup.mjs and
scripts/repair-plugin-cache.mjs run that normalizer and write hooks.json back to
disk. Any install that ran plugin setup converted the corrected manifest
straight back to the bare form, so the Windows startup failure returned.
plugin-shipping-surface.mjs accepts either spelling, so no drift check caught it.
Why braced is correct rather than merely different: the bare form relies on a
POSIX shell expanding an environment variable. The Windows prefix invokes node
directly with no sh in front of it, so the plugin root never expands there and
node fails loading <drive>:\scripts\run.cjs. Under sh both spellings expand
identically, which makes the braced form a no-op on Unix/macOS and the only
spelling that can work on native Windows.
- normalizer: both prefixes emit braced; input patterns accept bare or braced so
existing caches are repaired rather than skipped; all branches emit braced
- hooks.json: convert the remaining dev-only PostToolUse hook the original
change predated (52 occurrences total, previously 50)
- plugin-setup-deps test: exercise the real normalizer instead of a private copy
of its rules, since that duplicate is what let manifest and patcher drift
- repair-plugin-cache test: expect braced output while keeping bare fixtures as
input, proving legacy installs are upgraded
Refs #4042
* chore(inventory): regenerate inventory graph for #4042
* fix(hooks): migrate the TypeScript Windows patcher to the braced placeholder
src/hooks/setup/index.ts carries a third copy of the hook-command rewriting
rules, independent of scripts/lib/hook-command-normalizer.mjs, and it also
emitted the bare form. Left alone it would rewrite hooks.json back to
$CLAUDE_PLUGIN_ROOT on Windows init and reintroduce the failure.
Its input patterns now accept either spelling so existing caches are repaired,
and output is always braced. Contract tests that asserted the bare spelling are
updated: windows-patch, hooks-command-escaping, preemptive-compaction-hook and
setup-contracts-regression.
Full local suite: the CLAUDE_PLUGIN_ROOT-related failures are gone and the
remaining failures match the dev baseline exactly (10 pre-existing files,
unrelated to this change).
Refs #4042
* chore(inventory): regenerate inventory graph for #4042 windows patcher
* fix(hooks): update workflow-drift-guard Stop registration to the braced placeholder
The Stop-hook drift guard pins the exact hooks.json registration, so it still
asserted the bare spelling and failed on CI after the migration.
Refs #4042
* chore(inventory): regenerate inventory graph for #4042 drift-guard fix
---------
Co-authored-by: gaebal-gajae <clawdbot@users.noreply.github.com>
217 lines
9.1 KiB
JavaScript
217 lines
9.1 KiB
JavaScript
#!/usr/bin/env node
|
||
/**
|
||
* Plugin Post-Install Setup
|
||
*
|
||
* Configures HUD statusline when plugin is installed.
|
||
*/
|
||
|
||
import { existsSync, mkdirSync, writeFileSync, readFileSync, readdirSync, chmodSync, copyFileSync } from 'node:fs';
|
||
import { execSync } from 'node:child_process';
|
||
import { createRequire } from 'node:module';
|
||
import { homedir } from 'node:os';
|
||
import { join, dirname, resolve } from 'node:path';
|
||
import { fileURLToPath, pathToFileURL } from 'node:url';
|
||
import { getClaudeConfigDir } from './lib/config-dir.mjs';
|
||
import { buildHudWrapper } from './lib/hud-wrapper-template.mjs';
|
||
import { hookPrefixForPlatform, normalizeHooksDataForPlatform } from './lib/hook-command-normalizer.mjs';
|
||
|
||
const __filename = fileURLToPath(import.meta.url);
|
||
const __dirname = dirname(__filename);
|
||
const require = createRequire(import.meta.url);
|
||
|
||
const CLAUDE_DIR = getClaudeConfigDir();
|
||
const HUD_DIR = join(CLAUDE_DIR, 'hud');
|
||
const HUD_LIB_DIR = join(HUD_DIR, 'lib');
|
||
const SETTINGS_FILE = join(CLAUDE_DIR, 'settings.json');
|
||
// Store the absolute node binary path so find-node.sh can resolve Node for
|
||
// nvm/fnm users whose non-interactive hook shells do not include node on PATH
|
||
// (issue #892).
|
||
const nodeBin = process.execPath || 'node';
|
||
const isPublishedPluginCache = !existsSync(join(__dirname, '..', '.git'));
|
||
|
||
|
||
console.log('[OMC] Running post-install setup...');
|
||
|
||
|
||
// 1. Create HUD directory
|
||
if (!existsSync(HUD_DIR)) {
|
||
mkdirSync(HUD_DIR, { recursive: true });
|
||
}
|
||
|
||
if (!existsSync(HUD_LIB_DIR)) {
|
||
mkdirSync(HUD_LIB_DIR, { recursive: true });
|
||
}
|
||
copyFileSync(join(__dirname, 'lib', 'config-dir.mjs'), join(HUD_LIB_DIR, 'config-dir.mjs'));
|
||
copyFileSync(join(__dirname, 'lib', 'config-dir.sh'), join(HUD_LIB_DIR, 'config-dir.sh'));
|
||
copyFileSync(join(__dirname, 'find-node.sh'), join(HUD_DIR, 'find-node.sh'));
|
||
copyFileSync(join(__dirname, 'lib', 'hud-cache-wrapper.sh'), join(HUD_DIR, 'omc-hud-cache.sh'));
|
||
try { chmodSync(join(HUD_DIR, 'find-node.sh'), 0o755); } catch { /* Windows doesn't need this */ }
|
||
try { chmodSync(join(HUD_DIR, 'omc-hud-cache.sh'), 0o755); } catch { /* Windows doesn't need this */ }
|
||
|
||
// 2. Create HUD wrapper script
|
||
const hudScriptPath = join(HUD_DIR, 'omc-hud.mjs').replace(/\\/g, '/');
|
||
const hudScript = buildHudWrapper();
|
||
|
||
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'));
|
||
}
|
||
|
||
const statusLineCommand = process.platform === 'win32'
|
||
? `"${nodeBin}" "${hudScriptPath.replace(/\\/g, "/")}"`
|
||
: `sh "${join(HUD_DIR, 'omc-hud-cache.sh').replace(/\\/g, "/")}" "${hudScriptPath.replace(/\\/g, "/")}"`;
|
||
|
||
settings.statusLine = {
|
||
type: 'command',
|
||
command: statusLineCommand
|
||
};
|
||
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 packaged plugin-cache hooks.json to keep plugin-provided hook commands
|
||
// safe for the platform that is installing the plugin cache. Claude Code's
|
||
// plugin loader reads hooks/hooks.json directly, so the source manifest remains
|
||
// native Windows-spawnable (direct node -> run.cjs, no sh/find-node). During
|
||
// setup from a published plugin cache on Unix/macOS, repair the cached manifest
|
||
// back to the find-node.sh bootstrap so nvm/fnm users whose non-interactive hook
|
||
// PATH lacks node keep working.
|
||
//
|
||
// Keep stale cache self-healing for older manifests that used sh/find-node, an
|
||
// accidentally baked absolute node path, or the Windows-safe direct node form.
|
||
//
|
||
// Commands are emitted with the braced "${CLAUDE_PLUGIN_ROOT}" placeholder,
|
||
// which Claude Code substitutes itself. The bare "$CLAUDE_PLUGIN_ROOT" spelling
|
||
// only expands where a POSIX shell runs the command, so it cannot work behind
|
||
// the direct-node Windows prefix (#4042).
|
||
//
|
||
// Patterns handled (both the bare and braced spellings are accepted on input):
|
||
// 1. find-node.sh format – sh "${CLAUDE_PLUGIN_ROOT}"/scripts/find-node.sh ...
|
||
// 2. Legacy find-node.sh format – sh "${CLAUDE_PLUGIN_ROOT}/scripts/find-node.sh" ...
|
||
// 3. Direct run.cjs format from the Windows-safe shipped manifest
|
||
// 4. Absolute run.cjs format from older setup patches/publish mistakes
|
||
//
|
||
// Fixes issues #909, #899, #892, #869, #3121, #4042.
|
||
try {
|
||
const hooksJsonPath = isPublishedPluginCache ? join(__dirname, '..', 'hooks', 'hooks.json') : null;
|
||
if (hooksJsonPath && existsSync(hooksJsonPath)) {
|
||
const data = JSON.parse(readFileSync(hooksJsonPath, 'utf-8'));
|
||
const patched = normalizeHooksDataForPlatform(data);
|
||
|
||
if (patched) {
|
||
writeFileSync(hooksJsonPath, JSON.stringify(data, null, 2) + '\n');
|
||
const platformLabel = hookPrefixForPlatform().startsWith('node ') ? 'direct node run.cjs' : 'find-node.sh run.cjs';
|
||
console.log(`[OMC] Patched hooks.json to use ${platformLabel} hook commands`);
|
||
}
|
||
}
|
||
} 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');
|
||
const betterSqliteBindingCheck = join(
|
||
packageDir,
|
||
'node_modules',
|
||
'better-sqlite3',
|
||
'build',
|
||
'Release',
|
||
'better_sqlite3.node',
|
||
);
|
||
|
||
function probeBetterSqliteBinding() {
|
||
try {
|
||
const loaded = require('better-sqlite3');
|
||
const Database = typeof loaded === 'function' ? loaded : loaded?.default;
|
||
if (typeof Database !== 'function') return false;
|
||
const db = new Database(':memory:');
|
||
db.close();
|
||
return true;
|
||
} catch {
|
||
return false;
|
||
}
|
||
}
|
||
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');
|
||
}
|
||
|
||
// better-sqlite3 has a native install script. The dependency bootstrap above
|
||
// intentionally ignores package lifecycle scripts so it cannot recurse into
|
||
// this setup entry point; rebuild this one native package explicitly instead.
|
||
if (!existsSync(betterSqliteBindingCheck) && isPublishedPluginCache) {
|
||
console.log('[OMC] Building better-sqlite3 native binding...');
|
||
try {
|
||
execSync('npm rebuild better-sqlite3', {
|
||
cwd: packageDir,
|
||
stdio: 'pipe',
|
||
timeout: 60000,
|
||
});
|
||
if (existsSync(betterSqliteBindingCheck)) {
|
||
console.log('[OMC] better-sqlite3 native binding built successfully');
|
||
} else {
|
||
console.log('[OMC] Warning: npm rebuild completed without producing better_sqlite3.node; state mutation will use the file-lock fallback.');
|
||
}
|
||
} catch (e) {
|
||
console.log('[OMC] Warning: Could not build better-sqlite3 native binding:', e.message);
|
||
console.log('[OMC] State mutation will use the file-lock fallback until better-sqlite3 is rebuilt successfully.');
|
||
}
|
||
} else if (!existsSync(betterSqliteBindingCheck)) {
|
||
console.log('[OMC] better-sqlite3 native binding is absent in the repository checkout; runtime state mutation will use the file-lock fallback.');
|
||
}
|
||
|
||
const betterSqliteBindingHealthy = probeBetterSqliteBinding();
|
||
if (existsSync(betterSqliteBindingCheck) && !betterSqliteBindingHealthy) {
|
||
console.log('[OMC] Warning: better_sqlite3.node exists but could not be loaded; runtime state mutation will use the file-lock fallback until the native binding is rebuilt for this Node runtime.');
|
||
}
|
||
|
||
if (betterSqliteBindingHealthy) {
|
||
console.log('[OMC] Setup complete! Restart Claude Code to activate HUD.');
|
||
} else {
|
||
console.log('[OMC] Setup complete with file-lock fallback (better-sqlite3 native binding unavailable). Restart Claude Code to activate HUD.');
|
||
}
|