mirror of
https://github.com/aljazceru/claude-code-viewer.git
synced 2025-12-20 14:54:19 +01:00
refactor: split request handle logic to controller
This commit is contained in:
@@ -1,15 +1,25 @@
|
|||||||
import { NodeContext } from "@effect/platform-node";
|
import { NodeContext } from "@effect/platform-node";
|
||||||
import { Effect } from "effect";
|
import { Effect, Layer } from "effect";
|
||||||
import { handle } from "hono/vercel";
|
import { handle } from "hono/vercel";
|
||||||
|
import { ClaudeCodeController } from "../../../server/core/claude-code/presentation/ClaudeCodeController";
|
||||||
|
import { ClaudeCodePermissionController } from "../../../server/core/claude-code/presentation/ClaudeCodePermissionController";
|
||||||
|
import { ClaudeCodeSessionProcessController } from "../../../server/core/claude-code/presentation/ClaudeCodeSessionProcessController";
|
||||||
import { ClaudeCodeLifeCycleService } from "../../../server/core/claude-code/services/ClaudeCodeLifeCycleService";
|
import { ClaudeCodeLifeCycleService } from "../../../server/core/claude-code/services/ClaudeCodeLifeCycleService";
|
||||||
import { ClaudeCodePermissionService } from "../../../server/core/claude-code/services/ClaudeCodePermissionService";
|
import { ClaudeCodePermissionService } from "../../../server/core/claude-code/services/ClaudeCodePermissionService";
|
||||||
|
import { ClaudeCodeService } from "../../../server/core/claude-code/services/ClaudeCodeService";
|
||||||
import { ClaudeCodeSessionProcessService } from "../../../server/core/claude-code/services/ClaudeCodeSessionProcessService";
|
import { ClaudeCodeSessionProcessService } from "../../../server/core/claude-code/services/ClaudeCodeSessionProcessService";
|
||||||
|
import { SSEController } from "../../../server/core/events/presentation/SSEController";
|
||||||
import { EventBus } from "../../../server/core/events/services/EventBus";
|
import { EventBus } from "../../../server/core/events/services/EventBus";
|
||||||
import { FileWatcherService } from "../../../server/core/events/services/fileWatcher";
|
import { FileWatcherService } from "../../../server/core/events/services/fileWatcher";
|
||||||
|
import { FileSystemController } from "../../../server/core/file-system/presentation/FileSystemController";
|
||||||
|
import { GitController } from "../../../server/core/git/presentation/GitController";
|
||||||
|
import { HonoConfigService } from "../../../server/core/hono/services/HonoConfigService";
|
||||||
import { ProjectRepository } from "../../../server/core/project/infrastructure/ProjectRepository";
|
import { ProjectRepository } from "../../../server/core/project/infrastructure/ProjectRepository";
|
||||||
|
import { ProjectController } from "../../../server/core/project/presentation/ProjectController";
|
||||||
import { ProjectMetaService } from "../../../server/core/project/services/ProjectMetaService";
|
import { ProjectMetaService } from "../../../server/core/project/services/ProjectMetaService";
|
||||||
import { SessionRepository } from "../../../server/core/session/infrastructure/SessionRepository";
|
import { SessionRepository } from "../../../server/core/session/infrastructure/SessionRepository";
|
||||||
import { VirtualConversationDatabase } from "../../../server/core/session/infrastructure/VirtualConversationDatabase";
|
import { VirtualConversationDatabase } from "../../../server/core/session/infrastructure/VirtualConversationDatabase";
|
||||||
|
import { SessionController } from "../../../server/core/session/presentation/SessionController";
|
||||||
import { SessionMetaService } from "../../../server/core/session/services/SessionMetaService";
|
import { SessionMetaService } from "../../../server/core/session/services/SessionMetaService";
|
||||||
import { honoApp } from "../../../server/hono/app";
|
import { honoApp } from "../../../server/hono/app";
|
||||||
import { InitializeService } from "../../../server/hono/initialize";
|
import { InitializeService } from "../../../server/hono/initialize";
|
||||||
@@ -17,10 +27,27 @@ import { routes } from "../../../server/hono/route";
|
|||||||
|
|
||||||
const program = routes(honoApp);
|
const program = routes(honoApp);
|
||||||
|
|
||||||
|
/** Max count of pipe is 20, so merge some layers here */
|
||||||
|
const storageLayer = Layer.mergeAll(
|
||||||
|
ProjectMetaService.Live,
|
||||||
|
SessionMetaService.Live,
|
||||||
|
VirtualConversationDatabase.Live,
|
||||||
|
);
|
||||||
|
|
||||||
await Effect.runPromise(
|
await Effect.runPromise(
|
||||||
program.pipe(
|
program.pipe(
|
||||||
// 依存の浅い順にコンテナに pipe する必要がある
|
// 依存の浅い順にコンテナに pipe する必要がある
|
||||||
|
|
||||||
|
/** Presentation */
|
||||||
|
Effect.provide(ProjectController.Live),
|
||||||
|
Effect.provide(SessionController.Live),
|
||||||
|
Effect.provide(GitController.Live),
|
||||||
|
Effect.provide(ClaudeCodeController.Live),
|
||||||
|
Effect.provide(ClaudeCodeSessionProcessController.Live),
|
||||||
|
Effect.provide(ClaudeCodePermissionController.Live),
|
||||||
|
Effect.provide(FileSystemController.Live),
|
||||||
|
Effect.provide(SSEController.Live),
|
||||||
|
|
||||||
/** Application */
|
/** Application */
|
||||||
Effect.provide(InitializeService.Live),
|
Effect.provide(InitializeService.Live),
|
||||||
|
|
||||||
@@ -28,10 +55,12 @@ await Effect.runPromise(
|
|||||||
Effect.provide(ClaudeCodeLifeCycleService.Live),
|
Effect.provide(ClaudeCodeLifeCycleService.Live),
|
||||||
Effect.provide(ClaudeCodePermissionService.Live),
|
Effect.provide(ClaudeCodePermissionService.Live),
|
||||||
Effect.provide(ClaudeCodeSessionProcessService.Live),
|
Effect.provide(ClaudeCodeSessionProcessService.Live),
|
||||||
|
Effect.provide(ClaudeCodeService.Live),
|
||||||
|
|
||||||
// Shared Services
|
// Shared Services
|
||||||
Effect.provide(FileWatcherService.Live),
|
Effect.provide(FileWatcherService.Live),
|
||||||
Effect.provide(EventBus.Live),
|
Effect.provide(EventBus.Live),
|
||||||
|
Effect.provide(HonoConfigService.Live),
|
||||||
|
|
||||||
/** Infrastructure */
|
/** Infrastructure */
|
||||||
|
|
||||||
@@ -40,9 +69,7 @@ await Effect.runPromise(
|
|||||||
Effect.provide(SessionRepository.Live),
|
Effect.provide(SessionRepository.Live),
|
||||||
|
|
||||||
// StorageService
|
// StorageService
|
||||||
Effect.provide(ProjectMetaService.Live),
|
Effect.provide(storageLayer),
|
||||||
Effect.provide(SessionMetaService.Live),
|
|
||||||
Effect.provide(VirtualConversationDatabase.Live),
|
|
||||||
|
|
||||||
/** Platform */
|
/** Platform */
|
||||||
Effect.provide(NodeContext.layer),
|
Effect.provide(NodeContext.layer),
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import type { DirectoryListingResult } from "../../server/core/directory-browser/functions/getDirectoryListing";
|
import type { DirectoryListingResult } from "../../server/core/file-system/functions/getDirectoryListing";
|
||||||
import type { FileCompletionResult } from "../../server/core/file-completion/functions/getFileCompletion";
|
import type { FileCompletionResult } from "../../server/core/file-system/functions/getFileCompletion";
|
||||||
import { honoClient } from "./client";
|
import { honoClient } from "./client";
|
||||||
|
|
||||||
export const projectListQuery = {
|
export const projectListQuery = {
|
||||||
@@ -21,7 +21,7 @@ export const directoryListingQuery = (currentPath?: string) =>
|
|||||||
({
|
({
|
||||||
queryKey: ["directory-listing", currentPath],
|
queryKey: ["directory-listing", currentPath],
|
||||||
queryFn: async (): Promise<DirectoryListingResult> => {
|
queryFn: async (): Promise<DirectoryListingResult> => {
|
||||||
const response = await honoClient.api["directory-browser"].$get({
|
const response = await honoClient.api.fs["directory-browser"].$get({
|
||||||
query: currentPath ? { currentPath } : {},
|
query: currentPath ? { currentPath } : {},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -182,11 +182,8 @@ export const fileCompletionQuery = (projectId: string, basePath: string) =>
|
|||||||
({
|
({
|
||||||
queryKey: ["file-completion", projectId, basePath],
|
queryKey: ["file-completion", projectId, basePath],
|
||||||
queryFn: async (): Promise<FileCompletionResult> => {
|
queryFn: async (): Promise<FileCompletionResult> => {
|
||||||
const response = await honoClient.api.projects[":projectId"][
|
const response = await honoClient.api.fs["file-completion"].$get({
|
||||||
"file-completion"
|
query: { basePath, projectId },
|
||||||
].$get({
|
|
||||||
param: { projectId },
|
|
||||||
query: { basePath },
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
|
|||||||
@@ -0,0 +1,79 @@
|
|||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
import { parseMcpListOutput } from "./parseMcpListOutput";
|
||||||
|
|
||||||
|
describe("parseMcpListOutput", () => {
|
||||||
|
it("should parse claude mcp list output correctly", async () => {
|
||||||
|
const output = `2.0.21 (Claude Code)
|
||||||
|
Checking MCP server health...
|
||||||
|
|
||||||
|
context7: npx -y @upstash/context7-mcp@latest - ✓ Connected
|
||||||
|
`;
|
||||||
|
|
||||||
|
const result = parseMcpListOutput(output);
|
||||||
|
|
||||||
|
expect(result).toEqual([
|
||||||
|
{
|
||||||
|
name: "context7",
|
||||||
|
command: "npx -y @upstash/context7-mcp@latest",
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle multiple MCP servers", async () => {
|
||||||
|
const output = `2.0.21 (Claude Code)
|
||||||
|
Checking MCP server health...
|
||||||
|
|
||||||
|
context7: npx -y @upstash/context7-mcp@latest - ✓ Connected
|
||||||
|
filesystem: /usr/local/bin/mcp-server-fs - ✓ Connected
|
||||||
|
database: docker run db-mcp - ✗ Failed
|
||||||
|
`;
|
||||||
|
|
||||||
|
const result = parseMcpListOutput(output);
|
||||||
|
|
||||||
|
expect(result).toEqual([
|
||||||
|
{
|
||||||
|
name: "context7",
|
||||||
|
command: "npx -y @upstash/context7-mcp@latest",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "filesystem",
|
||||||
|
command: "/usr/local/bin/mcp-server-fs",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "database",
|
||||||
|
command: "docker run db-mcp",
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return empty array for output with no MCP servers", async () => {
|
||||||
|
const output = `2.0.21 (Claude Code)
|
||||||
|
Checking MCP server health...
|
||||||
|
|
||||||
|
`;
|
||||||
|
|
||||||
|
const result = parseMcpListOutput(output);
|
||||||
|
|
||||||
|
expect(result).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should skip malformed lines", async () => {
|
||||||
|
const output = `2.0.21 (Claude Code)
|
||||||
|
Checking MCP server health...
|
||||||
|
|
||||||
|
context7: npx -y @upstash/context7-mcp@latest - ✓ Connected
|
||||||
|
invalid line without colon
|
||||||
|
: command without name
|
||||||
|
name without command:
|
||||||
|
`;
|
||||||
|
|
||||||
|
const result = parseMcpListOutput(output);
|
||||||
|
|
||||||
|
expect(result).toEqual([
|
||||||
|
{
|
||||||
|
name: "context7",
|
||||||
|
command: "npx -y @upstash/context7-mcp@latest",
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
32
src/server/core/claude-code/functions/parseMcpListOutput.ts
Normal file
32
src/server/core/claude-code/functions/parseMcpListOutput.ts
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
export interface McpServer {
|
||||||
|
name: string;
|
||||||
|
command: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const parseMcpListOutput = (output: string) => {
|
||||||
|
const servers: McpServer[] = [];
|
||||||
|
const lines = output.trim().split("\n");
|
||||||
|
|
||||||
|
for (const line of lines) {
|
||||||
|
// Skip header lines and status indicators
|
||||||
|
if (line.includes("Checking MCP server health") || line.trim() === "") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse lines like "context7: npx -y @upstash/context7-mcp@latest - ✓ Connected"
|
||||||
|
const colonIndex = line.indexOf(":");
|
||||||
|
if (colonIndex > 0) {
|
||||||
|
const name = line.substring(0, colonIndex).trim();
|
||||||
|
const rest = line.substring(colonIndex + 1).trim();
|
||||||
|
|
||||||
|
// Remove status indicators (✓ Connected, ✗ Failed, etc.)
|
||||||
|
const command = rest.replace(/\s*-\s*[✓✗].*$/, "").trim();
|
||||||
|
|
||||||
|
if (name && command) {
|
||||||
|
servers.push({ name, command });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return servers;
|
||||||
|
};
|
||||||
@@ -37,6 +37,22 @@ export const Config = Effect.gen(function* () {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const getMcpListOutput = (projectCwd: string) =>
|
||||||
|
Effect.gen(function* () {
|
||||||
|
const { claudeCodeExecutablePath } = yield* Config;
|
||||||
|
const output = yield* Command.string(
|
||||||
|
Command.make(
|
||||||
|
"cd",
|
||||||
|
projectCwd,
|
||||||
|
"&&",
|
||||||
|
claudeCodeExecutablePath,
|
||||||
|
"mcp",
|
||||||
|
"list",
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return output;
|
||||||
|
});
|
||||||
|
|
||||||
export const getAvailableFeatures = (
|
export const getAvailableFeatures = (
|
||||||
claudeCodeVersion: ClaudeCodeVersion.ClaudeCodeVersion | null,
|
claudeCodeVersion: ClaudeCodeVersion.ClaudeCodeVersion | null,
|
||||||
) => ({
|
) => ({
|
||||||
|
|||||||
@@ -0,0 +1,94 @@
|
|||||||
|
import { Context, Effect, Layer } from "effect";
|
||||||
|
import { claudeCommandsDirPath } from "../../../lib/config/paths";
|
||||||
|
import type { ControllerResponse } from "../../../lib/effect/toEffectResponse";
|
||||||
|
import type { InferEffect } from "../../../lib/effect/types";
|
||||||
|
import { ProjectRepository } from "../../project/infrastructure/ProjectRepository";
|
||||||
|
import { ClaudeCodeService } from "../services/ClaudeCodeService";
|
||||||
|
import { FileSystem, Path } from "@effect/platform";
|
||||||
|
|
||||||
|
const LayerImpl = Effect.gen(function* () {
|
||||||
|
const projectRepository = yield* ProjectRepository;
|
||||||
|
const claudeCodeService = yield* ClaudeCodeService;
|
||||||
|
const fs = yield* FileSystem.FileSystem;
|
||||||
|
const path = yield* Path.Path;
|
||||||
|
|
||||||
|
const getClaudeCommands = (options: { projectId: string }) =>
|
||||||
|
Effect.gen(function* () {
|
||||||
|
const { projectId } = options;
|
||||||
|
|
||||||
|
const { project } = yield* projectRepository.getProject(projectId);
|
||||||
|
|
||||||
|
const globalCommands: string[] = yield* fs
|
||||||
|
.readDirectory(claudeCommandsDirPath)
|
||||||
|
.pipe(
|
||||||
|
Effect.map((items) =>
|
||||||
|
items
|
||||||
|
.filter((item) => item.endsWith(".md"))
|
||||||
|
.map((item) => item.replace(/\.md$/, "")),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.pipe(
|
||||||
|
Effect.match({
|
||||||
|
onSuccess: (items) => items,
|
||||||
|
onFailure: () => {
|
||||||
|
return [];
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
const projectCommands: string[] =
|
||||||
|
project.meta.projectPath === null
|
||||||
|
? []
|
||||||
|
: yield* fs
|
||||||
|
.readDirectory(
|
||||||
|
path.resolve(project.meta.projectPath, ".claude", "commands"),
|
||||||
|
)
|
||||||
|
.pipe(
|
||||||
|
Effect.map((items) =>
|
||||||
|
items
|
||||||
|
.filter((item) => item.endsWith(".md"))
|
||||||
|
.map((item) => item.replace(/\.md$/, "")),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.pipe(
|
||||||
|
Effect.match({
|
||||||
|
onSuccess: (items) => items,
|
||||||
|
onFailure: () => {
|
||||||
|
return [];
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
response: {
|
||||||
|
globalCommands: globalCommands,
|
||||||
|
projectCommands: projectCommands,
|
||||||
|
defaultCommands: ["init", "compact"],
|
||||||
|
},
|
||||||
|
status: 200,
|
||||||
|
} as const satisfies ControllerResponse;
|
||||||
|
});
|
||||||
|
|
||||||
|
const getMcpListRoute = (options: { projectId: string }) =>
|
||||||
|
Effect.gen(function* () {
|
||||||
|
const { projectId } = options;
|
||||||
|
const servers = yield* claudeCodeService.getMcpList(projectId);
|
||||||
|
return {
|
||||||
|
response: { servers },
|
||||||
|
status: 200,
|
||||||
|
} as const satisfies ControllerResponse;
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
getClaudeCommands,
|
||||||
|
getMcpListRoute,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
export type IClaudeCodeController = InferEffect<typeof LayerImpl>;
|
||||||
|
export class ClaudeCodeController extends Context.Tag("ClaudeCodeController")<
|
||||||
|
ClaudeCodeController,
|
||||||
|
IClaudeCodeController
|
||||||
|
>() {
|
||||||
|
static Live = Layer.effect(this, LayerImpl);
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
import { Context, Effect, Layer } from "effect";
|
||||||
|
import type { PermissionResponse } from "../../../../types/permissions";
|
||||||
|
import type { ControllerResponse } from "../../../lib/effect/toEffectResponse";
|
||||||
|
import type { InferEffect } from "../../../lib/effect/types";
|
||||||
|
import { ClaudeCodePermissionService } from "../services/ClaudeCodePermissionService";
|
||||||
|
|
||||||
|
const LayerImpl = Effect.gen(function* () {
|
||||||
|
const claudeCodePermissionService = yield* ClaudeCodePermissionService;
|
||||||
|
|
||||||
|
const permissionResponse = (options: {
|
||||||
|
permissionResponse: PermissionResponse;
|
||||||
|
}) =>
|
||||||
|
Effect.sync(() => {
|
||||||
|
const { permissionResponse } = options;
|
||||||
|
|
||||||
|
Effect.runFork(
|
||||||
|
claudeCodePermissionService.respondToPermissionRequest(
|
||||||
|
permissionResponse,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
status: 200,
|
||||||
|
response: {
|
||||||
|
message: "Permission response received",
|
||||||
|
},
|
||||||
|
} as const satisfies ControllerResponse;
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
permissionResponse,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
export type IClaudeCodePermissionController = InferEffect<typeof LayerImpl>;
|
||||||
|
export class ClaudeCodePermissionController extends Context.Tag(
|
||||||
|
"ClaudeCodePermissionController",
|
||||||
|
)<ClaudeCodePermissionController, IClaudeCodePermissionController>() {
|
||||||
|
static Live = Layer.effect(this, LayerImpl);
|
||||||
|
}
|
||||||
@@ -0,0 +1,125 @@
|
|||||||
|
import { Context, Effect, Layer } from "effect";
|
||||||
|
import type { PublicSessionProcess } from "../../../../types/session-process";
|
||||||
|
import type { ControllerResponse } from "../../../lib/effect/toEffectResponse";
|
||||||
|
import type { InferEffect } from "../../../lib/effect/types";
|
||||||
|
import { HonoConfigService } from "../../hono/services/HonoConfigService";
|
||||||
|
import { ProjectRepository } from "../../project/infrastructure/ProjectRepository";
|
||||||
|
import { ClaudeCodeLifeCycleService } from "../services/ClaudeCodeLifeCycleService";
|
||||||
|
|
||||||
|
const LayerImpl = Effect.gen(function* () {
|
||||||
|
const projectRepository = yield* ProjectRepository;
|
||||||
|
const claudeCodeLifeCycleService = yield* ClaudeCodeLifeCycleService;
|
||||||
|
const honoConfigService = yield* HonoConfigService;
|
||||||
|
|
||||||
|
const getSessionProcesses = () =>
|
||||||
|
Effect.gen(function* () {
|
||||||
|
const publicSessionProcesses =
|
||||||
|
yield* claudeCodeLifeCycleService.getPublicSessionProcesses();
|
||||||
|
|
||||||
|
return {
|
||||||
|
response: {
|
||||||
|
processes: publicSessionProcesses.map(
|
||||||
|
(p): PublicSessionProcess => ({
|
||||||
|
id: p.def.sessionProcessId,
|
||||||
|
projectId: p.def.projectId,
|
||||||
|
sessionId: p.sessionId,
|
||||||
|
status: p.type === "paused" ? "paused" : "running",
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
status: 200,
|
||||||
|
} as const satisfies ControllerResponse;
|
||||||
|
});
|
||||||
|
|
||||||
|
const createSessionProcess = (options: {
|
||||||
|
projectId: string;
|
||||||
|
message: string;
|
||||||
|
baseSessionId?: string | undefined;
|
||||||
|
}) =>
|
||||||
|
Effect.gen(function* () {
|
||||||
|
const { projectId, message, baseSessionId } = options;
|
||||||
|
|
||||||
|
const { project } = yield* projectRepository.getProject(projectId);
|
||||||
|
const config = yield* honoConfigService.getConfig();
|
||||||
|
|
||||||
|
if (project.meta.projectPath === null) {
|
||||||
|
return {
|
||||||
|
response: { error: "Project path not found" },
|
||||||
|
status: 400 as const,
|
||||||
|
} as const satisfies ControllerResponse;
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = yield* claudeCodeLifeCycleService.startTask({
|
||||||
|
baseSession: {
|
||||||
|
cwd: project.meta.projectPath,
|
||||||
|
projectId,
|
||||||
|
sessionId: baseSessionId,
|
||||||
|
},
|
||||||
|
config: config,
|
||||||
|
message,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { sessionId } = yield* result.yieldSessionInitialized();
|
||||||
|
|
||||||
|
return {
|
||||||
|
status: 201 as const,
|
||||||
|
response: {
|
||||||
|
sessionProcess: {
|
||||||
|
id: result.sessionProcess.def.sessionProcessId,
|
||||||
|
projectId,
|
||||||
|
sessionId,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} as const satisfies ControllerResponse;
|
||||||
|
});
|
||||||
|
|
||||||
|
const continueSessionProcess = (options: {
|
||||||
|
projectId: string;
|
||||||
|
continueMessage: string;
|
||||||
|
baseSessionId: string;
|
||||||
|
sessionProcessId: string;
|
||||||
|
}) =>
|
||||||
|
Effect.gen(function* () {
|
||||||
|
const { projectId, continueMessage, baseSessionId, sessionProcessId } =
|
||||||
|
options;
|
||||||
|
|
||||||
|
const { project } = yield* projectRepository.getProject(projectId);
|
||||||
|
|
||||||
|
if (project.meta.projectPath === null) {
|
||||||
|
return {
|
||||||
|
response: { error: "Project path not found" },
|
||||||
|
status: 400,
|
||||||
|
} as const satisfies ControllerResponse;
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = yield* claudeCodeLifeCycleService.continueTask({
|
||||||
|
sessionProcessId,
|
||||||
|
message: continueMessage,
|
||||||
|
baseSessionId,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
response: {
|
||||||
|
sessionProcess: {
|
||||||
|
id: result.sessionProcess.def.sessionProcessId,
|
||||||
|
projectId: result.sessionProcess.def.projectId,
|
||||||
|
sessionId: baseSessionId,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
status: 200,
|
||||||
|
} as const satisfies ControllerResponse;
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
getSessionProcesses,
|
||||||
|
createSessionProcess,
|
||||||
|
continueSessionProcess,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
export type IClaudeCodeSessionProcessController = InferEffect<typeof LayerImpl>;
|
||||||
|
export class ClaudeCodeSessionProcessController extends Context.Tag(
|
||||||
|
"ClaudeCodeSessionProcessController",
|
||||||
|
)<ClaudeCodeSessionProcessController, IClaudeCodeSessionProcessController>() {
|
||||||
|
static Live = Layer.effect(this, LayerImpl);
|
||||||
|
}
|
||||||
@@ -342,6 +342,10 @@ const LayerImpl = Effect.gen(function* () {
|
|||||||
await sessionInitializedPromise.promise,
|
await sessionInitializedPromise.promise,
|
||||||
awaitSessionFileCreated: async () =>
|
awaitSessionFileCreated: async () =>
|
||||||
await sessionFileCreatedPromise.promise,
|
await sessionFileCreatedPromise.promise,
|
||||||
|
yieldSessionInitialized: () =>
|
||||||
|
Effect.promise(() => sessionInitializedPromise.promise),
|
||||||
|
yieldSessionFileCreated: () =>
|
||||||
|
Effect.promise(() => sessionFileCreatedPromise.promise),
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
41
src/server/core/claude-code/services/ClaudeCodeService.ts
Normal file
41
src/server/core/claude-code/services/ClaudeCodeService.ts
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
import { Context, Data, Effect, Layer } from "effect";
|
||||||
|
import type { InferEffect } from "../../../lib/effect/types";
|
||||||
|
import { ProjectRepository } from "../../project/infrastructure/ProjectRepository";
|
||||||
|
import { parseMcpListOutput } from "../functions/parseMcpListOutput";
|
||||||
|
import * as ClaudeCode from "../models/ClaudeCode";
|
||||||
|
|
||||||
|
class ProjectPathNotFoundError extends Data.TaggedError(
|
||||||
|
"ProjectPathNotFoundError",
|
||||||
|
)<{
|
||||||
|
projectId: string;
|
||||||
|
}> {}
|
||||||
|
|
||||||
|
const LayerImpl = Effect.gen(function* () {
|
||||||
|
const projectRepository = yield* ProjectRepository;
|
||||||
|
|
||||||
|
const getMcpList = (projectId: string) =>
|
||||||
|
Effect.gen(function* () {
|
||||||
|
const { project } = yield* projectRepository.getProject(projectId);
|
||||||
|
if (project.meta.projectPath === null) {
|
||||||
|
return yield* Effect.fail(new ProjectPathNotFoundError({ projectId }));
|
||||||
|
}
|
||||||
|
|
||||||
|
const output = yield* ClaudeCode.getMcpListOutput(
|
||||||
|
project.meta.projectPath,
|
||||||
|
);
|
||||||
|
return parseMcpListOutput(output);
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
getMcpList,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
export type IClaudeCodeService = InferEffect<typeof LayerImpl>;
|
||||||
|
|
||||||
|
export class ClaudeCodeService extends Context.Tag("ClaudeCodeService")<
|
||||||
|
ClaudeCodeService,
|
||||||
|
IClaudeCodeService
|
||||||
|
>() {
|
||||||
|
static Live = Layer.effect(this, LayerImpl);
|
||||||
|
}
|
||||||
108
src/server/core/events/presentation/SSEController.ts
Normal file
108
src/server/core/events/presentation/SSEController.ts
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
import { Context, Effect, Layer } from "effect";
|
||||||
|
import type { SSEStreamingApi } from "hono/streaming";
|
||||||
|
import type { InferEffect } from "../../../lib/effect/types";
|
||||||
|
import { adaptInternalEventToSSE } from "../functions/adaptInternalEventToSSE";
|
||||||
|
import { TypeSafeSSE } from "../functions/typeSafeSSE";
|
||||||
|
import { EventBus } from "../services/EventBus";
|
||||||
|
import type { InternalEventDeclaration } from "../types/InternalEventDeclaration";
|
||||||
|
|
||||||
|
const LayerImpl = Effect.gen(function* () {
|
||||||
|
const eventBus = yield* EventBus;
|
||||||
|
|
||||||
|
const handleSSE = (rawStream: SSEStreamingApi) =>
|
||||||
|
Effect.gen(function* () {
|
||||||
|
const typeSafeSSE = yield* TypeSafeSSE;
|
||||||
|
|
||||||
|
// Send connect event
|
||||||
|
yield* typeSafeSSE.writeSSE("connect", {
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const onHeartbeat = () => {
|
||||||
|
Effect.runFork(
|
||||||
|
typeSafeSSE.writeSSE("heartbeat", {
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onSessionListChanged = (
|
||||||
|
event: InternalEventDeclaration["sessionListChanged"],
|
||||||
|
) => {
|
||||||
|
Effect.runFork(
|
||||||
|
typeSafeSSE.writeSSE("sessionListChanged", {
|
||||||
|
projectId: event.projectId,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onSessionChanged = (
|
||||||
|
event: InternalEventDeclaration["sessionChanged"],
|
||||||
|
) => {
|
||||||
|
Effect.runFork(
|
||||||
|
typeSafeSSE.writeSSE("sessionChanged", {
|
||||||
|
projectId: event.projectId,
|
||||||
|
sessionId: event.sessionId,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onSessionProcessChanged = (
|
||||||
|
event: InternalEventDeclaration["sessionProcessChanged"],
|
||||||
|
) => {
|
||||||
|
Effect.runFork(
|
||||||
|
typeSafeSSE.writeSSE("sessionProcessChanged", {
|
||||||
|
processes: event.processes,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onPermissionRequested = (
|
||||||
|
event: InternalEventDeclaration["permissionRequested"],
|
||||||
|
) => {
|
||||||
|
Effect.runFork(
|
||||||
|
typeSafeSSE.writeSSE("permissionRequested", {
|
||||||
|
permissionRequest: event.permissionRequest,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
yield* eventBus.on("sessionListChanged", onSessionListChanged);
|
||||||
|
yield* eventBus.on("sessionChanged", onSessionChanged);
|
||||||
|
yield* eventBus.on("sessionProcessChanged", onSessionProcessChanged);
|
||||||
|
yield* eventBus.on("heartbeat", onHeartbeat);
|
||||||
|
yield* eventBus.on("permissionRequested", onPermissionRequested);
|
||||||
|
|
||||||
|
const { connectionPromise } = adaptInternalEventToSSE(rawStream, {
|
||||||
|
timeout: 5 /* min */ * 60 /* sec */ * 1000,
|
||||||
|
cleanUp: async () => {
|
||||||
|
await Effect.runPromise(
|
||||||
|
Effect.gen(function* () {
|
||||||
|
yield* eventBus.off("sessionListChanged", onSessionListChanged);
|
||||||
|
yield* eventBus.off("sessionChanged", onSessionChanged);
|
||||||
|
yield* eventBus.off(
|
||||||
|
"sessionProcessChanged",
|
||||||
|
onSessionProcessChanged,
|
||||||
|
);
|
||||||
|
yield* eventBus.off("heartbeat", onHeartbeat);
|
||||||
|
yield* eventBus.off("permissionRequested", onPermissionRequested);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
yield* Effect.promise(() => connectionPromise);
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
handleSSE,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
export type ISSEController = InferEffect<typeof LayerImpl>;
|
||||||
|
export class SSEController extends Context.Tag("SSEController")<
|
||||||
|
SSEController,
|
||||||
|
ISSEController
|
||||||
|
>() {
|
||||||
|
static Live = Layer.effect(this, LayerImpl);
|
||||||
|
}
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
import { homedir } from "node:os";
|
||||||
|
import { Context, Effect, Layer } from "effect";
|
||||||
|
import type { ControllerResponse } from "../../../lib/effect/toEffectResponse";
|
||||||
|
import type { InferEffect } from "../../../lib/effect/types";
|
||||||
|
import { ProjectRepository } from "../../project/infrastructure/ProjectRepository";
|
||||||
|
import { getDirectoryListing } from "../functions/getDirectoryListing";
|
||||||
|
import { getFileCompletion } from "../functions/getFileCompletion";
|
||||||
|
|
||||||
|
const LayerImpl = Effect.gen(function* () {
|
||||||
|
const projectRepository = yield* ProjectRepository;
|
||||||
|
|
||||||
|
const getFileCompletionRoute = (options: {
|
||||||
|
projectId: string;
|
||||||
|
basePath: string;
|
||||||
|
}) =>
|
||||||
|
Effect.gen(function* () {
|
||||||
|
const { projectId, basePath } = options;
|
||||||
|
|
||||||
|
const { project } = yield* projectRepository.getProject(projectId);
|
||||||
|
|
||||||
|
if (project.meta.projectPath === null) {
|
||||||
|
return {
|
||||||
|
response: { error: "Project path not found" },
|
||||||
|
status: 400,
|
||||||
|
} as const satisfies ControllerResponse;
|
||||||
|
}
|
||||||
|
|
||||||
|
const projectPath = project.meta.projectPath;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = yield* Effect.promise(() =>
|
||||||
|
getFileCompletion(projectPath, basePath),
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
response: result,
|
||||||
|
status: 200,
|
||||||
|
} as const satisfies ControllerResponse;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("File completion error:", error);
|
||||||
|
return {
|
||||||
|
response: { error: "Failed to get file completion" },
|
||||||
|
status: 500,
|
||||||
|
} as const satisfies ControllerResponse;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const getDirectoryListingRoute = (options: {
|
||||||
|
currentPath?: string | undefined;
|
||||||
|
}) =>
|
||||||
|
Effect.promise(async () => {
|
||||||
|
const { currentPath } = options;
|
||||||
|
|
||||||
|
const rootPath = "/";
|
||||||
|
const defaultPath = homedir();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const targetPath = currentPath ?? defaultPath;
|
||||||
|
const relativePath = targetPath.startsWith(rootPath)
|
||||||
|
? targetPath.slice(rootPath.length)
|
||||||
|
: targetPath;
|
||||||
|
|
||||||
|
const result = await getDirectoryListing(rootPath, relativePath);
|
||||||
|
|
||||||
|
return {
|
||||||
|
response: result,
|
||||||
|
status: 200,
|
||||||
|
} as const satisfies ControllerResponse;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Directory listing error:", error);
|
||||||
|
return {
|
||||||
|
response: { error: "Failed to list directory" },
|
||||||
|
status: 500,
|
||||||
|
} as const satisfies ControllerResponse;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
getFileCompletionRoute,
|
||||||
|
getDirectoryListingRoute,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
export type IFileSystemController = InferEffect<typeof LayerImpl>;
|
||||||
|
export class FileSystemController extends Context.Tag("FileSystemController")<
|
||||||
|
FileSystemController,
|
||||||
|
IFileSystemController
|
||||||
|
>() {
|
||||||
|
static Live = Layer.effect(this, LayerImpl);
|
||||||
|
}
|
||||||
139
src/server/core/git/presentation/GitController.ts
Normal file
139
src/server/core/git/presentation/GitController.ts
Normal file
@@ -0,0 +1,139 @@
|
|||||||
|
import { Context, Effect, Layer } from "effect";
|
||||||
|
import type { ControllerResponse } from "../../../lib/effect/toEffectResponse";
|
||||||
|
import type { InferEffect } from "../../../lib/effect/types";
|
||||||
|
import { ProjectRepository } from "../../project/infrastructure/ProjectRepository";
|
||||||
|
import { getBranches } from "../functions/getBranches";
|
||||||
|
import { getCommits } from "../functions/getCommits";
|
||||||
|
import { getDiff } from "../functions/getDiff";
|
||||||
|
|
||||||
|
const LayerImpl = Effect.gen(function* () {
|
||||||
|
const projectRepository = yield* ProjectRepository;
|
||||||
|
|
||||||
|
const getGitBranches = (options: { projectId: string }) =>
|
||||||
|
Effect.gen(function* () {
|
||||||
|
const { projectId } = options;
|
||||||
|
|
||||||
|
const { project } = yield* projectRepository.getProject(projectId);
|
||||||
|
|
||||||
|
if (project.meta.projectPath === null) {
|
||||||
|
return {
|
||||||
|
response: { error: "Project path not found" },
|
||||||
|
status: 400,
|
||||||
|
} as const satisfies ControllerResponse;
|
||||||
|
}
|
||||||
|
|
||||||
|
const projectPath = project.meta.projectPath;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = yield* Effect.promise(() => getBranches(projectPath));
|
||||||
|
return {
|
||||||
|
response: result,
|
||||||
|
status: 200,
|
||||||
|
} as const satisfies ControllerResponse;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Get branches error:", error);
|
||||||
|
if (error instanceof Error) {
|
||||||
|
return {
|
||||||
|
response: { error: error.message },
|
||||||
|
status: 400,
|
||||||
|
} as const satisfies ControllerResponse;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
response: { error: "Failed to get branches" },
|
||||||
|
status: 500,
|
||||||
|
} as const satisfies ControllerResponse;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const getGitCommits = (options: { projectId: string }) =>
|
||||||
|
Effect.gen(function* () {
|
||||||
|
const { projectId } = options;
|
||||||
|
|
||||||
|
const { project } = yield* projectRepository.getProject(projectId);
|
||||||
|
|
||||||
|
if (project.meta.projectPath === null) {
|
||||||
|
return {
|
||||||
|
response: { error: "Project path not found" },
|
||||||
|
status: 400,
|
||||||
|
} as const satisfies ControllerResponse;
|
||||||
|
}
|
||||||
|
|
||||||
|
const projectPath = project.meta.projectPath;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = yield* Effect.promise(() => getCommits(projectPath));
|
||||||
|
return {
|
||||||
|
response: result,
|
||||||
|
status: 200,
|
||||||
|
} as const satisfies ControllerResponse;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Get commits error:", error);
|
||||||
|
if (error instanceof Error) {
|
||||||
|
return {
|
||||||
|
response: { error: error.message },
|
||||||
|
status: 400,
|
||||||
|
} as const satisfies ControllerResponse;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
response: { error: "Failed to get commits" },
|
||||||
|
status: 500,
|
||||||
|
} as const satisfies ControllerResponse;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const getGitDiff = (options: {
|
||||||
|
projectId: string;
|
||||||
|
fromRef: string;
|
||||||
|
toRef: string;
|
||||||
|
}) =>
|
||||||
|
Effect.gen(function* () {
|
||||||
|
const { projectId, fromRef, toRef } = options;
|
||||||
|
|
||||||
|
const { project } = yield* projectRepository.getProject(projectId);
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (project.meta.projectPath === null) {
|
||||||
|
return {
|
||||||
|
response: { error: "Project path not found" },
|
||||||
|
status: 400,
|
||||||
|
} as const satisfies ControllerResponse;
|
||||||
|
}
|
||||||
|
|
||||||
|
const projectPath = project.meta.projectPath;
|
||||||
|
|
||||||
|
const result = yield* Effect.promise(() =>
|
||||||
|
getDiff(projectPath, fromRef, toRef),
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
response: result,
|
||||||
|
status: 200,
|
||||||
|
} as const satisfies ControllerResponse;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Get diff error:", error);
|
||||||
|
if (error instanceof Error) {
|
||||||
|
return {
|
||||||
|
response: { error: error.message },
|
||||||
|
status: 400,
|
||||||
|
} as const satisfies ControllerResponse;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
response: { error: "Failed to get diff" },
|
||||||
|
status: 500,
|
||||||
|
} as const satisfies ControllerResponse;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
getGitBranches,
|
||||||
|
getGitCommits,
|
||||||
|
getGitDiff,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
export type IGitController = InferEffect<typeof LayerImpl>;
|
||||||
|
export class GitController extends Context.Tag("GitController")<
|
||||||
|
GitController,
|
||||||
|
IGitController
|
||||||
|
>() {
|
||||||
|
static Live = Layer.effect(this, LayerImpl);
|
||||||
|
}
|
||||||
36
src/server/core/hono/services/HonoConfigService.ts
Normal file
36
src/server/core/hono/services/HonoConfigService.ts
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
import { Context, Effect, Layer, Ref } from "effect";
|
||||||
|
import type { Config } from "../../../lib/config/config";
|
||||||
|
import type { InferEffect } from "../../../lib/effect/types";
|
||||||
|
|
||||||
|
const LayerImpl = Effect.gen(function* () {
|
||||||
|
const configRef = yield* Ref.make<Config>({
|
||||||
|
hideNoUserMessageSession: true,
|
||||||
|
unifySameTitleSession: true,
|
||||||
|
enterKeyBehavior: "shift-enter-send",
|
||||||
|
permissionMode: "default",
|
||||||
|
});
|
||||||
|
|
||||||
|
const setConfig = (newConfig: Config) =>
|
||||||
|
Effect.gen(function* () {
|
||||||
|
yield* Ref.update(configRef, () => newConfig);
|
||||||
|
});
|
||||||
|
|
||||||
|
const getConfig = () =>
|
||||||
|
Effect.gen(function* () {
|
||||||
|
const config = yield* Ref.get(configRef);
|
||||||
|
return config;
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
getConfig,
|
||||||
|
setConfig,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
export type IHonoConfigService = InferEffect<typeof LayerImpl>;
|
||||||
|
export class HonoConfigService extends Context.Tag("HonoConfigService")<
|
||||||
|
HonoConfigService,
|
||||||
|
IHonoConfigService
|
||||||
|
>() {
|
||||||
|
static Live = Layer.effect(this, LayerImpl);
|
||||||
|
}
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
import { execSync } from "node:child_process";
|
|
||||||
import { decodeProjectId } from "../../project/functions/id";
|
|
||||||
|
|
||||||
export interface McpServer {
|
|
||||||
name: string;
|
|
||||||
command: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const getMcpList = async (
|
|
||||||
projectId: string,
|
|
||||||
): Promise<{ servers: McpServer[] }> => {
|
|
||||||
try {
|
|
||||||
const output = execSync("claude mcp list", {
|
|
||||||
encoding: "utf8",
|
|
||||||
timeout: 10000,
|
|
||||||
cwd: decodeProjectId(projectId),
|
|
||||||
});
|
|
||||||
|
|
||||||
const servers: McpServer[] = [];
|
|
||||||
const lines = output.trim().split("\n");
|
|
||||||
|
|
||||||
for (const line of lines) {
|
|
||||||
// Skip header lines and status indicators
|
|
||||||
if (line.includes("Checking MCP server health") || line.trim() === "") {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse lines like "context7: npx -y @upstash/context7-mcp@latest - ✓ Connected"
|
|
||||||
const colonIndex = line.indexOf(":");
|
|
||||||
if (colonIndex > 0) {
|
|
||||||
const name = line.substring(0, colonIndex).trim();
|
|
||||||
const rest = line.substring(colonIndex + 1).trim();
|
|
||||||
|
|
||||||
// Remove status indicators (✓ Connected, ✗ Failed, etc.)
|
|
||||||
const command = rest.replace(/\s*-\s*[✓✗].*$/, "").trim();
|
|
||||||
|
|
||||||
if (name && command) {
|
|
||||||
servers.push({ name, command });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return { servers };
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Failed to get MCP list:", error);
|
|
||||||
// Return empty list if command fails
|
|
||||||
return { servers: [] };
|
|
||||||
}
|
|
||||||
};
|
|
||||||
164
src/server/core/project/presentation/ProjectController.ts
Normal file
164
src/server/core/project/presentation/ProjectController.ts
Normal file
@@ -0,0 +1,164 @@
|
|||||||
|
import { Context, Effect, Layer } from "effect";
|
||||||
|
import type { ControllerResponse } from "../../../lib/effect/toEffectResponse";
|
||||||
|
import type { InferEffect } from "../../../lib/effect/types";
|
||||||
|
import { computeClaudeProjectFilePath } from "../../claude-code/functions/computeClaudeProjectFilePath";
|
||||||
|
import { ClaudeCodeLifeCycleService } from "../../claude-code/services/ClaudeCodeLifeCycleService";
|
||||||
|
import { HonoConfigService } from "../../hono/services/HonoConfigService";
|
||||||
|
import { SessionRepository } from "../../session/infrastructure/SessionRepository";
|
||||||
|
import { encodeProjectId } from "../functions/id";
|
||||||
|
import { ProjectRepository } from "../infrastructure/ProjectRepository";
|
||||||
|
|
||||||
|
const LayerImpl = Effect.gen(function* () {
|
||||||
|
const projectRepository = yield* ProjectRepository;
|
||||||
|
const claudeCodeLifeCycleService = yield* ClaudeCodeLifeCycleService;
|
||||||
|
const honoConfigService = yield* HonoConfigService;
|
||||||
|
const sessionRepository = yield* SessionRepository;
|
||||||
|
|
||||||
|
const getProjects = () =>
|
||||||
|
Effect.gen(function* () {
|
||||||
|
const { projects } = yield* projectRepository.getProjects();
|
||||||
|
return {
|
||||||
|
status: 200,
|
||||||
|
response: { projects },
|
||||||
|
} as const satisfies ControllerResponse;
|
||||||
|
});
|
||||||
|
|
||||||
|
const getProject = (options: { projectId: string; cursor?: string }) =>
|
||||||
|
Effect.gen(function* () {
|
||||||
|
const { projectId, cursor } = options;
|
||||||
|
|
||||||
|
const config = yield* honoConfigService.getConfig();
|
||||||
|
|
||||||
|
const { project } = yield* projectRepository.getProject(projectId);
|
||||||
|
const { sessions } = yield* sessionRepository.getSessions(projectId, {
|
||||||
|
cursor,
|
||||||
|
});
|
||||||
|
|
||||||
|
let filteredSessions = sessions;
|
||||||
|
|
||||||
|
// Filter sessions based on hideNoUserMessageSession setting
|
||||||
|
if (config.hideNoUserMessageSession) {
|
||||||
|
filteredSessions = filteredSessions.filter((session) => {
|
||||||
|
return session.meta.firstCommand !== null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unify sessions with same title if unifySameTitleSession is enabled
|
||||||
|
if (config.unifySameTitleSession) {
|
||||||
|
const sessionMap = new Map<string, (typeof filteredSessions)[0]>();
|
||||||
|
|
||||||
|
for (const session of filteredSessions) {
|
||||||
|
// Generate title for comparison
|
||||||
|
const title =
|
||||||
|
session.meta.firstCommand !== null
|
||||||
|
? (() => {
|
||||||
|
const cmd = session.meta.firstCommand;
|
||||||
|
switch (cmd.kind) {
|
||||||
|
case "command":
|
||||||
|
return cmd.commandArgs === undefined
|
||||||
|
? cmd.commandName
|
||||||
|
: `${cmd.commandName} ${cmd.commandArgs}`;
|
||||||
|
case "local-command":
|
||||||
|
return cmd.stdout;
|
||||||
|
case "text":
|
||||||
|
return cmd.content;
|
||||||
|
default:
|
||||||
|
return session.id;
|
||||||
|
}
|
||||||
|
})()
|
||||||
|
: session.id;
|
||||||
|
|
||||||
|
const existingSession = sessionMap.get(title);
|
||||||
|
if (existingSession) {
|
||||||
|
// Keep the session with the latest modification date
|
||||||
|
if (session.lastModifiedAt && existingSession.lastModifiedAt) {
|
||||||
|
if (session.lastModifiedAt > existingSession.lastModifiedAt) {
|
||||||
|
sessionMap.set(title, session);
|
||||||
|
}
|
||||||
|
} else if (
|
||||||
|
session.lastModifiedAt &&
|
||||||
|
!existingSession.lastModifiedAt
|
||||||
|
) {
|
||||||
|
sessionMap.set(title, session);
|
||||||
|
}
|
||||||
|
// If no modification dates, keep the existing one
|
||||||
|
} else {
|
||||||
|
sessionMap.set(title, session);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
filteredSessions = Array.from(sessionMap.values());
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasMore = sessions.length >= 20;
|
||||||
|
return {
|
||||||
|
status: 200,
|
||||||
|
response: {
|
||||||
|
project,
|
||||||
|
sessions: filteredSessions,
|
||||||
|
nextCursor: hasMore ? sessions.at(-1)?.id : undefined,
|
||||||
|
},
|
||||||
|
} as const satisfies ControllerResponse;
|
||||||
|
});
|
||||||
|
|
||||||
|
const getProjectLatestSession = (options: { projectId: string }) =>
|
||||||
|
Effect.gen(function* () {
|
||||||
|
const { projectId } = options;
|
||||||
|
const { sessions } = yield* sessionRepository.getSessions(projectId, {
|
||||||
|
maxCount: 1,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
status: 200,
|
||||||
|
response: {
|
||||||
|
latestSession: sessions[0] ?? null,
|
||||||
|
},
|
||||||
|
} as const satisfies ControllerResponse;
|
||||||
|
});
|
||||||
|
|
||||||
|
const createProject = (options: { projectPath: string }) =>
|
||||||
|
Effect.gen(function* () {
|
||||||
|
const { projectPath } = options;
|
||||||
|
|
||||||
|
// No project validation needed - startTask will create a new project
|
||||||
|
// if it doesn't exist when running /init command
|
||||||
|
const claudeProjectFilePath = computeClaudeProjectFilePath(projectPath);
|
||||||
|
const projectId = encodeProjectId(claudeProjectFilePath);
|
||||||
|
const config = yield* honoConfigService.getConfig();
|
||||||
|
|
||||||
|
const result = yield* claudeCodeLifeCycleService.startTask({
|
||||||
|
baseSession: {
|
||||||
|
cwd: projectPath,
|
||||||
|
projectId,
|
||||||
|
sessionId: undefined,
|
||||||
|
},
|
||||||
|
config: config,
|
||||||
|
message: "/init",
|
||||||
|
});
|
||||||
|
|
||||||
|
const { sessionId } = yield* result.yieldSessionFileCreated();
|
||||||
|
|
||||||
|
return {
|
||||||
|
status: 201,
|
||||||
|
response: {
|
||||||
|
projectId,
|
||||||
|
sessionId,
|
||||||
|
},
|
||||||
|
} as const satisfies ControllerResponse;
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
getProjects,
|
||||||
|
getProject,
|
||||||
|
getProjectLatestSession,
|
||||||
|
createProject,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
export type IProjectController = InferEffect<typeof LayerImpl>;
|
||||||
|
export class ProjectController extends Context.Tag("ProjectController")<
|
||||||
|
ProjectController,
|
||||||
|
IProjectController
|
||||||
|
>() {
|
||||||
|
static Live = Layer.effect(this, LayerImpl);
|
||||||
|
}
|
||||||
35
src/server/core/session/presentation/SessionController.ts
Normal file
35
src/server/core/session/presentation/SessionController.ts
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
import { Context, Effect, Layer } from "effect";
|
||||||
|
import type { ControllerResponse } from "../../../lib/effect/toEffectResponse";
|
||||||
|
import type { InferEffect } from "../../../lib/effect/types";
|
||||||
|
import { SessionRepository } from "../../session/infrastructure/SessionRepository";
|
||||||
|
|
||||||
|
const LayerImpl = Effect.gen(function* () {
|
||||||
|
const sessionRepository = yield* SessionRepository;
|
||||||
|
|
||||||
|
const getSession = (options: { projectId: string; sessionId: string }) =>
|
||||||
|
Effect.gen(function* () {
|
||||||
|
const { projectId, sessionId } = options;
|
||||||
|
|
||||||
|
const { session } = yield* sessionRepository.getSession(
|
||||||
|
projectId,
|
||||||
|
sessionId,
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
status: 200,
|
||||||
|
response: { session },
|
||||||
|
} as const satisfies ControllerResponse;
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
getSession,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
export type ISessionController = InferEffect<typeof LayerImpl>;
|
||||||
|
export class SessionController extends Context.Tag("SessionController")<
|
||||||
|
SessionController,
|
||||||
|
ISessionController
|
||||||
|
>() {
|
||||||
|
static Live = Layer.effect(this, LayerImpl);
|
||||||
|
}
|
||||||
@@ -1,6 +1,3 @@
|
|||||||
import { readdir } from "node:fs/promises";
|
|
||||||
import { homedir } from "node:os";
|
|
||||||
import { resolve } from "node:path";
|
|
||||||
import type { CommandExecutor, FileSystem, Path } from "@effect/platform";
|
import type { CommandExecutor, FileSystem, Path } from "@effect/platform";
|
||||||
import { zValidator } from "@hono/zod-validator";
|
import { zValidator } from "@hono/zod-validator";
|
||||||
import { Effect, Runtime } from "effect";
|
import { Effect, Runtime } from "effect";
|
||||||
@@ -8,28 +5,21 @@ import { setCookie } from "hono/cookie";
|
|||||||
import { streamSSE } from "hono/streaming";
|
import { streamSSE } from "hono/streaming";
|
||||||
import prexit from "prexit";
|
import prexit from "prexit";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import type { PublicSessionProcess } from "../../types/session-process";
|
import { ClaudeCodeController } from "../core/claude-code/presentation/ClaudeCodeController";
|
||||||
import { computeClaudeProjectFilePath } from "../core/claude-code/functions/computeClaudeProjectFilePath";
|
import { ClaudeCodePermissionController } from "../core/claude-code/presentation/ClaudeCodePermissionController";
|
||||||
|
import { ClaudeCodeSessionProcessController } from "../core/claude-code/presentation/ClaudeCodeSessionProcessController";
|
||||||
import { ClaudeCodeLifeCycleService } from "../core/claude-code/services/ClaudeCodeLifeCycleService";
|
import { ClaudeCodeLifeCycleService } from "../core/claude-code/services/ClaudeCodeLifeCycleService";
|
||||||
import { ClaudeCodePermissionService } from "../core/claude-code/services/ClaudeCodePermissionService";
|
|
||||||
import { getDirectoryListing } from "../core/directory-browser/functions/getDirectoryListing";
|
|
||||||
import { adaptInternalEventToSSE } from "../core/events/functions/adaptInternalEventToSSE";
|
|
||||||
import { TypeSafeSSE } from "../core/events/functions/typeSafeSSE";
|
import { TypeSafeSSE } from "../core/events/functions/typeSafeSSE";
|
||||||
import { EventBus } from "../core/events/services/EventBus";
|
import { SSEController } from "../core/events/presentation/SSEController";
|
||||||
import type { InternalEventDeclaration } from "../core/events/types/InternalEventDeclaration";
|
import { FileSystemController } from "../core/file-system/presentation/FileSystemController";
|
||||||
import { getFileCompletion } from "../core/file-completion/functions/getFileCompletion";
|
import { GitController } from "../core/git/presentation/GitController";
|
||||||
import { getBranches } from "../core/git/functions/getBranches";
|
import { HonoConfigService } from "../core/hono/services/HonoConfigService";
|
||||||
import { getCommits } from "../core/git/functions/getCommits";
|
import { ProjectController } from "../core/project/presentation/ProjectController";
|
||||||
import { getDiff } from "../core/git/functions/getDiff";
|
|
||||||
import { getMcpList } from "../core/mcp/functions/getMcpList";
|
|
||||||
import { encodeProjectId } from "../core/project/functions/id";
|
|
||||||
import { ProjectRepository } from "../core/project/infrastructure/ProjectRepository";
|
|
||||||
import type { ProjectMetaService } from "../core/project/services/ProjectMetaService";
|
|
||||||
import { SessionRepository } from "../core/session/infrastructure/SessionRepository";
|
|
||||||
import type { VirtualConversationDatabase } from "../core/session/infrastructure/VirtualConversationDatabase";
|
import type { VirtualConversationDatabase } from "../core/session/infrastructure/VirtualConversationDatabase";
|
||||||
|
import { SessionController } from "../core/session/presentation/SessionController";
|
||||||
import type { SessionMetaService } from "../core/session/services/SessionMetaService";
|
import type { SessionMetaService } from "../core/session/services/SessionMetaService";
|
||||||
import { configSchema } from "../lib/config/config";
|
import { configSchema } from "../lib/config/config";
|
||||||
import { claudeCommandsDirPath } from "../lib/config/paths";
|
import { effectToResponse } from "../lib/effect/toEffectResponse";
|
||||||
import { env } from "../lib/env";
|
import { env } from "../lib/env";
|
||||||
import type { HonoAppType } from "./app";
|
import type { HonoAppType } from "./app";
|
||||||
import { InitializeService } from "./initialize";
|
import { InitializeService } from "./initialize";
|
||||||
@@ -37,15 +27,24 @@ import { configMiddleware } from "./middleware/config.middleware";
|
|||||||
|
|
||||||
export const routes = (app: HonoAppType) =>
|
export const routes = (app: HonoAppType) =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const sessionRepository = yield* SessionRepository;
|
// controllers
|
||||||
const projectRepository = yield* ProjectRepository;
|
const projectController = yield* ProjectController;
|
||||||
|
const sessionController = yield* SessionController;
|
||||||
|
const gitController = yield* GitController;
|
||||||
|
const claudeCodeSessionProcessController =
|
||||||
|
yield* ClaudeCodeSessionProcessController;
|
||||||
|
const claudeCodePermissionController =
|
||||||
|
yield* ClaudeCodePermissionController;
|
||||||
|
const sseController = yield* SSEController;
|
||||||
|
const fileSystemController = yield* FileSystemController;
|
||||||
|
const claudeCodeController = yield* ClaudeCodeController;
|
||||||
|
|
||||||
|
// services
|
||||||
|
const honoConfigService = yield* HonoConfigService;
|
||||||
const claudeCodeLifeCycleService = yield* ClaudeCodeLifeCycleService;
|
const claudeCodeLifeCycleService = yield* ClaudeCodeLifeCycleService;
|
||||||
const claudeCodePermissionService = yield* ClaudeCodePermissionService;
|
|
||||||
const initializeService = yield* InitializeService;
|
const initializeService = yield* InitializeService;
|
||||||
const eventBus = yield* EventBus;
|
|
||||||
|
|
||||||
const runtime = yield* Effect.runtime<
|
const runtime = yield* Effect.runtime<
|
||||||
| ProjectMetaService
|
|
||||||
| SessionMetaService
|
| SessionMetaService
|
||||||
| VirtualConversationDatabase
|
| VirtualConversationDatabase
|
||||||
| FileSystem.FileSystem
|
| FileSystem.FileSystem
|
||||||
@@ -65,7 +64,13 @@ export const routes = (app: HonoAppType) =>
|
|||||||
app
|
app
|
||||||
// middleware
|
// middleware
|
||||||
.use(configMiddleware)
|
.use(configMiddleware)
|
||||||
.use(async (_c, next) => {
|
.use(async (c, next) => {
|
||||||
|
await Effect.runPromise(
|
||||||
|
honoConfigService.setConfig({
|
||||||
|
...c.get("config"),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
await next();
|
await next();
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -86,16 +91,35 @@ export const routes = (app: HonoAppType) =>
|
|||||||
});
|
});
|
||||||
})
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ProjectController Routes
|
||||||
|
*/
|
||||||
|
|
||||||
.get("/projects", async (c) => {
|
.get("/projects", async (c) => {
|
||||||
const program = Effect.gen(function* () {
|
const response = await effectToResponse(
|
||||||
return yield* projectRepository.getProjects();
|
c,
|
||||||
});
|
projectController.getProjects(),
|
||||||
|
);
|
||||||
const { projects } = await Runtime.runPromise(runtime)(program);
|
return response;
|
||||||
|
|
||||||
return c.json({ projects });
|
|
||||||
})
|
})
|
||||||
|
|
||||||
|
.get(
|
||||||
|
"/projects/:projectId",
|
||||||
|
zValidator("query", z.object({ cursor: z.string().optional() })),
|
||||||
|
async (c) => {
|
||||||
|
const response = await effectToResponse(
|
||||||
|
c,
|
||||||
|
projectController
|
||||||
|
.getProject({
|
||||||
|
...c.req.param(),
|
||||||
|
...c.req.valid("query"),
|
||||||
|
})
|
||||||
|
.pipe(Effect.provide(runtime)),
|
||||||
|
);
|
||||||
|
return response;
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
.post(
|
.post(
|
||||||
"/projects",
|
"/projects",
|
||||||
zValidator(
|
zValidator(
|
||||||
@@ -105,369 +129,68 @@ export const routes = (app: HonoAppType) =>
|
|||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
async (c) => {
|
async (c) => {
|
||||||
const { projectPath } = c.req.valid("json");
|
const response = await effectToResponse(
|
||||||
|
c,
|
||||||
// No project validation needed - startTask will create a new project
|
projectController.createProject({
|
||||||
// if it doesn't exist when running /init command
|
...c.req.valid("json"),
|
||||||
const claudeProjectFilePath =
|
|
||||||
computeClaudeProjectFilePath(projectPath);
|
|
||||||
const projectId = encodeProjectId(claudeProjectFilePath);
|
|
||||||
|
|
||||||
const program = Effect.gen(function* () {
|
|
||||||
const result = yield* claudeCodeLifeCycleService.startTask({
|
|
||||||
baseSession: {
|
|
||||||
cwd: projectPath,
|
|
||||||
projectId,
|
|
||||||
sessionId: undefined,
|
|
||||||
},
|
|
||||||
config: c.get("config"),
|
|
||||||
message: "/init",
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
result,
|
|
||||||
status: 200 as const,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await Runtime.runPromise(runtime)(program);
|
|
||||||
|
|
||||||
if (result.status === 200) {
|
|
||||||
const { sessionId } =
|
|
||||||
await result.result.awaitSessionFileCreated();
|
|
||||||
|
|
||||||
return c.json({
|
|
||||||
projectId: result.result.sessionProcess.def.projectId,
|
|
||||||
sessionId,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return c.json({ error: "Failed to create project" }, 500);
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
.get(
|
|
||||||
"/directory-browser",
|
|
||||||
zValidator(
|
|
||||||
"query",
|
|
||||||
z.object({
|
|
||||||
currentPath: z.string().optional(),
|
|
||||||
}),
|
}),
|
||||||
),
|
|
||||||
async (c) => {
|
|
||||||
const { currentPath } = c.req.valid("query");
|
|
||||||
const rootPath = "/";
|
|
||||||
const defaultPath = homedir();
|
|
||||||
|
|
||||||
try {
|
|
||||||
const targetPath = currentPath || defaultPath;
|
|
||||||
const relativePath = targetPath.startsWith(rootPath)
|
|
||||||
? targetPath.slice(rootPath.length)
|
|
||||||
: targetPath;
|
|
||||||
|
|
||||||
const result = await getDirectoryListing(rootPath, relativePath);
|
|
||||||
return c.json(result);
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Directory listing error:", error);
|
|
||||||
if (error instanceof Error) {
|
|
||||||
return c.json({ error: error.message }, 400);
|
|
||||||
}
|
|
||||||
return c.json({ error: "Failed to list directory" }, 500);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
.get(
|
|
||||||
"/projects/:projectId",
|
|
||||||
zValidator("query", z.object({ cursor: z.string().optional() })),
|
|
||||||
async (c) => {
|
|
||||||
const { projectId } = c.req.param();
|
|
||||||
const { cursor } = c.req.valid("query");
|
|
||||||
const config = c.get("config");
|
|
||||||
|
|
||||||
const program = Effect.gen(function* () {
|
|
||||||
const { project } =
|
|
||||||
yield* projectRepository.getProject(projectId);
|
|
||||||
const { sessions } = yield* sessionRepository.getSessions(
|
|
||||||
projectId,
|
|
||||||
{ cursor },
|
|
||||||
);
|
);
|
||||||
|
return response;
|
||||||
let filteredSessions = sessions;
|
|
||||||
|
|
||||||
// Filter sessions based on hideNoUserMessageSession setting
|
|
||||||
if (config.hideNoUserMessageSession) {
|
|
||||||
filteredSessions = filteredSessions.filter((session) => {
|
|
||||||
return session.meta.firstCommand !== null;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Unify sessions with same title if unifySameTitleSession is enabled
|
|
||||||
if (config.unifySameTitleSession) {
|
|
||||||
const sessionMap = new Map<
|
|
||||||
string,
|
|
||||||
(typeof filteredSessions)[0]
|
|
||||||
>();
|
|
||||||
|
|
||||||
for (const session of filteredSessions) {
|
|
||||||
// Generate title for comparison
|
|
||||||
const title =
|
|
||||||
session.meta.firstCommand !== null
|
|
||||||
? (() => {
|
|
||||||
const cmd = session.meta.firstCommand;
|
|
||||||
switch (cmd.kind) {
|
|
||||||
case "command":
|
|
||||||
return cmd.commandArgs === undefined
|
|
||||||
? cmd.commandName
|
|
||||||
: `${cmd.commandName} ${cmd.commandArgs}`;
|
|
||||||
case "local-command":
|
|
||||||
return cmd.stdout;
|
|
||||||
case "text":
|
|
||||||
return cmd.content;
|
|
||||||
default:
|
|
||||||
return session.id;
|
|
||||||
}
|
|
||||||
})()
|
|
||||||
: session.id;
|
|
||||||
|
|
||||||
const existingSession = sessionMap.get(title);
|
|
||||||
if (existingSession) {
|
|
||||||
// Keep the session with the latest modification date
|
|
||||||
if (
|
|
||||||
session.lastModifiedAt &&
|
|
||||||
existingSession.lastModifiedAt
|
|
||||||
) {
|
|
||||||
if (
|
|
||||||
session.lastModifiedAt > existingSession.lastModifiedAt
|
|
||||||
) {
|
|
||||||
sessionMap.set(title, session);
|
|
||||||
}
|
|
||||||
} else if (
|
|
||||||
session.lastModifiedAt &&
|
|
||||||
!existingSession.lastModifiedAt
|
|
||||||
) {
|
|
||||||
sessionMap.set(title, session);
|
|
||||||
}
|
|
||||||
// If no modification dates, keep the existing one
|
|
||||||
} else {
|
|
||||||
sessionMap.set(title, session);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
filteredSessions = Array.from(sessionMap.values());
|
|
||||||
}
|
|
||||||
|
|
||||||
const hasMore = sessions.length >= 20;
|
|
||||||
return {
|
|
||||||
project,
|
|
||||||
sessions: filteredSessions,
|
|
||||||
nextCursor: hasMore ? sessions.at(-1)?.id : undefined,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await Runtime.runPromise(runtime)(program);
|
|
||||||
return c.json(result);
|
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
.get("/projects/:projectId/latest-session", async (c) => {
|
.get("/projects/:projectId/latest-session", async (c) => {
|
||||||
const { projectId } = c.req.param();
|
const response = await effectToResponse(
|
||||||
|
c,
|
||||||
const program = Effect.gen(function* () {
|
projectController
|
||||||
const { sessions } = yield* sessionRepository.getSessions(
|
.getProjectLatestSession({
|
||||||
projectId,
|
...c.req.param(),
|
||||||
{ maxCount: 1 },
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
|
||||||
latestSession: sessions[0] ?? null,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await Runtime.runPromise(runtime)(program);
|
|
||||||
return c.json(result);
|
|
||||||
})
|
})
|
||||||
|
.pipe(Effect.provide(runtime)),
|
||||||
|
);
|
||||||
|
return response;
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SessionController Routes
|
||||||
|
*/
|
||||||
|
|
||||||
.get("/projects/:projectId/sessions/:sessionId", async (c) => {
|
.get("/projects/:projectId/sessions/:sessionId", async (c) => {
|
||||||
const { projectId, sessionId } = c.req.param();
|
const response = await effectToResponse(
|
||||||
|
c,
|
||||||
const program = Effect.gen(function* () {
|
sessionController
|
||||||
const { session } = yield* sessionRepository.getSession(
|
.getSession({ ...c.req.param() })
|
||||||
projectId,
|
.pipe(Effect.provide(runtime)),
|
||||||
sessionId,
|
|
||||||
);
|
);
|
||||||
return { session };
|
return response;
|
||||||
});
|
|
||||||
|
|
||||||
const result = await Runtime.runPromise(runtime)(program);
|
|
||||||
return c.json(result);
|
|
||||||
})
|
})
|
||||||
|
|
||||||
.get(
|
/**
|
||||||
"/projects/:projectId/file-completion",
|
* GitController Routes
|
||||||
zValidator(
|
*/
|
||||||
"query",
|
|
||||||
z.object({
|
|
||||||
basePath: z.string().optional().default("/"),
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
async (c) => {
|
|
||||||
const { projectId } = c.req.param();
|
|
||||||
const { basePath } = c.req.valid("query");
|
|
||||||
|
|
||||||
const program = Effect.gen(function* () {
|
|
||||||
const { project } =
|
|
||||||
yield* projectRepository.getProject(projectId);
|
|
||||||
|
|
||||||
if (project.meta.projectPath === null) {
|
|
||||||
return {
|
|
||||||
error: "Project path not found",
|
|
||||||
status: 400 as const,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const projectPath = project.meta.projectPath;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const result = yield* Effect.promise(() =>
|
|
||||||
getFileCompletion(projectPath, basePath),
|
|
||||||
);
|
|
||||||
return { data: result, status: 200 as const };
|
|
||||||
} catch (error) {
|
|
||||||
console.error("File completion error:", error);
|
|
||||||
return {
|
|
||||||
error: "Failed to get file completion",
|
|
||||||
status: 500 as const,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await Runtime.runPromise(runtime)(program);
|
|
||||||
|
|
||||||
if (result.status === 200) {
|
|
||||||
return c.json(result.data);
|
|
||||||
}
|
|
||||||
return c.json({ error: result.error }, result.status);
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
.get("/projects/:projectId/claude-commands", async (c) => {
|
|
||||||
const { projectId } = c.req.param();
|
|
||||||
|
|
||||||
const program = Effect.gen(function* () {
|
|
||||||
const { project } = yield* projectRepository.getProject(projectId);
|
|
||||||
|
|
||||||
const [globalCommands, projectCommands] = yield* Effect.promise(
|
|
||||||
() =>
|
|
||||||
Promise.allSettled([
|
|
||||||
readdir(claudeCommandsDirPath, {
|
|
||||||
withFileTypes: true,
|
|
||||||
}).then((dirents) =>
|
|
||||||
dirents
|
|
||||||
.filter((d) => d.isFile() && d.name.endsWith(".md"))
|
|
||||||
.map((d) => d.name.replace(/\.md$/, "")),
|
|
||||||
),
|
|
||||||
project.meta.projectPath !== null
|
|
||||||
? readdir(
|
|
||||||
resolve(
|
|
||||||
project.meta.projectPath,
|
|
||||||
".claude",
|
|
||||||
"commands",
|
|
||||||
),
|
|
||||||
{
|
|
||||||
withFileTypes: true,
|
|
||||||
},
|
|
||||||
).then((dirents) =>
|
|
||||||
dirents
|
|
||||||
.filter((d) => d.isFile() && d.name.endsWith(".md"))
|
|
||||||
.map((d) => d.name.replace(/\.md$/, "")),
|
|
||||||
)
|
|
||||||
: [],
|
|
||||||
]),
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
|
||||||
globalCommands:
|
|
||||||
globalCommands.status === "fulfilled"
|
|
||||||
? globalCommands.value
|
|
||||||
: [],
|
|
||||||
projectCommands:
|
|
||||||
projectCommands.status === "fulfilled"
|
|
||||||
? projectCommands.value
|
|
||||||
: [],
|
|
||||||
defaultCommands: ["init", "compact"],
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await Runtime.runPromise(runtime)(program);
|
|
||||||
return c.json(result);
|
|
||||||
})
|
|
||||||
|
|
||||||
.get("/projects/:projectId/git/branches", async (c) => {
|
.get("/projects/:projectId/git/branches", async (c) => {
|
||||||
const { projectId } = c.req.param();
|
const response = await effectToResponse(
|
||||||
|
c,
|
||||||
const program = Effect.gen(function* () {
|
gitController
|
||||||
const { project } = yield* projectRepository.getProject(projectId);
|
.getGitBranches({
|
||||||
|
...c.req.param(),
|
||||||
if (project.meta.projectPath === null) {
|
})
|
||||||
return { error: "Project path not found", status: 400 as const };
|
.pipe(Effect.provide(runtime)),
|
||||||
}
|
|
||||||
|
|
||||||
const projectPath = project.meta.projectPath;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const result = yield* Effect.promise(() =>
|
|
||||||
getBranches(projectPath),
|
|
||||||
);
|
);
|
||||||
return { data: result, status: 200 as const };
|
return response;
|
||||||
} catch (error) {
|
|
||||||
console.error("Get branches error:", error);
|
|
||||||
if (error instanceof Error) {
|
|
||||||
return { error: error.message, status: 400 as const };
|
|
||||||
}
|
|
||||||
return { error: "Failed to get branches", status: 500 as const };
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await Runtime.runPromise(runtime)(program);
|
|
||||||
if (result.status === 200) {
|
|
||||||
return c.json(result.data);
|
|
||||||
}
|
|
||||||
|
|
||||||
return c.json({ error: result.error }, result.status);
|
|
||||||
})
|
})
|
||||||
|
|
||||||
.get("/projects/:projectId/git/commits", async (c) => {
|
.get("/projects/:projectId/git/commits", async (c) => {
|
||||||
const { projectId } = c.req.param();
|
const response = await effectToResponse(
|
||||||
|
c,
|
||||||
const program = Effect.gen(function* () {
|
gitController
|
||||||
const { project } = yield* projectRepository.getProject(projectId);
|
.getGitCommits({
|
||||||
|
...c.req.param(),
|
||||||
if (project.meta.projectPath === null) {
|
})
|
||||||
return { error: "Project path not found", status: 400 as const };
|
.pipe(Effect.provide(runtime)),
|
||||||
}
|
|
||||||
|
|
||||||
const projectPath = project.meta.projectPath;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const result = yield* Effect.promise(() =>
|
|
||||||
getCommits(projectPath),
|
|
||||||
);
|
);
|
||||||
return { data: result, status: 200 as const };
|
return response;
|
||||||
} catch (error) {
|
|
||||||
console.error("Get commits error:", error);
|
|
||||||
if (error instanceof Error) {
|
|
||||||
return { error: error.message, status: 400 as const };
|
|
||||||
}
|
|
||||||
return { error: "Failed to get commits", status: 500 as const };
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await Runtime.runPromise(runtime)(program);
|
|
||||||
if (result.status === 200) {
|
|
||||||
return c.json(result.data);
|
|
||||||
}
|
|
||||||
return c.json({ error: result.error }, result.status);
|
|
||||||
})
|
})
|
||||||
|
|
||||||
.post(
|
.post(
|
||||||
@@ -480,64 +203,55 @@ export const routes = (app: HonoAppType) =>
|
|||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
async (c) => {
|
async (c) => {
|
||||||
const { projectId } = c.req.param();
|
const response = await effectToResponse(
|
||||||
const { fromRef, toRef } = c.req.valid("json");
|
c,
|
||||||
|
gitController
|
||||||
const program = Effect.gen(function* () {
|
.getGitDiff({
|
||||||
const { project } =
|
...c.req.param(),
|
||||||
yield* projectRepository.getProject(projectId);
|
...c.req.valid("json"),
|
||||||
|
})
|
||||||
try {
|
.pipe(Effect.provide(runtime)),
|
||||||
if (project.meta.projectPath === null) {
|
|
||||||
return {
|
|
||||||
error: "Project path not found",
|
|
||||||
status: 400 as const,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const projectPath = project.meta.projectPath;
|
|
||||||
|
|
||||||
const result = yield* Effect.promise(() =>
|
|
||||||
getDiff(projectPath, fromRef, toRef),
|
|
||||||
);
|
);
|
||||||
return { data: result, status: 200 as const };
|
return response;
|
||||||
} catch (error) {
|
|
||||||
console.error("Get diff error:", error);
|
|
||||||
if (error instanceof Error) {
|
|
||||||
return { error: error.message, status: 400 as const };
|
|
||||||
}
|
|
||||||
return { error: "Failed to get diff", status: 500 as const };
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await Runtime.runPromise(runtime)(program);
|
|
||||||
if (result.status === 200) {
|
|
||||||
return c.json(result.data);
|
|
||||||
}
|
|
||||||
return c.json({ error: result.error }, result.status);
|
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
.get("/projects/:projectId/mcp/list", async (c) => {
|
/**
|
||||||
const { projectId } = c.req.param();
|
* ClaudeCodeController Routes
|
||||||
const { servers } = await getMcpList(projectId);
|
*/
|
||||||
return c.json({ servers });
|
|
||||||
|
.get("/projects/:projectId/claude-commands", async (c) => {
|
||||||
|
const response = await effectToResponse(
|
||||||
|
c,
|
||||||
|
claudeCodeController.getClaudeCommands({
|
||||||
|
...c.req.param(),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
return response;
|
||||||
})
|
})
|
||||||
|
|
||||||
.get("/cc/session-processes", async (c) => {
|
.get("/projects/:projectId/mcp/list", async (c) => {
|
||||||
const publicProcesses = await Runtime.runPromise(runtime)(
|
const response = await effectToResponse(
|
||||||
claudeCodeLifeCycleService.getPublicSessionProcesses(),
|
c,
|
||||||
|
claudeCodeController
|
||||||
|
.getMcpListRoute({
|
||||||
|
...c.req.param(),
|
||||||
|
})
|
||||||
|
.pipe(Effect.provide(runtime)),
|
||||||
);
|
);
|
||||||
return c.json({
|
return response;
|
||||||
processes: publicProcesses.map(
|
})
|
||||||
(process): PublicSessionProcess => ({
|
|
||||||
id: process.def.sessionProcessId,
|
/**
|
||||||
projectId: process.def.projectId,
|
* ClaudeCodeSessionProcessController Routes
|
||||||
sessionId: process.sessionId,
|
*/
|
||||||
status: process.type === "paused" ? "paused" : "running",
|
|
||||||
}),
|
.get("/cc/session-processes", async (c) => {
|
||||||
),
|
const response = await effectToResponse(
|
||||||
});
|
c,
|
||||||
|
claudeCodeSessionProcessController.getSessionProcesses(),
|
||||||
|
);
|
||||||
|
return response;
|
||||||
})
|
})
|
||||||
|
|
||||||
// new or resume
|
// new or resume
|
||||||
@@ -552,51 +266,13 @@ export const routes = (app: HonoAppType) =>
|
|||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
async (c) => {
|
async (c) => {
|
||||||
const { projectId, message, baseSessionId } = c.req.valid("json");
|
const response = await effectToResponse(
|
||||||
|
c,
|
||||||
const program = Effect.gen(function* () {
|
claudeCodeSessionProcessController.createSessionProcess(
|
||||||
const { project } =
|
c.req.valid("json"),
|
||||||
yield* projectRepository.getProject(projectId);
|
),
|
||||||
|
);
|
||||||
if (project.meta.projectPath === null) {
|
return response;
|
||||||
return {
|
|
||||||
error: "Project path not found",
|
|
||||||
status: 400 as const,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = yield* claudeCodeLifeCycleService.startTask({
|
|
||||||
baseSession: {
|
|
||||||
cwd: project.meta.projectPath,
|
|
||||||
projectId,
|
|
||||||
sessionId: baseSessionId,
|
|
||||||
},
|
|
||||||
config: c.get("config"),
|
|
||||||
message,
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
result,
|
|
||||||
status: 200 as const,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await Runtime.runPromise(runtime)(program);
|
|
||||||
|
|
||||||
if (result.status === 200) {
|
|
||||||
const { sessionId } =
|
|
||||||
await result.result.awaitSessionInitialized();
|
|
||||||
|
|
||||||
return c.json({
|
|
||||||
sessionProcess: {
|
|
||||||
id: result.result.sessionProcess.def.sessionProcessId,
|
|
||||||
projectId: result.result.sessionProcess.def.projectId,
|
|
||||||
sessionId,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return c.json({ error: result.error }, result.status);
|
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -612,45 +288,16 @@ export const routes = (app: HonoAppType) =>
|
|||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
async (c) => {
|
async (c) => {
|
||||||
const { sessionProcessId } = c.req.param();
|
const response = await effectToResponse(
|
||||||
const { projectId, continueMessage, baseSessionId } =
|
c,
|
||||||
c.req.valid("json");
|
claudeCodeSessionProcessController
|
||||||
|
.continueSessionProcess({
|
||||||
const program = Effect.gen(function* () {
|
...c.req.param(),
|
||||||
const { project } =
|
...c.req.valid("json"),
|
||||||
yield* projectRepository.getProject(projectId);
|
})
|
||||||
|
.pipe(Effect.provide(runtime)),
|
||||||
if (project.meta.projectPath === null) {
|
);
|
||||||
return {
|
return response;
|
||||||
error: "Project path not found",
|
|
||||||
status: 400 as const,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = yield* claudeCodeLifeCycleService.continueTask({
|
|
||||||
sessionProcessId,
|
|
||||||
message: continueMessage,
|
|
||||||
baseSessionId,
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
data: {
|
|
||||||
sessionProcess: {
|
|
||||||
id: result.sessionProcess.def.sessionProcessId,
|
|
||||||
projectId: result.sessionProcess.def.projectId,
|
|
||||||
sessionId: baseSessionId,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
status: 200 as const,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await Runtime.runPromise(runtime)(program);
|
|
||||||
if (result.status === 200) {
|
|
||||||
return c.json(result.data);
|
|
||||||
}
|
|
||||||
|
|
||||||
return c.json({ error: result.error }, result.status);
|
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -666,6 +313,10 @@ export const routes = (app: HonoAppType) =>
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ClaudeCodePermissionController Routes
|
||||||
|
*/
|
||||||
|
|
||||||
.post(
|
.post(
|
||||||
"/cc/permission-response",
|
"/cc/permission-response",
|
||||||
zValidator(
|
zValidator(
|
||||||
@@ -676,135 +327,79 @@ export const routes = (app: HonoAppType) =>
|
|||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
async (c) => {
|
async (c) => {
|
||||||
const permissionResponse = c.req.valid("json");
|
const response = await effectToResponse(
|
||||||
Effect.runFork(
|
c,
|
||||||
claudeCodePermissionService.respondToPermissionRequest(
|
claudeCodePermissionController.permissionResponse({
|
||||||
permissionResponse,
|
permissionResponse: c.req.valid("json"),
|
||||||
),
|
}),
|
||||||
);
|
);
|
||||||
return c.json({ message: "Permission response received" });
|
return response;
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SSEController Routes
|
||||||
|
*/
|
||||||
|
|
||||||
.get("/sse", async (c) => {
|
.get("/sse", async (c) => {
|
||||||
return streamSSE(
|
return streamSSE(
|
||||||
c,
|
c,
|
||||||
async (rawStream) => {
|
async (rawStream) => {
|
||||||
const handleSSE = Effect.gen(function* () {
|
await Runtime.runPromise(runtime)(
|
||||||
const typeSafeSSE = yield* TypeSafeSSE;
|
sseController
|
||||||
|
.handleSSE(rawStream)
|
||||||
// Send connect event
|
.pipe(Effect.provide(TypeSafeSSE.make(rawStream))),
|
||||||
yield* typeSafeSSE.writeSSE("connect", {
|
|
||||||
timestamp: new Date().toISOString(),
|
|
||||||
});
|
|
||||||
|
|
||||||
const onHeartbeat = () => {
|
|
||||||
Effect.runFork(
|
|
||||||
typeSafeSSE.writeSSE("heartbeat", {
|
|
||||||
timestamp: new Date().toISOString(),
|
|
||||||
}),
|
|
||||||
);
|
);
|
||||||
};
|
|
||||||
|
|
||||||
const onSessionListChanged = (
|
|
||||||
event: InternalEventDeclaration["sessionListChanged"],
|
|
||||||
) => {
|
|
||||||
Effect.runFork(
|
|
||||||
typeSafeSSE.writeSSE("sessionListChanged", {
|
|
||||||
projectId: event.projectId,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const onSessionChanged = (
|
|
||||||
event: InternalEventDeclaration["sessionChanged"],
|
|
||||||
) => {
|
|
||||||
Effect.runFork(
|
|
||||||
typeSafeSSE.writeSSE("sessionChanged", {
|
|
||||||
projectId: event.projectId,
|
|
||||||
sessionId: event.sessionId,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const onSessionProcessChanged = (
|
|
||||||
event: InternalEventDeclaration["sessionProcessChanged"],
|
|
||||||
) => {
|
|
||||||
Effect.runFork(
|
|
||||||
typeSafeSSE.writeSSE("sessionProcessChanged", {
|
|
||||||
processes: event.processes,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const onPermissionRequested = (
|
|
||||||
event: InternalEventDeclaration["permissionRequested"],
|
|
||||||
) => {
|
|
||||||
Effect.runFork(
|
|
||||||
typeSafeSSE.writeSSE("permissionRequested", {
|
|
||||||
permissionRequest: event.permissionRequest,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
yield* eventBus.on("sessionListChanged", onSessionListChanged);
|
|
||||||
yield* eventBus.on("sessionChanged", onSessionChanged);
|
|
||||||
yield* eventBus.on(
|
|
||||||
"sessionProcessChanged",
|
|
||||||
onSessionProcessChanged,
|
|
||||||
);
|
|
||||||
yield* eventBus.on("heartbeat", onHeartbeat);
|
|
||||||
yield* eventBus.on(
|
|
||||||
"permissionRequested",
|
|
||||||
onPermissionRequested,
|
|
||||||
);
|
|
||||||
|
|
||||||
const { connectionPromise } = adaptInternalEventToSSE(
|
|
||||||
rawStream,
|
|
||||||
{
|
|
||||||
timeout: 5 /* min */ * 60 /* sec */ * 1000,
|
|
||||||
cleanUp: async () => {
|
|
||||||
await Effect.runPromise(
|
|
||||||
Effect.gen(function* () {
|
|
||||||
yield* eventBus.off(
|
|
||||||
"sessionListChanged",
|
|
||||||
onSessionListChanged,
|
|
||||||
);
|
|
||||||
yield* eventBus.off(
|
|
||||||
"sessionChanged",
|
|
||||||
onSessionChanged,
|
|
||||||
);
|
|
||||||
yield* eventBus.off(
|
|
||||||
"sessionProcessChanged",
|
|
||||||
onSessionProcessChanged,
|
|
||||||
);
|
|
||||||
yield* eventBus.off("heartbeat", onHeartbeat);
|
|
||||||
yield* eventBus.off(
|
|
||||||
"permissionRequested",
|
|
||||||
onPermissionRequested,
|
|
||||||
);
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
|
||||||
connectionPromise,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
const { connectionPromise } = await Runtime.runPromise(runtime)(
|
|
||||||
handleSSE.pipe(Effect.provide(TypeSafeSSE.make(rawStream))),
|
|
||||||
);
|
|
||||||
|
|
||||||
await connectionPromise;
|
|
||||||
},
|
},
|
||||||
async (err) => {
|
async (err) => {
|
||||||
console.error("Streaming error:", err);
|
console.error("Streaming error:", err);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* FileSystemController Routes
|
||||||
|
*/
|
||||||
|
|
||||||
|
.get(
|
||||||
|
"/fs/file-completion",
|
||||||
|
zValidator(
|
||||||
|
"query",
|
||||||
|
z.object({
|
||||||
|
projectId: z.string(),
|
||||||
|
basePath: z.string().optional().default("/"),
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
async (c) => {
|
||||||
|
const response = await effectToResponse(
|
||||||
|
c,
|
||||||
|
fileSystemController.getFileCompletionRoute({
|
||||||
|
...c.req.valid("query"),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
return response;
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
.get(
|
||||||
|
"/fs/directory-browser",
|
||||||
|
zValidator(
|
||||||
|
"query",
|
||||||
|
z.object({
|
||||||
|
currentPath: z.string().optional(),
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
async (c) => {
|
||||||
|
const response = await effectToResponse(
|
||||||
|
c,
|
||||||
|
fileSystemController.getDirectoryListingRoute({
|
||||||
|
...c.req.valid("query"),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
return response;
|
||||||
|
},
|
||||||
|
)
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
43
src/server/lib/effect/toEffectResponse.ts
Normal file
43
src/server/lib/effect/toEffectResponse.ts
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
import { Effect } from "effect";
|
||||||
|
import type { Context, Input } from "hono";
|
||||||
|
import type { ContentfulStatusCode } from "hono/utils/http-status";
|
||||||
|
import type { HonoContext } from "../../hono/app";
|
||||||
|
|
||||||
|
export type ControllerResponse = {
|
||||||
|
status: ContentfulStatusCode;
|
||||||
|
response: object;
|
||||||
|
};
|
||||||
|
|
||||||
|
declare const dummyCtx: Context<HonoContext, string, Input>;
|
||||||
|
const dummyJson = <S extends ContentfulStatusCode, T extends object>(
|
||||||
|
s: S,
|
||||||
|
t: T,
|
||||||
|
) => dummyCtx.json(t, s);
|
||||||
|
type ResponseType<
|
||||||
|
S extends ContentfulStatusCode,
|
||||||
|
T extends object,
|
||||||
|
> = ReturnType<typeof dummyJson<S, T>>;
|
||||||
|
|
||||||
|
export const effectToResponse = async <
|
||||||
|
const P extends string,
|
||||||
|
const I extends Input,
|
||||||
|
const CR extends ControllerResponse,
|
||||||
|
const E,
|
||||||
|
Ret = CR extends infer I
|
||||||
|
? I extends { status: infer S; response: infer T }
|
||||||
|
? S extends ContentfulStatusCode
|
||||||
|
? T extends object
|
||||||
|
? ResponseType<S, T>
|
||||||
|
: never
|
||||||
|
: never
|
||||||
|
: never
|
||||||
|
: never,
|
||||||
|
>(
|
||||||
|
ctx: Context<HonoContext, P, I>,
|
||||||
|
effect: Effect.Effect<CR, E, never>,
|
||||||
|
) => {
|
||||||
|
const result = await Effect.runPromise(effect);
|
||||||
|
const result2 = ctx.json(result.response, result.status);
|
||||||
|
|
||||||
|
return result2 as Ret;
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user