mirror of
https://github.com/wu736139669/hapi.git
synced 2026-08-06 06:41:56 +00:00
refactor(cli): split command routing and modularize RPC handlers
Restructure CLI entry point and RPC handler registration to improve maintainability: - Introduce command registry pattern with CommandDefinition interface for consistent command structure - Extract individual command modules (claude, codex, daemon, doctor, gemini, auth, connect, etc.) - Move RPC handlers into separate feature modules (bash, files, git, directories, ripgrep, difftastic, slashCommands) - Create shared RPC types and response helpers (rpcTypes.ts, rpcResponses.ts) - Move SpawnSessionOptions and SpawnSessionResult to rpcTypes for proper type organization - Simplify index.ts to minimal entrypoint using command registry - Update imports in apiMachine.ts and daemon/run.ts to use rpcTypes module This enables: - Better code organization by feature/command - Easier testing of individual commands - Safer RPC handler registration without side effects - Clearer separation of concerns between routing and business logic
This commit is contained in:
@@ -1,135 +1 @@
|
||||
import { execFile, type ExecFileOptions } from 'child_process'
|
||||
import { promisify } from 'util'
|
||||
import { RpcHandlerManager } from '../../api/rpc/RpcHandlerManager'
|
||||
import { validatePath } from './pathSecurity'
|
||||
|
||||
const execFileAsync = promisify(execFile)
|
||||
|
||||
interface GitStatusRequest {
|
||||
cwd?: string
|
||||
timeout?: number
|
||||
}
|
||||
|
||||
interface GitDiffNumstatRequest {
|
||||
cwd?: string
|
||||
staged?: boolean
|
||||
timeout?: number
|
||||
}
|
||||
|
||||
interface GitDiffFileRequest {
|
||||
cwd?: string
|
||||
filePath: string
|
||||
staged?: boolean
|
||||
timeout?: number
|
||||
}
|
||||
|
||||
interface GitCommandResponse {
|
||||
success: boolean
|
||||
stdout?: string
|
||||
stderr?: string
|
||||
exitCode?: number
|
||||
error?: string
|
||||
}
|
||||
|
||||
function resolveCwd(requestedCwd: string | undefined, workingDirectory: string): { cwd: string; error?: string } {
|
||||
const cwd = requestedCwd ?? workingDirectory
|
||||
const validation = validatePath(cwd, workingDirectory)
|
||||
if (!validation.valid) {
|
||||
return { cwd, error: validation.error ?? 'Invalid working directory' }
|
||||
}
|
||||
return { cwd }
|
||||
}
|
||||
|
||||
function validateFilePath(filePath: string, workingDirectory: string): string | null {
|
||||
const validation = validatePath(filePath, workingDirectory)
|
||||
if (!validation.valid) {
|
||||
return validation.error ?? 'Invalid file path'
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
async function runGitCommand(
|
||||
args: string[],
|
||||
cwd: string,
|
||||
timeout?: number
|
||||
): Promise<GitCommandResponse> {
|
||||
try {
|
||||
const options: ExecFileOptions = {
|
||||
cwd,
|
||||
timeout: timeout ?? 10_000
|
||||
}
|
||||
const { stdout, stderr } = await execFileAsync('git', args, options)
|
||||
return {
|
||||
success: true,
|
||||
stdout: stdout ? stdout.toString() : '',
|
||||
stderr: stderr ? stderr.toString() : '',
|
||||
exitCode: 0
|
||||
}
|
||||
} catch (error) {
|
||||
const execError = error as NodeJS.ErrnoException & {
|
||||
stdout?: string
|
||||
stderr?: string
|
||||
code?: number | string
|
||||
killed?: boolean
|
||||
}
|
||||
|
||||
if (execError.code === 'ETIMEDOUT' || execError.killed) {
|
||||
return {
|
||||
success: false,
|
||||
stdout: execError.stdout ? execError.stdout.toString() : '',
|
||||
stderr: execError.stderr ? execError.stderr.toString() : '',
|
||||
exitCode: typeof execError.code === 'number' ? execError.code : -1,
|
||||
error: 'Command timed out'
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: false,
|
||||
stdout: execError.stdout ? execError.stdout.toString() : '',
|
||||
stderr: execError.stderr ? execError.stderr.toString() : execError.message || 'Command failed',
|
||||
exitCode: typeof execError.code === 'number' ? execError.code : 1,
|
||||
error: execError.message || 'Command failed'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function registerGitHandlers(rpcHandlerManager: RpcHandlerManager, workingDirectory: string): void {
|
||||
rpcHandlerManager.registerHandler<GitStatusRequest, GitCommandResponse>('git-status', async (data) => {
|
||||
const resolved = resolveCwd(data.cwd, workingDirectory)
|
||||
if (resolved.error) {
|
||||
return { success: false, error: resolved.error }
|
||||
}
|
||||
return await runGitCommand(
|
||||
['status', '--porcelain=v2', '--branch', '--untracked-files=all'],
|
||||
resolved.cwd,
|
||||
data.timeout
|
||||
)
|
||||
})
|
||||
|
||||
rpcHandlerManager.registerHandler<GitDiffNumstatRequest, GitCommandResponse>('git-diff-numstat', async (data) => {
|
||||
const resolved = resolveCwd(data.cwd, workingDirectory)
|
||||
if (resolved.error) {
|
||||
return { success: false, error: resolved.error }
|
||||
}
|
||||
const args = data.staged
|
||||
? ['diff', '--cached', '--numstat']
|
||||
: ['diff', '--numstat']
|
||||
return await runGitCommand(args, resolved.cwd, data.timeout)
|
||||
})
|
||||
|
||||
rpcHandlerManager.registerHandler<GitDiffFileRequest, GitCommandResponse>('git-diff-file', async (data) => {
|
||||
const resolved = resolveCwd(data.cwd, workingDirectory)
|
||||
if (resolved.error) {
|
||||
return { success: false, error: resolved.error }
|
||||
}
|
||||
const fileError = validateFilePath(data.filePath, workingDirectory)
|
||||
if (fileError) {
|
||||
return { success: false, error: fileError }
|
||||
}
|
||||
|
||||
const args = data.staged
|
||||
? ['diff', '--cached', '--no-ext-diff', '--', data.filePath]
|
||||
: ['diff', '--no-ext-diff', '--', data.filePath]
|
||||
return await runGitCommand(args, resolved.cwd, data.timeout)
|
||||
})
|
||||
}
|
||||
export { registerGitHandlers } from './handlers/git'
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
import { logger } from '@/ui/logger'
|
||||
import { exec, type ExecOptions } from 'child_process'
|
||||
import { promisify } from 'util'
|
||||
import type { RpcHandlerManager } from '@/api/rpc/RpcHandlerManager'
|
||||
import { validatePath } from '../pathSecurity'
|
||||
import { getErrorMessage, rpcError } from '../rpcResponses'
|
||||
|
||||
const execAsync = promisify(exec)
|
||||
|
||||
interface BashRequest {
|
||||
command: string
|
||||
cwd?: string
|
||||
timeout?: number
|
||||
}
|
||||
|
||||
interface BashResponse {
|
||||
success: boolean
|
||||
stdout?: string
|
||||
stderr?: string
|
||||
exitCode?: number
|
||||
error?: string
|
||||
}
|
||||
|
||||
export function registerBashHandlers(rpcHandlerManager: RpcHandlerManager, workingDirectory: string): void {
|
||||
rpcHandlerManager.registerHandler<BashRequest, BashResponse>('bash', async (data) => {
|
||||
logger.debug('Shell command request:', data.command)
|
||||
|
||||
if (data.cwd) {
|
||||
const validation = validatePath(data.cwd, workingDirectory)
|
||||
if (!validation.valid) {
|
||||
return rpcError(validation.error ?? 'Invalid working directory')
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const options: ExecOptions = {
|
||||
cwd: data.cwd,
|
||||
timeout: data.timeout || 30000
|
||||
}
|
||||
|
||||
const { stdout, stderr } = await execAsync(data.command, options)
|
||||
|
||||
return {
|
||||
success: true,
|
||||
stdout: stdout ? stdout.toString() : '',
|
||||
stderr: stderr ? stderr.toString() : '',
|
||||
exitCode: 0
|
||||
}
|
||||
} catch (error) {
|
||||
const execError = error as NodeJS.ErrnoException & {
|
||||
stdout?: string
|
||||
stderr?: string
|
||||
code?: number | string
|
||||
killed?: boolean
|
||||
}
|
||||
|
||||
if (execError.code === 'ETIMEDOUT' || execError.killed) {
|
||||
return rpcError('Command timed out', {
|
||||
stdout: execError.stdout ? execError.stdout.toString() : '',
|
||||
stderr: execError.stderr ? execError.stderr.toString() : '',
|
||||
exitCode: typeof execError.code === 'number' ? execError.code : -1
|
||||
})
|
||||
}
|
||||
|
||||
return rpcError(getErrorMessage(execError, 'Command failed'), {
|
||||
stdout: execError.stdout ? execError.stdout.toString() : '',
|
||||
stderr: execError.stderr ? execError.stderr.toString() : execError.message || 'Command failed',
|
||||
exitCode: typeof execError.code === 'number' ? execError.code : 1
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
import { logger } from '@/ui/logger'
|
||||
import type { RpcHandlerManager } from '@/api/rpc/RpcHandlerManager'
|
||||
import { run as runDifftastic } from '@/modules/difftastic/index'
|
||||
import { validatePath } from '../pathSecurity'
|
||||
import { getErrorMessage, rpcError } from '../rpcResponses'
|
||||
|
||||
interface DifftasticRequest {
|
||||
args: string[]
|
||||
cwd?: string
|
||||
}
|
||||
|
||||
interface DifftasticResponse {
|
||||
success: boolean
|
||||
exitCode?: number
|
||||
stdout?: string
|
||||
stderr?: string
|
||||
error?: string
|
||||
}
|
||||
|
||||
export function registerDifftasticHandlers(rpcHandlerManager: RpcHandlerManager, workingDirectory: string): void {
|
||||
rpcHandlerManager.registerHandler<DifftasticRequest, DifftasticResponse>('difftastic', async (data) => {
|
||||
logger.debug('Difftastic request with args:', data.args, 'cwd:', data.cwd)
|
||||
|
||||
if (data.cwd) {
|
||||
const validation = validatePath(data.cwd, workingDirectory)
|
||||
if (!validation.valid) {
|
||||
return rpcError(validation.error ?? 'Invalid working directory')
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await runDifftastic(data.args, { cwd: data.cwd })
|
||||
return {
|
||||
success: true,
|
||||
exitCode: result.exitCode,
|
||||
stdout: result.stdout.toString(),
|
||||
stderr: result.stderr.toString()
|
||||
}
|
||||
} catch (error) {
|
||||
logger.debug('Failed to run difftastic:', error)
|
||||
return rpcError(getErrorMessage(error, 'Failed to run difftastic'))
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,173 @@
|
||||
import { logger } from '@/ui/logger'
|
||||
import { readdir, stat } from 'fs/promises'
|
||||
import { basename, join } from 'path'
|
||||
import type { RpcHandlerManager } from '@/api/rpc/RpcHandlerManager'
|
||||
import { validatePath } from '../pathSecurity'
|
||||
import { getErrorMessage, rpcError } from '../rpcResponses'
|
||||
|
||||
interface ListDirectoryRequest {
|
||||
path: string
|
||||
}
|
||||
|
||||
interface DirectoryEntry {
|
||||
name: string
|
||||
type: 'file' | 'directory' | 'other'
|
||||
size?: number
|
||||
modified?: number
|
||||
}
|
||||
|
||||
interface ListDirectoryResponse {
|
||||
success: boolean
|
||||
entries?: DirectoryEntry[]
|
||||
error?: string
|
||||
}
|
||||
|
||||
interface GetDirectoryTreeRequest {
|
||||
path: string
|
||||
maxDepth: number
|
||||
}
|
||||
|
||||
interface TreeNode {
|
||||
name: string
|
||||
path: string
|
||||
type: 'file' | 'directory'
|
||||
size?: number
|
||||
modified?: number
|
||||
children?: TreeNode[]
|
||||
}
|
||||
|
||||
interface GetDirectoryTreeResponse {
|
||||
success: boolean
|
||||
tree?: TreeNode
|
||||
error?: string
|
||||
}
|
||||
|
||||
export function registerDirectoryHandlers(rpcHandlerManager: RpcHandlerManager, workingDirectory: string): void {
|
||||
rpcHandlerManager.registerHandler<ListDirectoryRequest, ListDirectoryResponse>('listDirectory', async (data) => {
|
||||
logger.debug('List directory request:', data.path)
|
||||
|
||||
const validation = validatePath(data.path, workingDirectory)
|
||||
if (!validation.valid) {
|
||||
return rpcError(validation.error ?? 'Invalid directory path')
|
||||
}
|
||||
|
||||
try {
|
||||
const entries = await readdir(data.path, { withFileTypes: true })
|
||||
|
||||
const directoryEntries: DirectoryEntry[] = await Promise.all(
|
||||
entries.map(async (entry) => {
|
||||
const fullPath = join(data.path, entry.name)
|
||||
let type: 'file' | 'directory' | 'other' = 'other'
|
||||
let size: number | undefined
|
||||
let modified: number | undefined
|
||||
|
||||
if (entry.isDirectory()) {
|
||||
type = 'directory'
|
||||
} else if (entry.isFile()) {
|
||||
type = 'file'
|
||||
}
|
||||
|
||||
try {
|
||||
const stats = await stat(fullPath)
|
||||
size = stats.size
|
||||
modified = stats.mtime.getTime()
|
||||
} catch (error) {
|
||||
logger.debug(`Failed to stat ${fullPath}:`, error)
|
||||
}
|
||||
|
||||
return {
|
||||
name: entry.name,
|
||||
type,
|
||||
size,
|
||||
modified
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
directoryEntries.sort((a, b) => {
|
||||
if (a.type === 'directory' && b.type !== 'directory') return -1
|
||||
if (a.type !== 'directory' && b.type === 'directory') return 1
|
||||
return a.name.localeCompare(b.name)
|
||||
})
|
||||
|
||||
return { success: true, entries: directoryEntries }
|
||||
} catch (error) {
|
||||
logger.debug('Failed to list directory:', error)
|
||||
return rpcError(getErrorMessage(error, 'Failed to list directory'))
|
||||
}
|
||||
})
|
||||
|
||||
rpcHandlerManager.registerHandler<GetDirectoryTreeRequest, GetDirectoryTreeResponse>('getDirectoryTree', async (data) => {
|
||||
logger.debug('Get directory tree request:', data.path, 'maxDepth:', data.maxDepth)
|
||||
|
||||
const validation = validatePath(data.path, workingDirectory)
|
||||
if (!validation.valid) {
|
||||
return rpcError(validation.error ?? 'Invalid directory path')
|
||||
}
|
||||
|
||||
async function buildTree(path: string, name: string, currentDepth: number): Promise<TreeNode | null> {
|
||||
try {
|
||||
const stats = await stat(path)
|
||||
|
||||
const node: TreeNode = {
|
||||
name,
|
||||
path,
|
||||
type: stats.isDirectory() ? 'directory' : 'file',
|
||||
size: stats.size,
|
||||
modified: stats.mtime.getTime()
|
||||
}
|
||||
|
||||
if (stats.isDirectory() && currentDepth < data.maxDepth) {
|
||||
const entries = await readdir(path, { withFileTypes: true })
|
||||
const children: TreeNode[] = []
|
||||
|
||||
await Promise.all(
|
||||
entries.map(async (entry) => {
|
||||
if (entry.isSymbolicLink()) {
|
||||
logger.debug(`Skipping symlink: ${join(path, entry.name)}`)
|
||||
return
|
||||
}
|
||||
|
||||
const childPath = join(path, entry.name)
|
||||
const childNode = await buildTree(childPath, entry.name, currentDepth + 1)
|
||||
if (childNode) {
|
||||
children.push(childNode)
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
children.sort((a, b) => {
|
||||
if (a.type === 'directory' && b.type !== 'directory') return -1
|
||||
if (a.type !== 'directory' && b.type === 'directory') return 1
|
||||
return a.name.localeCompare(b.name)
|
||||
})
|
||||
|
||||
node.children = children
|
||||
}
|
||||
|
||||
return node
|
||||
} catch (error) {
|
||||
logger.debug(`Failed to process ${path}:`, error instanceof Error ? error.message : String(error))
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
if (data.maxDepth < 0) {
|
||||
return rpcError('maxDepth must be non-negative')
|
||||
}
|
||||
|
||||
const baseName = data.path === '/' ? '/' : basename(data.path) || data.path
|
||||
const tree = await buildTree(data.path, baseName, 0)
|
||||
|
||||
if (!tree) {
|
||||
return rpcError('Failed to access the specified path')
|
||||
}
|
||||
|
||||
return { success: true, tree }
|
||||
} catch (error) {
|
||||
logger.debug('Failed to get directory tree:', error)
|
||||
return rpcError(getErrorMessage(error, 'Failed to get directory tree'))
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
import { logger } from '@/ui/logger'
|
||||
import { readFile, stat, writeFile } from 'fs/promises'
|
||||
import { createHash } from 'crypto'
|
||||
import { resolve } from 'path'
|
||||
import type { RpcHandlerManager } from '@/api/rpc/RpcHandlerManager'
|
||||
import { validatePath } from '../pathSecurity'
|
||||
import { getErrorMessage, rpcError } from '../rpcResponses'
|
||||
|
||||
interface ReadFileRequest {
|
||||
path: string
|
||||
}
|
||||
|
||||
interface ReadFileResponse {
|
||||
success: boolean
|
||||
content?: string
|
||||
error?: string
|
||||
}
|
||||
|
||||
interface WriteFileRequest {
|
||||
path: string
|
||||
content: string
|
||||
expectedHash?: string | null
|
||||
}
|
||||
|
||||
interface WriteFileResponse {
|
||||
success: boolean
|
||||
hash?: string
|
||||
error?: string
|
||||
}
|
||||
|
||||
export function registerFileHandlers(rpcHandlerManager: RpcHandlerManager, workingDirectory: string): void {
|
||||
rpcHandlerManager.registerHandler<ReadFileRequest, ReadFileResponse>('readFile', async (data) => {
|
||||
logger.debug('Read file request:', data.path)
|
||||
|
||||
const validation = validatePath(data.path, workingDirectory)
|
||||
if (!validation.valid) {
|
||||
return rpcError(validation.error ?? 'Invalid file path')
|
||||
}
|
||||
|
||||
try {
|
||||
const resolvedPath = resolve(workingDirectory, data.path)
|
||||
const buffer = await readFile(resolvedPath)
|
||||
const content = buffer.toString('base64')
|
||||
return { success: true, content }
|
||||
} catch (error) {
|
||||
logger.debug('Failed to read file:', error)
|
||||
return rpcError(getErrorMessage(error, 'Failed to read file'))
|
||||
}
|
||||
})
|
||||
|
||||
rpcHandlerManager.registerHandler<WriteFileRequest, WriteFileResponse>('writeFile', async (data) => {
|
||||
logger.debug('Write file request:', data.path)
|
||||
|
||||
const validation = validatePath(data.path, workingDirectory)
|
||||
if (!validation.valid) {
|
||||
return rpcError(validation.error ?? 'Invalid file path')
|
||||
}
|
||||
|
||||
try {
|
||||
if (data.expectedHash !== null && data.expectedHash !== undefined) {
|
||||
try {
|
||||
const existingBuffer = await readFile(data.path)
|
||||
const existingHash = createHash('sha256').update(existingBuffer).digest('hex')
|
||||
|
||||
if (existingHash !== data.expectedHash) {
|
||||
return rpcError(`File hash mismatch. Expected: ${data.expectedHash}, Actual: ${existingHash}`)
|
||||
}
|
||||
} catch (error) {
|
||||
const nodeError = error as NodeJS.ErrnoException
|
||||
if (nodeError.code !== 'ENOENT') {
|
||||
throw error
|
||||
}
|
||||
return rpcError('File does not exist but hash was provided')
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
await stat(data.path)
|
||||
return rpcError('File already exists but was expected to be new')
|
||||
} catch (error) {
|
||||
const nodeError = error as NodeJS.ErrnoException
|
||||
if (nodeError.code !== 'ENOENT') {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const buffer = Buffer.from(data.content, 'base64')
|
||||
await writeFile(data.path, buffer)
|
||||
|
||||
const hash = createHash('sha256').update(buffer).digest('hex')
|
||||
|
||||
return { success: true, hash }
|
||||
} catch (error) {
|
||||
logger.debug('Failed to write file:', error)
|
||||
return rpcError(getErrorMessage(error, 'Failed to write file'))
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
import { execFile, type ExecFileOptions } from 'child_process'
|
||||
import { promisify } from 'util'
|
||||
import type { RpcHandlerManager } from '@/api/rpc/RpcHandlerManager'
|
||||
import { validatePath } from '../pathSecurity'
|
||||
import { rpcError } from '../rpcResponses'
|
||||
|
||||
const execFileAsync = promisify(execFile)
|
||||
|
||||
interface GitStatusRequest {
|
||||
cwd?: string
|
||||
timeout?: number
|
||||
}
|
||||
|
||||
interface GitDiffNumstatRequest {
|
||||
cwd?: string
|
||||
staged?: boolean
|
||||
timeout?: number
|
||||
}
|
||||
|
||||
interface GitDiffFileRequest {
|
||||
cwd?: string
|
||||
filePath: string
|
||||
staged?: boolean
|
||||
timeout?: number
|
||||
}
|
||||
|
||||
interface GitCommandResponse {
|
||||
success: boolean
|
||||
stdout?: string
|
||||
stderr?: string
|
||||
exitCode?: number
|
||||
error?: string
|
||||
}
|
||||
|
||||
function resolveCwd(requestedCwd: string | undefined, workingDirectory: string): { cwd: string; error?: string } {
|
||||
const cwd = requestedCwd ?? workingDirectory
|
||||
const validation = validatePath(cwd, workingDirectory)
|
||||
if (!validation.valid) {
|
||||
return { cwd, error: validation.error ?? 'Invalid working directory' }
|
||||
}
|
||||
return { cwd }
|
||||
}
|
||||
|
||||
function validateFilePath(filePath: string, workingDirectory: string): string | null {
|
||||
const validation = validatePath(filePath, workingDirectory)
|
||||
if (!validation.valid) {
|
||||
return validation.error ?? 'Invalid file path'
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
async function runGitCommand(
|
||||
args: string[],
|
||||
cwd: string,
|
||||
timeout?: number
|
||||
): Promise<GitCommandResponse> {
|
||||
try {
|
||||
const options: ExecFileOptions = {
|
||||
cwd,
|
||||
timeout: timeout ?? 10_000
|
||||
}
|
||||
const { stdout, stderr } = await execFileAsync('git', args, options)
|
||||
return {
|
||||
success: true,
|
||||
stdout: stdout ? stdout.toString() : '',
|
||||
stderr: stderr ? stderr.toString() : '',
|
||||
exitCode: 0
|
||||
}
|
||||
} catch (error) {
|
||||
const execError = error as NodeJS.ErrnoException & {
|
||||
stdout?: string
|
||||
stderr?: string
|
||||
code?: number | string
|
||||
killed?: boolean
|
||||
}
|
||||
|
||||
if (execError.code === 'ETIMEDOUT' || execError.killed) {
|
||||
return rpcError('Command timed out', {
|
||||
stdout: execError.stdout ? execError.stdout.toString() : '',
|
||||
stderr: execError.stderr ? execError.stderr.toString() : '',
|
||||
exitCode: typeof execError.code === 'number' ? execError.code : -1
|
||||
})
|
||||
}
|
||||
|
||||
return rpcError(execError.message || 'Command failed', {
|
||||
stdout: execError.stdout ? execError.stdout.toString() : '',
|
||||
stderr: execError.stderr ? execError.stderr.toString() : execError.message || 'Command failed',
|
||||
exitCode: typeof execError.code === 'number' ? execError.code : 1
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export function registerGitHandlers(rpcHandlerManager: RpcHandlerManager, workingDirectory: string): void {
|
||||
rpcHandlerManager.registerHandler<GitStatusRequest, GitCommandResponse>('git-status', async (data) => {
|
||||
const resolved = resolveCwd(data.cwd, workingDirectory)
|
||||
if (resolved.error) {
|
||||
return rpcError(resolved.error)
|
||||
}
|
||||
return await runGitCommand(
|
||||
['status', '--porcelain=v2', '--branch', '--untracked-files=all'],
|
||||
resolved.cwd,
|
||||
data.timeout
|
||||
)
|
||||
})
|
||||
|
||||
rpcHandlerManager.registerHandler<GitDiffNumstatRequest, GitCommandResponse>('git-diff-numstat', async (data) => {
|
||||
const resolved = resolveCwd(data.cwd, workingDirectory)
|
||||
if (resolved.error) {
|
||||
return rpcError(resolved.error)
|
||||
}
|
||||
const args = data.staged
|
||||
? ['diff', '--cached', '--numstat']
|
||||
: ['diff', '--numstat']
|
||||
return await runGitCommand(args, resolved.cwd, data.timeout)
|
||||
})
|
||||
|
||||
rpcHandlerManager.registerHandler<GitDiffFileRequest, GitCommandResponse>('git-diff-file', async (data) => {
|
||||
const resolved = resolveCwd(data.cwd, workingDirectory)
|
||||
if (resolved.error) {
|
||||
return rpcError(resolved.error)
|
||||
}
|
||||
const fileError = validateFilePath(data.filePath, workingDirectory)
|
||||
if (fileError) {
|
||||
return rpcError(fileError)
|
||||
}
|
||||
|
||||
const args = data.staged
|
||||
? ['diff', '--cached', '--no-ext-diff', '--', data.filePath]
|
||||
: ['diff', '--no-ext-diff', '--', data.filePath]
|
||||
return await runGitCommand(args, resolved.cwd, data.timeout)
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
import { logger } from '@/ui/logger'
|
||||
import type { RpcHandlerManager } from '@/api/rpc/RpcHandlerManager'
|
||||
import { run as runRipgrep } from '@/modules/ripgrep/index'
|
||||
import { validatePath } from '../pathSecurity'
|
||||
import { getErrorMessage, rpcError } from '../rpcResponses'
|
||||
|
||||
interface RipgrepRequest {
|
||||
args: string[]
|
||||
cwd?: string
|
||||
}
|
||||
|
||||
interface RipgrepResponse {
|
||||
success: boolean
|
||||
exitCode?: number
|
||||
stdout?: string
|
||||
stderr?: string
|
||||
error?: string
|
||||
}
|
||||
|
||||
export function registerRipgrepHandlers(rpcHandlerManager: RpcHandlerManager, workingDirectory: string): void {
|
||||
rpcHandlerManager.registerHandler<RipgrepRequest, RipgrepResponse>('ripgrep', async (data) => {
|
||||
logger.debug('Ripgrep request with args:', data.args, 'cwd:', data.cwd)
|
||||
|
||||
if (data.cwd) {
|
||||
const validation = validatePath(data.cwd, workingDirectory)
|
||||
if (!validation.valid) {
|
||||
return rpcError(validation.error ?? 'Invalid working directory')
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await runRipgrep(data.args, { cwd: data.cwd })
|
||||
return {
|
||||
success: true,
|
||||
exitCode: result.exitCode,
|
||||
stdout: result.stdout.toString(),
|
||||
stderr: result.stderr.toString()
|
||||
}
|
||||
} catch (error) {
|
||||
logger.debug('Failed to run ripgrep:', error)
|
||||
return rpcError(getErrorMessage(error, 'Failed to run ripgrep'))
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import { logger } from '@/ui/logger'
|
||||
import type { RpcHandlerManager } from '@/api/rpc/RpcHandlerManager'
|
||||
import { listSlashCommands, type ListSlashCommandsRequest, type ListSlashCommandsResponse } from '../slashCommands'
|
||||
import { getErrorMessage, rpcError } from '../rpcResponses'
|
||||
|
||||
export function registerSlashCommandHandlers(rpcHandlerManager: RpcHandlerManager): void {
|
||||
rpcHandlerManager.registerHandler<ListSlashCommandsRequest, ListSlashCommandsResponse>('listSlashCommands', async (data) => {
|
||||
logger.debug('List slash commands request for agent:', data.agent)
|
||||
|
||||
try {
|
||||
const commands = await listSlashCommands(data.agent)
|
||||
return { success: true, commands }
|
||||
} catch (error) {
|
||||
logger.debug('Failed to list slash commands:', error)
|
||||
return rpcError(getErrorMessage(error, 'Failed to list slash commands'))
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -1,507 +1,18 @@
|
||||
import { logger } from '@/ui/logger';
|
||||
import { exec, ExecOptions } from 'child_process';
|
||||
import { promisify } from 'util';
|
||||
import { readFile, writeFile, readdir, stat } from 'fs/promises';
|
||||
import { createHash } from 'crypto';
|
||||
import { basename, join, resolve } from 'path';
|
||||
import { run as runRipgrep } from '@/modules/ripgrep/index';
|
||||
import { run as runDifftastic } from '@/modules/difftastic/index';
|
||||
import { RpcHandlerManager } from '../../api/rpc/RpcHandlerManager';
|
||||
import { registerGitHandlers } from './gitHandlers';
|
||||
import { validatePath } from './pathSecurity';
|
||||
import { listSlashCommands, type ListSlashCommandsRequest, type ListSlashCommandsResponse } from './slashCommands';
|
||||
import type { RpcHandlerManager } from '@/api/rpc/RpcHandlerManager'
|
||||
import { registerBashHandlers } from './handlers/bash'
|
||||
import { registerDirectoryHandlers } from './handlers/directories'
|
||||
import { registerDifftasticHandlers } from './handlers/difftastic'
|
||||
import { registerFileHandlers } from './handlers/files'
|
||||
import { registerGitHandlers } from './gitHandlers'
|
||||
import { registerRipgrepHandlers } from './handlers/ripgrep'
|
||||
import { registerSlashCommandHandlers } from './handlers/slashCommands'
|
||||
|
||||
const execAsync = promisify(exec);
|
||||
|
||||
interface BashRequest {
|
||||
command: string;
|
||||
cwd?: string;
|
||||
timeout?: number; // timeout in milliseconds
|
||||
}
|
||||
|
||||
interface BashResponse {
|
||||
success: boolean;
|
||||
stdout?: string;
|
||||
stderr?: string;
|
||||
exitCode?: number;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
interface ReadFileRequest {
|
||||
path: string;
|
||||
}
|
||||
|
||||
interface ReadFileResponse {
|
||||
success: boolean;
|
||||
content?: string; // base64 encoded
|
||||
error?: string;
|
||||
}
|
||||
|
||||
interface WriteFileRequest {
|
||||
path: string;
|
||||
content: string; // base64 encoded
|
||||
expectedHash?: string | null; // null for new files, hash for existing files
|
||||
}
|
||||
|
||||
interface WriteFileResponse {
|
||||
success: boolean;
|
||||
hash?: string; // hash of written file
|
||||
error?: string;
|
||||
}
|
||||
|
||||
interface ListDirectoryRequest {
|
||||
path: string;
|
||||
}
|
||||
|
||||
interface DirectoryEntry {
|
||||
name: string;
|
||||
type: 'file' | 'directory' | 'other';
|
||||
size?: number;
|
||||
modified?: number; // timestamp
|
||||
}
|
||||
|
||||
interface ListDirectoryResponse {
|
||||
success: boolean;
|
||||
entries?: DirectoryEntry[];
|
||||
error?: string;
|
||||
}
|
||||
|
||||
interface GetDirectoryTreeRequest {
|
||||
path: string;
|
||||
maxDepth: number;
|
||||
}
|
||||
|
||||
interface TreeNode {
|
||||
name: string;
|
||||
path: string;
|
||||
type: 'file' | 'directory';
|
||||
size?: number;
|
||||
modified?: number;
|
||||
children?: TreeNode[]; // Only present for directories
|
||||
}
|
||||
|
||||
interface GetDirectoryTreeResponse {
|
||||
success: boolean;
|
||||
tree?: TreeNode;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
interface RipgrepRequest {
|
||||
args: string[];
|
||||
cwd?: string;
|
||||
}
|
||||
|
||||
interface RipgrepResponse {
|
||||
success: boolean;
|
||||
exitCode?: number;
|
||||
stdout?: string;
|
||||
stderr?: string;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
interface DifftasticRequest {
|
||||
args: string[];
|
||||
cwd?: string;
|
||||
}
|
||||
|
||||
interface DifftasticResponse {
|
||||
success: boolean;
|
||||
exitCode?: number;
|
||||
stdout?: string;
|
||||
stderr?: string;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
/*
|
||||
* Spawn Session Options and Result
|
||||
* This rpc type is used by the daemon, all other RPCs here are for sessions
|
||||
*/
|
||||
|
||||
export interface SpawnSessionOptions {
|
||||
machineId?: string;
|
||||
directory: string;
|
||||
sessionId?: string;
|
||||
approvedNewDirectoryCreation?: boolean;
|
||||
agent?: 'claude' | 'codex' | 'gemini';
|
||||
yolo?: boolean;
|
||||
token?: string;
|
||||
sessionType?: 'simple' | 'worktree';
|
||||
worktreeName?: string;
|
||||
}
|
||||
|
||||
export type SpawnSessionResult =
|
||||
| { type: 'success'; sessionId: string }
|
||||
| { type: 'requestToApproveDirectoryCreation'; directory: string }
|
||||
| { type: 'error'; errorMessage: string };
|
||||
|
||||
/**
|
||||
* Register all RPC handlers with the session
|
||||
*/
|
||||
export function registerCommonHandlers(rpcHandlerManager: RpcHandlerManager, workingDirectory: string) {
|
||||
|
||||
// Shell command handler - executes commands in the default shell
|
||||
rpcHandlerManager.registerHandler<BashRequest, BashResponse>('bash', async (data) => {
|
||||
logger.debug('Shell command request:', data.command);
|
||||
|
||||
// Validate cwd if provided
|
||||
if (data.cwd) {
|
||||
const validation = validatePath(data.cwd, workingDirectory);
|
||||
if (!validation.valid) {
|
||||
return { success: false, error: validation.error };
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
// Build options with shell enabled by default
|
||||
// Note: ExecOptions doesn't support boolean for shell, but exec() uses the default shell when shell is undefined
|
||||
const options: ExecOptions = {
|
||||
cwd: data.cwd,
|
||||
timeout: data.timeout || 30000, // Default 30 seconds timeout
|
||||
};
|
||||
|
||||
const { stdout, stderr } = await execAsync(data.command, options);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
stdout: stdout ? stdout.toString() : '',
|
||||
stderr: stderr ? stderr.toString() : '',
|
||||
exitCode: 0
|
||||
};
|
||||
} catch (error) {
|
||||
const execError = error as NodeJS.ErrnoException & {
|
||||
stdout?: string;
|
||||
stderr?: string;
|
||||
code?: number | string;
|
||||
killed?: boolean;
|
||||
};
|
||||
|
||||
// Check if the error was due to timeout
|
||||
if (execError.code === 'ETIMEDOUT' || execError.killed) {
|
||||
return {
|
||||
success: false,
|
||||
stdout: execError.stdout || '',
|
||||
stderr: execError.stderr || '',
|
||||
exitCode: typeof execError.code === 'number' ? execError.code : -1,
|
||||
error: 'Command timed out'
|
||||
};
|
||||
}
|
||||
|
||||
// If exec fails, it includes stdout/stderr in the error
|
||||
return {
|
||||
success: false,
|
||||
stdout: execError.stdout ? execError.stdout.toString() : '',
|
||||
stderr: execError.stderr ? execError.stderr.toString() : execError.message || 'Command failed',
|
||||
exitCode: typeof execError.code === 'number' ? execError.code : 1,
|
||||
error: execError.message || 'Command failed'
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
// Read file handler - returns base64 encoded content
|
||||
rpcHandlerManager.registerHandler<ReadFileRequest, ReadFileResponse>('readFile', async (data) => {
|
||||
logger.debug('Read file request:', data.path);
|
||||
|
||||
// Validate path is within working directory
|
||||
const validation = validatePath(data.path, workingDirectory);
|
||||
if (!validation.valid) {
|
||||
return { success: false, error: validation.error };
|
||||
}
|
||||
|
||||
try {
|
||||
const resolvedPath = resolve(workingDirectory, data.path);
|
||||
const buffer = await readFile(resolvedPath);
|
||||
const content = buffer.toString('base64');
|
||||
return { success: true, content };
|
||||
} catch (error) {
|
||||
logger.debug('Failed to read file:', error);
|
||||
return { success: false, error: error instanceof Error ? error.message : 'Failed to read file' };
|
||||
}
|
||||
});
|
||||
|
||||
// Write file handler - with hash verification
|
||||
rpcHandlerManager.registerHandler<WriteFileRequest, WriteFileResponse>('writeFile', async (data) => {
|
||||
logger.debug('Write file request:', data.path);
|
||||
|
||||
// Validate path is within working directory
|
||||
const validation = validatePath(data.path, workingDirectory);
|
||||
if (!validation.valid) {
|
||||
return { success: false, error: validation.error };
|
||||
}
|
||||
|
||||
try {
|
||||
// If expectedHash is provided (not null), verify existing file
|
||||
if (data.expectedHash !== null && data.expectedHash !== undefined) {
|
||||
try {
|
||||
const existingBuffer = await readFile(data.path);
|
||||
const existingHash = createHash('sha256').update(existingBuffer).digest('hex');
|
||||
|
||||
if (existingHash !== data.expectedHash) {
|
||||
return {
|
||||
success: false,
|
||||
error: `File hash mismatch. Expected: ${data.expectedHash}, Actual: ${existingHash}`
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
const nodeError = error as NodeJS.ErrnoException;
|
||||
if (nodeError.code !== 'ENOENT') {
|
||||
throw error;
|
||||
}
|
||||
// File doesn't exist but hash was provided
|
||||
return {
|
||||
success: false,
|
||||
error: 'File does not exist but hash was provided'
|
||||
};
|
||||
}
|
||||
} else {
|
||||
// expectedHash is null - expecting new file
|
||||
try {
|
||||
await stat(data.path);
|
||||
// File exists but we expected it to be new
|
||||
return {
|
||||
success: false,
|
||||
error: 'File already exists but was expected to be new'
|
||||
};
|
||||
} catch (error) {
|
||||
const nodeError = error as NodeJS.ErrnoException;
|
||||
if (nodeError.code !== 'ENOENT') {
|
||||
throw error;
|
||||
}
|
||||
// File doesn't exist - this is expected
|
||||
}
|
||||
}
|
||||
|
||||
// Write the file
|
||||
const buffer = Buffer.from(data.content, 'base64');
|
||||
await writeFile(data.path, buffer);
|
||||
|
||||
// Calculate and return hash of written file
|
||||
const hash = createHash('sha256').update(buffer).digest('hex');
|
||||
|
||||
return { success: true, hash };
|
||||
} catch (error) {
|
||||
logger.debug('Failed to write file:', error);
|
||||
return { success: false, error: error instanceof Error ? error.message : 'Failed to write file' };
|
||||
}
|
||||
});
|
||||
|
||||
// List directory handler
|
||||
rpcHandlerManager.registerHandler<ListDirectoryRequest, ListDirectoryResponse>('listDirectory', async (data) => {
|
||||
logger.debug('List directory request:', data.path);
|
||||
|
||||
// Validate path is within working directory
|
||||
const validation = validatePath(data.path, workingDirectory);
|
||||
if (!validation.valid) {
|
||||
return { success: false, error: validation.error };
|
||||
}
|
||||
|
||||
try {
|
||||
const entries = await readdir(data.path, { withFileTypes: true });
|
||||
|
||||
const directoryEntries: DirectoryEntry[] = await Promise.all(
|
||||
entries.map(async (entry) => {
|
||||
const fullPath = join(data.path, entry.name);
|
||||
let type: 'file' | 'directory' | 'other' = 'other';
|
||||
let size: number | undefined;
|
||||
let modified: number | undefined;
|
||||
|
||||
if (entry.isDirectory()) {
|
||||
type = 'directory';
|
||||
} else if (entry.isFile()) {
|
||||
type = 'file';
|
||||
}
|
||||
|
||||
try {
|
||||
const stats = await stat(fullPath);
|
||||
size = stats.size;
|
||||
modified = stats.mtime.getTime();
|
||||
} catch (error) {
|
||||
// Ignore stat errors for individual files
|
||||
logger.debug(`Failed to stat ${fullPath}:`, error);
|
||||
}
|
||||
|
||||
return {
|
||||
name: entry.name,
|
||||
type,
|
||||
size,
|
||||
modified
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
// Sort entries: directories first, then files, alphabetically
|
||||
directoryEntries.sort((a, b) => {
|
||||
if (a.type === 'directory' && b.type !== 'directory') return -1;
|
||||
if (a.type !== 'directory' && b.type === 'directory') return 1;
|
||||
return a.name.localeCompare(b.name);
|
||||
});
|
||||
|
||||
return { success: true, entries: directoryEntries };
|
||||
} catch (error) {
|
||||
logger.debug('Failed to list directory:', error);
|
||||
return { success: false, error: error instanceof Error ? error.message : 'Failed to list directory' };
|
||||
}
|
||||
});
|
||||
|
||||
// Get directory tree handler - recursive with depth control
|
||||
rpcHandlerManager.registerHandler<GetDirectoryTreeRequest, GetDirectoryTreeResponse>('getDirectoryTree', async (data) => {
|
||||
logger.debug('Get directory tree request:', data.path, 'maxDepth:', data.maxDepth);
|
||||
|
||||
// Validate path is within working directory
|
||||
const validation = validatePath(data.path, workingDirectory);
|
||||
if (!validation.valid) {
|
||||
return { success: false, error: validation.error };
|
||||
}
|
||||
|
||||
// Helper function to build tree recursively
|
||||
async function buildTree(path: string, name: string, currentDepth: number): Promise<TreeNode | null> {
|
||||
try {
|
||||
const stats = await stat(path);
|
||||
|
||||
// Base node information
|
||||
const node: TreeNode = {
|
||||
name,
|
||||
path,
|
||||
type: stats.isDirectory() ? 'directory' : 'file',
|
||||
size: stats.size,
|
||||
modified: stats.mtime.getTime()
|
||||
};
|
||||
|
||||
// If it's a directory and we haven't reached max depth, get children
|
||||
if (stats.isDirectory() && currentDepth < data.maxDepth) {
|
||||
const entries = await readdir(path, { withFileTypes: true });
|
||||
const children: TreeNode[] = [];
|
||||
|
||||
// Process entries in parallel, filtering out symlinks
|
||||
await Promise.all(
|
||||
entries.map(async (entry) => {
|
||||
// Skip symbolic links completely
|
||||
if (entry.isSymbolicLink()) {
|
||||
logger.debug(`Skipping symlink: ${join(path, entry.name)}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const childPath = join(path, entry.name);
|
||||
const childNode = await buildTree(childPath, entry.name, currentDepth + 1);
|
||||
if (childNode) {
|
||||
children.push(childNode);
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
// Sort children: directories first, then files, alphabetically
|
||||
children.sort((a, b) => {
|
||||
if (a.type === 'directory' && b.type !== 'directory') return -1;
|
||||
if (a.type !== 'directory' && b.type === 'directory') return 1;
|
||||
return a.name.localeCompare(b.name);
|
||||
});
|
||||
|
||||
node.children = children;
|
||||
}
|
||||
|
||||
return node;
|
||||
} catch (error) {
|
||||
// Log error but continue traversal
|
||||
logger.debug(`Failed to process ${path}:`, error instanceof Error ? error.message : String(error));
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
// Validate maxDepth
|
||||
if (data.maxDepth < 0) {
|
||||
return { success: false, error: 'maxDepth must be non-negative' };
|
||||
}
|
||||
|
||||
// Get the base name for the root node (cross-platform)
|
||||
const baseName = data.path === '/' ? '/' : basename(data.path) || data.path;
|
||||
|
||||
// Build the tree starting from the requested path
|
||||
const tree = await buildTree(data.path, baseName, 0);
|
||||
|
||||
if (!tree) {
|
||||
return { success: false, error: 'Failed to access the specified path' };
|
||||
}
|
||||
|
||||
return { success: true, tree };
|
||||
} catch (error) {
|
||||
logger.debug('Failed to get directory tree:', error);
|
||||
return { success: false, error: error instanceof Error ? error.message : 'Failed to get directory tree' };
|
||||
}
|
||||
});
|
||||
|
||||
// Ripgrep handler - raw interface to ripgrep
|
||||
rpcHandlerManager.registerHandler<RipgrepRequest, RipgrepResponse>('ripgrep', async (data) => {
|
||||
logger.debug('Ripgrep request with args:', data.args, 'cwd:', data.cwd);
|
||||
|
||||
// Validate cwd if provided
|
||||
if (data.cwd) {
|
||||
const validation = validatePath(data.cwd, workingDirectory);
|
||||
if (!validation.valid) {
|
||||
return { success: false, error: validation.error };
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await runRipgrep(data.args, { cwd: data.cwd });
|
||||
return {
|
||||
success: true,
|
||||
exitCode: result.exitCode,
|
||||
stdout: result.stdout.toString(),
|
||||
stderr: result.stderr.toString()
|
||||
};
|
||||
} catch (error) {
|
||||
logger.debug('Failed to run ripgrep:', error);
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Failed to run ripgrep'
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
// Difftastic handler - raw interface to difftastic
|
||||
rpcHandlerManager.registerHandler<DifftasticRequest, DifftasticResponse>('difftastic', async (data) => {
|
||||
logger.debug('Difftastic request with args:', data.args, 'cwd:', data.cwd);
|
||||
|
||||
// Validate cwd if provided
|
||||
if (data.cwd) {
|
||||
const validation = validatePath(data.cwd, workingDirectory);
|
||||
if (!validation.valid) {
|
||||
return { success: false, error: validation.error };
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await runDifftastic(data.args, { cwd: data.cwd });
|
||||
return {
|
||||
success: true,
|
||||
exitCode: result.exitCode,
|
||||
stdout: result.stdout.toString(),
|
||||
stderr: result.stderr.toString()
|
||||
};
|
||||
} catch (error) {
|
||||
logger.debug('Failed to run difftastic:', error);
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Failed to run difftastic'
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
// Slash commands handler - lists available slash commands for an agent
|
||||
rpcHandlerManager.registerHandler<ListSlashCommandsRequest, ListSlashCommandsResponse>('listSlashCommands', async (data) => {
|
||||
logger.debug('List slash commands request for agent:', data.agent);
|
||||
|
||||
try {
|
||||
const commands = await listSlashCommands(data.agent);
|
||||
return { success: true, commands };
|
||||
} catch (error) {
|
||||
logger.debug('Failed to list slash commands:', error);
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Failed to list slash commands'
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
registerGitHandlers(rpcHandlerManager, workingDirectory);
|
||||
export function registerCommonHandlers(rpcHandlerManager: RpcHandlerManager, workingDirectory: string): void {
|
||||
registerBashHandlers(rpcHandlerManager, workingDirectory)
|
||||
registerFileHandlers(rpcHandlerManager, workingDirectory)
|
||||
registerDirectoryHandlers(rpcHandlerManager, workingDirectory)
|
||||
registerRipgrepHandlers(rpcHandlerManager, workingDirectory)
|
||||
registerDifftasticHandlers(rpcHandlerManager, workingDirectory)
|
||||
registerSlashCommandHandlers(rpcHandlerManager)
|
||||
registerGitHandlers(rpcHandlerManager, workingDirectory)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
export type RpcErrorResponse = { success: false; error: string }
|
||||
|
||||
export type RpcSuccessResponse<T extends object> = { success: true } & T
|
||||
|
||||
export function rpcError<T extends Record<string, unknown> = Record<string, unknown>>(
|
||||
message: string,
|
||||
extras?: T
|
||||
): RpcErrorResponse & T {
|
||||
const payload = {
|
||||
success: false,
|
||||
error: message,
|
||||
...(extras ?? {})
|
||||
}
|
||||
|
||||
return payload as RpcErrorResponse & T
|
||||
}
|
||||
|
||||
export function getErrorMessage(error: unknown, fallback: string): string {
|
||||
if (error instanceof Error && error.message) {
|
||||
return error.message
|
||||
}
|
||||
return fallback
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
export interface SpawnSessionOptions {
|
||||
machineId?: string
|
||||
directory: string
|
||||
sessionId?: string
|
||||
approvedNewDirectoryCreation?: boolean
|
||||
agent?: 'claude' | 'codex' | 'gemini'
|
||||
yolo?: boolean
|
||||
token?: string
|
||||
sessionType?: 'simple' | 'worktree'
|
||||
worktreeName?: string
|
||||
}
|
||||
|
||||
export type SpawnSessionResult =
|
||||
| { type: 'success'; sessionId: string }
|
||||
| { type: 'requestToApproveDirectoryCreation'; directory: string }
|
||||
| { type: 'error'; errorMessage: string }
|
||||
Reference in New Issue
Block a user