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 { 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,58 +30,49 @@ 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),
|
||||||
Effect.provide(GitController.Live),
|
Effect.provide(GitController.Live),
|
||||||
Effect.provide(ClaudeCodeController.Live),
|
Effect.provide(ClaudeCodeController.Live),
|
||||||
Effect.provide(ClaudeCodeSessionProcessController.Live),
|
Effect.provide(ClaudeCodeSessionProcessController.Live),
|
||||||
Effect.provide(ClaudeCodePermissionController.Live),
|
Effect.provide(ClaudeCodePermissionController.Live),
|
||||||
Effect.provide(FileSystemController.Live),
|
Effect.provide(FileSystemController.Live),
|
||||||
Effect.provide(SSEController.Live),
|
Effect.provide(SSEController.Live),
|
||||||
|
)
|
||||||
/** Application */
|
.pipe(
|
||||||
Effect.provide(InitializeService.Live),
|
/** Application */
|
||||||
|
Effect.provide(InitializeService.Live),
|
||||||
/** Domain */
|
Effect.provide(FileWatcherService.Live),
|
||||||
Effect.provide(ClaudeCodeLifeCycleService.Live),
|
)
|
||||||
Effect.provide(ClaudeCodePermissionService.Live),
|
.pipe(
|
||||||
Effect.provide(ClaudeCodeSessionProcessService.Live),
|
/** Domain */
|
||||||
Effect.provide(ClaudeCodeService.Live),
|
Effect.provide(ClaudeCodeLifeCycleService.Live),
|
||||||
Effect.provide(GitService.Live),
|
Effect.provide(ClaudeCodePermissionService.Live),
|
||||||
|
Effect.provide(ClaudeCodeSessionProcessService.Live),
|
||||||
// Shared Services
|
Effect.provide(ClaudeCodeService.Live),
|
||||||
Effect.provide(FileWatcherService.Live),
|
Effect.provide(GitService.Live),
|
||||||
Effect.provide(EventBus.Live),
|
)
|
||||||
Effect.provide(HonoConfigService.Live),
|
.pipe(
|
||||||
|
/** Infrastructure */
|
||||||
/** Infrastructure */
|
Effect.provide(ProjectRepository.Live),
|
||||||
|
Effect.provide(SessionRepository.Live),
|
||||||
// Repository
|
Effect.provide(ProjectMetaService.Live),
|
||||||
Effect.provide(repositoryLayer),
|
Effect.provide(SessionMetaService.Live),
|
||||||
|
Effect.provide(VirtualConversationDatabase.Live),
|
||||||
// StorageService
|
)
|
||||||
Effect.provide(storageLayer),
|
.pipe(
|
||||||
|
/** Platform */
|
||||||
/** Platform */
|
Effect.provide(ApplicationContext.Live),
|
||||||
Effect.provide(NodeContext.layer),
|
Effect.provide(UserConfigService.Live),
|
||||||
),
|
Effect.provide(EventBus.Live),
|
||||||
|
Effect.provide(EnvService.Live),
|
||||||
|
Effect.provide(NodeContext.layer),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
export const GET = handle(honoApp);
|
export const GET = handle(honoApp);
|
||||||
|
|||||||
@@ -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],
|
||||||
|
|||||||
@@ -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"]}/`
|
||||||
|
: "/",
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -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),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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}`;
|
||||||
|
|||||||
@@ -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),
|
||||||
),
|
),
|
||||||
|
|||||||
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 { 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);
|
||||||
}
|
}
|
||||||
@@ -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),
|
||||||
|
|||||||
@@ -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(() =>
|
||||||
|
|||||||
@@ -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",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -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,142 +13,132 @@ 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,
|
const fs = yield* FileSystem.FileSystem;
|
||||||
{
|
const path = yield* Path.Path;
|
||||||
readonly getProjectMeta: (
|
const projectPathCache = yield* FileCacheStorage<string | null>();
|
||||||
projectId: string,
|
const projectMetaCacheRef = yield* Ref.make(new Map<string, ProjectMeta>());
|
||||||
) => Effect.Effect<ProjectMeta, Error>;
|
|
||||||
readonly invalidateProject: (projectId: string) => Effect.Effect<void>;
|
const extractProjectPathFromJsonl = (
|
||||||
}
|
filePath: string,
|
||||||
>() {
|
): Effect.Effect<string | null, Error> =>
|
||||||
static Live = Layer.effect(
|
|
||||||
this,
|
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const fs = yield* FileSystem.FileSystem;
|
const cached = yield* projectPathCache.get(filePath);
|
||||||
const path = yield* Path.Path;
|
if (cached !== undefined) {
|
||||||
const projectPathCache = yield* FileCacheStorage<string | null>();
|
return cached;
|
||||||
const projectMetaCacheRef = yield* Ref.make(
|
}
|
||||||
new Map<string, ProjectMeta>(),
|
|
||||||
|
const content = yield* fs.readFileString(filePath);
|
||||||
|
const lines = content.split("\n");
|
||||||
|
|
||||||
|
let cwd: string | null = null;
|
||||||
|
|
||||||
|
for (const line of lines) {
|
||||||
|
const conversation = parseJsonl(line).at(0);
|
||||||
|
|
||||||
|
if (
|
||||||
|
conversation === undefined ||
|
||||||
|
conversation.type === "summary" ||
|
||||||
|
conversation.type === "x-error"
|
||||||
|
) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
cwd = conversation.cwd;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cwd !== null) {
|
||||||
|
yield* projectPathCache.set(filePath, cwd);
|
||||||
|
}
|
||||||
|
|
||||||
|
return cwd;
|
||||||
|
});
|
||||||
|
|
||||||
|
const getProjectMeta = (
|
||||||
|
projectId: string,
|
||||||
|
): Effect.Effect<ProjectMeta, Error> =>
|
||||||
|
Effect.gen(function* () {
|
||||||
|
const metaCache = yield* Ref.get(projectMetaCacheRef);
|
||||||
|
const cached = metaCache.get(projectId);
|
||||||
|
if (cached !== undefined) {
|
||||||
|
return cached;
|
||||||
|
}
|
||||||
|
|
||||||
|
const claudeProjectPath = decodeProjectId(projectId);
|
||||||
|
|
||||||
|
const dirents = yield* fs.readDirectory(claudeProjectPath);
|
||||||
|
const fileEntries = yield* Effect.all(
|
||||||
|
dirents
|
||||||
|
.filter((name) => name.endsWith(".jsonl"))
|
||||||
|
.map((name) =>
|
||||||
|
Effect.gen(function* () {
|
||||||
|
const fullPath = path.resolve(claudeProjectPath, name);
|
||||||
|
const stat = yield* fs.stat(fullPath);
|
||||||
|
const mtime = Option.getOrElse(stat.mtime, () => new Date(0));
|
||||||
|
return {
|
||||||
|
fullPath,
|
||||||
|
mtime,
|
||||||
|
} as const;
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
{ concurrency: "unbounded" },
|
||||||
);
|
);
|
||||||
|
|
||||||
const extractProjectPathFromJsonl = (
|
const files = fileEntries.sort((a, b) => {
|
||||||
filePath: string,
|
return a.mtime.getTime() - b.mtime.getTime();
|
||||||
): Effect.Effect<string | null, Error> =>
|
});
|
||||||
Effect.gen(function* () {
|
|
||||||
const cached = yield* projectPathCache.get(filePath);
|
|
||||||
if (cached !== undefined) {
|
|
||||||
return cached;
|
|
||||||
}
|
|
||||||
|
|
||||||
const content = yield* fs.readFileString(filePath);
|
let projectPath: string | null = null;
|
||||||
const lines = content.split("\n");
|
|
||||||
|
|
||||||
let cwd: string | null = null;
|
for (const file of files) {
|
||||||
|
projectPath = yield* extractProjectPathFromJsonl(file.fullPath);
|
||||||
|
|
||||||
for (const line of lines) {
|
if (projectPath === null) {
|
||||||
const conversation = parseJsonl(line).at(0);
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
if (
|
break;
|
||||||
conversation === undefined ||
|
}
|
||||||
conversation.type === "summary" ||
|
|
||||||
conversation.type === "x-error"
|
|
||||||
) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
cwd = conversation.cwd;
|
const projectMeta: ProjectMeta = {
|
||||||
break;
|
projectName: projectPath ? path.basename(projectPath) : null,
|
||||||
}
|
projectPath,
|
||||||
|
sessionCount: files.length,
|
||||||
if (cwd !== null) {
|
|
||||||
yield* projectPathCache.set(filePath, cwd);
|
|
||||||
}
|
|
||||||
|
|
||||||
return cwd;
|
|
||||||
});
|
|
||||||
|
|
||||||
const getProjectMeta = (
|
|
||||||
projectId: string,
|
|
||||||
): Effect.Effect<ProjectMeta, Error> =>
|
|
||||||
Effect.gen(function* () {
|
|
||||||
const metaCache = yield* Ref.get(projectMetaCacheRef);
|
|
||||||
const cached = metaCache.get(projectId);
|
|
||||||
if (cached !== undefined) {
|
|
||||||
return cached;
|
|
||||||
}
|
|
||||||
|
|
||||||
const claudeProjectPath = decodeProjectId(projectId);
|
|
||||||
|
|
||||||
const dirents = yield* fs.readDirectory(claudeProjectPath);
|
|
||||||
const fileEntries = yield* Effect.all(
|
|
||||||
dirents
|
|
||||||
.filter((name) => name.endsWith(".jsonl"))
|
|
||||||
.map((name) =>
|
|
||||||
Effect.gen(function* () {
|
|
||||||
const fullPath = path.resolve(claudeProjectPath, name);
|
|
||||||
const stat = yield* fs.stat(fullPath);
|
|
||||||
const mtime = Option.getOrElse(stat.mtime, () => new Date(0));
|
|
||||||
return {
|
|
||||||
fullPath,
|
|
||||||
mtime,
|
|
||||||
} as const;
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
{ concurrency: "unbounded" },
|
|
||||||
);
|
|
||||||
|
|
||||||
const files = fileEntries.sort((a, b) => {
|
|
||||||
return a.mtime.getTime() - b.mtime.getTime();
|
|
||||||
});
|
|
||||||
|
|
||||||
let projectPath: string | null = null;
|
|
||||||
|
|
||||||
for (const file of files) {
|
|
||||||
projectPath = yield* extractProjectPathFromJsonl(file.fullPath);
|
|
||||||
|
|
||||||
if (projectPath === null) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
const projectMeta: ProjectMeta = {
|
|
||||||
projectName: projectPath ? path.basename(projectPath) : null,
|
|
||||||
projectPath,
|
|
||||||
sessionCount: files.length,
|
|
||||||
};
|
|
||||||
|
|
||||||
yield* Ref.update(projectMetaCacheRef, (cache) => {
|
|
||||||
cache.set(projectId, projectMeta);
|
|
||||||
return cache;
|
|
||||||
});
|
|
||||||
|
|
||||||
return projectMeta;
|
|
||||||
});
|
|
||||||
|
|
||||||
const invalidateProject = (projectId: string): Effect.Effect<void> =>
|
|
||||||
Effect.gen(function* () {
|
|
||||||
yield* Ref.update(projectMetaCacheRef, (cache) => {
|
|
||||||
cache.delete(projectId);
|
|
||||||
return cache;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
getProjectMeta,
|
|
||||||
invalidateProject,
|
|
||||||
};
|
};
|
||||||
}),
|
|
||||||
).pipe(
|
yield* Ref.update(projectMetaCacheRef, (cache) => {
|
||||||
|
cache.set(projectId, projectMeta);
|
||||||
|
return cache;
|
||||||
|
});
|
||||||
|
|
||||||
|
return projectMeta;
|
||||||
|
});
|
||||||
|
|
||||||
|
const invalidateProject = (projectId: string): Effect.Effect<void> =>
|
||||||
|
Effect.gen(function* () {
|
||||||
|
yield* Ref.update(projectMetaCacheRef, (cache) => {
|
||||||
|
cache.delete(projectId);
|
||||||
|
return cache;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
getProjectMeta,
|
||||||
|
invalidateProject,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
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
|
|
||||||
>;
|
|
||||||
|
|||||||
@@ -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;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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");
|
||||||
|
|||||||
@@ -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();
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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));
|
||||||
|
|||||||
@@ -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>;
|
||||||
|
|||||||
@@ -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(),
|
||||||
|
|||||||
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