refactor: add platform effects

This commit is contained in:
d-kimsuon
2025-10-18 02:34:33 +09:00
parent 4de41129fe
commit e45a841656
29 changed files with 445 additions and 315 deletions

View File

@@ -1,5 +1,5 @@
import { NodeContext } from "@effect/platform-node"; import { NodeContext } from "@effect/platform-node";
import { Effect, Layer } from "effect"; import { Effect } from "effect";
import { handle } from "hono/vercel"; import { handle } from "hono/vercel";
import { ClaudeCodeController } from "../../../server/core/claude-code/presentation/ClaudeCodeController"; import { ClaudeCodeController } from "../../../server/core/claude-code/presentation/ClaudeCodeController";
import { ClaudeCodePermissionController } from "../../../server/core/claude-code/presentation/ClaudeCodePermissionController"; 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 { FileSystemController } from "../../../server/core/file-system/presentation/FileSystemController";
import { GitController } from "../../../server/core/git/presentation/GitController"; import { GitController } from "../../../server/core/git/presentation/GitController";
import { GitService } from "../../../server/core/git/services/GitService"; 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 { ProjectRepository } from "../../../server/core/project/infrastructure/ProjectRepository";
import { ProjectController } from "../../../server/core/project/presentation/ProjectController"; import { ProjectController } from "../../../server/core/project/presentation/ProjectController";
import { ProjectMetaService } from "../../../server/core/project/services/ProjectMetaService"; import { ProjectMetaService } from "../../../server/core/project/services/ProjectMetaService";
@@ -28,22 +30,10 @@ 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,
);
const repositoryLayer = Layer.mergeAll(
ProjectRepository.Live,
SessionRepository.Live,
);
await Effect.runPromise( await Effect.runPromise(
program.pipe( program
// 依存の浅い順にコンテナに pipe する必要がある // 依存の浅い順にコンテナに pipe する必要がある
.pipe(
/** Presentation */ /** Presentation */
Effect.provide(ProjectController.Live), Effect.provide(ProjectController.Live),
Effect.provide(SessionController.Live), Effect.provide(SessionController.Live),
@@ -53,31 +43,34 @@ await Effect.runPromise(
Effect.provide(ClaudeCodePermissionController.Live), Effect.provide(ClaudeCodePermissionController.Live),
Effect.provide(FileSystemController.Live), Effect.provide(FileSystemController.Live),
Effect.provide(SSEController.Live), Effect.provide(SSEController.Live),
)
.pipe(
/** Application */ /** Application */
Effect.provide(InitializeService.Live), Effect.provide(InitializeService.Live),
Effect.provide(FileWatcherService.Live),
)
.pipe(
/** Domain */ /** Domain */
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), Effect.provide(ClaudeCodeService.Live),
Effect.provide(GitService.Live), Effect.provide(GitService.Live),
)
// Shared Services .pipe(
Effect.provide(FileWatcherService.Live),
Effect.provide(EventBus.Live),
Effect.provide(HonoConfigService.Live),
/** Infrastructure */ /** Infrastructure */
Effect.provide(ProjectRepository.Live),
// Repository Effect.provide(SessionRepository.Live),
Effect.provide(repositoryLayer), Effect.provide(ProjectMetaService.Live),
Effect.provide(SessionMetaService.Live),
// StorageService Effect.provide(VirtualConversationDatabase.Live),
Effect.provide(storageLayer), )
.pipe(
/** Platform */ /** Platform */
Effect.provide(ApplicationContext.Live),
Effect.provide(UserConfigService.Live),
Effect.provide(EventBus.Live),
Effect.provide(EnvService.Live),
Effect.provide(NodeContext.layer), Effect.provide(NodeContext.layer),
), ),
); );

View File

