diff --git a/src/app/api/[[...route]]/route.ts b/src/app/api/[[...route]]/route.ts index 8b79189..c7c062f 100644 --- a/src/app/api/[[...route]]/route.ts +++ b/src/app/api/[[...route]]/route.ts @@ -1,5 +1,5 @@ import { NodeContext } from "@effect/platform-node"; -import { Effect, Layer } from "effect"; +import { Effect } from "effect"; import { handle } from "hono/vercel"; import { ClaudeCodeController } from "../../../server/core/claude-code/presentation/ClaudeCodeController"; import { ClaudeCodePermissionController } from "../../../server/core/claude-code/presentation/ClaudeCodePermissionController"; @@ -14,7 +14,9 @@ import { FileWatcherService } from "../../../server/core/events/services/fileWat import { FileSystemController } from "../../../server/core/file-system/presentation/FileSystemController"; import { GitController } from "../../../server/core/git/presentation/GitController"; import { GitService } from "../../../server/core/git/services/GitService"; -import { HonoConfigService } from "../../../server/core/hono/services/HonoConfigService"; +import { ApplicationContext } from "../../../server/core/platform/services/ApplicationContext"; +import { EnvService } from "../../../server/core/platform/services/EnvService"; +import { UserConfigService } from "../../../server/core/platform/services/UserConfigService"; import { ProjectRepository } from "../../../server/core/project/infrastructure/ProjectRepository"; import { ProjectController } from "../../../server/core/project/presentation/ProjectController"; import { ProjectMetaService } from "../../../server/core/project/services/ProjectMetaService"; @@ -28,58 +30,49 @@ import { routes } from "../../../server/hono/route"; 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, -); - -const repositoryLayer = Layer.mergeAll( - ProjectRepository.Live, - SessionRepository.Live, -); - await Effect.runPromise( - program.pipe( + program // 依存の浅い順にコンテナに 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 */ - Effect.provide(InitializeService.Live), - - /** Domain */ - Effect.provide(ClaudeCodeLifeCycleService.Live), - Effect.provide(ClaudeCodePermissionService.Live), - Effect.provide(ClaudeCodeSessionProcessService.Live), - Effect.provide(ClaudeCodeService.Live), - Effect.provide(GitService.Live), - - // Shared Services - Effect.provide(FileWatcherService.Live), - Effect.provide(EventBus.Live), - Effect.provide(HonoConfigService.Live), - - /** Infrastructure */ - - // Repository - Effect.provide(repositoryLayer), - - // StorageService - Effect.provide(storageLayer), - - /** Platform */ - Effect.provide(NodeContext.layer), - ), + .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), + ) + .pipe( + /** Application */ + Effect.provide(InitializeService.Live), + Effect.provide(FileWatcherService.Live), + ) + .pipe( + /** Domain */ + Effect.provide(ClaudeCodeLifeCycleService.Live), + Effect.provide(ClaudeCodePermissionService.Live), + Effect.provide(ClaudeCodeSessionProcessService.Live), + Effect.provide(ClaudeCodeService.Live), + Effect.provide(GitService.Live), + ) + .pipe( + /** Infrastructure */ + Effect.provide(ProjectRepository.Live), + Effect.provide(SessionRepository.Live), + Effect.provide(ProjectMetaService.Live), + Effect.provide(SessionMetaService.Live), + Effect.provide(VirtualConversationDatabase.Live), + ) + .pipe( + /** Platform */ + Effect.provide(ApplicationContext.Live), + Effect.provide(UserConfigService.Live), + Effect.provide(EventBus.Live), + Effect.provide(EnvService.Live), + Effect.provide(NodeContext.layer), + ), ); export const GET = handle(honoApp); diff --git a/src/app/hooks/useConfig.ts b/src/app/hooks/useConfig.ts index 63de9c7..47ad420 100644 --- a/src/app/hooks/useConfig.ts +++ b/src/app/hooks/useConfig.ts @@ -6,7 +6,7 @@ import { import { useCallback } from "react"; import { honoClient } from "../../lib/api/client"; import { configQuery } from "../../lib/api/queries"; -import type { Config } from "../../server/lib/config/config"; +import type { UserConfig } from "../../server/lib/config/config"; export const useConfig = () => { const queryClient = useQueryClient(); @@ -16,7 +16,7 @@ export const useConfig = () => { queryFn: configQuery.queryFn, }); const updateConfigMutation = useMutation({ - mutationFn: async (config: Config) => { + mutationFn: async (config: UserConfig) => { const response = await honoClient.api.config.$put({ json: config, }); @@ -32,7 +32,7 @@ export const useConfig = () => { return { config: data?.config, updateConfig: useCallback( - (config: Config) => { + (config: UserConfig) => { updateConfigMutation.mutate(config); }, [updateConfigMutation], diff --git a/src/lib/api/client.ts b/src/lib/api/client.ts index c4025bc..ba8a1b6 100644 --- a/src/lib/api/client.ts +++ b/src/lib/api/client.ts @@ -1,7 +1,10 @@ import { hc } from "hono/client"; import type { RouteType } from "../../server/hono/route"; -import { env } from "../../server/lib/env"; export const honoClient = hc( - typeof window === "undefined" ? `http://localhost:${env.get("PORT")}/` : "/", + typeof window === "undefined" + ? // biome-ignore lint/complexity/useLiteralKeys: allow here + // biome-ignore lint/style/noProcessEnv: allow here + `http://localhost:${process.env["PORT"]}/` + : "/", ); diff --git a/src/server/core/claude-code/functions/computeClaudeProjectFilePath.test.ts b/src/server/core/claude-code/functions/computeClaudeProjectFilePath.test.ts index 72ecb1f..40742b4 100644 --- a/src/server/core/claude-code/functions/computeClaudeProjectFilePath.test.ts +++ b/src/server/core/claude-code/functions/computeClaudeProjectFilePath.test.ts @@ -1,24 +1,10 @@ import path from "node:path"; -import { beforeEach, describe, expect, it, vi } from "vitest"; +import { describe, expect, it } from "vitest"; describe("computeClaudeProjectFilePath", () => { const TEST_GLOBAL_CLAUDE_DIR = "/test/mock/claude"; const TEST_PROJECTS_DIR = path.join(TEST_GLOBAL_CLAUDE_DIR, "projects"); - beforeEach(async () => { - vi.resetModules(); - vi.doMock("../../../lib/env", () => ({ - env: { - get: (key: string) => { - if (key === "GLOBAL_CLAUDE_DIR") { - return TEST_GLOBAL_CLAUDE_DIR; - } - return undefined; - }, - }, - })); - }); - it("プロジェクトパスからClaudeの設定ディレクトリパスを計算する", async () => { const { computeClaudeProjectFilePath } = await import( "./computeClaudeProjectFilePath" @@ -27,7 +13,10 @@ describe("computeClaudeProjectFilePath", () => { const projectPath = "/home/me/dev/example"; const expected = `${TEST_PROJECTS_DIR}/-home-me-dev-example`; - const result = computeClaudeProjectFilePath(projectPath); + const result = computeClaudeProjectFilePath({ + projectPath, + claudeProjectsDirPath: TEST_PROJECTS_DIR, + }); expect(result).toBe(expected); }); @@ -40,7 +29,10 @@ describe("computeClaudeProjectFilePath", () => { const projectPath = "/home/me/dev/example/"; const expected = `${TEST_PROJECTS_DIR}/-home-me-dev-example`; - const result = computeClaudeProjectFilePath(projectPath); + const result = computeClaudeProjectFilePath({ + projectPath, + claudeProjectsDirPath: TEST_PROJECTS_DIR, + }); expect(result).toBe(expected); }); diff --git a/src/server/core/claude-code/functions/computeClaudeProjectFilePath.ts b/src/server/core/claude-code/functions/computeClaudeProjectFilePath.ts index df5cb2a..c33cad7 100644 --- a/src/server/core/claude-code/functions/computeClaudeProjectFilePath.ts +++ b/src/server/core/claude-code/functions/computeClaudeProjectFilePath.ts @@ -1,10 +1,13 @@ import { Path } from "@effect/platform"; import { Effect } from "effect"; -import { claudeProjectsDirPath } from "../../../lib/config/paths"; -export const computeClaudeProjectFilePath = (projectPath: string) => +export const computeClaudeProjectFilePath = (options: { + projectPath: string; + claudeProjectsDirPath: string; +}) => Effect.gen(function* () { const path = yield* Path.Path; + const { projectPath, claudeProjectsDirPath } = options; return path.join( claudeProjectsDirPath, diff --git a/src/server/core/claude-code/models/ClaudeCode.test.ts b/src/server/core/claude-code/models/ClaudeCode.test.ts index 16ba1de..5872a44 100644 --- a/src/server/core/claude-code/models/ClaudeCode.test.ts +++ b/src/server/core/claude-code/models/ClaudeCode.test.ts @@ -1,6 +1,7 @@ import { CommandExecutor, Path } from "@effect/platform"; import { NodeContext } from "@effect/platform-node"; import { Effect, Layer } from "effect"; +import { EnvService } from "../../platform/services/EnvService"; import * as ClaudeCode from "./ClaudeCode"; describe("ClaudeCode.Config", () => { @@ -19,6 +20,7 @@ describe("ClaudeCode.Config", () => { const config = await Effect.runPromise( ClaudeCode.Config.pipe( + Effect.provide(EnvService.Live), Effect.provide(Path.layer), Effect.provide(CommandExecutorTest), ), diff --git a/src/server/core/claude-code/models/ClaudeCode.ts b/src/server/core/claude-code/models/ClaudeCode.ts index 5189d34..bc35522 100644 --- a/src/server/core/claude-code/models/ClaudeCode.ts +++ b/src/server/core/claude-code/models/ClaudeCode.ts @@ -1,7 +1,7 @@ import { query as originalQuery } from "@anthropic-ai/claude-code"; import { Command, Path } from "@effect/platform"; import { Effect } from "effect"; -import { env } from "../../../lib/env"; +import { EnvService } from "../../platform/services/EnvService"; import * as ClaudeCodeVersion from "./ClaudeCodeVersion"; type CCQuery = typeof originalQuery; @@ -10,8 +10,9 @@ type CCQueryOptions = NonNullable[0]["options"]>; export const Config = Effect.gen(function* () { const path = yield* Path.Path; + const envService = yield* EnvService; - const specifiedExecutablePath = env.get( + const specifiedExecutablePath = yield* envService.getEnv( "CLAUDE_CODE_VIEWER_CC_EXECUTABLE_PATH", ); @@ -21,7 +22,7 @@ export const Config = Effect.gen(function* () { : (yield* Command.string( Command.make("which", "claude").pipe( Command.env({ - PATH: env.get("PATH"), + PATH: yield* envService.getEnv("PATH"), }), Command.runInShell(true), ), diff --git a/src/server/core/claude-code/presentation/ClaudeCodeController.ts b/src/server/core/claude-code/presentation/ClaudeCodeController.ts index e6f5fb6..0239c4a 100644 --- a/src/server/core/claude-code/presentation/ClaudeCodeController.ts +++ b/src/server/core/claude-code/presentation/ClaudeCodeController.ts @@ -1,14 +1,15 @@ import { FileSystem, Path } from "@effect/platform"; 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 { ApplicationContext } from "../../platform/services/ApplicationContext"; import { ProjectRepository } from "../../project/infrastructure/ProjectRepository"; import { ClaudeCodeService } from "../services/ClaudeCodeService"; const LayerImpl = Effect.gen(function* () { const projectRepository = yield* ProjectRepository; const claudeCodeService = yield* ClaudeCodeService; + const context = yield* ApplicationContext; const fs = yield* FileSystem.FileSystem; const path = yield* Path.Path; @@ -19,7 +20,7 @@ const LayerImpl = Effect.gen(function* () { const { project } = yield* projectRepository.getProject(projectId); const globalCommands: string[] = yield* fs - .readDirectory(claudeCommandsDirPath) + .readDirectory(context.claudeCodePaths.claudeCommandsDirPath) .pipe( Effect.map((items) => items diff --git a/src/server/core/claude-code/presentation/ClaudeCodeSessionProcessController.ts b/src/server/core/claude-code/presentation/ClaudeCodeSessionProcessController.ts index d5e756d..1e741c5 100644 --- a/src/server/core/claude-code/presentation/ClaudeCodeSessionProcessController.ts +++ b/src/server/core/claude-code/presentation/ClaudeCodeSessionProcessController.ts @@ -2,14 +2,14 @@ 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 { UserConfigService } from "../../platform/services/UserConfigService"; 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 userConfigService = yield* UserConfigService; const getSessionProcesses = () => Effect.gen(function* () { @@ -40,7 +40,7 @@ const LayerImpl = Effect.gen(function* () { const { projectId, message, baseSessionId } = options; const { project } = yield* projectRepository.getProject(projectId); - const config = yield* honoConfigService.getConfig(); + const userConfig = yield* userConfigService.getUserConfig(); if (project.meta.projectPath === null) { return { @@ -55,7 +55,7 @@ const LayerImpl = Effect.gen(function* () { projectId, sessionId: baseSessionId, }, - config: config, + userConfig, message, }); diff --git a/src/server/core/claude-code/services/ClaudeCodeLifeCycleService.ts b/src/server/core/claude-code/services/ClaudeCodeLifeCycleService.ts index 9684eca..ea9420b 100644 --- a/src/server/core/claude-code/services/ClaudeCodeLifeCycleService.ts +++ b/src/server/core/claude-code/services/ClaudeCodeLifeCycleService.ts @@ -4,9 +4,10 @@ import type { CommandExecutor } from "@effect/platform/CommandExecutor"; import { Context, Effect, Layer, Runtime } from "effect"; import { ulid } from "ulid"; import { controllablePromise } from "../../../../lib/controllablePromise"; -import type { Config } from "../../../lib/config/config"; +import type { UserConfig } from "../../../lib/config/config"; import type { InferEffect } from "../../../lib/effect/types"; import { EventBus } from "../../events/services/EventBus"; +import type { EnvService } from "../../platform/services/EnvService"; import { SessionRepository } from "../../session/infrastructure/SessionRepository"; import { VirtualConversationDatabase } from "../../session/infrastructure/VirtualConversationDatabase"; import type { SessionMetaService } from "../../session/services/SessionMetaService"; @@ -36,6 +37,7 @@ const LayerImpl = Effect.gen(function* () { | VirtualConversationDatabase | SessionMetaService | ClaudeCodePermissionService + | EnvService >(); const continueTask = (options: { @@ -78,7 +80,7 @@ const LayerImpl = Effect.gen(function* () { }; const startTask = (options: { - config: Config; + userConfig: UserConfig; baseSession: { cwd: string; projectId: string; @@ -86,7 +88,7 @@ const LayerImpl = Effect.gen(function* () { }; message: string; }) => { - const { baseSession, message, config } = options; + const { baseSession, message, userConfig } = options; return Effect.gen(function* () { const { @@ -258,7 +260,7 @@ const LayerImpl = Effect.gen(function* () { const permissionOptions = yield* permissionService.createCanUseToolRelatedOptions({ taskId: task.def.taskId, - config, + userConfig, sessionId: task.def.baseSessionId, }); diff --git a/src/server/core/claude-code/services/ClaudeCodePermissionService.ts b/src/server/core/claude-code/services/ClaudeCodePermissionService.ts index c94f7e9..f455c75 100644 --- a/src/server/core/claude-code/services/ClaudeCodePermissionService.ts +++ b/src/server/core/claude-code/services/ClaudeCodePermissionService.ts @@ -5,7 +5,7 @@ import type { PermissionRequest, PermissionResponse, } from "../../../../types/permissions"; -import type { Config } from "../../../lib/config/config"; +import type { UserConfig } from "../../../lib/config/config"; import type { InferEffect } from "../../../lib/effect/types"; import { EventBus } from "../../events/services/EventBus"; import * as ClaudeCode from "../models/ClaudeCode"; @@ -51,10 +51,10 @@ const LayerImpl = Effect.gen(function* () { const createCanUseToolRelatedOptions = (options: { taskId: string; - config: Config; + userConfig: UserConfig; sessionId?: string; }) => { - const { taskId, config, sessionId } = options; + const { taskId, userConfig, sessionId } = options; return Effect.gen(function* () { const claudeCodeConfig = yield* ClaudeCode.Config; @@ -69,11 +69,11 @@ const LayerImpl = Effect.gen(function* () { } const canUseTool: CanUseTool = async (toolName, toolInput, _options) => { - if (config.permissionMode !== "default") { + if (userConfig.permissionMode !== "default") { // Convert Claude Code permission modes to canUseTool behaviors if ( - config.permissionMode === "bypassPermissions" || - config.permissionMode === "acceptEdits" + userConfig.permissionMode === "bypassPermissions" || + userConfig.permissionMode === "acceptEdits" ) { return { behavior: "allow" as const, @@ -123,7 +123,7 @@ const LayerImpl = Effect.gen(function* () { return { canUseTool, - permissionMode: config.permissionMode, + permissionMode: userConfig.permissionMode, } as const; }); }; diff --git a/src/server/core/events/services/fileWatcher.test.ts b/src/server/core/events/services/fileWatcher.test.ts index ce95b2c..e83ea49 100644 --- a/src/server/core/events/services/fileWatcher.test.ts +++ b/src/server/core/events/services/fileWatcher.test.ts @@ -1,6 +1,8 @@ import { Path } from "@effect/platform"; import { Effect } from "effect"; import { describe, expect, it } from "vitest"; +import { ApplicationContext } from "../../platform/services/ApplicationContext"; +import { EnvService } from "../../platform/services/EnvService"; import type { InternalEventDeclaration } from "../types/InternalEventDeclaration"; import { EventBus } from "./EventBus"; import { FileWatcherService } from "./fileWatcher"; @@ -21,7 +23,9 @@ describe("FileWatcherService", () => { const result = await Effect.runPromise( program.pipe( Effect.provide(FileWatcherService.Live), + Effect.provide(ApplicationContext.Live), Effect.provide(EventBus.Live), + Effect.provide(EnvService.Live), Effect.provide(Path.layer), ), ); @@ -45,7 +49,9 @@ describe("FileWatcherService", () => { const result = await Effect.runPromise( program.pipe( Effect.provide(FileWatcherService.Live), + Effect.provide(ApplicationContext.Live), Effect.provide(EventBus.Live), + Effect.provide(EnvService.Live), Effect.provide(Path.layer), ), ); @@ -69,7 +75,9 @@ describe("FileWatcherService", () => { const result = await Effect.runPromise( program.pipe( Effect.provide(FileWatcherService.Live), + Effect.provide(ApplicationContext.Live), Effect.provide(EventBus.Live), + Effect.provide(EnvService.Live), Effect.provide(Path.layer), ), ); @@ -99,7 +107,9 @@ describe("FileWatcherService", () => { const result = await Effect.runPromise( program.pipe( Effect.provide(FileWatcherService.Live), + Effect.provide(ApplicationContext.Live), Effect.provide(EventBus.Live), + Effect.provide(EnvService.Live), Effect.provide(Path.layer), ), ); @@ -146,7 +156,9 @@ describe("FileWatcherService", () => { const result = await Effect.runPromise( program.pipe( Effect.provide(FileWatcherService.Live), + Effect.provide(ApplicationContext.Live), Effect.provide(EventBus.Live), + Effect.provide(EnvService.Live), Effect.provide(Path.layer), ), ); @@ -172,7 +184,9 @@ describe("FileWatcherService", () => { const result = await Effect.runPromise( program.pipe( Effect.provide(FileWatcherService.Live), + Effect.provide(ApplicationContext.Live), Effect.provide(EventBus.Live), + Effect.provide(EnvService.Live), Effect.provide(Path.layer), ), ); diff --git a/src/server/core/events/services/fileWatcher.ts b/src/server/core/events/services/fileWatcher.ts index f382fc5..d179820 100644 --- a/src/server/core/events/services/fileWatcher.ts +++ b/src/server/core/events/services/fileWatcher.ts @@ -2,7 +2,7 @@ import { type FSWatcher, watch } from "node:fs"; import { Path } from "@effect/platform"; import { Context, Effect, Layer, Ref } from "effect"; import z from "zod"; -import { claudeProjectsDirPath } from "../../../lib/config/paths"; +import { ApplicationContext } from "../../platform/services/ApplicationContext"; import { encodeProjectIdFromSessionFilePath } from "../../project/functions/id"; import { EventBus } from "./EventBus"; @@ -26,6 +26,8 @@ export class FileWatcherService extends Context.Tag("FileWatcherService")< Effect.gen(function* () { const path = yield* Path.Path; const eventBus = yield* EventBus; + const context = yield* ApplicationContext; + const isWatchingRef = yield* Ref.make(false); const watcherRef = yield* Ref.make(null); const projectWatchersRef = yield* Ref.make>( @@ -44,10 +46,13 @@ export class FileWatcherService extends Context.Tag("FileWatcherService")< yield* Effect.tryPromise({ try: async () => { - console.log("Starting file watcher on:", claudeProjectsDirPath); + console.log( + "Starting file watcher on:", + context.claudeCodePaths.claudeProjectsDirPath, + ); const watcher = watch( - claudeProjectsDirPath, + context.claudeCodePaths.claudeProjectsDirPath, { persistent: false, recursive: true }, (_eventType, filename) => { if (!filename) return; @@ -61,7 +66,10 @@ export class FileWatcherService extends Context.Tag("FileWatcherService")< const { sessionId } = groups.data; // フルパスを構築してエンコードされた projectId を取得 - const fullPath = path.join(claudeProjectsDirPath, filename); + const fullPath = path.join( + context.claudeCodePaths.claudeProjectsDirPath, + filename, + ); const encodedProjectId = encodeProjectIdFromSessionFilePath(fullPath); const debounceKey = `${encodedProjectId}/${sessionId}`; diff --git a/src/server/core/git/services/GitService.ts b/src/server/core/git/services/GitService.ts index d8053ea..165b0bc 100644 --- a/src/server/core/git/services/GitService.ts +++ b/src/server/core/git/services/GitService.ts @@ -1,7 +1,7 @@ import { Command, FileSystem, Path } from "@effect/platform"; import { Context, Data, Effect, Either, Layer } from "effect"; import type { InferEffect } from "../../../lib/effect/types"; -import { env } from "../../../lib/env"; +import { EnvService } from "../../platform/services/EnvService"; import { parseGitBranchesOutput } from "../functions/parseGitBranchesOutput"; import { parseGitCommitsOutput } from "../functions/parseGitCommitsOutput"; @@ -21,6 +21,7 @@ class DetachedHeadError extends Data.TaggedError("DetachedHeadError")<{ const LayerImpl = Effect.gen(function* () { const fs = yield* FileSystem.FileSystem; const path = yield* Path.Path; + const envService = yield* EnvService; const execGitCommand = (args: string[], cwd: string) => Effect.gen(function* () { @@ -41,7 +42,7 @@ const LayerImpl = Effect.gen(function* () { const command = Command.string( Command.make("cd", absoluteCwd, "&&", "git", ...args).pipe( Command.env({ - PATH: env.get("PATH"), + PATH: yield* envService.getEnv("PATH"), }), Command.runInShell(true), ), diff --git a/src/server/lib/env/schema.ts b/src/server/core/platform/schema.ts similarity index 100% rename from src/server/lib/env/schema.ts rename to src/server/core/platform/schema.ts diff --git a/src/server/core/platform/services/ApplicationContext.ts b/src/server/core/platform/services/ApplicationContext.ts new file mode 100644 index 0000000..b3d4be2 --- /dev/null +++ b/src/server/core/platform/services/ApplicationContext.ts @@ -0,0 +1,42 @@ +import { homedir } from "node:os"; +import { Path } from "@effect/platform"; +import { Effect, Context as EffectContext, Layer } from "effect"; +import type { InferEffect } from "../../../lib/effect/types"; +import { EnvService } from "./EnvService"; + +const LayerImpl = Effect.gen(function* () { + const path = yield* Path.Path; + const envService = yield* EnvService; + + const globalClaudeDirectoryPath = yield* envService + .getEnv("CLAUDE_CODE_VIEWER_CC_EXECUTABLE_PATH") + .pipe( + Effect.map((envVar) => + envVar === undefined + ? path.resolve(homedir(), ".claude") + : path.resolve(envVar), + ), + ); + + const claudeCodePaths = { + globalClaudeDirectoryPath, + claudeCommandsDirPath: path.resolve(globalClaudeDirectoryPath, "commands"), + claudeProjectsDirPath: path.resolve(globalClaudeDirectoryPath, "projects"), + } as const satisfies { + globalClaudeDirectoryPath: string; + claudeCommandsDirPath: string; + claudeProjectsDirPath: string; + }; + + return { + claudeCodePaths, + }; +}); + +export type IApplicationContext = InferEffect; +export class ApplicationContext extends EffectContext.Tag("ApplicationContext")< + ApplicationContext, + IApplicationContext +>() { + static Live = Layer.effect(this, LayerImpl); +} diff --git a/src/server/core/platform/services/EnvService.ts b/src/server/core/platform/services/EnvService.ts new file mode 100644 index 0000000..192cd2d --- /dev/null +++ b/src/server/core/platform/services/EnvService.ts @@ -0,0 +1,53 @@ +import { Context, Effect, Layer, Ref } from "effect"; +import type { InferEffect } from "../../../lib/effect/types"; +import { type EnvSchema, envSchema } from "../schema"; + +const LayerImpl = Effect.gen(function* () { + const envRef = yield* Ref.make(undefined); + + const parseEnv = () => { + // biome-ignore lint/style/noProcessEnv: allow only here + const parsed = envSchema.safeParse(process.env); + if (!parsed.success) { + console.error(parsed.error); + throw new Error(`Invalid environment variables: ${parsed.error.message}`); + } + + return parsed.data; + }; + + const getEnv = ( + key: Key, + ): Effect.Effect => { + return Effect.gen(function* () { + yield* Ref.update(envRef, (existingEnv) => { + if (existingEnv === undefined) { + return parseEnv(); + } + return existingEnv; + }); + + const env = yield* Ref.get(envRef); + if (env === undefined) { + throw new Error( + "Unexpected error: Environment variables are not loaded", + ); + } + + return env[key]; + }); + }; + + return { + getEnv, + }; +}); + +export type IEnvService = InferEffect; + +export class EnvService extends Context.Tag("EnvService")< + EnvService, + IEnvService +>() { + static Live = Layer.effect(this, LayerImpl); +} diff --git a/src/server/core/hono/services/HonoConfigService.ts b/src/server/core/platform/services/UserConfigService.ts similarity index 59% rename from src/server/core/hono/services/HonoConfigService.ts rename to src/server/core/platform/services/UserConfigService.ts index c776600..6a71e28 100644 --- a/src/server/core/hono/services/HonoConfigService.ts +++ b/src/server/core/platform/services/UserConfigService.ts @@ -1,36 +1,36 @@ import { Context, Effect, Layer, Ref } from "effect"; -import type { Config } from "../../../lib/config/config"; +import type { UserConfig } from "../../../lib/config/config"; import type { InferEffect } from "../../../lib/effect/types"; const LayerImpl = Effect.gen(function* () { - const configRef = yield* Ref.make({ + const configRef = yield* Ref.make({ hideNoUserMessageSession: true, unifySameTitleSession: true, enterKeyBehavior: "shift-enter-send", permissionMode: "default", }); - const setConfig = (newConfig: Config) => + const setUserConfig = (newConfig: UserConfig) => Effect.gen(function* () { yield* Ref.update(configRef, () => newConfig); }); - const getConfig = () => + const getUserConfig = () => Effect.gen(function* () { const config = yield* Ref.get(configRef); return config; }); return { - getConfig, - setConfig, + getUserConfig, + setUserConfig, }; }); -export type IHonoConfigService = InferEffect; -export class HonoConfigService extends Context.Tag("HonoConfigService")< - HonoConfigService, - IHonoConfigService +export type IUserConfigService = InferEffect; +export class UserConfigService extends Context.Tag("UserConfigService")< + UserConfigService, + IUserConfigService >() { static Live = Layer.effect(this, LayerImpl); } diff --git a/src/server/core/project/infrastructure/ProjectRepository.test.ts b/src/server/core/project/infrastructure/ProjectRepository.test.ts index e4ecf86..0670cf0 100644 --- a/src/server/core/project/infrastructure/ProjectRepository.test.ts +++ b/src/server/core/project/infrastructure/ProjectRepository.test.ts @@ -2,6 +2,8 @@ import { FileSystem, Path } from "@effect/platform"; import { SystemError } from "@effect/platform/Error"; import { Effect, Layer, Option } from "effect"; import { PersistentService } from "../../../lib/storage/FileCacheStorage/PersistentService"; +import { ApplicationContext } from "../../platform/services/ApplicationContext"; +import { EnvService } from "../../platform/services/EnvService"; import type { ProjectMeta } from "../../types"; import { ProjectMetaService } from "../services/ProjectMetaService"; import { ProjectRepository } from "./ProjectRepository"; @@ -96,6 +98,8 @@ describe("ProjectRepository", () => { const result = await Effect.runPromise( program.pipe( Effect.provide(ProjectRepository.Live), + Effect.provide(ApplicationContext.Live), + Effect.provide(EnvService.Live), Effect.provide(ProjectMetaServiceMock), Effect.provide(FileSystemMock), Effect.provide(PathMock), @@ -138,6 +142,8 @@ describe("ProjectRepository", () => { Effect.runPromise( program.pipe( Effect.provide(ProjectRepository.Live), + Effect.provide(ApplicationContext.Live), + Effect.provide(EnvService.Live), Effect.provide(ProjectMetaServiceMock), Effect.provide(FileSystemMock), Effect.provide(PathMock), @@ -173,6 +179,8 @@ describe("ProjectRepository", () => { const result = await Effect.runPromise( program.pipe( Effect.provide(ProjectRepository.Live), + Effect.provide(ApplicationContext.Live), + Effect.provide(EnvService.Live), Effect.provide(ProjectMetaServiceMock), Effect.provide(FileSystemMock), Effect.provide(PathMock), @@ -221,6 +229,8 @@ describe("ProjectRepository", () => { const result = await Effect.runPromise( program.pipe( Effect.provide(ProjectRepository.Live), + Effect.provide(ApplicationContext.Live), + Effect.provide(EnvService.Live), Effect.provide(ProjectMetaService.Live), Effect.provide(FileSystemMock), Effect.provide(PathMock), @@ -264,6 +274,8 @@ describe("ProjectRepository", () => { const result = await Effect.runPromise( program.pipe( Effect.provide(ProjectRepository.Live), + Effect.provide(ApplicationContext.Live), + Effect.provide(EnvService.Live), Effect.provide(ProjectMetaService.Live), Effect.provide(FileSystemMock), Effect.provide(PathMock), @@ -313,6 +325,8 @@ describe("ProjectRepository", () => { const result = await Effect.runPromise( program.pipe( Effect.provide(ProjectRepository.Live), + Effect.provide(ApplicationContext.Live), + Effect.provide(EnvService.Live), Effect.provide(ProjectMetaService.Live), Effect.provide(FileSystemMock), Effect.provide(PathMock), diff --git a/src/server/core/project/infrastructure/ProjectRepository.ts b/src/server/core/project/infrastructure/ProjectRepository.ts index 77eaf0c..b2f6db5 100644 --- a/src/server/core/project/infrastructure/ProjectRepository.ts +++ b/src/server/core/project/infrastructure/ProjectRepository.ts @@ -1,7 +1,7 @@ import { FileSystem, Path } from "@effect/platform"; import { Context, Effect, Layer, Option } from "effect"; -import { claudeProjectsDirPath } from "../../../lib/config/paths"; import type { InferEffect } from "../../../lib/effect/types"; +import { ApplicationContext } from "../../platform/services/ApplicationContext"; import type { Project } from "../../types"; import { decodeProjectId, encodeProjectId } from "../functions/id"; import { ProjectMetaService } from "../services/ProjectMetaService"; @@ -10,6 +10,7 @@ const LayerImpl = Effect.gen(function* () { const fs = yield* FileSystem.FileSystem; const path = yield* Path.Path; const projectMetaService = yield* ProjectMetaService; + const context = yield* ApplicationContext; const getProject = (projectId: string) => Effect.gen(function* () { @@ -40,21 +41,28 @@ const LayerImpl = Effect.gen(function* () { const getProjects = () => Effect.gen(function* () { // Check if the claude projects directory exists - const dirExists = yield* fs.exists(claudeProjectsDirPath); + const dirExists = yield* fs.exists( + context.claudeCodePaths.claudeProjectsDirPath, + ); if (!dirExists) { console.warn( - `Claude projects directory not found at ${claudeProjectsDirPath}`, + `Claude projects directory not found at ${context.claudeCodePaths.claudeProjectsDirPath}`, ); return { projects: [] }; } // Read directory entries - const entries = yield* fs.readDirectory(claudeProjectsDirPath); + const entries = yield* fs.readDirectory( + context.claudeCodePaths.claudeProjectsDirPath, + ); // Filter directories and map to Project objects const projectEffects = entries.map((entry) => Effect.gen(function* () { - const fullPath = path.resolve(claudeProjectsDirPath, entry); + const fullPath = path.resolve( + context.claudeCodePaths.claudeProjectsDirPath, + entry, + ); // Check if it's a directory const stat = yield* Effect.tryPromise(() => diff --git a/src/server/core/project/presentation/ProjectController.ts b/src/server/core/project/presentation/ProjectController.ts index ce4cc24..42ee8e9 100644 --- a/src/server/core/project/presentation/ProjectController.ts +++ b/src/server/core/project/presentation/ProjectController.ts @@ -3,7 +3,8 @@ 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 { ApplicationContext } from "../../platform/services/ApplicationContext"; +import { UserConfigService } from "../../platform/services/UserConfigService"; import { SessionRepository } from "../../session/infrastructure/SessionRepository"; import { encodeProjectId } from "../functions/id"; import { ProjectRepository } from "../infrastructure/ProjectRepository"; @@ -11,8 +12,9 @@ import { ProjectRepository } from "../infrastructure/ProjectRepository"; const LayerImpl = Effect.gen(function* () { const projectRepository = yield* ProjectRepository; const claudeCodeLifeCycleService = yield* ClaudeCodeLifeCycleService; - const honoConfigService = yield* HonoConfigService; + const userConfigService = yield* UserConfigService; const sessionRepository = yield* SessionRepository; + const context = yield* ApplicationContext; const getProjects = () => Effect.gen(function* () { @@ -27,7 +29,7 @@ const LayerImpl = Effect.gen(function* () { Effect.gen(function* () { const { projectId, cursor } = options; - const config = yield* honoConfigService.getConfig(); + const userConfig = yield* userConfigService.getUserConfig(); const { project } = yield* projectRepository.getProject(projectId); const { sessions } = yield* sessionRepository.getSessions(projectId, { @@ -37,14 +39,14 @@ const LayerImpl = Effect.gen(function* () { let filteredSessions = sessions; // Filter sessions based on hideNoUserMessageSession setting - if (config.hideNoUserMessageSession) { + if (userConfig.hideNoUserMessageSession) { filteredSessions = filteredSessions.filter((session) => { return session.meta.firstCommand !== null; }); } // Unify sessions with same title if unifySameTitleSession is enabled - if (config.unifySameTitleSession) { + if (userConfig.unifySameTitleSession) { const sessionMap = new Map(); for (const session of filteredSessions) { @@ -122,10 +124,12 @@ const LayerImpl = Effect.gen(function* () { // No project validation needed - startTask will create a new project // if it doesn't exist when running /init command - const claudeProjectFilePath = - yield* computeClaudeProjectFilePath(projectPath); + const claudeProjectFilePath = yield* computeClaudeProjectFilePath({ + projectPath, + claudeProjectsDirPath: context.claudeCodePaths.claudeProjectsDirPath, + }); const projectId = encodeProjectId(claudeProjectFilePath); - const config = yield* honoConfigService.getConfig(); + const userConfig = yield* userConfigService.getUserConfig(); const result = yield* claudeCodeLifeCycleService.startTask({ baseSession: { @@ -133,7 +137,7 @@ const LayerImpl = Effect.gen(function* () { projectId, sessionId: undefined, }, - config: config, + userConfig, message: "/init", }); diff --git a/src/server/core/project/services/ProjectMetaService.ts b/src/server/core/project/services/ProjectMetaService.ts index 217d44a..921cb3a 100644 --- a/src/server/core/project/services/ProjectMetaService.ts +++ b/src/server/core/project/services/ProjectMetaService.ts @@ -1,6 +1,7 @@ import { FileSystem, Path } from "@effect/platform"; import { Context, Effect, Layer, Option, Ref } from "effect"; import { z } from "zod"; +import type { InferEffect } from "../../../lib/effect/types"; import { FileCacheStorage, makeFileCacheStorageLayer, @@ -12,142 +13,132 @@ import { decodeProjectId } from "../functions/id"; const ProjectPathSchema = z.string().nullable(); -export class ProjectMetaService extends Context.Tag("ProjectMetaService")< - ProjectMetaService, - { - readonly getProjectMeta: ( - projectId: string, - ) => Effect.Effect; - readonly invalidateProject: (projectId: string) => Effect.Effect; - } ->() { - static Live = Layer.effect( - this, +const LayerImpl = Effect.gen(function* () { + const fs = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + const projectPathCache = yield* FileCacheStorage(); + const projectMetaCacheRef = yield* Ref.make(new Map()); + + const extractProjectPathFromJsonl = ( + filePath: string, + ): Effect.Effect => Effect.gen(function* () { - const fs = yield* FileSystem.FileSystem; - const path = yield* Path.Path; - const projectPathCache = yield* FileCacheStorage(); - const projectMetaCacheRef = yield* Ref.make( - new Map(), + const cached = yield* projectPathCache.get(filePath); + if (cached !== undefined) { + return cached; + } + + const content = yield* fs.readFileString(filePath); + const lines = content.split("\n"); + + let cwd: string | null = null; + + for (const line of lines) { + const conversation = parseJsonl(line).at(0); + + if ( + conversation === undefined || + conversation.type === "summary" || + conversation.type === "x-error" + ) { + continue; + } + + cwd = conversation.cwd; + break; + } + + if (cwd !== null) { + yield* projectPathCache.set(filePath, cwd); + } + + return cwd; + }); + + const getProjectMeta = ( + projectId: string, + ): Effect.Effect => + Effect.gen(function* () { + const metaCache = yield* Ref.get(projectMetaCacheRef); + const cached = metaCache.get(projectId); + if (cached !== undefined) { + return cached; + } + + const claudeProjectPath = decodeProjectId(projectId); + + const dirents = yield* fs.readDirectory(claudeProjectPath); + const fileEntries = yield* Effect.all( + dirents + .filter((name) => name.endsWith(".jsonl")) + .map((name) => + Effect.gen(function* () { + const fullPath = path.resolve(claudeProjectPath, name); + const stat = yield* fs.stat(fullPath); + const mtime = Option.getOrElse(stat.mtime, () => new Date(0)); + return { + fullPath, + mtime, + } as const; + }), + ), + { concurrency: "unbounded" }, ); - const extractProjectPathFromJsonl = ( - filePath: string, - ): Effect.Effect => - Effect.gen(function* () { - const cached = yield* projectPathCache.get(filePath); - if (cached !== undefined) { - return cached; - } + const files = fileEntries.sort((a, b) => { + return a.mtime.getTime() - b.mtime.getTime(); + }); - const content = yield* fs.readFileString(filePath); - const lines = content.split("\n"); + let projectPath: string | null = null; - let cwd: string | null = null; + for (const file of files) { + projectPath = yield* extractProjectPathFromJsonl(file.fullPath); - for (const line of lines) { - const conversation = parseJsonl(line).at(0); + if (projectPath === null) { + continue; + } - if ( - conversation === undefined || - conversation.type === "summary" || - conversation.type === "x-error" - ) { - continue; - } + break; + } - cwd = conversation.cwd; - break; - } - - if (cwd !== null) { - yield* projectPathCache.set(filePath, cwd); - } - - return cwd; - }); - - const getProjectMeta = ( - projectId: string, - ): Effect.Effect => - Effect.gen(function* () { - const metaCache = yield* Ref.get(projectMetaCacheRef); - const cached = metaCache.get(projectId); - if (cached !== undefined) { - return cached; - } - - const claudeProjectPath = decodeProjectId(projectId); - - const dirents = yield* fs.readDirectory(claudeProjectPath); - const fileEntries = yield* Effect.all( - dirents - .filter((name) => name.endsWith(".jsonl")) - .map((name) => - Effect.gen(function* () { - const fullPath = path.resolve(claudeProjectPath, name); - const stat = yield* fs.stat(fullPath); - const mtime = Option.getOrElse(stat.mtime, () => new Date(0)); - return { - fullPath, - mtime, - } as const; - }), - ), - { concurrency: "unbounded" }, - ); - - const files = fileEntries.sort((a, b) => { - return a.mtime.getTime() - b.mtime.getTime(); - }); - - let projectPath: string | null = null; - - for (const file of files) { - projectPath = yield* extractProjectPathFromJsonl(file.fullPath); - - if (projectPath === null) { - continue; - } - - break; - } - - const projectMeta: ProjectMeta = { - projectName: projectPath ? path.basename(projectPath) : null, - projectPath, - sessionCount: files.length, - }; - - yield* Ref.update(projectMetaCacheRef, (cache) => { - cache.set(projectId, projectMeta); - return cache; - }); - - return projectMeta; - }); - - const invalidateProject = (projectId: string): Effect.Effect => - Effect.gen(function* () { - yield* Ref.update(projectMetaCacheRef, (cache) => { - cache.delete(projectId); - return cache; - }); - }); - - return { - getProjectMeta, - invalidateProject, + const projectMeta: ProjectMeta = { + projectName: projectPath ? path.basename(projectPath) : null, + projectPath, + sessionCount: files.length, }; - }), - ).pipe( + + yield* Ref.update(projectMetaCacheRef, (cache) => { + cache.set(projectId, projectMeta); + return cache; + }); + + return projectMeta; + }); + + const invalidateProject = (projectId: string): Effect.Effect => + Effect.gen(function* () { + yield* Ref.update(projectMetaCacheRef, (cache) => { + cache.delete(projectId); + return cache; + }); + }); + + return { + getProjectMeta, + invalidateProject, + }; +}); + +export type IProjectMetaService = InferEffect; + +export class ProjectMetaService extends Context.Tag("ProjectMetaService")< + ProjectMetaService, + IProjectMetaService +>() { + static Live = Layer.effect(this, LayerImpl).pipe( Layer.provide( makeFileCacheStorageLayer("project-path-cache", ProjectPathSchema), ), Layer.provide(PersistentService.Live), ); } - -export type IProjectMetaService = Context.Tag.Service< - typeof ProjectMetaService ->; diff --git a/src/server/hono/app.ts b/src/server/hono/app.ts index caeec71..4d77a14 100644 --- a/src/server/hono/app.ts +++ b/src/server/hono/app.ts @@ -1,9 +1,9 @@ import { Hono } from "hono"; -import type { Config } from "../lib/config/config"; +import type { UserConfig } from "../lib/config/config"; export type HonoContext = { Variables: { - config: Config; + userConfig: UserConfig; }; }; diff --git a/src/server/hono/initialize.test.ts b/src/server/hono/initialize.test.ts index 2778b9f..0a1b8d9 100644 --- a/src/server/hono/initialize.test.ts +++ b/src/server/hono/initialize.test.ts @@ -4,6 +4,8 @@ import { describe, expect, it, vi } from "vitest"; import { EventBus } from "../core/events/services/EventBus"; import { FileWatcherService } from "../core/events/services/fileWatcher"; import type { InternalEventDeclaration } from "../core/events/types/InternalEventDeclaration"; +import { ApplicationContext } from "../core/platform/services/ApplicationContext"; +import { EnvService } from "../core/platform/services/EnvService"; import { ProjectRepository } from "../core/project/infrastructure/ProjectRepository"; import { ProjectMetaService } from "../core/project/services/ProjectMetaService"; import { SessionRepository } from "../core/session/infrastructure/SessionRepository"; @@ -160,7 +162,12 @@ describe("InitializeService", () => { ); const result = await Effect.runPromise( - program.pipe(Effect.provide(testLayer), Effect.provide(Path.layer)), + program.pipe( + Effect.provide(testLayer), + Effect.provide(ApplicationContext.Live), + Effect.provide(EnvService.Live), + Effect.provide(Path.layer), + ), ); expect(result).toBeUndefined(); @@ -180,7 +187,12 @@ describe("InitializeService", () => { const testLayer = createTestLayer(); const result = await Effect.runPromise( - program.pipe(Effect.provide(testLayer), Effect.provide(Path.layer)), + program.pipe( + Effect.provide(testLayer), + Effect.provide(ApplicationContext.Live), + Effect.provide(EnvService.Live), + Effect.provide(Path.layer), + ), ); expect(result).toBe("file watcher started"); @@ -219,7 +231,12 @@ describe("InitializeService", () => { const testLayer = createTestLayer(); const result = await Effect.runPromise( - program.pipe(Effect.provide(testLayer), Effect.provide(Path.layer)), + program.pipe( + Effect.provide(testLayer), + Effect.provide(ApplicationContext.Live), + Effect.provide(EnvService.Live), + Effect.provide(Path.layer), + ), ); expect(result).toHaveLength(1); @@ -253,7 +270,12 @@ describe("InitializeService", () => { const testLayer = createTestLayer(); const result = await Effect.runPromise( - program.pipe(Effect.provide(testLayer), Effect.provide(Path.layer)), + program.pipe( + Effect.provide(testLayer), + Effect.provide(ApplicationContext.Live), + Effect.provide(EnvService.Live), + Effect.provide(Path.layer), + ), ); // heartbeat is emitted immediately once first, then every 10 seconds @@ -317,7 +339,12 @@ describe("InitializeService", () => { ); await Effect.runPromise( - program.pipe(Effect.provide(testLayer), Effect.provide(Path.layer)), + program.pipe( + Effect.provide(testLayer), + Effect.provide(ApplicationContext.Live), + Effect.provide(EnvService.Live), + Effect.provide(Path.layer), + ), ); expect(getProjectsCalled).toHaveBeenCalledTimes(1); @@ -341,7 +368,12 @@ describe("InitializeService", () => { // Completes without throwing error await expect( Effect.runPromise( - program.pipe(Effect.provide(testLayer), Effect.provide(Path.layer)), + program.pipe( + Effect.provide(testLayer), + Effect.provide(ApplicationContext.Live), + Effect.provide(EnvService.Live), + Effect.provide(Path.layer), + ), ), ).resolves.toBeUndefined(); }); @@ -359,7 +391,12 @@ describe("InitializeService", () => { const testLayer = createTestLayer(); const result = await Effect.runPromise( - program.pipe(Effect.provide(testLayer), Effect.provide(Path.layer)), + program.pipe( + Effect.provide(testLayer), + Effect.provide(ApplicationContext.Live), + Effect.provide(EnvService.Live), + Effect.provide(Path.layer), + ), ); expect(result).toBe("cleaned up"); diff --git a/src/server/hono/middleware/config.middleware.ts b/src/server/hono/middleware/config.middleware.ts index ec566fa..fd5d296 100644 --- a/src/server/hono/middleware/config.middleware.ts +++ b/src/server/hono/middleware/config.middleware.ts @@ -1,6 +1,6 @@ import { getCookie, setCookie } from "hono/cookie"; import { createMiddleware } from "hono/factory"; -import { configSchema } from "../../lib/config/config"; +import { userConfigSchema } from "../../lib/config/config"; import type { HonoContext } from "../app"; export const configMiddleware = createMiddleware( @@ -8,9 +8,9 @@ export const configMiddleware = createMiddleware( const cookie = getCookie(c, "ccv-config"); const parsed = (() => { try { - return configSchema.parse(JSON.parse(cookie ?? "{}")); + return userConfigSchema.parse(JSON.parse(cookie ?? "{}")); } catch { - return configSchema.parse({}); + return userConfigSchema.parse({}); } })(); @@ -25,7 +25,7 @@ export const configMiddleware = createMiddleware( ); } - c.set("config", parsed); + c.set("userConfig", parsed); await next(); }, diff --git a/src/server/hono/route.ts b/src/server/hono/route.ts index cd3a157..3a81987 100644 --- a/src/server/hono/route.ts +++ b/src/server/hono/route.ts @@ -13,14 +13,14 @@ import { TypeSafeSSE } from "../core/events/functions/typeSafeSSE"; import { SSEController } from "../core/events/presentation/SSEController"; import { FileSystemController } from "../core/file-system/presentation/FileSystemController"; import { GitController } from "../core/git/presentation/GitController"; -import { HonoConfigService } from "../core/hono/services/HonoConfigService"; +import { EnvService } from "../core/platform/services/EnvService"; +import { UserConfigService } from "../core/platform/services/UserConfigService"; import { ProjectController } from "../core/project/presentation/ProjectController"; import type { VirtualConversationDatabase } from "../core/session/infrastructure/VirtualConversationDatabase"; import { SessionController } from "../core/session/presentation/SessionController"; import type { SessionMetaService } from "../core/session/services/SessionMetaService"; -import { configSchema } from "../lib/config/config"; +import { userConfigSchema } from "../lib/config/config"; import { effectToResponse } from "../lib/effect/toEffectResponse"; -import { env } from "../lib/env"; import type { HonoAppType } from "./app"; import { InitializeService } from "./initialize"; import { configMiddleware } from "./middleware/config.middleware"; @@ -40,11 +40,13 @@ export const routes = (app: HonoAppType) => const claudeCodeController = yield* ClaudeCodeController; // services - const honoConfigService = yield* HonoConfigService; + const envService = yield* EnvService; + const userConfigService = yield* UserConfigService; const claudeCodeLifeCycleService = yield* ClaudeCodeLifeCycleService; const initializeService = yield* InitializeService; const runtime = yield* Effect.runtime< + | EnvService | SessionMetaService | VirtualConversationDatabase | FileSystem.FileSystem @@ -52,7 +54,7 @@ export const routes = (app: HonoAppType) => | CommandExecutor.CommandExecutor >(); - if (env.get("NEXT_PHASE") !== "phase-production-build") { + if ((yield* envService.getEnv("NEXT_PHASE")) !== "phase-production-build") { yield* initializeService.startInitialization(); prexit(async () => { @@ -66,8 +68,8 @@ export const routes = (app: HonoAppType) => .use(configMiddleware) .use(async (c, next) => { await Effect.runPromise( - honoConfigService.setConfig({ - ...c.get("config"), + userConfigService.setUserConfig({ + ...c.get("userConfig"), }), ); @@ -77,11 +79,11 @@ export const routes = (app: HonoAppType) => // routes .get("/config", async (c) => { return c.json({ - config: c.get("config"), + config: c.get("userConfig"), }); }) - .put("/config", zValidator("json", configSchema), async (c) => { + .put("/config", zValidator("json", userConfigSchema), async (c) => { const { ...config } = c.req.valid("json"); setCookie(c, "ccv-config", JSON.stringify(config)); diff --git a/src/server/lib/config/config.ts b/src/server/lib/config/config.ts index 021c0c9..ff295ce 100644 --- a/src/server/lib/config/config.ts +++ b/src/server/lib/config/config.ts @@ -1,6 +1,6 @@ import z from "zod"; -export const configSchema = z.object({ +export const userConfigSchema = z.object({ hideNoUserMessageSession: z.boolean().optional().default(true), unifySameTitleSession: z.boolean().optional().default(true), enterKeyBehavior: z @@ -13,4 +13,4 @@ export const configSchema = z.object({ .default("default"), }); -export type Config = z.infer; +export type UserConfig = z.infer; diff --git a/src/server/lib/config/paths.ts b/src/server/lib/config/paths.ts index 9215825..f3d551c 100644 --- a/src/server/lib/config/paths.ts +++ b/src/server/lib/config/paths.ts @@ -1,23 +1,5 @@ import { homedir } from "node:os"; import { resolve } from "node:path"; -import { env } from "../env"; - -const GLOBAL_CLAUDE_DIR = env.get("GLOBAL_CLAUDE_DIR"); - -export const globalClaudeDirectoryPath = - GLOBAL_CLAUDE_DIR === undefined - ? resolve(homedir(), ".claude") - : resolve(GLOBAL_CLAUDE_DIR); - -export const claudeProjectsDirPath = resolve( - globalClaudeDirectoryPath, - "projects", -); - -export const claudeCommandsDirPath = resolve( - globalClaudeDirectoryPath, - "commands", -); export const claudeCodeViewerCacheDirPath = resolve( homedir(), diff --git a/src/server/lib/env/index.ts b/src/server/lib/env/index.ts deleted file mode 100644 index bf312dd..0000000 --- a/src/server/lib/env/index.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { type EnvSchema, envSchema } from "./schema"; - -const parseEnv = () => { - // biome-ignore lint/style/noProcessEnv: allow only here - const parsed = envSchema.safeParse(process.env); - if (!parsed.success) { - console.error(parsed.error); - throw new Error(`Invalid environment variables: ${parsed.error.message}`); - } - - return parsed.data; -}; - -export const env = (() => { - let parsedEnv: EnvSchema | undefined; - - return { - get: (key: Key): EnvSchema[Key] => { - parsedEnv ??= parseEnv(); - return parsedEnv[key]; - }, - }; -})();