refactor: consolidate hook settings generation into shared common module

This commit is contained in:
weishu
2026-01-23 16:04:37 +08:00
parent 6567f22978
commit c78ba1da1a
5 changed files with 116 additions and 148 deletions
+6 -3
View File
@@ -9,7 +9,7 @@ import { parseSpecialCommand } from '@/parsers/specialCommands';
import { getEnvironmentInfo } from '@/ui/doctor';
import { startHappyServer } from '@/claude/utils/startHappyServer';
import { startHookServer } from '@/claude/utils/startHookServer';
import { generateHookSettingsFile, cleanupHookSettingsFile } from '@/claude/utils/generateHookSettings';
import { generateHookSettingsFile, cleanupHookSettingsFile } from '@/modules/common/hooks/generateHookSettings';
import { registerKillSessionHandler } from './registerKillSessionHandler';
import type { Session } from './session';
import { bootstrapSession } from '@/agent/sessionFactory';
@@ -101,7 +101,10 @@ export async function runClaude(options: StartOptions = {}): Promise<void> {
});
logger.debug(`[START] Hook server started on port ${hookServer.port}`);
const hookSettingsPath = generateHookSettingsFile(hookServer.port, hookServer.token);
const hookSettingsPath = generateHookSettingsFile(hookServer.port, hookServer.token, {
filenamePrefix: 'session-hook',
logLabel: 'generateHookSettings'
});
logger.debug(`[START] Generated hook settings file: ${hookSettingsPath}`);
// Print log file path
@@ -116,7 +119,7 @@ export async function runClaude(options: StartOptions = {}): Promise<void> {
onAfterClose: () => {
happyServer.stop();
hookServer.stop();
cleanupHookSettingsFile(hookSettingsPath);
cleanupHookSettingsFile(hookSettingsPath, 'generateHookSettings');
}
});
@@ -1,77 +0,0 @@
/**
* Generate temporary settings file with Claude hooks for session tracking.
*
* Creates a settings.json file that configures Claude's SessionStart hook
* to notify our HTTP server when sessions change (new session, resume, compact, etc.).
*/
import { join } from 'node:path';
import { writeFileSync, mkdirSync, unlinkSync, existsSync } from 'node:fs';
import { configuration } from '@/configuration';
import { logger } from '@/ui/logger';
import { getHappyCliCommand } from '@/utils/spawnHappyCLI';
function shellQuote(value: string): string {
if (value.length === 0) {
return '""';
}
if (/^[A-Za-z0-9_\/:=-]+$/.test(value)) {
return value;
}
return '"' + value.replace(/(["\\$`])/g, '\\$1') + '"';
}
function shellJoin(parts: string[]): string {
return parts.map(shellQuote).join(' ');
}
/**
* Generate a temporary settings file with SessionStart hook configuration.
*/
export function generateHookSettingsFile(port: number, token: string): string {
const hooksDir = join(configuration.happyHomeDir, 'tmp', 'hooks');
mkdirSync(hooksDir, { recursive: true });
const filename = `session-hook-${process.pid}.json`;
const filepath = join(hooksDir, filename);
const { command, args } = getHappyCliCommand(['hook-forwarder', '--port', String(port), '--token', token]);
const hookCommand = shellJoin([command, ...args]);
const settings = {
hooks: {
SessionStart: [
{
matcher: '*',
hooks: [
{
type: 'command',
command: hookCommand
}
]
}
]
}
};
writeFileSync(filepath, JSON.stringify(settings, null, 4));
logger.debug(`[generateHookSettings] Created hook settings file: ${filepath}`);
return filepath;
}
/**
* Clean up the temporary hook settings file.
*/
export function cleanupHookSettingsFile(filepath: string): void {
try {
if (existsSync(filepath)) {
unlinkSync(filepath);
logger.debug(`[generateHookSettings] Cleaned up hook settings file: ${filepath}`);
}
} catch (error) {
logger.debug(`[generateHookSettings] Failed to cleanup hook settings file: ${error}`);
}
}
+7 -3
View File
@@ -9,7 +9,7 @@ import type { GeminiMode, PermissionMode } from './types';
import { bootstrapSession } from '@/agent/sessionFactory';
import { createModeChangeHandler, createRunnerLifecycle, setControlledByUser } from '@/agent/runnerLifecycle';
import { startHookServer } from '@/claude/utils/startHookServer';
import { cleanupGeminiHookSettingsFile, generateGeminiHookSettingsFile } from './utils/generateHookSettings';
import { cleanupHookSettingsFile, generateHookSettingsFile } from '@/modules/common/hooks/generateHookSettings';
import { resolveGeminiRuntimeConfig } from './utils/config';
import { isPermissionModeAllowedForFlavor } from '@hapi/protocol';
import { PermissionModeSchema } from '@hapi/protocol/schemas';
@@ -72,7 +72,11 @@ export async function runGemini(opts: {
}
});
const hookSettingsPath = generateGeminiHookSettingsFile(hookServer.port, hookServer.token);
const hookSettingsPath = generateHookSettingsFile(hookServer.port, hookServer.token, {
filenamePrefix: 'gemini-session-hook',
logLabel: 'gemini-hook-settings',
hooksEnabled: true
});
const lifecycle = createRunnerLifecycle({
session,
@@ -80,7 +84,7 @@ export async function runGemini(opts: {
stopKeepAlive: () => sessionWrapperRef.current?.stopKeepAlive(),
onAfterClose: () => {
hookServer.stop();
cleanupGeminiHookSettingsFile(hookSettingsPath);
cleanupHookSettingsFile(hookSettingsPath, 'gemini-hook-settings');
}
});
@@ -1,65 +0,0 @@
import { join } from 'node:path';
import { writeFileSync, mkdirSync, unlinkSync, existsSync } from 'node:fs';
import { configuration } from '@/configuration';
import { logger } from '@/ui/logger';
import { getHappyCliCommand } from '@/utils/spawnHappyCLI';
function shellQuote(value: string): string {
if (value.length === 0) {
return '""';
}
if (/^[A-Za-z0-9_\/:=-]+$/.test(value)) {
return value;
}
return '"' + value.replace(/(["\\$`])/g, '\\$1') + '"';
}
function shellJoin(parts: string[]): string {
return parts.map(shellQuote).join(' ');
}
export function generateGeminiHookSettingsFile(port: number, token: string): string {
const hooksDir = join(configuration.happyHomeDir, 'tmp', 'hooks');
mkdirSync(hooksDir, { recursive: true });
const filename = `gemini-session-hook-${process.pid}.json`;
const filepath = join(hooksDir, filename);
const { command, args } = getHappyCliCommand(['hook-forwarder', '--port', String(port), '--token', token]);
const hookCommand = shellJoin([command, ...args]);
const settings = {
hooks: {
enabled: true,
SessionStart: [
{
matcher: '*',
hooks: [
{
type: 'command',
command: hookCommand
}
]
}
]
}
};
writeFileSync(filepath, JSON.stringify(settings, null, 4));
logger.debug(`[gemini-hook-settings] Created hook settings file: ${filepath}`);
return filepath;
}
export function cleanupGeminiHookSettingsFile(filepath: string): void {
try {
if (existsSync(filepath)) {
unlinkSync(filepath);
logger.debug(`[gemini-hook-settings] Cleaned up hook settings file: ${filepath}`);
}
} catch (error) {
logger.debug(`[gemini-hook-settings] Failed to cleanup hook settings file: ${error}`);
}
}
@@ -0,0 +1,103 @@
import { join } from 'node:path';
import { writeFileSync, mkdirSync, unlinkSync, existsSync } from 'node:fs';
import { configuration } from '@/configuration';
import { logger } from '@/ui/logger';
import { getHappyCliCommand } from '@/utils/spawnHappyCLI';
type HookCommandConfig = {
matcher: string;
hooks: Array<{
type: 'command';
command: string;
}>;
};
type HookSettings = {
hooks: {
enabled?: boolean;
SessionStart: HookCommandConfig[];
};
};
export type HookSettingsOptions = {
filenamePrefix: string;
logLabel: string;
hooksEnabled?: boolean;
};
function shellQuote(value: string): string {
if (value.length === 0) {
return '""';
}
if (/^[A-Za-z0-9_\/:=-]+$/.test(value)) {
return value;
}
return '"' + value.replace(/(["\\$`])/g, '\\$1') + '"';
}
function shellJoin(parts: string[]): string {
return parts.map(shellQuote).join(' ');
}
function buildHookSettings(command: string, hooksEnabled?: boolean): HookSettings {
const hooks: HookSettings['hooks'] = {
SessionStart: [
{
matcher: '*',
hooks: [
{
type: 'command',
command
}
]
}
]
};
if (hooksEnabled !== undefined) {
hooks.enabled = hooksEnabled;
}
return { hooks };
}
export function generateHookSettingsFile(
port: number,
token: string,
options: HookSettingsOptions
): string {
const hooksDir = join(configuration.happyHomeDir, 'tmp', 'hooks');
mkdirSync(hooksDir, { recursive: true });
const filename = `${options.filenamePrefix}-${process.pid}.json`;
const filepath = join(hooksDir, filename);
const { command, args } = getHappyCliCommand([
'hook-forwarder',
'--port',
String(port),
'--token',
token
]);
const hookCommand = shellJoin([command, ...args]);
const settings = buildHookSettings(hookCommand, options.hooksEnabled);
writeFileSync(filepath, JSON.stringify(settings, null, 4));
logger.debug(`[${options.logLabel}] Created hook settings file: ${filepath}`);
return filepath;
}
export function cleanupHookSettingsFile(filepath: string, logLabel: string): void {
try {
if (existsSync(filepath)) {
unlinkSync(filepath);
logger.debug(`[${logLabel}] Cleaned up hook settings file: ${filepath}`);
}
} catch (error) {
logger.debug(`[${logLabel}] Failed to cleanup hook settings file: ${error}`);
}
}