Merge pull request #45 from colbymchenry/fix/silent-failed-install
fix: Stop silent install failures — always run npm install -g, add preuninstall cleanup
This commit is contained in:
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@colbymchenry/codegraph",
|
||||
"version": "0.5.5",
|
||||
"version": "0.6.2",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@colbymchenry/codegraph",
|
||||
"version": "0.5.5",
|
||||
"version": "0.6.2",
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
|
||||
+2
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@colbymchenry/codegraph",
|
||||
"version": "0.5.5",
|
||||
"version": "0.6.2",
|
||||
"description": "Supercharge Claude Code with semantic code intelligence. 30% fewer tokens, 25% fewer tool calls, 100% local.",
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
@@ -15,6 +15,7 @@
|
||||
"scripts": {
|
||||
"build": "tsc && npm run copy-assets",
|
||||
"postinstall": "node scripts/postinstall.js",
|
||||
"preuninstall": "node dist/bin/uninstall.js",
|
||||
"copy-assets": "node -e \"const fs=require('fs');fs.mkdirSync('dist/db',{recursive:true});fs.copyFileSync('src/db/schema.sql','dist/db/schema.sql');fs.mkdirSync('dist/extraction/wasm',{recursive:true});fs.readdirSync('src/extraction/wasm').filter(f=>f.endsWith('.wasm')).forEach(f=>fs.copyFileSync('src/extraction/wasm/'+f,'dist/extraction/wasm/'+f))\"",
|
||||
"dev": "tsc --watch",
|
||||
"cli": "npm run build && node dist/bin/codegraph.js",
|
||||
|
||||
@@ -1015,17 +1015,17 @@ program
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
// Spawn `codegraph sync` as a detached background process
|
||||
// so this hook exits immediately and doesn't block Claude Code
|
||||
const isWindows = process.platform === 'win32';
|
||||
// Spawn sync as a detached background process
|
||||
// so this hook exits immediately and doesn't block Claude Code.
|
||||
// Uses process.argv[0]/[1] (e.g. node /path/to/codegraph.js) so it
|
||||
// works whether invoked via global install, npx, or directly.
|
||||
const child = spawn(
|
||||
isWindows ? 'codegraph' : process.argv[0]!,
|
||||
isWindows ? ['sync', '--quiet', projectRoot!] : [process.argv[1]!, 'sync', '--quiet', projectRoot!],
|
||||
process.argv[0]!,
|
||||
[process.argv[1]!, 'sync', '--quiet', projectRoot!],
|
||||
{
|
||||
detached: true,
|
||||
stdio: 'ignore',
|
||||
windowsHide: true,
|
||||
shell: isWindows,
|
||||
}
|
||||
);
|
||||
child.unref();
|
||||
|
||||
@@ -0,0 +1,151 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* CodeGraph preuninstall cleanup script
|
||||
*
|
||||
* Runs automatically when `npm uninstall -g @colbymchenry/codegraph` is called.
|
||||
* Removes all CodeGraph configuration from Claude Code:
|
||||
* - MCP server entry from ~/.claude.json
|
||||
* - Permissions from ~/.claude/settings.json
|
||||
* - Hooks from ~/.claude/settings.json
|
||||
* - CodeGraph section from ~/.claude/CLAUDE.md
|
||||
*
|
||||
* This script must never throw — a failed cleanup must not block uninstall.
|
||||
*/
|
||||
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import * as os from 'os';
|
||||
|
||||
const CODEGRAPH_SECTION_START = '<!-- CODEGRAPH_START -->';
|
||||
const CODEGRAPH_SECTION_END = '<!-- CODEGRAPH_END -->';
|
||||
|
||||
function readJson(filePath: string): Record<string, any> | null {
|
||||
try {
|
||||
if (!fs.existsSync(filePath)) return null;
|
||||
return JSON.parse(fs.readFileSync(filePath, 'utf-8'));
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function writeJson(filePath: string, data: Record<string, any>): void {
|
||||
fs.writeFileSync(filePath, JSON.stringify(data, null, 2) + '\n');
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove CodeGraph MCP server from ~/.claude.json
|
||||
*/
|
||||
function removeMcpConfig(): void {
|
||||
const filePath = path.join(os.homedir(), '.claude.json');
|
||||
const config = readJson(filePath);
|
||||
if (!config?.mcpServers?.codegraph) return;
|
||||
|
||||
delete config.mcpServers.codegraph;
|
||||
|
||||
// Clean up empty mcpServers object
|
||||
if (Object.keys(config.mcpServers).length === 0) {
|
||||
delete config.mcpServers;
|
||||
}
|
||||
|
||||
writeJson(filePath, config);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove CodeGraph permissions and hooks from ~/.claude/settings.json
|
||||
*/
|
||||
function removeSettings(): void {
|
||||
const filePath = path.join(os.homedir(), '.claude', 'settings.json');
|
||||
const settings = readJson(filePath);
|
||||
if (!settings) return;
|
||||
|
||||
let changed = false;
|
||||
|
||||
// Remove codegraph permissions
|
||||
if (Array.isArray(settings.permissions?.allow)) {
|
||||
const before = settings.permissions.allow.length;
|
||||
settings.permissions.allow = settings.permissions.allow.filter(
|
||||
(p: string) => !p.startsWith('mcp__codegraph__')
|
||||
);
|
||||
if (settings.permissions.allow.length !== before) changed = true;
|
||||
|
||||
// Clean up empty allow array
|
||||
if (settings.permissions.allow.length === 0) {
|
||||
delete settings.permissions.allow;
|
||||
}
|
||||
// Clean up empty permissions object
|
||||
if (Object.keys(settings.permissions).length === 0) {
|
||||
delete settings.permissions;
|
||||
}
|
||||
}
|
||||
|
||||
// Remove codegraph hooks
|
||||
if (settings.hooks) {
|
||||
for (const event of Object.keys(settings.hooks)) {
|
||||
if (!Array.isArray(settings.hooks[event])) continue;
|
||||
|
||||
const before = settings.hooks[event].length;
|
||||
settings.hooks[event] = settings.hooks[event].filter((entry: any) => {
|
||||
const json = JSON.stringify(entry);
|
||||
return !json.includes('codegraph mark-dirty') && !json.includes('codegraph sync-if-dirty');
|
||||
});
|
||||
if (settings.hooks[event].length !== before) changed = true;
|
||||
|
||||
// Clean up empty event arrays
|
||||
if (settings.hooks[event].length === 0) {
|
||||
delete settings.hooks[event];
|
||||
}
|
||||
}
|
||||
|
||||
// Clean up empty hooks object
|
||||
if (Object.keys(settings.hooks).length === 0) {
|
||||
delete settings.hooks;
|
||||
}
|
||||
}
|
||||
|
||||
if (changed) {
|
||||
writeJson(filePath, settings);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove CodeGraph section from ~/.claude/CLAUDE.md
|
||||
*/
|
||||
function removeClaudeMd(): void {
|
||||
const filePath = path.join(os.homedir(), '.claude', 'CLAUDE.md');
|
||||
try {
|
||||
if (!fs.existsSync(filePath)) return;
|
||||
let content = fs.readFileSync(filePath, 'utf-8');
|
||||
|
||||
// Remove marked section
|
||||
const startIdx = content.indexOf(CODEGRAPH_SECTION_START);
|
||||
const endIdx = content.indexOf(CODEGRAPH_SECTION_END);
|
||||
|
||||
if (startIdx !== -1 && endIdx > startIdx) {
|
||||
const before = content.substring(0, startIdx).trimEnd();
|
||||
const after = content.substring(endIdx + CODEGRAPH_SECTION_END.length).trimStart();
|
||||
content = before + (before && after ? '\n\n' : '') + after;
|
||||
|
||||
if (content.trim() === '') {
|
||||
// File is empty after removing section — delete it
|
||||
fs.unlinkSync(filePath);
|
||||
} else {
|
||||
fs.writeFileSync(filePath, content.trim() + '\n');
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Never fail
|
||||
}
|
||||
}
|
||||
|
||||
// Run cleanup — never throw
|
||||
try {
|
||||
removeMcpConfig();
|
||||
} catch { /* ignore */ }
|
||||
|
||||
try {
|
||||
removeSettings();
|
||||
} catch { /* ignore */ }
|
||||
|
||||
try {
|
||||
removeClaudeMd();
|
||||
} catch { /* ignore */ }
|
||||
@@ -113,16 +113,18 @@ export function warn(message: string): void {
|
||||
/**
|
||||
* Show the "next steps" section after installation
|
||||
*/
|
||||
export function showNextSteps(location: 'global' | 'local', useNpx?: boolean): void {
|
||||
export function showNextSteps(location: 'global' | 'local'): void {
|
||||
console.log();
|
||||
console.log(chalk.bold(' Done!') + ' Restart Claude Code to use CodeGraph.');
|
||||
console.log();
|
||||
|
||||
if (location === 'global') {
|
||||
const cmd = useNpx ? 'npx @colbymchenry/codegraph' : 'codegraph';
|
||||
console.log(chalk.dim(' Quick start:'));
|
||||
console.log(chalk.dim(' cd your-project'));
|
||||
console.log(chalk.cyan(` ${cmd} init -i`));
|
||||
console.log(chalk.cyan(' codegraph init -i'));
|
||||
console.log();
|
||||
console.log(chalk.dim(' To uninstall:'));
|
||||
console.log(chalk.dim(' npm uninstall -g @colbymchenry/codegraph'));
|
||||
} else {
|
||||
console.log(chalk.dim(' CodeGraph is ready to use in this project!'));
|
||||
}
|
||||
|
||||
@@ -98,32 +98,13 @@ function writeJsonFile(filePath: string, data: Record<string, any>): void {
|
||||
}
|
||||
|
||||
/**
|
||||
* When true, all configs use `npx @colbymchenry/codegraph` instead of the
|
||||
* bare `codegraph` command. Set by the installer when global install fails.
|
||||
* Get the MCP server configuration
|
||||
*/
|
||||
let useNpxFallback = false;
|
||||
|
||||
export function setUseNpxFallback(value: boolean): void {
|
||||
useNpxFallback = value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the MCP server configuration for the given location
|
||||
*/
|
||||
function getMcpServerConfig(location: InstallLocation): Record<string, any> {
|
||||
if (location === 'global' && !useNpxFallback) {
|
||||
// Global: use 'codegraph' command directly (globally installed and in PATH)
|
||||
return {
|
||||
type: 'stdio',
|
||||
command: 'codegraph',
|
||||
args: ['serve', '--mcp'],
|
||||
};
|
||||
}
|
||||
// Local or npx fallback: use npx to run the package
|
||||
function getMcpServerConfig(): Record<string, any> {
|
||||
return {
|
||||
type: 'stdio',
|
||||
command: 'npx',
|
||||
args: ['@colbymchenry/codegraph', 'serve', '--mcp'],
|
||||
command: 'codegraph',
|
||||
args: ['serve', '--mcp'],
|
||||
};
|
||||
}
|
||||
|
||||
@@ -140,7 +121,7 @@ export function writeMcpConfig(location: InstallLocation): void {
|
||||
}
|
||||
|
||||
// Add or update codegraph server
|
||||
config.mcpServers.codegraph = getMcpServerConfig(location);
|
||||
config.mcpServers.codegraph = getMcpServerConfig();
|
||||
|
||||
writeJsonFile(claudeJsonPath, config);
|
||||
}
|
||||
@@ -221,8 +202,8 @@ export function hasPermissions(location: InstallLocation): boolean {
|
||||
* PostToolUse(Edit|Write) → mark-dirty (async, non-blocking)
|
||||
* Stop → sync-if-dirty (sync, ensures fresh index before next user turn)
|
||||
*/
|
||||
function getHooksConfig(location: InstallLocation): Record<string, any> {
|
||||
const command = (location === 'global' && !useNpxFallback) ? 'codegraph' : 'npx @colbymchenry/codegraph';
|
||||
function getHooksConfig(): Record<string, any> {
|
||||
const command = 'codegraph';
|
||||
|
||||
return {
|
||||
PostToolUse: [
|
||||
@@ -277,7 +258,7 @@ export function writeHooks(location: InstallLocation): void {
|
||||
settings.hooks = {};
|
||||
}
|
||||
|
||||
const newHooks = getHooksConfig(location);
|
||||
const newHooks = getHooksConfig();
|
||||
|
||||
// For each hook event (PostToolUse, Stop), merge with existing entries
|
||||
for (const [event, newEntries] of Object.entries(newHooks)) {
|
||||
|
||||
+12
-28
@@ -8,7 +8,7 @@
|
||||
import { execSync } from 'child_process';
|
||||
import { showBanner, showNextSteps, success, error, info, chalk } from './banner';
|
||||
import { promptInstallLocation, promptAutoAllow, InstallLocation } from './prompts';
|
||||
import { writeMcpConfig, writePermissions, writeClaudeMd, writeHooks, hasMcpConfig, hasPermissions, hasHooks, setUseNpxFallback } from './config-writer';
|
||||
import { writeMcpConfig, writePermissions, writeClaudeMd, writeHooks, hasMcpConfig, hasPermissions, hasHooks } from './config-writer';
|
||||
|
||||
/**
|
||||
* Format a number with commas
|
||||
@@ -25,40 +25,24 @@ export async function runInstaller(): Promise<void> {
|
||||
showBanner();
|
||||
|
||||
try {
|
||||
// Step 1: Check if codegraph is available (skip install if already there)
|
||||
let codegraphAvailable = false;
|
||||
// Step 1: Install codegraph globally.
|
||||
// Always run npm install -g — we can't use `command -v codegraph` to check
|
||||
// because npx puts a temporary binary in PATH that vanishes when npx exits.
|
||||
console.log(chalk.dim(' Installing codegraph globally...'));
|
||||
try {
|
||||
const checkCmd = process.platform === 'win32' ? 'where codegraph' : 'command -v codegraph';
|
||||
execSync(checkCmd, { stdio: 'pipe' });
|
||||
codegraphAvailable = true;
|
||||
execSync('npm install -g @colbymchenry/codegraph', { stdio: 'pipe' });
|
||||
success('Installed codegraph command globally');
|
||||
} catch {
|
||||
// Not installed globally yet
|
||||
}
|
||||
|
||||
if (!codegraphAvailable) {
|
||||
console.log(chalk.dim(' Installing codegraph globally...'));
|
||||
try {
|
||||
execSync('npm install -g @colbymchenry/codegraph', { stdio: 'pipe' });
|
||||
success('Installed codegraph command globally');
|
||||
codegraphAvailable = true;
|
||||
} catch {
|
||||
// May fail if no permissions, but that's ok - npx still works
|
||||
info('Could not install globally — will use npx instead');
|
||||
info('(MCP server and hooks will use npx @colbymchenry/codegraph)');
|
||||
}
|
||||
console.log();
|
||||
}
|
||||
|
||||
// If codegraph binary isn't in PATH, tell config-writer to use npx for everything
|
||||
if (!codegraphAvailable) {
|
||||
setUseNpxFallback(true);
|
||||
info('Could not install globally (permission denied)');
|
||||
info('Try: sudo npm install -g @colbymchenry/codegraph');
|
||||
}
|
||||
console.log();
|
||||
|
||||
// Step 2: Ask for installation location
|
||||
const location = await promptInstallLocation();
|
||||
console.log();
|
||||
|
||||
// Step 3: Write MCP configuration
|
||||
// Step 3: Write MCP configuration (always uses npx for reliability)
|
||||
const alreadyHasMcp = hasMcpConfig(location);
|
||||
writeMcpConfig(location);
|
||||
|
||||
@@ -111,7 +95,7 @@ export async function runInstaller(): Promise<void> {
|
||||
}
|
||||
|
||||
// Show next steps
|
||||
showNextSteps(location, !codegraphAvailable);
|
||||
showNextSteps(location);
|
||||
} catch (err) {
|
||||
console.log();
|
||||
if (err instanceof Error && err.message.includes('readline was closed')) {
|
||||
|
||||
Reference in New Issue
Block a user