mirror of
https://github.com/aljazceru/claude-code-viewer.git
synced 2025-12-19 14:24:20 +01:00
refactor: add platform effects
This commit is contained in:
@@ -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,22 +30,10 @@ 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 する必要がある
|
||||
|
||||
.pipe(
|
||||
/** Presentation */
|
||||
Effect.provide(ProjectController.Live),
|
||||
Effect.provide(SessionController.Live),
|
||||
@@ -53,31 +43,34 @@ await Effect.runPromise(
|
||||
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),
|
||||
|
||||
// Shared Services
|
||||
Effect.provide(FileWatcherService.Live),
|
||||
Effect.provide(EventBus.Live),
|
||||
Effect.provide(HonoConfigService.Live),
|
||||
|
||||
)
|
||||
.pipe(
|
||||
/** Infrastructure */
|
||||
|
||||
// Repository
|
||||
Effect.provide(repositoryLayer),
|
||||
|
||||
// StorageService
|
||||
Effect.provide(storageLayer),
|
||||
|
||||
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),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -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],
|
||||
|
||||
@@ -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<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"]}/`
|
||||
: "/",
|
||||
);
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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),
|
||||
),
|
||||
|
||||
@@ -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<Parameters<CCQuery>[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),
|
||||
),
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
|
||||
@@ -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;
|
||||
});
|
||||
};
|
||||
|
||||
@@ -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),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -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<FSWatcher | null>(null);
|
||||
const projectWatchersRef = yield* Ref.make<Map<string, FSWatcher>>(
|
||||
@@ -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}`;
|
||||
|
||||
@@ -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),
|
||||
),
|
||||
|
||||
42
src/server/core/platform/services/ApplicationContext.ts
Normal file
42
src/server/core/platform/services/ApplicationContext.ts
Normal 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);
|
||||
}
|
||||
53
src/server/core/platform/services/EnvService.ts
Normal file
53
src/server/core/platform/services/EnvService.ts
Normal 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);
|
||||
}
|
||||
@@ -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<Config>({
|
||||
const configRef = yield* Ref.make<UserConfig>({
|
||||
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<typeof LayerImpl>;
|
||||
export class HonoConfigService extends Context.Tag("HonoConfigService")<
|
||||
HonoConfigService,
|
||||
IHonoConfigService
|
||||
export type IUserConfigService = InferEffect<typeof LayerImpl>;
|
||||
export class UserConfigService extends Context.Tag("UserConfigService")<
|
||||
UserConfigService,
|
||||
IUserConfigService
|
||||
>() {
|
||||
static Live = Layer.effect(this, LayerImpl);
|
||||
}
|
||||
@@ -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),
|
||||
|
||||
@@ -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(() =>
|
||||
|
||||
@@ -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<string, (typeof filteredSessions)[0]>();
|
||||
|
||||
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",
|
||||
});
|
||||
|
||||
|
||||
@@ -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,24 +13,11 @@ import { decodeProjectId } from "../functions/id";
|
||||
|
||||
const ProjectPathSchema = z.string().nullable();
|
||||
|
||||
export class ProjectMetaService extends Context.Tag("ProjectMetaService")<
|
||||
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 LayerImpl = Effect.gen(function* () {
|
||||
const fs = yield* FileSystem.FileSystem;
|
||||
const path = yield* Path.Path;
|
||||
const projectPathCache = yield* FileCacheStorage<string | null>();
|
||||
const projectMetaCacheRef = yield* Ref.make(
|
||||
new Map<string, ProjectMeta>(),
|
||||
);
|
||||
const projectMetaCacheRef = yield* Ref.make(new Map<string, ProjectMeta>());
|
||||
|
||||
const extractProjectPathFromJsonl = (
|
||||
filePath: string,
|
||||
@@ -139,15 +127,18 @@ export class ProjectMetaService extends Context.Tag("ProjectMetaService")<
|
||||
getProjectMeta,
|
||||
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(
|
||||
makeFileCacheStorageLayer("project-path-cache", ProjectPathSchema),
|
||||
),
|
||||
Layer.provide(PersistentService.Live),
|
||||
);
|
||||
}
|
||||
|
||||
export type IProjectMetaService = Context.Tag.Service<
|
||||
typeof ProjectMetaService
|
||||
>;
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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<HonoContext>(
|
||||
@@ -8,9 +8,9 @@ export const configMiddleware = createMiddleware<HonoContext>(
|
||||
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<HonoContext>(
|
||||
);
|
||||
}
|
||||
|
||||
c.set("config", parsed);
|
||||
c.set("userConfig", parsed);
|
||||
|
||||
await next();
|
||||
},
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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<typeof configSchema>;
|
||||
export type UserConfig = z.infer<typeof userConfigSchema>;
|
||||
|
||||
@@ -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(),
|
||||
|
||||
23
src/server/lib/env/index.ts
vendored
23
src/server/lib/env/index.ts
vendored
@@ -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];
|
||||
},
|
||||
};
|
||||
})();
|
||||
Reference in New Issue
Block a user