mirror of
https://github.com/wu736139669/hapi.git
synced 2026-08-05 06:24:37 +00:00
refactor: consolidate hook settings generation into shared common module
This commit is contained in:
@@ -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}`);
|
||||
}
|
||||
}
|
||||
@@ -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}`);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user