@@ -6,7 +6,7 @@ import {
import { useCallback } from "react"; import { useCallback } from "react";
import { honoClient } from "../../lib/api/client"; import { honoClient } from "../../lib/api/client";
import { configQuery } from "../../lib/api/queries"; 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 = () => { export const useConfig = () => {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
@@ -16,7 +16,7 @@ export const useConfig = () => {
queryFn: configQuery.queryFn, queryFn: configQuery.queryFn,
}); });
const updateConfigMutation = useMutation({ const updateConfigMutation = useMutation({
mutationFn: async (config: Config) => { mutationFn: async (config: UserConfig) => {
const response = await honoClient.api.config.$put({ const response = await honoClient.api.config.$put({
json: config, json: config,
}); });
@@ -32,7 +32,7 @@ export const useConfig = () => {
return { return {
config: data?.config, config: data?.config,
updateConfig: useCallback( updateConfig: useCallback(
(config: Config) => { (config: UserConfig) => {
updateConfigMutation.mutate(config); updateConfigMutation.mutate(config);
}, },
[updateConfigMutation], [updateConfigMutation],

View File

@@ -1,7 +1,10 @@
import { hc } from "hono/client"; import { hc } from "hono/client";
import type { RouteType } from "../../server/hono/route"; import type { RouteType } from "../../server/hono/route";
import { env } from "../../server/lib/env";
export const honoClient = hc<RouteType>( export const honoClient = hc<RouteType>(
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"]}/`
: "/",
); );

View File

@@ -1,24 +1,10 @@
import path from "node:path"; import path from "node:path";
import { beforeEach, describe, expect, it, vi } from "vitest"; import { describe, expect, it } from "vitest";
describe("computeClaudeProjectFilePath", () => { describe("computeClaudeProjectFilePath", () => {
const TEST_GLOBAL_CLAUDE_DIR = "/test/mock/claude"; const TEST_GLOBAL_CLAUDE_DIR = "/test/mock/claude";
const TEST_PROJECTS_DIR = path.join(TEST_GLOBAL_CLAUDE_DIR, "projects"); 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 () => { it("プロジェクトパスからClaudeの設定ディレクトリパスを計算する", async () => {
const { computeClaudeProjectFilePath } = await import( const { computeClaudeProjectFilePath } = await import(
"./computeClaudeProjectFilePath" "./computeClaudeProjectFilePath"
@@ -27,7 +13,10 @@ describe("computeClaudeProjectFilePath", () => {
const projectPath = "/home/me/dev/example"; const projectPath = "/home/me/dev/example";
const expected = `${TEST_PROJECTS_DIR}/-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); expect(result).toBe(expected);
}); });
@@ -40,7 +29,10 @@ describe("computeClaudeProjectFilePath", () => {
const projectPath = "/home/me/dev/example/"; const projectPath = "/home/me/dev/example/";
const expected = `${TEST_PROJECTS_DIR}/-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); expect(result).toBe(expected);
}); });

View File

@@ -1,10 +1,13 @@
import { Path } from "@effect/platform"; import { Path } from "@effect/platform";
import { Effect } from "effect"; 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* () { Effect.gen(function* () {
const path = yield* Path.Path; const path = yield* Path.Path;
const { projectPath, claudeProjectsDirPath } = options;
return path.join( return path.join(
claudeProjectsDirPath, claudeProjectsDirPath,

View File

@@ -1,6 +1,7 @@
import { CommandExecutor, Path } from "@effect/platform"; import { CommandExecutor, Path } from "@effect/platform";
import { NodeContext } from "@effect/platform-node"; import { NodeContext } from "@effect/platform-node";
import { Effect, Layer } from "effect"; import { Effect, Layer } from "effect";
import { EnvService } from "../../platform/services/EnvService";
import * as ClaudeCode from "./ClaudeCode"; import * as ClaudeCode from "./ClaudeCode";
describe("ClaudeCode.Config", () => { describe("ClaudeCode.Config", () => {
@@ -19,6 +20,7 @@ describe("ClaudeCode.Config", () => {
const config = await Effect.runPromise( const config = await Effect.runPromise(
ClaudeCode.Config.pipe( ClaudeCode.Config.pipe(
Effect.provide(EnvService.Live),
Effect.provide(Path.layer), Effect.provide(Path.layer),
Effect.provide(CommandExecutorTest), Effect.provide(CommandExecutorTest),
), ),

View File

@@ -1,7 +1,7 @@
import { query as originalQuery } from "@anthropic-ai/claude-code"; import { query as originalQuery } from "@anthropic-ai/claude-code";
import { Command, Path } from "@effect/platform"; import { Command, Path } from "@effect/platform";
import { Effect } from "effect"; import { Effect } from "effect";
import { env } from "../../../lib/env"; import { EnvService } from "../../platform/services/EnvService";
import * as ClaudeCodeVersion from "./ClaudeCodeVersion"; import * as ClaudeCodeVersion from "./ClaudeCodeVersion";
type CCQuery = typeof originalQuery; type CCQuery = typeof originalQuery;
@@ -10,8 +10,9 @@ type CCQueryOptions = NonNullable<Parameters<CCQuery>[0]["options"]>;
export const Config = Effect.gen(function* () { export const Config = Effect.gen(function* () {
const path = yield* Path.Path; const path = yield* Path.Path;
const envService = yield* EnvService;
const specifiedExecutablePath = env.get( const specifiedExecutablePath = yield* envService.getEnv(
"CLAUDE_CODE_VIEWER_CC_EXECUTABLE_PATH", "CLAUDE_CODE_VIEWER_CC_EXECUTABLE_PATH",
); );
@@ -21,7 +22,7 @@ export const Config = Effect.gen(function* () {
: (yield* Command.string( : (yield* Command.string(
Command.make("which", "claude").pipe( Command.make("which", "claude").pipe(
Command.env({ Command.env({
PATH: env.get("PATH"), PATH: yield* envService.getEnv("PATH"),
}), }),
Command.runInShell(true), Command.runInShell(true),
), ),

View File

@@ -1,14 +1,15 @@
import { FileSystem, Path } from "@effect/platform"; import { FileSystem, Path } from "@effect/platform";
import { Context, Effect, Layer } from "effect"; import { Context, Effect, Layer } from "effect";
import { claudeCommandsDirPath } from "../../../lib/config/paths";
import type { ControllerResponse } from "../../../lib/effect/toEffectResponse"; import type { ControllerResponse } from "../../../lib/effect/toEffectResponse";
import type { InferEffect } from "../../../lib/effect/types"; import type { InferEffect } from "../../../lib/effect/types";
import { ApplicationContext } from "../../platform/services/ApplicationContext";
import { ProjectRepository } from "../../project/infrastructure/ProjectRepository"; import { ProjectRepository } from "../../project/infrastructure/ProjectRepository";
import { ClaudeCodeService } from "../services/ClaudeCodeService"; import { ClaudeCodeService } from "../services/ClaudeCodeService";
const LayerImpl = Effect.gen(function* () { const LayerImpl = Effect.gen(function* () {
const projectRepository = yield* ProjectRepository; const projectRepository = yield* ProjectRepository;
const claudeCodeService = yield* ClaudeCodeService; const claudeCodeService = yield* ClaudeCodeService;
const context = yield* ApplicationContext;
const fs = yield* FileSystem.FileSystem; const fs = yield* FileSystem.FileSystem;
const path = yield* Path.Path; const path = yield* Path.Path;
@@ -19,7 +20,7 @@ const LayerImpl = Effect.gen(function* () {
const { project } = yield* projectRepository.getProject(projectId); const { project } = yield* projectRepository.getProject(projectId);
const globalCommands: string[] = yield* fs const globalCommands: string[] = yield* fs
.readDirectory(claudeCommandsDirPath) .readDirectory(context.claudeCodePaths.claudeCommandsDirPath)
.pipe( .pipe(
Effect.map((items) => Effect.map((items) =>
items items

View File

@@ -2,14 +2,14 @@ import { Context, Effect, Layer } from "effect";
import type { PublicSessionProcess } from "../../../../types/session-process"; import type { PublicSessionProcess } from "../../../../types/session-process";
import type { ControllerResponse } from "../../../lib/effect/toEffectResponse"; import type { ControllerResponse } from "../../../lib/effect/toEffectResponse";
import type { InferEffect } from "../../../lib/effect/types"; 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 { ProjectRepository } from "../../project/infrastructure/ProjectRepository";
import { ClaudeCodeLifeCycleService } from "../services/ClaudeCodeLifeCycleService"; import { ClaudeCodeLifeCycleService } from "../services/ClaudeCodeLifeCycleService";
const LayerImpl = Effect.gen(function* () { const LayerImpl = Effect.gen(function* () {
const projectRepository = yield* ProjectRepository; const projectRepository = yield* ProjectRepository;
const claudeCodeLifeCycleService = yield* ClaudeCodeLifeCycleService; const claudeCodeLifeCycleService = yield* ClaudeCodeLifeCycleService;
const honoConfigService = yield* HonoConfigService; const userConfigService = yield* UserConfigService;
const getSessionProcesses = () => const getSessionProcesses = () =>
Effect.gen(function* () { Effect.gen(function* () {
@@ -40,7 +40,7 @@ const LayerImpl = Effect.gen(function* () {
const { projectId, message, baseSessionId } = options; const { projectId, message, baseSessionId } = options;
const { project } = yield* projectRepository.getProject(projectId); const { project } = yield* projectRepository.getProject(projectId);
const config = yield* honoConfigService.getConfig(); const userConfig = yield* userConfigService.getUserConfig();
if (project.meta.projectPath === null) { if (project.meta.projectPath === null) {
return { return {
@@ -55,7 +55,7 @@ const LayerImpl = Effect.gen(function* () {
projectId, projectId,
sessionId: baseSessionId, sessionId: baseSessionId,
}, },
config: config, userConfig,
message, message,
}); });

View File

@@ -4,9 +4,10 @@ import type { CommandExecutor } from "@effect/platform/CommandExecutor";
import { Context, Effect, Layer, Runtime } from "effect"; import { Context, Effect, Layer, Runtime } from "effect";
import { ulid } from "ulid"; import { ulid } from "ulid";
import { controllablePromise } from "../../../../lib/controllablePromise"; 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 type { InferEffect } from "../../../lib/effect/types";
import { EventBus } from "../../events/services/EventBus"; import { EventBus } from "../../events/services/EventBus";
import type { EnvService } from "../../platform/services/EnvService";
import { SessionRepository } from "../../session/infrastructure/SessionRepository"; import { SessionRepository } from "../../session/infrastructure/SessionRepository";
import { VirtualConversationDatabase } from "../../session/infrastructure/VirtualConversationDatabase"; import { VirtualConversationDatabase } from "../../session/infrastructure/VirtualConversationDatabase";
import type { SessionMetaService } from "../../session/services/SessionMetaService"; import type { SessionMetaService } from "../../session/services/SessionMetaService";
@@ -36,6 +37,7 @@ const LayerImpl = Effect.gen(function* () {
| VirtualConversationDatabase | VirtualConversationDatabase
| SessionMetaService | SessionMetaService
| ClaudeCodePermissionService | ClaudeCodePermissionService
| EnvService
>(); >();
const continueTask = (options: { const continueTask = (options: {
@@ -78,7 +80,7 @@ const LayerImpl = Effect.gen(function* () {
}; };
const startTask = (options: { const startTask = (options: {
config: Config; userConfig: UserConfig;
baseSession: { baseSession: {
cwd: string; cwd: string;
projectId: string; projectId: string;
@@ -86,7 +88,7 @@ const LayerImpl = Effect.gen(function* () {
}; };
message: string; message: string;
}) => { }) => {
const { baseSession, message, config } = options; const { baseSession, message, userConfig } = options;
return Effect.gen(function* () { return Effect.gen(function* () {
const { const {
@@ -258,7 +260,7 @@ const LayerImpl = Effect.gen(function* () {
const permissionOptions = const permissionOptions =
yield* permissionService.createCanUseToolRelatedOptions({ yield* permissionService.createCanUseToolRelatedOptions({
taskId: task.def.taskId, taskId: task.def.taskId,
config, userConfig,
sessionId: task.def.baseSessionId, sessionId: task.def.baseSessionId,
}); });

View File

@@ -5,7 +5,7 @@ import type {
PermissionRequest, PermissionRequest,
PermissionResponse, PermissionResponse,
} from "../../../../types/permissions"; } 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 type { InferEffect } from "../../../lib/effect/types";
import { EventBus } from "../../events/services/EventBus"; import { EventBus } from "../../events/services/EventBus";
import * as ClaudeCode from "../models/ClaudeCode"; import * as ClaudeCode from "../models/ClaudeCode";
@@ -51,10 +51,10 @@ const LayerImpl = Effect.gen(function* () {
const createCanUseToolRelatedOptions = (options: { const createCanUseToolRelatedOptions = (options: {
taskId: string; taskId: string;
config: Config; userConfig: UserConfig;
sessionId?: string; sessionId?: string;
}) => { }) => {
const { taskId, config, sessionId } = options; const { taskId, userConfig, sessionId } = options;
return Effect.gen(function* () { return Effect.gen(function* () {
const claudeCodeConfig = yield* ClaudeCode.Config; const claudeCodeConfig = yield* ClaudeCode.Config;
@@ -69,11 +69,11 @@ const LayerImpl = Effect.gen(function* () {
} }
const canUseTool: CanUseTool = async (toolName, toolInput, _options) => { const canUseTool: CanUseTool = async (toolName, toolInput, _options) => {
if (config.permissionMode !== "default") { if (userConfig.permissionMode !== "default") {
// Convert Claude Code permission modes to canUseTool behaviors // Convert Claude Code permission modes to canUseTool behaviors
if ( if (
config.permissionMode === "bypassPermissions" || userConfig.permissionMode === "bypassPermissions" ||
config.permissionMode === "acceptEdits" userConfig.permissionMode === "acceptEdits"
) { ) {
return { return {
behavior: "allow" as const, behavior: "allow" as const,
@@ -123,7 +123,7 @@ const LayerImpl = Effect.gen(function* () {
return { return {
canUseTool, canUseTool,
permissionMode: config.permissionMode, permissionMode: userConfig.permissionMode,
} as const; } as const;
}); });
}; };

View File

@@ -1,6 +1,8 @@
import { Path } from "@effect/platform"; import { Path } from "@effect/platform";
import { Effect } from "effect"; import { Effect } from "effect";
import { describe, expect, it } from "vitest"; 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 type { InternalEventDeclaration } from "../types/InternalEventDeclaration";
import { EventBus } from "./EventBus"; import { EventBus } from "./EventBus";
import { FileWatcherService } from "./fileWatcher"; import { FileWatcherService } from "./fileWatcher";
@@ -21,7 +23,9 @@ describe("FileWatcherService", () => {
const result = await Effect.runPromise( const result = await Effect.runPromise(
program.pipe( program.pipe(
Effect.provide(FileWatcherService.Live), Effect.provide(FileWatcherService.Live),
Effect.provide(ApplicationContext.Live),
Effect.provide(EventBus.Live), Effect.provide(EventBus.Live),
Effect.provide(EnvService.Live),
Effect.provide(Path.layer), Effect.provide(Path.layer),
), ),
); );
@@ -45,7 +49,9 @@ describe("FileWatcherService", () => {
const result = await Effect.runPromise( const result = await Effect.runPromise(
program.pipe( program.pipe(
Effect.provide(FileWatcherService.Live), Effect.provide(FileWatcherService.Live),
Effect.provide(ApplicationContext.Live),
Effect.provide(EventBus.Live), Effect.provide(EventBus.Live),
Effect.provide(EnvService.Live),
Effect.provide(Path.layer), Effect.provide(Path.layer),
), ),
); );
@@ -69,7 +75,9 @@ describe("FileWatcherService", () => {
const result = await Effect.runPromise( const result = await Effect.runPromise(
program.pipe( program.pipe(
Effect.provide(FileWatcherService.Live), Effect.provide(FileWatcherService.Live),
Effect.provide(ApplicationContext.Live),
Effect.provide(EventBus.Live), Effect.provide(EventBus.Live),
Effect.provide(EnvService.Live),
Effect.provide(Path.layer), Effect.provide(Path.layer),
), ),
); );
@@ -99,7 +107,9 @@ describe("FileWatcherService", () => {
const result = await Effect.runPromise( const result = await Effect.runPromise(
program.pipe( program.pipe(
Effect.provide(FileWatcherService.Live), Effect.provide(FileWatcherService.Live),
Effect.provide(ApplicationContext.Live),
Effect.provide(EventBus.Live), Effect.provide(EventBus.Live),
Effect.provide(EnvService.Live),
Effect.provide(Path.layer), Effect.provide(Path.layer),
), ),
); );
@@ -146,7 +156,9 @@ describe("FileWatcherService", () => {
const result = await Effect.runPromise( const result = await Effect.runPromise(
program.pipe( program.pipe(
Effect.provide(FileWatcherService.Live), Effect.provide(FileWatcherService.Live),
Effect.provide(ApplicationContext.Live),
Effect.provide(EventBus.Live), Effect.provide(EventBus.Live),
Effect.provide(EnvService.Live),
Effect.provide(Path.layer), Effect.provide(Path.layer),
), ),
); );
@@ -172,7 +184,9 @@ describe("FileWatcherService", () => {
const result = await Effect.runPromise( const result = await Effect.runPromise(
program.pipe( program.pipe(
Effect.provide(FileWatcherService.Live), Effect.provide(FileWatcherService.Live),
Effect.provide(ApplicationContext.Live),
Effect.provide(EventBus.Live), Effect.provide(EventBus.Live),
Effect.provide(EnvService.Live),
Effect.provide(Path.layer), Effect.provide(Path.layer),
), ),
); );

View File

@@ -2,7 +2,7 @@ import { type FSWatcher, watch } from "node:fs";
import { Path } from "@effect/platform"; import { Path } from "@effect/platform";
import { Context, Effect, Layer, Ref } from "effect"; import { Context, Effect, Layer, Ref } from "effect";
import z from "zod"; import z from "zod";
import { claudeProjectsDirPath } from "../../../lib/config/paths"; import { ApplicationContext } from "../../platform/services/ApplicationContext";
import { encodeProjectIdFromSessionFilePath } from "../../project/functions/id"; import { encodeProjectIdFromSessionFilePath } from "../../project/functions/id";
import { EventBus } from "./EventBus"; import { EventBus } from "./EventBus";
@@ -26,6 +26,8 @@ export class FileWatcherService extends Context.Tag("FileWatcherService")<
Effect.gen(function* () { Effect.gen(function* () {
const path = yield* Path.Path; const path = yield* Path.Path;
const eventBus = yield* EventBus; const eventBus = yield* EventBus;
const context = yield* ApplicationContext;
const isWatchingRef = yield* Ref.make(false); const isWatchingRef = yield* Ref.make(false);
const watcherRef = yield* Ref.make<FSWatcher | null>(null); const watcherRef = yield* Ref.make<FSWatcher | null>(null);
const projectWatchersRef = yield* Ref.make<Map<string, FSWatcher>>( const projectWatchersRef = yield* Ref.make<Map<string, FSWatcher>>(
@@ -44,10 +46,13 @@ export class FileWatcherService extends Context.Tag("FileWatcherService")<
yield* Effect.tryPromise({ yield* Effect.tryPromise({
try: async () => { try: async () => {
console.log("Starting file watcher on:", claudeProjectsDirPath); console.log(
"Starting file watcher on:",
context.claudeCodePaths.claudeProjectsDirPath,
);
const watcher = watch( const watcher = watch(
claudeProjectsDirPath, context.claudeCodePaths.claudeProjectsDirPath,
{ persistent: false, recursive: true }, { persistent: false, recursive: true },
(_eventType, filename) => { (_eventType, filename) => {
if (!filename) return; if (!filename) return;
@@ -61,7 +66,10 @@ export class FileWatcherService extends Context.Tag("FileWatcherService")<
const { sessionId } = groups.data; const { sessionId } = groups.data;
// フルパスを構築してエンコードされた projectId を取得 // フルパスを構築してエンコードされた projectId を取得
const fullPath = path.join(claudeProjectsDirPath, filename); const fullPath = path.join(
context.claudeCodePaths.claudeProjectsDirPath,
filename,
);
const encodedProjectId = const encodedProjectId =
encodeProjectIdFromSessionFilePath(fullPath); encodeProjectIdFromSessionFilePath(fullPath);
const debounceKey = `${encodedProjectId}/${sessionId}`; const debounceKey = `${encodedProjectId}/${sessionId}`;

View File

@@ -1,7 +1,7 @@
import { Command, FileSystem, Path } from "@effect/platform"; import { Command, FileSystem, Path } from "@effect/platform";
import { Context, Data, Effect, Either, Layer } from "effect"; import { Context, Data, Effect, Either, Layer } from "effect";
import type { InferEffect } from "../../../lib/effect/types"; import type { InferEffect } from "../../../lib/effect/types";
import { env } from "../../../lib/env"; import { EnvService } from "../../platform/services/EnvService";
import { parseGitBranchesOutput } from "../functions/parseGitBranchesOutput"; import { parseGitBranchesOutput } from "../functions/parseGitBranchesOutput";
import { parseGitCommitsOutput } from "../functions/parseGitCommitsOutput"; import { parseGitCommitsOutput } from "../functions/parseGitCommitsOutput";
@@ -21,6 +21,7 @@ class DetachedHeadError extends Data.TaggedError("DetachedHeadError")<{
const LayerImpl = Effect.gen(function* () { const LayerImpl = Effect.gen(function* () {
const fs = yield* FileSystem.FileSystem; const fs = yield* FileSystem.FileSystem;
const path = yield* Path.Path; const path = yield* Path.Path;
const envService = yield* EnvService;
const execGitCommand = (args: string[], cwd: string) => const execGitCommand = (args: string[], cwd: string) =>
Effect.gen(function* () { Effect.gen(function* () {
@@ -41,7 +42,7 @@ const LayerImpl = Effect.gen(function* () {
const command = Command.string( const command = Command.string(
Command.make("cd", absoluteCwd, "&&", "git", ...args).pipe( Command.make("cd", absoluteCwd, "&&", "git", ...args).pipe(
Command.env({ Command.env({
PATH: env.get("PATH"), PATH: yield* envService.getEnv("PATH"),
}), }),
Command.runInShell(true), Command.runInShell(true),
), ),

View File

@@ -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<typeof LayerImpl>;
export class ApplicationContext extends EffectContext.Tag("ApplicationContext")<
ApplicationContext,
IApplicationContext
>() {
static Live = Layer.effect(this, LayerImpl);
}

View File

@@ -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<EnvSchema | undefined>(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 extends keyof EnvSchema>(
key: Key,
): Effect.Effect<EnvSchema[Key]> => {
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<typeof LayerImpl>;
export class EnvService extends Context.Tag("EnvService")<
EnvService,
IEnvService
>() {
static Live = Layer.effect(this, LayerImpl);
}

View File

@@ -1,36 +1,36 @@
import { Context, Effect, Layer, Ref } from "effect"; 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"; import type { InferEffect } from "../../../lib/effect/types";
const LayerImpl = Effect.gen(function* () { const LayerImpl = Effect.gen(function* () {
const configRef = yield* Ref.make<Config>({ const configRef = yield* Ref.make<UserConfig>({
hideNoUserMessageSession: true, hideNoUserMessageSession: true,
unifySameTitleSession: true, unifySameTitleSession: true,
enterKeyBehavior: "shift-enter-send", enterKeyBehavior: "shift-enter-send",
permissionMode: "default", permissionMode: "default",
}); });
const setConfig = (newConfig: Config) => const setUserConfig = (newConfig: UserConfig) =>
Effect.gen(function* () { Effect.gen(function* () {
yield* Ref.update(configRef, () => newConfig); yield* Ref.update(configRef, () => newConfig);
}); });
const getConfig = () => const getUserConfig = () =>
Effect.gen(function* () { Effect.gen(function* () {
const config = yield* Ref.get(configRef); const config = yield* Ref.get(configRef);
return config; return config;
}); });
return { return {
getConfig, getUserConfig,
setConfig, setUserConfig,
}; };
}); });
export type IHonoConfigService = InferEffect<typeof LayerImpl>; export type IUserConfigService = InferEffect<typeof LayerImpl>;
export class HonoConfigService extends Context.Tag("HonoConfigService")< export class UserConfigService extends Context.Tag("UserConfigService")<
HonoConfigService, UserConfigService,
IHonoConfigService IUserConfigService
>() { >() {
static Live = Layer.effect(this, LayerImpl); static Live = Layer.effect(this, LayerImpl);
} }

View File

@@ -2,6 +2,8 @@ import { FileSystem, Path } from "@effect/platform";
import { SystemError } from "@effect/platform/Error"; import { SystemError } from "@effect/platform/Error";
import { Effect, Layer, Option } from "effect"; import { Effect, Layer, Option } from "effect";
import { PersistentService } from "../../../lib/storage/FileCacheStorage/PersistentService"; 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 type { ProjectMeta } from "../../types";
import { ProjectMetaService } from "../services/ProjectMetaService"; import { ProjectMetaService } from "../services/ProjectMetaService";
import { ProjectRepository } from "./ProjectRepository"; import { ProjectRepository } from "./ProjectRepository";
@@ -96,6 +98,8 @@ describe("ProjectRepository", () => {
const result = await Effect.runPromise( const result = await Effect.runPromise(
program.pipe( program.pipe(
Effect.provide(ProjectRepository.Live), Effect.provide(ProjectRepository.Live),
Effect.provide(ApplicationContext.Live),
Effect.provide(EnvService.Live),
Effect.provide(ProjectMetaServiceMock), Effect.provide(ProjectMetaServiceMock),
Effect.provide(FileSystemMock), Effect.provide(FileSystemMock),
Effect.provide(PathMock), Effect.provide(PathMock),
@@ -138,6 +142,8 @@ describe("ProjectRepository", () => {
Effect.runPromise( Effect.runPromise(
program.pipe( program.pipe(
Effect.provide(ProjectRepository.Live), Effect.provide(ProjectRepository.Live),
Effect.provide(ApplicationContext.Live),
Effect.provide(EnvService.Live),
Effect.provide(ProjectMetaServiceMock), Effect.provide(ProjectMetaServiceMock),
Effect.provide(FileSystemMock), Effect.provide(FileSystemMock),
Effect.provide(PathMock), Effect.provide(PathMock),
@@ -173,6 +179,8 @@ describe("ProjectRepository", () => {
const result = await Effect.runPromise( const result = await Effect.runPromise(
program.pipe( program.pipe(
Effect.provide(ProjectRepository.Live), Effect.provide(ProjectRepository.Live),
Effect.provide(ApplicationContext.Live),
Effect.provide(EnvService.Live),
Effect.provide(ProjectMetaServiceMock), Effect.provide(ProjectMetaServiceMock),
Effect.provide(FileSystemMock), Effect.provide(FileSystemMock),
Effect.provide(PathMock), Effect.provide(PathMock),
@@ -221,6 +229,8 @@ describe("ProjectRepository", () => {
const result = await Effect.runPromise( const result = await Effect.runPromise(
program.pipe( program.pipe(
Effect.provide(ProjectRepository.Live), Effect.provide(ProjectRepository.Live),
Effect.provide(ApplicationContext.Live),
Effect.provide(EnvService.Live),
Effect.provide(ProjectMetaService.Live), Effect.provide(ProjectMetaService.Live),
Effect.provide(FileSystemMock), Effect.provide(FileSystemMock),
Effect.provide(PathMock), Effect.provide(PathMock),
@@ -264,6 +274,8 @@ describe("ProjectRepository", () => {
const result = await Effect.runPromise( const result = await Effect.runPromise(
program.pipe( program.pipe(
Effect.provide(ProjectRepository.Live), Effect.provide(ProjectRepository.Live),
Effect.provide(ApplicationContext.Live),
Effect.provide(EnvService.Live),
Effect.provide(ProjectMetaService.Live), Effect.provide(ProjectMetaService.Live),
Effect.provide(FileSystemMock), Effect.provide(FileSystemMock),
Effect.provide(PathMock), Effect.provide(PathMock),
@@ -313,6 +325,8 @@ describe("ProjectRepository", () => {
const result = await Effect.runPromise( const result = await Effect.runPromise(
program.pipe( program.pipe(
Effect.provide(ProjectRepository.Live), Effect.provide(ProjectRepository.Live),
Effect.provide(ApplicationContext.Live),
Effect.provide(EnvService.Live),
Effect.provide(ProjectMetaService.Live), Effect.provide(ProjectMetaService.Live),
Effect.provide(FileSystemMock), Effect.provide(FileSystemMock),
Effect.provide(PathMock), Effect.provide(PathMock),

View File

@@ -1,7 +1,7 @@
import { FileSystem, Path } from "@effect/platform"; import { FileSystem, Path } from "@effect/platform";
import { Context, Effect, Layer, Option } from "effect"; import { Context, Effect, Layer, Option } from "effect";
import { claudeProjectsDirPath } from "../../../lib/config/paths";
import type { InferEffect } from "../../../lib/effect/types"; import type { InferEffect } from "../../../lib/effect/types";
import { ApplicationContext } from "../../platform/services/ApplicationContext";
import type { Project } from "../../types"; import type { Project } from "../../types";
import { decodeProjectId, encodeProjectId } from "../functions/id"; import { decodeProjectId, encodeProjectId } from "../functions/id";
import { ProjectMetaService } from "../services/ProjectMetaService"; import { ProjectMetaService } from "../services/ProjectMetaService";
@@ -10,6 +10,7 @@ const LayerImpl = Effect.gen(function* () {
const fs = yield* FileSystem.FileSystem; const fs = yield* FileSystem.FileSystem;
const path = yield* Path.Path; const path = yield* Path.Path;
const projectMetaService = yield* ProjectMetaService; const projectMetaService = yield* ProjectMetaService;
const context = yield* ApplicationContext;
const getProject = (projectId: string) => const getProject = (projectId: string) =>
Effect.gen(function* () { Effect.gen(function* () {
@@ -40,21 +41,28 @@ const LayerImpl = Effect.gen(function* () {
const getProjects = () => const getProjects = () =>
Effect.gen(function* () { Effect.gen(function* () {
// Check if the claude projects directory exists // Check if the claude projects directory exists
const dirExists = yield* fs.exists(claudeProjectsDirPath); const dirExists = yield* fs.exists(
context.claudeCodePaths.claudeProjectsDirPath,
);
if (!dirExists) { if (!dirExists) {
console.warn( console.warn(
`Claude projects directory not found at ${claudeProjectsDirPath}`, `Claude projects directory not found at ${context.claudeCodePaths.claudeProjectsDirPath}`,
); );
return { projects: [] }; return { projects: [] };
} }
// Read directory entries // Read directory entries
const entries = yield* fs.readDirectory(claudeProjectsDirPath); const entries = yield* fs.readDirectory(
context.claudeCodePaths.claudeProjectsDirPath,
);
// Filter directories and map to Project objects // Filter directories and map to Project objects
const projectEffects = entries.map((entry) => const projectEffects = entries.map((entry) =>
Effect.gen(function* () { Effect.gen(function* () {
const fullPath = path.resolve(claudeProjectsDirPath, entry); const fullPath = path.resolve(
context.claudeCodePaths.claudeProjectsDirPath,
entry,
);
// Check if it's a directory // Check if it's a directory
const stat = yield* Effect.tryPromise(() => const stat = yield* Effect.tryPromise(() =>

View File

@@ -3,7 +3,8 @@ import type { ControllerResponse } from "../../../lib/effect/toEffectResponse";
import type { InferEffect } from "../../../lib/effect/types"; import type { InferEffect } from "../../../lib/effect/types";
import { computeClaudeProjectFilePath } from "../../claude-code/functions/computeClaudeProjectFilePath"; import { computeClaudeProjectFilePath } from "../../claude-code/functions/computeClaudeProjectFilePath";
import { ClaudeCodeLifeCycleService } from "../../claude-code/services/ClaudeCodeLifeCycleService"; 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 { SessionRepository } from "../../session/infrastructure/SessionRepository";
import { encodeProjectId } from "../functions/id"; import { encodeProjectId } from "../functions/id";
import { ProjectRepository } from "../infrastructure/ProjectRepository"; import { ProjectRepository } from "../infrastructure/ProjectRepository";
@@ -11,8 +12,9 @@ import { ProjectRepository } from "../infrastructure/ProjectRepository";
const LayerImpl = Effect.gen(function* () { const LayerImpl = Effect.gen(function* () {
const projectRepository = yield* ProjectRepository; const projectRepository = yield* ProjectRepository;
const claudeCodeLifeCycleService = yield* ClaudeCodeLifeCycleService; const claudeCodeLifeCycleService = yield* ClaudeCodeLifeCycleService;
const honoConfigService = yield* HonoConfigService; const userConfigService = yield* UserConfigService;
const sessionRepository = yield* SessionRepository; const sessionRepository = yield* SessionRepository;
const context = yield* ApplicationContext;
const getProjects = () => const getProjects = () =>
Effect.gen(function* () { Effect.gen(function* () {
@@ -27,7 +29,7 @@ const LayerImpl = Effect.gen(function* () {
Effect.gen(function* () { Effect.gen(function* () {
const { projectId, cursor } = options; const { projectId, cursor } = options;
const config = yield* honoConfigService.getConfig(); const userConfig = yield* userConfigService.getUserConfig();
const { project } = yield* projectRepository.getProject(projectId); const { project } = yield* projectRepository.getProject(projectId);
const { sessions } = yield* sessionRepository.getSessions(projectId, { const { sessions } = yield* sessionRepository.getSessions(projectId, {
@@ -37,14 +39,14 @@ const LayerImpl = Effect.gen(function* () {
let filteredSessions = sessions; let filteredSessions = sessions;
// Filter sessions based on hideNoUserMessageSession setting // Filter sessions based on hideNoUserMessageSession setting
if (config.hideNoUserMessageSession) { if (userConfig.hideNoUserMessageSession) {
filteredSessions = filteredSessions.filter((session) => { filteredSessions = filteredSessions.filter((session) => {
return session.meta.firstCommand !== null; return session.meta.firstCommand !== null;
}); });
} }
// Unify sessions with same title if unifySameTitleSession is enabled // Unify sessions with same title if unifySameTitleSession is enabled
if (config.unifySameTitleSession) { if (userConfig.unifySameTitleSession) {
const sessionMap = new Map<string, (typeof filteredSessions)[0]>(); const sessionMap = new Map<string, (typeof filteredSessions)[0]>();
for (const session of filteredSessions) { for (const session of filteredSessions) {
@@ -122,10 +124,12 @@ const LayerImpl = Effect.gen(function* () {
// No project validation needed - startTask will create a new project // No project validation needed - startTask will create a new project
// if it doesn't exist when running /init command // if it doesn't exist when running /init command
const claudeProjectFilePath = const claudeProjectFilePath = yield* computeClaudeProjectFilePath({
yield* computeClaudeProjectFilePath(projectPath); projectPath,
claudeProjectsDirPath: context.claudeCodePaths.claudeProjectsDirPath,
});
const projectId = encodeProjectId(claudeProjectFilePath); const projectId = encodeProjectId(claudeProjectFilePath);
const config = yield* honoConfigService.getConfig(); const userConfig = yield* userConfigService.getUserConfig();
const result = yield* claudeCodeLifeCycleService.startTask({ const result = yield* claudeCodeLifeCycleService.startTask({
baseSession: { baseSession: {
@@ -133,7 +137,7 @@ const LayerImpl = Effect.gen(function* () {
projectId, projectId,
sessionId: undefined, sessionId: undefined,
}, },
config: config, userConfig,
message: "/init", message: "/init",
}); });

View File

@@ -1,6 +1,7 @@
import { FileSystem, Path } from "@effect/platform"; import { FileSystem, Path } from "@effect/platform";
import { Context, Effect, Layer, Option, Ref } from "effect"; import { Context, Effect, Layer, Option, Ref } from "effect";
import { z } from "zod"; import { z } from "zod";
import type { InferEffect } from "../../../lib/effect/types";
import { import {
FileCacheStorage, FileCacheStorage,
makeFileCacheStorageLayer, makeFileCacheStorageLayer,
@@ -12,24 +13,11 @@ import { decodeProjectId } from "../functions/id";
const ProjectPathSchema = z.string().nullable(); const ProjectPathSchema = z.string().nullable();
export class ProjectMetaService extends Context.Tag("ProjectMetaService")< const LayerImpl = Effect.gen(function* () {
ProjectMetaService,
{
readonly getProjectMeta: (
projectId: string,
) => Effect.Effect<ProjectMeta, Error>;
readonly invalidateProject: (projectId: string) => Effect.Effect<void>;
}
>() {
static Live = Layer.effect(
this,
Effect.gen(function* () {
const fs = yield* FileSystem.FileSystem; const fs = yield* FileSystem.FileSystem;
const path = yield* Path.Path; const path = yield* Path.Path;
const projectPathCache = yield* FileCacheStorage<string | null>(); const projectPathCache = yield* FileCacheStorage<string | null>();
const projectMetaCacheRef = yield* Ref.make( const projectMetaCacheRef = yield* Ref.make(new Map<string, ProjectMeta>());
new Map<string, ProjectMeta>(),
);
const extractProjectPathFromJsonl = ( const extractProjectPathFromJsonl = (
filePath: string, filePath: string,
@@ -139,15 +127,18 @@ export class ProjectMetaService extends Context.Tag("ProjectMetaService")<
getProjectMeta, getProjectMeta,
invalidateProject, invalidateProject,
}; };
}), });
).pipe(
export type IProjectMetaService = InferEffect<typeof LayerImpl>;
export class ProjectMetaService extends Context.Tag("ProjectMetaService")<
ProjectMetaService,
IProjectMetaService
>() {
static Live = Layer.effect(this, LayerImpl).pipe(
Layer.provide( Layer.provide(
makeFileCacheStorageLayer("project-path-cache", ProjectPathSchema), makeFileCacheStorageLayer("project-path-cache", ProjectPathSchema),
), ),
Layer.provide(PersistentService.Live), Layer.provide(PersistentService.Live),
); );
} }
export type IProjectMetaService = Context.Tag.Service<
typeof ProjectMetaService
>;

View File

@@ -1,9 +1,9 @@
import { Hono } from "hono"; import { Hono } from "hono";
import type { Config } from "../lib/config/config"; import type { UserConfig } from "../lib/config/config";
export type HonoContext = { export type HonoContext = {
Variables: { Variables: {
config: Config; userConfig: UserConfig;
}; };
}; };

View File

@@ -4,6 +4,8 @@ import { describe, expect, it, vi } from "vitest";
import { EventBus } from "../core/events/services/EventBus"; import { EventBus } from "../core/events/services/EventBus";
import { FileWatcherService } from "../core/events/services/fileWatcher"; import { FileWatcherService } from "../core/events/services/fileWatcher";
import type { InternalEventDeclaration } from "../core/events/types/InternalEventDeclaration"; 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 { ProjectRepository } from "../core/project/infrastructure/ProjectRepository";
import { ProjectMetaService } from "../core/project/services/ProjectMetaService"; import { ProjectMetaService } from "../core/project/services/ProjectMetaService";
import { SessionRepository } from "../core/session/infrastructure/SessionRepository"; import { SessionRepository } from "../core/session/infrastructure/SessionRepository";
@@ -160,7 +162,12 @@ describe("InitializeService", () => {
); );
const result = await Effect.runPromise( 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(); expect(result).toBeUndefined();
@@ -180,7 +187,12 @@ describe("InitializeService", () => {
const testLayer = createTestLayer(); const testLayer = createTestLayer();
const result = await Effect.runPromise( 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"); expect(result).toBe("file watcher started");
@@ -219,7 +231,12 @@ describe("InitializeService", () => {
const testLayer = createTestLayer(); const testLayer = createTestLayer();
const result = await Effect.runPromise( 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); expect(result).toHaveLength(1);
@@ -253,7 +270,12 @@ describe("InitializeService", () => {
const testLayer = createTestLayer(); const testLayer = createTestLayer();
const result = await Effect.runPromise( 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 // heartbeat is emitted immediately once first, then every 10 seconds
@@ -317,7 +339,12 @@ describe("InitializeService", () => {
); );
await Effect.runPromise( 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); expect(getProjectsCalled).toHaveBeenCalledTimes(1);
@@ -341,7 +368,12 @@ describe("InitializeService", () => {
// Completes without throwing error // Completes without throwing error
await expect( await expect(
Effect.runPromise( 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(); ).resolves.toBeUndefined();
}); });
@@ -359,7 +391,12 @@ describe("InitializeService", () => {
const testLayer = createTestLayer(); const testLayer = createTestLayer();
const result = await Effect.runPromise( 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"); expect(result).toBe("cleaned up");

View File

@@ -1,6 +1,6 @@
import { getCookie, setCookie } from "hono/cookie"; import { getCookie, setCookie } from "hono/cookie";
import { createMiddleware } from "hono/factory"; import { createMiddleware } from "hono/factory";
import { configSchema } from "../../lib/config/config"; import { userConfigSchema } from "../../lib/config/config";
import type { HonoContext } from "../app"; import type { HonoContext } from "../app";
export const configMiddleware = createMiddleware<HonoContext>( export const configMiddleware = createMiddleware<HonoContext>(
@@ -8,9 +8,9 @@ export const configMiddleware = createMiddleware<HonoContext>(
const cookie = getCookie(c, "ccv-config"); const cookie = getCookie(c, "ccv-config");
const parsed = (() => { const parsed = (() => {
try { try {
return configSchema.parse(JSON.parse(cookie ?? "{}")); return userConfigSchema.parse(JSON.parse(cookie ?? "{}"));
} catch { } catch {
return configSchema.parse({}); return userConfigSchema.parse({});
} }
})(); })();
@@ -25,7 +25,7 @@ export const configMiddleware = createMiddleware<HonoContext>(
); );
} }
c.set("config", parsed); c.set("userConfig", parsed);
await next(); await next();
}, },

View File

@@ -13,14 +13,14 @@ import { TypeSafeSSE } from "../core/events/functions/typeSafeSSE";
import { SSEController } from "../core/events/presentation/SSEController"; import { SSEController } from "../core/events/presentation/SSEController";
import { FileSystemController } from "../core/file-system/presentation/FileSystemController"; import { FileSystemController } from "../core/file-system/presentation/FileSystemController";
import { GitController } from "../core/git/presentation/GitController"; 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 { ProjectController } from "../core/project/presentation/ProjectController";
import type { VirtualConversationDatabase } from "../core/session/infrastructure/VirtualConversationDatabase"; import type { VirtualConversationDatabase } from "../core/session/infrastructure/VirtualConversationDatabase";
import { SessionController } from "../core/session/presentation/SessionController"; 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 { userConfigSchema } from "../lib/config/config";
import { effectToResponse } from "../lib/effect/toEffectResponse"; import { effectToResponse } from "../lib/effect/toEffectResponse";
import { env } from "../lib/env";
import type { HonoAppType } from "./app"; import type { HonoAppType } from "./app";
import { InitializeService } from "./initialize"; import { InitializeService } from "./initialize";
import { configMiddleware } from "./middleware/config.middleware"; import { configMiddleware } from "./middleware/config.middleware";
@@ -40,11 +40,13 @@ export const routes = (app: HonoAppType) =>
const claudeCodeController = yield* ClaudeCodeController; const claudeCodeController = yield* ClaudeCodeController;
// services // services
const honoConfigService = yield* HonoConfigService; const envService = yield* EnvService;
const userConfigService = yield* UserConfigService;
const claudeCodeLifeCycleService = yield* ClaudeCodeLifeCycleService; const claudeCodeLifeCycleService = yield* ClaudeCodeLifeCycleService;
const initializeService = yield* InitializeService; const initializeService = yield* InitializeService;
const runtime = yield* Effect.runtime< const runtime = yield* Effect.runtime<
| EnvService
| SessionMetaService | SessionMetaService
| VirtualConversationDatabase | VirtualConversationDatabase
| FileSystem.FileSystem | FileSystem.FileSystem
@@ -52,7 +54,7 @@ export const routes = (app: HonoAppType) =>
| CommandExecutor.CommandExecutor | CommandExecutor.CommandExecutor
>(); >();
if (env.get("NEXT_PHASE") !== "phase-production-build") { if ((yield* envService.getEnv("NEXT_PHASE")) !== "phase-production-build") {
yield* initializeService.startInitialization(); yield* initializeService.startInitialization();
prexit(async () => { prexit(async () => {
@@ -66,8 +68,8 @@ export const routes = (app: HonoAppType) =>
.use(configMiddleware) .use(configMiddleware)
.use(async (c, next) => { .use(async (c, next) => {
await Effect.runPromise( await Effect.runPromise(
honoConfigService.setConfig({ userConfigService.setUserConfig({
...c.get("config"), ...c.get("userConfig"),
}), }),
); );
@@ -77,11 +79,11 @@ export const routes = (app: HonoAppType) =>
// routes // routes
.get("/config", async (c) => { .get("/config", async (c) => {
return c.json({ 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"); const { ...config } = c.req.valid("json");
setCookie(c, "ccv-config", JSON.stringify(config)); setCookie(c, "ccv-config", JSON.stringify(config));

View File

@@ -1,6 +1,6 @@
import z from "zod"; import z from "zod";
export const configSchema = z.object({ export const userConfigSchema = z.object({
hideNoUserMessageSession: z.boolean().optional().default(true), hideNoUserMessageSession: z.boolean().optional().default(true),
unifySameTitleSession: z.boolean().optional().default(true), unifySameTitleSession: z.boolean().optional().default(true),
enterKeyBehavior: z enterKeyBehavior: z
@@ -13,4 +13,4 @@ export const configSchema = z.object({
.default("default"), .default("default"),
}); });
export type Config = z.infer<typeof configSchema>; export type UserConfig = z.infer<typeof userConfigSchema>;

View File

@@ -1,23 +1,5 @@
import { homedir } from "node:os"; import { homedir } from "node:os";
import { resolve } from "node:path"; 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( export const claudeCodeViewerCacheDirPath = resolve(
homedir(), homedir(),

View File

@@ -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 extends keyof EnvSchema>(key: Key): EnvSchema[Key] => {
parsedEnv ??= parseEnv();
return parsedEnv[key];
},
};
})();