From 4933cc3a17676de567f41221e3f0abd2a0086e8f Mon Sep 17 00:00:00 2001 From: Youngsup Oh Date: Fri, 20 Mar 2026 14:23:19 +0900 Subject: [PATCH 1/4] refactor: migrate MCP, Plugin, File, Memory domains to features directory (#38) Move remaining 4 entity domains (mcp, plugin, file, memory) to src/features/ completing the co-located feature directory pattern for all 7 domains (hook, skill, subagent already migrated). Each domain now follows the standard structure: src/features/{domain}/ config.ts, service.ts, server.ts, queries.ts, utils.ts/constants.ts, components/, *.test.ts Removed scattered files from services/, server/, hooks/, lib/, components/, config/entities/. Updated ~50 import references across the codebase. Added 16 integration tests for migration integrity and restored 19 getPluginMcpServers unit tests. Constraint: shared files (entity-inspector, entities/index, BoardLayout) updated in separate phase to avoid parallel agent conflicts Rejected: separate worktrees per domain | shared file merge conflicts outweigh isolation benefit Confidence: high Scope-risk: broad Not-tested: runtime UI rendering (only static imports and unit tests verified) Co-Authored-By: Claude Opus 4.6 (1M context) --- .claude/CLAUDE.md | 162 ------------------ docs/ARCHITECTURE.md | 77 ++++----- docs/CONVENTIONS.md | 2 +- docs/ROADMAP.md | 1 + src/components/board/BoardLayout.tsx | 6 +- src/components/board/FilesPanel.tsx | 2 +- src/components/board/LspServersPanel.tsx | 2 +- src/components/board/PluginsPanel.tsx | 6 +- src/components/board/entity-inspector.tsx | 8 +- .../files-editor/api/files.functions.ts | 2 +- .../files-editor/api/files.queries.ts | 2 +- src/components/files-editor/constants.ts | 2 +- src/config/entities/index.ts | 18 +- .../file/components}/file-inspector.tsx | 2 +- .../file/config.ts} | 0 .../file/constants.ts} | 0 .../use-files.ts => features/file/queries.ts} | 20 ++- .../files.ts => features/file/server.ts} | 4 +- .../file/service.ts} | 0 .../mcp/components/add-mcp-dialog.tsx} | 2 +- .../mcp/components}/mcp-inspector.tsx | 6 +- .../mcp-config.tsx => features/mcp/config.ts} | 0 .../features/mcp/plugin-mcp-servers.test.ts | 46 +---- .../use-mcp.ts => features/mcp/queries.ts} | 6 +- .../mcp-fns.ts => features/mcp/server.ts} | 6 +- .../mcp/service.test.ts} | 4 +- .../mcp/service.ts} | 2 +- .../mcp-status.ts => features/mcp/utils.ts} | 0 .../memory/components}/memory-inspector.tsx | 0 .../memory/config.ts} | 0 .../memory.ts => features/memory/server.ts} | 2 +- .../memory/service.test.ts} | 2 +- .../memory/service.ts} | 0 .../plugin/components}/plugin-inspector.tsx | 4 +- .../plugin/config.ts} | 0 .../plugin/constants.ts} | 0 .../plugin/queries.ts} | 6 +- .../plugin/server.ts} | 2 +- .../plugin/service.test.ts} | 4 +- .../plugin/service.ts} | 0 .../skill/components/skill-inspector.tsx | 2 +- .../components/subagent-inspector.tsx | 2 +- src/hooks/use-claude-md-files.ts | 19 -- src/hooks/use-config.ts | 2 +- src/services/overview-service.ts | 6 +- tests/integration/error-edge-cases.test.ts | 2 +- tests/integration/feature-migration.test.ts | 107 ++++++++++++ 47 files changed, 224 insertions(+), 324 deletions(-) delete mode 100644 .claude/CLAUDE.md rename src/{components/file => features/file/components}/file-inspector.tsx (97%) rename src/{config/entities/file-config.tsx => features/file/config.ts} (100%) rename src/{lib/file-constants.ts => features/file/constants.ts} (100%) rename src/{hooks/use-files.ts => features/file/queries.ts} (63%) rename src/{server/files.ts => features/file/server.ts} (79%) rename src/{services/files-scanner.service.ts => features/file/service.ts} (100%) rename src/{components/board/AddMcpDialog.tsx => features/mcp/components/add-mcp-dialog.tsx} (99%) rename src/{components/mcp => features/mcp/components}/mcp-inspector.tsx (97%) rename src/{config/entities/mcp-config.tsx => features/mcp/config.ts} (100%) rename tests/unit/mcp-service.test.ts => src/features/mcp/plugin-mcp-servers.test.ts (80%) rename src/{hooks/use-mcp.ts => features/mcp/queries.ts} (98%) rename src/{server/mcp-fns.ts => features/mcp/server.ts} (93%) rename src/{services/mcp-service.test.ts => features/mcp/service.test.ts} (98%) rename src/{services/mcp-service.ts => features/mcp/service.ts} (99%) rename src/{lib/mcp-status.ts => features/mcp/utils.ts} (100%) rename src/{components/memory => features/memory/components}/memory-inspector.tsx (100%) rename src/{config/entities/memory-config.tsx => features/memory/config.ts} (100%) rename src/{server/memory.ts => features/memory/server.ts} (80%) rename src/{services/memory-service.test.ts => features/memory/service.test.ts} (98%) rename src/{services/memory-service.ts => features/memory/service.ts} (100%) rename src/{components/plugin => features/plugin/components}/plugin-inspector.tsx (97%) rename src/{config/entities/plugin-config.tsx => features/plugin/config.ts} (100%) rename src/{lib/plugin-constants.ts => features/plugin/constants.ts} (100%) rename src/{hooks/use-plugins.ts => features/plugin/queries.ts} (97%) rename src/{server/plugins-fns.ts => features/plugin/server.ts} (96%) rename src/{services/plugin-service.test.ts => features/plugin/service.test.ts} (99%) rename src/{services/plugin-service.ts => features/plugin/service.ts} (100%) delete mode 100644 src/hooks/use-claude-md-files.ts create mode 100644 tests/integration/feature-migration.test.ts diff --git a/.claude/CLAUDE.md b/.claude/CLAUDE.md deleted file mode 100644 index a152f3a..0000000 --- a/.claude/CLAUDE.md +++ /dev/null @@ -1,162 +0,0 @@ -<\!-- OMC:START --> - -# oh-my-claudecode - Intelligent Multi-Agent Orchestration - -You are running with oh-my-claudecode (OMC), a multi-agent orchestration layer for Claude Code. -Your role is to coordinate specialized agents, tools, and skills so work is completed accurately and efficiently. - - -- Delegate specialized work to the most appropriate agent. -- Keep users informed with concise progress updates. -- Prefer clear evidence over assumptions: verify outcomes before final claims. -- Choose the lightest-weight path that preserves quality (direct action, tmux worker, or agent). -- Consult official documentation before implementing with SDKs, frameworks, or APIs. - - - -Delegate for: multi-file changes, refactors, debugging, reviews, planning, research, verification, specialist work. -Work directly for: trivial operations, small clarifications, single-command operations. -Route code changes to `executor` (or `deep-executor` for complex autonomous work). -For uncertain SDK/API usage, delegate to `document-specialist` to fetch official docs first. - - - -Pass `model` on Task calls: `haiku` (quick lookups), `sonnet` (standard implementation), `opus` (architecture, deep analysis). -Direct writes OK for: `~/.claude/**`, `.omc/**`, `.claude/**`, `CLAUDE.md`, `AGENTS.md`. -For source-code edits, prefer delegation to implementation agents. - - - -Use `oh-my-claudecode:` prefix for Task subagent types. - -Build/Analysis: -- `explore` (haiku): codebase discovery, symbol/file mapping -- `analyst` (opus): requirements clarity, acceptance criteria -- `planner` (opus): task sequencing, execution plans -- `architect` (opus): system design, boundaries, interfaces -- `debugger` (sonnet): root-cause analysis, regression isolation -- `executor` (sonnet): code implementation, refactoring -- `deep-executor` (opus): complex autonomous goal-oriented tasks -- `verifier` (sonnet): completion evidence, claim validation - -Review: -- `quality-reviewer` (sonnet): logic defects, maintainability, anti-patterns, performance -- `security-reviewer` (sonnet): vulnerabilities, trust boundaries, authn/authz -- `code-reviewer` (opus): comprehensive review, API contracts, backward compatibility - -Domain: -- `test-engineer` (sonnet): test strategy, coverage, flaky-test hardening -- `build-fixer` (sonnet): build/toolchain/type failures -- `designer` (sonnet): UX/UI architecture, interaction design -- `writer` (haiku): docs, migration notes, user guidance -- `qa-tester` (sonnet): interactive CLI/service runtime validation -- `scientist` (sonnet): data/statistical analysis -- `document-specialist` (sonnet): external documentation & reference lookup -- `git-master` (sonnet): git operations, commit history management -- `code-simplifier` (opus): code clarity and simplification - -Coordination: -- `critic` (opus): plan/design critical challenge - - - -External AI (tmux CLI workers): -- Claude agents: `/team N:executor "task"` via `TeamCreate`/`Task` -- Codex/Gemini workers: `/omc-teams N:codex "task"` via tmux panes -- MCP tools: `omc_run_team_start`, `omc_run_team_wait`, `omc_run_team_status`, `omc_run_team_cleanup` - -OMC State: `state_read`, `state_write`, `state_clear`, `state_list_active`, `state_get_status` -- Stored at `{worktree}/.omc/state/{mode}-state.json`; session-scoped under `.omc/state/sessions/{sessionId}/` - -Team Coordination: `TeamCreate`, `TeamDelete`, `SendMessage`, `TaskCreate`, `TaskList`, `TaskGet`, `TaskUpdate` - -Notepad (`{worktree}/.omc/notepad.md`): `notepad_read`, `notepad_write_priority`, `notepad_write_working`, `notepad_write_manual`, `notepad_prune`, `notepad_stats` - -Project Memory (`{worktree}/.omc/project-memory.json`): `project_memory_read`, `project_memory_write`, `project_memory_add_note`, `project_memory_add_directive` - -Code Intelligence: -- LSP: `lsp_hover`, `lsp_goto_definition`, `lsp_find_references`, `lsp_document_symbols`, `lsp_workspace_symbols`, `lsp_diagnostics`, `lsp_diagnostics_directory`, `lsp_prepare_rename`, `lsp_rename`, `lsp_code_actions`, `lsp_code_action_resolve`, `lsp_servers` -- AST: `ast_grep_search`, `ast_grep_replace` -- `python_repl`: persistent Python REPL for data analysis - - - -Skills are user-invocable commands (`/oh-my-claudecode:`). When you detect trigger patterns, invoke the corresponding skill. - -Workflow: -- `autopilot` ("autopilot", "build me", "I want a"): full autonomous execution from idea to working code -- `ralph` ("ralph", "don't stop", "must complete"): self-referential loop with verifier verification; includes ultrawork -- `ultrawork` ("ulw", "ultrawork"): maximum parallelism with parallel agent orchestration -- `team` ("team", "coordinated team", "team ralph"): N coordinated Claude agents with stage-aware routing; `team ralph` for persistent team execution -- `omc-teams` ("omc-teams", "codex", "gemini"): spawn CLI workers in tmux panes -- `ccg` ("ccg", "tri-model", "claude codex gemini"): fan out to Codex + Gemini, Claude synthesizes -- `ultraqa` (activated by autopilot): QA cycling -- test, verify, fix, repeat -- `omc-plan` ("plan this", "plan the"): strategic planning; supports `--consensus` and `--review` -- `ralplan` ("ralplan", "consensus plan"): alias for `/omc-plan --consensus` -- iterative planning with Planner, Architect, Critic until consensus; short deliberation by default, `--deliberate` for high-risk work (adds pre-mortem + expanded unit/integration/e2e/observability test planning) -- `sciomc` ("sciomc"): parallel scientist agents for comprehensive analysis -- `external-context`: parallel document-specialist agents for web searches -- `deepinit` ("deepinit"): deep codebase init with hierarchical AGENTS.md - -Agent Shortcuts (thin wrappers): -- `analyze` -> `debugger`: "analyze", "debug", "investigate" -- `tdd` -> `test-engineer`: "tdd", "test first", "red green" -- `build-fix` -> `build-fixer`: "fix build", "type errors" -- `code-review` -> `code-reviewer`: "review code" -- `security-review` -> `security-reviewer`: "security review" -- `review` -> `omc-plan --review`: "review plan", "critique plan" - -Notifications: `configure-notifications` ("configure discord", "setup telegram", "configure slack") -Utilities: `cancel`, `note`, `learner`, `omc-setup`, `mcp-setup`, `hud`, `omc-doctor`, `omc-help`, `trace`, `release`, `project-session-manager`, `skill`, `writer-memory`, `ralph-init`, `learn-about-omc` - -Disambiguation: bare "codex"/"gemini" -> omc-teams; "claude codex gemini" -> ccg. Ralph includes ultrawork. - - - -Team is the default multi-agent orchestrator: `team-plan -> team-prd -> team-exec -> team-verify -> team-fix (loop)` - -Stage routing: -- `team-plan`: `explore` + `planner`, optionally `analyst`/`architect` -- `team-prd`: `analyst`, optionally `critic` -- `team-exec`: `executor` + specialists (`designer`, `build-fixer`, `writer`, `test-engineer`, `deep-executor`) -- `team-verify`: `verifier` + reviewers as needed -- `team-fix`: `executor`/`build-fixer`/`debugger` depending on defect type - -Fix loop bounded by max attempts. Terminal states: `complete`, `failed`, `cancelled`. -`team ralph` links both modes; cancelling either cancels both. - - - -Verify before claiming completion. Sizing: small (<5 files) -> `verifier` haiku; standard -> sonnet; large/security -> opus. -Loop: identify proof, run verification, read output, report with evidence. If verification fails, keep iterating. - - - -Broad requests (vague verbs, no file/function targets, 3+ areas): explore first, then use plan skill. -Parallelization: 2+ independent tasks in parallel; Team mode preferred; `run_in_background` for builds/tests. -Continuation: before concluding, confirm zero pending tasks, tests passing, zero errors, verifier evidence collected. - - - -Hooks inject context via `` tags: -- `hook success: Success` -- proceed normally -- `hook additional context: ...` -- read it; relevant to your task -- `[MAGIC KEYWORD: ...]` -- invoke the indicated skill immediately -- `The boulder never stops` -- ralph/ultrawork mode; keep working - -Persistence: `info` (7 days), `info` (permanent). -Kill switches: `DISABLE_OMC` (all hooks), `OMC_SKIP_HOOKS` (comma-separated). - - - -Invoke `/oh-my-claudecode:cancel` to end execution modes (`--force` to clear all state). -Cancel when: tasks done and verified, work blocked (explain first), user says "stop". -Do not cancel when: stop hook fires but work is still incomplete. - - - -All OMC state lives under git worktree root: `.omc/state/` (mode state), `.omc/state/sessions/{sessionId}/` (session state), `.omc/notepad.md`, `.omc/project-memory.json`, `.omc/plans/`, `.omc/research/`, `.omc/logs/`. - - -## Setup -Say "setup omc" or run `/oh-my-claudecode:omc-setup`. Announce major behavior activations to keep users informed. -<\!-- OMC:END --> diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index c7d8367..7f22181 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -69,14 +69,13 @@ src/ hook/ ← Hook 도메인 (types, constants, utils, service, server, queries, config, components/) skill/ ← Skill/Command 도메인 (types, utils, config, service, server, queries, components/) subagent/ ← Subagent 도메인 (config, server, queries, components/) + mcp/ ← MCP 도메인 (config, utils, service, server, queries, components/) + plugin/ ← Plugin 도메인 (config, constants, service, server, queries, components/) + file/ ← File 도메인 (config, constants, service, server, queries, components/) + memory/ ← Memory 도메인 (config, service, server, components/) components/ ui/ ← shadcn primitives (Button, Sheet, ListItem, Inspector 등) - memory/ ← MemoryInspector - agent/ ← AgentInspector - mcp/ ← McpInspector - plugin/ ← PluginInspector - file/ ← FileInspector - board/ ← BoardLayout, EntityListPanel, EntityInspector, Add*Dialog + board/ ← BoardLayout, EntityListPanel, EntityInspector, FilesPanel, PluginsPanel layout/ ← Sidebar, StatusBar, AppHeader config-editor/ ← Settings 페이지 (ConfigPage, 카테고리별 설정) files-editor/ ← 파일 트리 (FileTree, FileViewerPanel) @@ -84,28 +83,10 @@ src/ icons/ ← entity-icons, agent-logos, editor-icons config/ entity-registry.ts ← EntityConfig 타입 + 레지스트리 - entities/ ← 엔티티별 config (skill, command, agent, mcp, plugin, memory, file) - hooks/ ← React Query 커스텀 훅 (use-mcp, use-plugins 등) - server/ ← Server Functions (createServerFn 기반) - skills.ts, marketplace.ts, items.ts, agents.ts - mcp-fns.ts, plugins-fns.ts, files.ts, overview.ts - claude-md.ts, config-settings.ts, memory.ts, editor.ts - config.ts, validation.ts, projects.ts, middleware/auth.ts - services/ ← 서버 사이드 서비스 - config-service.ts ← 설정 파일 파싱 (스킬, 커맨드, 에이전트) - agent-file-service.ts ← AgentFile CRUD - skills-service.ts ← 스킬 설치/삭제 (skills.sh CLI) - mcp-service.ts ← MCP 서버 관리 (Claude CLI 위임) - plugin-service.ts ← 플러그인 관리 - memory-service.ts ← 메모리 파일 조회 - overview-service.ts ← 대시보드 개요 데이터 - files-scanner.service.ts ← 파일 트리 스캔 - claude-cli.ts ← Claude CLI spawn 래퍼 - file-writer.ts ← 파일 쓰기 - project-store.ts ← 프로젝트 경로 관리 - config-settings.service.ts ← settings.json 관리 - scope-management.ts ← 스코프 이동/복사 - agentfiles-config.ts ← agentfiles 자체 설정 + entities/ ← index.ts (등록 진입점, 모든 config를 features/에서 import) + hooks/ ← 공용 React Query 훅 (use-config 등) + server/ ← 공용 Server Functions (overview, projects, editor 등) + services/ ← 공용 서비스 (claude-cli, file-writer, project-store 등) routes/ ← TanStack Router 라우트 (/, /marketplace, /settings) __root.tsx ← 루트 레이아웃 index.tsx ← Dashboard (/) @@ -114,8 +95,8 @@ src/ api/ ← API 라우트 lib/ ← 유틸리티, 상수, 엔티티 액션/아이콘 정의 shared/ - types.ts ← 공유 타입·스키마 (AgentType, AgentConfig, Skill 등) - agents.ts ← agents Record (에이전트별 설정 데이터) + types.ts ← 공유 타입·스키마 + agents.ts ← agents Record messages/ ← i18n 메시지 (en/ko) bin/cli.ts ← CLI 진입점 tests/ ← 통합/E2E 테스트 @@ -223,22 +204,28 @@ interface EntityConfig { v1에서 서비스를 추가하고, v2에서 AI 서비스를 추가한다. ```text -현재 Services v1+ 추가 v2 추가 -┌────────────────────┐ ┌──────────────────┐ ┌──────────────────┐ -│ ConfigService │ │ MarketplaceService│ │ AIService │ -│ AgentFileService │ └──────────────────┘ │ ReleaseService │ -│ SkillsService │ └──────────────────┘ -│ HooksService │ -│ McpService │ -│ PluginService │ -│ MemoryService │ -│ OverviewService │ -│ FilesScannerService│ -│ FileWriter │ -│ Claude CLI │ -│ ProjectStore │ -│ ScopeManagement │ +현재 Services → 위치 +┌────────────────────┐ +│ ConfigService │ services/ +│ AgentFileService │ services/ +│ SkillsService │ features/skill/ +│ HooksService │ features/hook/ +│ McpService │ features/mcp/ +│ PluginService │ features/plugin/ +│ MemoryService │ features/memory/ +│ FilesScannerService│ features/file/ +│ OverviewService │ services/ +│ FileWriter │ services/ +│ Claude CLI │ services/ +│ ProjectStore │ services/ +│ ScopeManagement │ services/ └────────────────────┘ + +v1+ 추가 v2 추가 +┌──────────────────┐ ┌──────────────────┐ +│ MarketplaceService│ │ AIService │ +└──────────────────┘ │ ReleaseService │ + └──────────────────┘ ``` --- diff --git a/docs/CONVENTIONS.md b/docs/CONVENTIONS.md index 1867a9b..113215a 100644 --- a/docs/CONVENTIONS.md +++ b/docs/CONVENTIONS.md @@ -24,7 +24,7 @@ - 인프라(CLI spawn, 파일 I/O)와 비즈니스 로직을 같은 서비스 파일에 배치 (`claude-cli.ts`, `features/skill/service.ts` 참조) - 별도 repository 파일로 분리하지 말 것 — 프로젝트 규모에서 과도한 추상화 -- Feature directory 패턴: 도메인별 타입, 서비스, 서버 함수, 쿼리, 컴포넌트를 `src/features/{domain}/`에 co-locate (예: `features/hook/`, `features/skill/`) +- Feature directory 패턴: 도메인별 타입, 서비스, 서버 함수, 쿼리, 컴포넌트를 `src/features/{domain}/`에 co-locate. 전체 7개 도메인: `hook`, `skill`, `subagent`, `mcp`, `plugin`, `file`, `memory` ## shared/ 디렉토리 규칙 diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index eb9baca..22affa6 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -129,6 +129,7 @@ Claude Code가 자체 설정 관리 GUI를 추가하면, 그걸 쓰면 된다. a ## Shipped +- **MCP/Plugin/File/Memory feature directory 이관** (2026-03-20) — 나머지 4개 도메인을 `src/features/`로 통합. 전체 7개 엔티티 도메인 co-located 구조 완성. 통합 테스트 16개 + 복원 테스트 19개 추가. ARCHITECTURE.md 동기화. - **Agent→Subagent 리네이밍 + feature directory 이관** (2026-03-19) — 엔티티 타입 `"agent"`→`"subagent"` 리네이밍. `src/features/subagent/`로 이관. i18n 키, board column, entity icons/actions 전면 업데이트. CONVENTIONS.md에 Agent vs Subagent 용어 규칙 추가. - **Skill feature directory 이관** (2026-03-19) — Skill/Command 도메인을 `src/features/skill/`로 통합. utils 4개 파일 병합, config 병합, 유닛 테스트 co-locate. - **Hook feature directory 이관 + 리스트 액션 제거** (2026-03-19) — Hook 도메인 파일을 `src/features/hook/`로 통합. 서버 Zod 스키마 중복 제거, Inspector에 EntityActionDropdown 적용. EntityListPanel에서 더보기 버튼/컨텍스트 메뉴 제거 (조작은 Inspector에서만). diff --git a/src/components/board/BoardLayout.tsx b/src/components/board/BoardLayout.tsx index 360efba..bef72d2 100644 --- a/src/components/board/BoardLayout.tsx +++ b/src/components/board/BoardLayout.tsx @@ -55,13 +55,13 @@ import { AddHookDialog } from "@/features/hook/components/add-hook-dialog" import { useHooksQuery } from "@/features/hook/queries" import type { HookScope, HooksSettings } from "@/features/hook/types" import { getHookIcon } from "@/features/hook/utils" +import { AddMcpDialog } from "@/features/mcp/components/add-mcp-dialog" +import { useMcpMutations, useMcpQuery } from "@/features/mcp/queries" +import { usePluginsQuery } from "@/features/plugin/queries" import { AddSubagentDialog } from "@/features/subagent/components/add-subagent-dialog" import { useAgentFiles, useMemoryFiles } from "@/hooks/use-config" -import { useMcpMutations, useMcpQuery } from "@/hooks/use-mcp" -import { usePluginsQuery } from "@/hooks/use-plugins" import { m } from "@/paraglide/messages" import type { BoardColumnId, Scope } from "@/shared/types" -import { AddMcpDialog } from "./AddMcpDialog" import { AddSkillDialog } from "./AddSkillDialog" import { BoardColumnSettings } from "./BoardColumnSettings" import { EntityListPanel } from "./EntityListPanel" diff --git a/src/components/board/FilesPanel.tsx b/src/components/board/FilesPanel.tsx index 408d8c9..7869494 100644 --- a/src/components/board/FilesPanel.tsx +++ b/src/components/board/FilesPanel.tsx @@ -5,8 +5,8 @@ import { FileTreeNode } from "@/components/files-editor/components/FileTree" import { useProjectContext } from "@/components/ProjectContext" import { Empty, EmptyDescription, EmptyMedia } from "@/components/ui/empty" import { Skeleton } from "@/components/ui/skeleton" +import { getFileTreeFn } from "@/features/file/server" import { m } from "@/paraglide/messages" -import { getFileTreeFn } from "@/server/files" interface FilesPanelProps { scopeFilter?: string diff --git a/src/components/board/LspServersPanel.tsx b/src/components/board/LspServersPanel.tsx index 585443d..de9fd3b 100644 --- a/src/components/board/LspServersPanel.tsx +++ b/src/components/board/LspServersPanel.tsx @@ -8,7 +8,7 @@ import { EntityActionDropdown, } from "@/components/ui/entity-action-menu" import { ListItem } from "@/components/ui/list-item" -import { usePluginsQuery } from "@/hooks/use-plugins" +import { usePluginsQuery } from "@/features/plugin/queries" import type { EntityActionId } from "@/lib/entity-actions" import { ENTITY_ACTIONS } from "@/lib/entity-actions" import { m } from "@/paraglide/messages" diff --git a/src/components/board/PluginsPanel.tsx b/src/components/board/PluginsPanel.tsx index 356ce95..92d96a8 100644 --- a/src/components/board/PluginsPanel.tsx +++ b/src/components/board/PluginsPanel.tsx @@ -12,13 +12,13 @@ import { EntityActionDropdown, } from "@/components/ui/entity-action-menu" import { ListItem, ListSubItem } from "@/components/ui/list-item" +import { useMcpStatusQuery } from "@/features/mcp/queries" +import { getMcpIconClass } from "@/features/mcp/utils" +import { usePluginsQuery } from "@/features/plugin/queries" import { SkillListItem } from "@/features/skill/components/skill-list-item" -import { useMcpStatusQuery } from "@/hooks/use-mcp" -import { usePluginsQuery } from "@/hooks/use-plugins" import type { EntityActionId } from "@/lib/entity-actions" import { ENTITY_ACTIONS } from "@/lib/entity-actions" import { titleCase } from "@/lib/format" -import { getMcpIconClass } from "@/lib/mcp-status" import { cn } from "@/lib/utils" import { m } from "@/paraglide/messages" import type { McpConnectionStatus, Plugin } from "@/shared/types" diff --git a/src/components/board/entity-inspector.tsx b/src/components/board/entity-inspector.tsx index b16cbb1..db879d1 100644 --- a/src/components/board/entity-inspector.tsx +++ b/src/components/board/entity-inspector.tsx @@ -1,10 +1,10 @@ // src/components/board/entity-inspector.tsx -import { FileInspector } from "@/components/file/file-inspector" -import { McpInspector } from "@/components/mcp/mcp-inspector" -import { MemoryInspector } from "@/components/memory/memory-inspector" -import { PluginInspector } from "@/components/plugin/plugin-inspector" +import { FileInspector } from "@/features/file/components/file-inspector" import { HookInspector } from "@/features/hook/components/hook-inspector" +import { McpInspector } from "@/features/mcp/components/mcp-inspector" +import { MemoryInspector } from "@/features/memory/components/memory-inspector" +import { PluginInspector } from "@/features/plugin/components/plugin-inspector" import { SkillInspector } from "@/features/skill/components/skill-inspector" import { SubagentInspector } from "@/features/subagent/components/subagent-inspector" diff --git a/src/components/files-editor/api/files.functions.ts b/src/components/files-editor/api/files.functions.ts index 3bce4c3..a4ec050 100644 --- a/src/components/files-editor/api/files.functions.ts +++ b/src/components/files-editor/api/files.functions.ts @@ -1,2 +1,2 @@ // Re-export from canonical location -export * from "@/server/files" +export * from "@/features/file/server" diff --git a/src/components/files-editor/api/files.queries.ts b/src/components/files-editor/api/files.queries.ts index eb12865..ce742c8 100644 --- a/src/components/files-editor/api/files.queries.ts +++ b/src/components/files-editor/api/files.queries.ts @@ -1,2 +1,2 @@ // Re-export from canonical location -export * from "@/hooks/use-files" +export * from "@/features/file/queries" diff --git a/src/components/files-editor/constants.ts b/src/components/files-editor/constants.ts index 05d26e7..23a1e70 100644 --- a/src/components/files-editor/constants.ts +++ b/src/components/files-editor/constants.ts @@ -1,2 +1,2 @@ // Re-export from canonical location -export * from "@/lib/file-constants" +export * from "@/features/file/constants" diff --git a/src/config/entities/index.ts b/src/config/entities/index.ts index c50246f..154a65f 100644 --- a/src/config/entities/index.ts +++ b/src/config/entities/index.ts @@ -1,11 +1,11 @@ import { registerEntity } from "@/config/entity-registry" +import { fileConfig } from "@/features/file/config" import { hookConfig } from "@/features/hook/config" +import { mcpConfig } from "@/features/mcp/config" +import { memoryConfig } from "@/features/memory/config" +import { pluginConfig } from "@/features/plugin/config" import { commandConfig, skillConfig } from "@/features/skill/config" import { subagentConfig } from "@/features/subagent/config" -import { fileConfig } from "./file-config" -import { mcpConfig } from "./mcp-config" -import { memoryConfig } from "./memory-config" -import { pluginConfig } from "./plugin-config" registerEntity(skillConfig) registerEntity(commandConfig) @@ -16,12 +16,12 @@ registerEntity(pluginConfig) registerEntity(memoryConfig) registerEntity(fileConfig) +export type { FileItem } from "@/features/file/config" +export { fileConfig } from "@/features/file/config" export type { HookItem } from "@/features/hook/config" export { hookConfig } from "@/features/hook/config" +export { mcpConfig } from "@/features/mcp/config" +export { memoryConfig } from "@/features/memory/config" +export { pluginConfig } from "@/features/plugin/config" export { commandConfig, skillConfig } from "@/features/skill/config" export { subagentConfig } from "@/features/subagent/config" -export type { FileItem } from "./file-config" -export { fileConfig } from "./file-config" -export { mcpConfig } from "./mcp-config" -export { memoryConfig } from "./memory-config" -export { pluginConfig } from "./plugin-config" diff --git a/src/components/file/file-inspector.tsx b/src/features/file/components/file-inspector.tsx similarity index 97% rename from src/components/file/file-inspector.tsx rename to src/features/file/components/file-inspector.tsx index bf795e4..d23017d 100644 --- a/src/components/file/file-inspector.tsx +++ b/src/features/file/components/file-inspector.tsx @@ -16,7 +16,7 @@ import { InspectorTitle, } from "@/components/ui/inspector" import { Skeleton } from "@/components/ui/skeleton" -import { useFileContentQuery } from "@/hooks/use-files" +import { useFileContentQuery } from "@/features/file/queries" import { m } from "@/paraglide/messages" interface FileInspectorProps { diff --git a/src/config/entities/file-config.tsx b/src/features/file/config.ts similarity index 100% rename from src/config/entities/file-config.tsx rename to src/features/file/config.ts diff --git a/src/lib/file-constants.ts b/src/features/file/constants.ts similarity index 100% rename from src/lib/file-constants.ts rename to src/features/file/constants.ts diff --git a/src/hooks/use-files.ts b/src/features/file/queries.ts similarity index 63% rename from src/hooks/use-files.ts rename to src/features/file/queries.ts index 4e513dd..f4aa879 100644 --- a/src/hooks/use-files.ts +++ b/src/features/file/queries.ts @@ -1,8 +1,9 @@ import { useQuery } from "@tanstack/react-query" import { useProjectContext } from "@/components/ProjectContext" +import type { FilesScope } from "@/features/file/constants" +import { getFileContentFn, getFileTreeFn } from "@/features/file/server" import { INFREQUENT_REFETCH } from "@/hooks/use-config" -import type { FilesScope } from "@/lib/file-constants" -import { getFileContentFn, getFileTreeFn } from "@/server/files" +import { queryKeys } from "@/lib/query-keys" // ── Feature-local query keys ── @@ -43,3 +44,18 @@ export function useFileContentQuery(filePath: string | null) { ...INFREQUENT_REFETCH, }) } + +export function useClaudeMdFiles() { + const { activeProjectPath } = useProjectContext() + return useQuery({ + queryKey: queryKeys.claudeMdFiles.byProject(activeProjectPath), + queryFn: async () => { + if (!activeProjectPath) return [] + const { scanClaudeMdFilesFn } = await import("@/server/projects") + return scanClaudeMdFilesFn({ data: { projectPath: activeProjectPath } }) + }, + enabled: !!activeProjectPath, + refetchOnWindowFocus: true, + refetchInterval: 30_000, + }) +} diff --git a/src/server/files.ts b/src/features/file/server.ts similarity index 79% rename from src/server/files.ts rename to src/features/file/server.ts index 8af1756..b6cd872 100644 --- a/src/server/files.ts +++ b/src/features/file/server.ts @@ -11,7 +11,7 @@ export const getFileTreeFn = createServerFn({ method: "GET" }) }), ) .handler(async ({ data }) => { - const { scanClaudeDir } = await import("@/services/files-scanner.service") + const { scanClaudeDir } = await import("@/features/file/service") return scanClaudeDir(data.scope, data.projectPath) }) @@ -22,6 +22,6 @@ export const getFileContentFn = createServerFn({ method: "GET" }) }), ) .handler(async ({ data }) => { - const { readFileContent } = await import("@/services/files-scanner.service") + const { readFileContent } = await import("@/features/file/service") return readFileContent(data.filePath) }) diff --git a/src/services/files-scanner.service.ts b/src/features/file/service.ts similarity index 100% rename from src/services/files-scanner.service.ts rename to src/features/file/service.ts diff --git a/src/components/board/AddMcpDialog.tsx b/src/features/mcp/components/add-mcp-dialog.tsx similarity index 99% rename from src/components/board/AddMcpDialog.tsx rename to src/features/mcp/components/add-mcp-dialog.tsx index 278860d..6a1ee36 100644 --- a/src/components/board/AddMcpDialog.tsx +++ b/src/features/mcp/components/add-mcp-dialog.tsx @@ -17,7 +17,7 @@ import { SelectTrigger, SelectValue, } from "@/components/ui/select" -import { useMcpMutations } from "@/hooks/use-mcp" +import { useMcpMutations } from "@/features/mcp/queries" import { m } from "@/paraglide/messages" import type { McpServer, Scope } from "@/shared/types" diff --git a/src/components/mcp/mcp-inspector.tsx b/src/features/mcp/components/mcp-inspector.tsx similarity index 97% rename from src/components/mcp/mcp-inspector.tsx rename to src/features/mcp/components/mcp-inspector.tsx index 462b151..04685cc 100644 --- a/src/components/mcp/mcp-inspector.tsx +++ b/src/features/mcp/components/mcp-inspector.tsx @@ -20,8 +20,8 @@ import { useMcpMutations, useMcpQuery, useMcpStatusQuery, -} from "@/hooks/use-mcp" -import { usePluginsQuery } from "@/hooks/use-plugins" +} from "@/features/mcp/queries" +import { usePluginsQuery } from "@/features/plugin/queries" import { ENTITY_ACTIONS, type EntityActionId } from "@/lib/entity-actions" import { queryKeys } from "@/lib/query-keys" import { m } from "@/paraglide/messages" @@ -122,7 +122,7 @@ export function McpInspector({ itemKey }: McpInspectorProps) { break } case "delete": { - const { removeMcpServerFn } = await import("@/server/mcp-fns") + const { removeMcpServerFn } = await import("@/features/mcp/server") await toast.promise( removeMcpServerFn({ data: { diff --git a/src/config/entities/mcp-config.tsx b/src/features/mcp/config.ts similarity index 100% rename from src/config/entities/mcp-config.tsx rename to src/features/mcp/config.ts diff --git a/tests/unit/mcp-service.test.ts b/src/features/mcp/plugin-mcp-servers.test.ts similarity index 80% rename from tests/unit/mcp-service.test.ts rename to src/features/mcp/plugin-mcp-servers.test.ts index d9358c3..e6e5cb5 100644 --- a/tests/unit/mcp-service.test.ts +++ b/src/features/mcp/plugin-mcp-servers.test.ts @@ -1,7 +1,7 @@ /** * 단위 테스트: getPluginMcpServers() * - * plugin-service의 getPlugins()를 mock하여 + * plugin service의 getPlugins()를 mock하여 * 플러그인에서 MCP 서버를 읽는 로직을 검증한다. */ @@ -15,15 +15,15 @@ vi.mock("node:fs/promises", () => ({ }, })) -// plugin-service mock -vi.mock("@/services/plugin-service", () => ({ +// plugin service mock +vi.mock("@/features/plugin/service", () => ({ getPlugins: vi.fn(), })) // mock 설정 후 import import fs from "node:fs/promises" -import { getPluginMcpServers } from "@/services/mcp-service" -import { getPlugins } from "@/services/plugin-service" +import { getPlugins } from "@/features/plugin/service" +import { getPluginMcpServers } from "./service" // fs.readFile 오버로드 중 string 반환 버전을 사용 const mockedReadFile = vi.mocked( @@ -93,7 +93,7 @@ describe("getPluginMcpServers()", () => { }) }) - it("user scope 플러그인은 scope: global로 변환", async () => { + it("user scope 플러그인은 scope: user로 변환", async () => { const plugin = makePlugin({ scope: "user", enabled: true }) mockedGetPlugins.mockResolvedValue([plugin]) @@ -160,35 +160,6 @@ describe("getPluginMcpServers()", () => { expect(result).toEqual([]) }) - it(".mcp.json이 mcpServers 래퍼 없이 flat 형식인 경우에도 서버 반환", async () => { - // 실제 context7 플러그인처럼 { "serverName": { command, args } } 형식 - const plugin = makePlugin({ - name: "context7", - installPath: "/home/user/.claude/plugins/context7", - enabled: true, - }) - mockedGetPlugins.mockResolvedValue([plugin]) - - mockedReadFile.mockResolvedValue( - JSON.stringify({ - context7: { - command: "npx", - args: ["-y", "@upstash/context7-mcp"], - }, - }), - ) - - const result = await getPluginMcpServers() - - expect(result).toHaveLength(1) - expect(result[0]).toMatchObject({ - name: "context7", - fromPlugin: "context7", - command: "npx", - type: "stdio", - }) - }) - it("여러 플러그인에서 서버를 모두 수집", async () => { const plugin1 = makePlugin({ name: "plugin-a", @@ -202,9 +173,8 @@ describe("getPluginMcpServers()", () => { }) mockedGetPlugins.mockResolvedValue([plugin1, plugin2]) - mockedReadFile.mockImplementation((filePath: unknown) => { - const p = filePath as string - if (p.includes("/plugins/a")) { + mockedReadFile.mockImplementation((filePath: string) => { + if (filePath.includes("/plugins/a")) { return Promise.resolve( JSON.stringify({ mcpServers: { "server-a": { command: "node", args: ["a.js"] } }, diff --git a/src/hooks/use-mcp.ts b/src/features/mcp/queries.ts similarity index 98% rename from src/hooks/use-mcp.ts rename to src/features/mcp/queries.ts index e098490..0520d73 100644 --- a/src/hooks/use-mcp.ts +++ b/src/features/mcp/queries.ts @@ -1,14 +1,14 @@ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query" import { useProjectContext } from "@/components/ProjectContext" -import { INFREQUENT_REFETCH } from "@/hooks/use-config" -import { queryKeys } from "@/lib/query-keys" import { addMcpServerFn, getMcpServersFn, getMcpStatusFn, removeMcpServerFn, toggleMcpServerFn, -} from "@/server/mcp-fns" +} from "@/features/mcp/server" +import { INFREQUENT_REFETCH } from "@/hooks/use-config" +import { queryKeys } from "@/lib/query-keys" import type { McpConnectionStatus, Scope } from "@/shared/types" // Feature-local query keys diff --git a/src/server/mcp-fns.ts b/src/features/mcp/server.ts similarity index 93% rename from src/server/mcp-fns.ts rename to src/features/mcp/server.ts index f9754e5..c4c24eb 100644 --- a/src/server/mcp-fns.ts +++ b/src/features/mcp/server.ts @@ -5,7 +5,7 @@ import { scopeSchema } from "@/shared/types" export const getMcpServersFn = createServerFn({ method: "GET" }) .inputValidator(z.object({ projectPath: z.string().optional() })) .handler(async ({ data }) => { - const { getMcpServers } = await import("@/services/mcp-service") + const { getMcpServers } = await import("@/features/mcp/service") return getMcpServers(data.projectPath) }) @@ -54,7 +54,7 @@ export const toggleMcpServerFn = createServerFn({ method: "POST" }) }), ) .handler(async ({ data }) => { - const { toggleMcpServer } = await import("@/services/mcp-service") + const { toggleMcpServer } = await import("@/features/mcp/service") await toggleMcpServer(data.name, data.enable, data.projectPath) return { success: true } }) @@ -63,7 +63,7 @@ export const getMcpStatusFn = createServerFn({ method: "GET" }) .inputValidator(z.object({ projectPath: z.string().optional() })) .handler(async ({ data }) => { const { mcpListStatus } = await import("@/services/claude-cli") - const { parseMcpList } = await import("@/services/mcp-service") + const { parseMcpList } = await import("@/features/mcp/service") try { // Run `claude mcp list` from the active project dir so it picks up // project-scoped servers. Falls back to home dir if no project selected. diff --git a/src/services/mcp-service.test.ts b/src/features/mcp/service.test.ts similarity index 98% rename from src/services/mcp-service.test.ts rename to src/features/mcp/service.test.ts index 6c3456c..9aeedf9 100644 --- a/src/services/mcp-service.test.ts +++ b/src/features/mcp/service.test.ts @@ -5,8 +5,8 @@ import { createTmpDir, removeTmpDir, writeFile, -} from "../../tests/helpers/test-utils" -import { getMcpServers, parseMcpList } from "./mcp-service" +} from "../../../tests/helpers/test-utils" +import { getMcpServers, parseMcpList } from "./service" async function writeJson(filePath: string, data: unknown): Promise { await writeFile(filePath, JSON.stringify(data, null, 2)) diff --git a/src/services/mcp-service.ts b/src/features/mcp/service.ts similarity index 99% rename from src/services/mcp-service.ts rename to src/features/mcp/service.ts index 4877949..e07f0b4 100644 --- a/src/services/mcp-service.ts +++ b/src/features/mcp/service.ts @@ -1,7 +1,7 @@ import fs from "node:fs/promises" import os from "node:os" import path from "node:path" -import { getPlugins } from "@/services/plugin-service" +import { getPlugins } from "@/features/plugin/service" import type { McpConnectionStatus, McpServer, Scope } from "@/shared/types" /** diff --git a/src/lib/mcp-status.ts b/src/features/mcp/utils.ts similarity index 100% rename from src/lib/mcp-status.ts rename to src/features/mcp/utils.ts diff --git a/src/components/memory/memory-inspector.tsx b/src/features/memory/components/memory-inspector.tsx similarity index 100% rename from src/components/memory/memory-inspector.tsx rename to src/features/memory/components/memory-inspector.tsx diff --git a/src/config/entities/memory-config.tsx b/src/features/memory/config.ts similarity index 100% rename from src/config/entities/memory-config.tsx rename to src/features/memory/config.ts diff --git a/src/server/memory.ts b/src/features/memory/server.ts similarity index 80% rename from src/server/memory.ts rename to src/features/memory/server.ts index 8a59345..94cc1fa 100644 --- a/src/server/memory.ts +++ b/src/features/memory/server.ts @@ -3,6 +3,6 @@ import { createServerFn } from "@tanstack/react-start" export const getMemoryFilesFn = createServerFn({ method: "GET" }) .inputValidator((data: { projectPath: string }) => data) .handler(async ({ data }) => { - const { getMemoryFiles } = await import("@/services/memory-service") + const { getMemoryFiles } = await import("@/features/memory/service") return getMemoryFiles(data.projectPath) }) diff --git a/src/services/memory-service.test.ts b/src/features/memory/service.test.ts similarity index 98% rename from src/services/memory-service.test.ts rename to src/features/memory/service.test.ts index 58dc79c..434bddf 100644 --- a/src/services/memory-service.test.ts +++ b/src/features/memory/service.test.ts @@ -9,7 +9,7 @@ import { getMemoryDir, getMemoryFiles, projectPathToSlug, -} from "@/services/memory-service" +} from "@/features/memory/service" describe("projectPathToSlug", () => { it("converts absolute path to slug", () => { diff --git a/src/services/memory-service.ts b/src/features/memory/service.ts similarity index 100% rename from src/services/memory-service.ts rename to src/features/memory/service.ts diff --git a/src/components/plugin/plugin-inspector.tsx b/src/features/plugin/components/plugin-inspector.tsx similarity index 97% rename from src/components/plugin/plugin-inspector.tsx rename to src/features/plugin/components/plugin-inspector.tsx index d293667..5b71599 100644 --- a/src/components/plugin/plugin-inspector.tsx +++ b/src/features/plugin/components/plugin-inspector.tsx @@ -13,7 +13,7 @@ import { InspectorSkeleton, InspectorTitle, } from "@/components/ui/inspector" -import { usePluginsQuery } from "@/hooks/use-plugins" +import { usePluginsQuery } from "@/features/plugin/queries" import { ENTITY_ACTIONS, type EntityActionId } from "@/lib/entity-actions" import { queryKeys } from "@/lib/query-keys" import { m } from "@/paraglide/messages" @@ -49,7 +49,7 @@ export function PluginInspector({ itemKey }: PluginInspectorProps) { break } case "delete": { - const { uninstallPluginFn } = await import("@/server/plugins-fns") + const { uninstallPluginFn } = await import("@/features/plugin/server") await toast.promise( uninstallPluginFn({ data: { diff --git a/src/config/entities/plugin-config.tsx b/src/features/plugin/config.ts similarity index 100% rename from src/config/entities/plugin-config.tsx rename to src/features/plugin/config.ts diff --git a/src/lib/plugin-constants.ts b/src/features/plugin/constants.ts similarity index 100% rename from src/lib/plugin-constants.ts rename to src/features/plugin/constants.ts diff --git a/src/hooks/use-plugins.ts b/src/features/plugin/queries.ts similarity index 97% rename from src/hooks/use-plugins.ts rename to src/features/plugin/queries.ts index 56fc77a..f6c3122 100644 --- a/src/hooks/use-plugins.ts +++ b/src/features/plugin/queries.ts @@ -1,12 +1,12 @@ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query" -import { FREQUENT_REFETCH } from "@/hooks/use-config" -import { queryKeys } from "@/lib/query-keys" import { getPluginsFn, togglePluginFn, uninstallPluginFn, updatePluginFn, -} from "@/server/plugins-fns" +} from "@/features/plugin/server" +import { FREQUENT_REFETCH } from "@/hooks/use-config" +import { queryKeys } from "@/lib/query-keys" import type { Scope } from "@/shared/types" const pluginKeys = { diff --git a/src/server/plugins-fns.ts b/src/features/plugin/server.ts similarity index 96% rename from src/server/plugins-fns.ts rename to src/features/plugin/server.ts index 6a482ba..977a5fc 100644 --- a/src/server/plugins-fns.ts +++ b/src/features/plugin/server.ts @@ -7,7 +7,7 @@ export const getPluginsFn = createServerFn({ method: "GET" }) .inputValidator(z.object({ projectPath: z.string().optional() })) .handler(async ({ data }) => { const { validateProjectPath } = await import("@/server/validation") - const { getPlugins } = await import("@/services/plugin-service") + const { getPlugins } = await import("@/features/plugin/service") const projectPath = data.projectPath ? validateProjectPath(data.projectPath) : undefined diff --git a/src/services/plugin-service.test.ts b/src/features/plugin/service.test.ts similarity index 99% rename from src/services/plugin-service.test.ts rename to src/features/plugin/service.test.ts index 34a875e..4d7d9e0 100644 --- a/src/services/plugin-service.test.ts +++ b/src/features/plugin/service.test.ts @@ -7,12 +7,12 @@ import { getPlugins, readPluginManifest, scanPluginComponents, -} from "@/services/plugin-service" +} from "@/features/plugin/service" import { createTmpDir, removeTmpDir, writeFile, -} from "../../tests/helpers/test-utils" +} from "../../../tests/helpers/test-utils" async function writeJson(filePath: string, data: unknown): Promise { await writeFile(filePath, JSON.stringify(data, null, 2)) diff --git a/src/services/plugin-service.ts b/src/features/plugin/service.ts similarity index 100% rename from src/services/plugin-service.ts rename to src/features/plugin/service.ts diff --git a/src/features/skill/components/skill-inspector.tsx b/src/features/skill/components/skill-inspector.tsx index e87daee..ff72249 100644 --- a/src/features/skill/components/skill-inspector.tsx +++ b/src/features/skill/components/skill-inspector.tsx @@ -33,8 +33,8 @@ import { } from "@/components/ui/inspector" import { Separator } from "@/components/ui/separator" import { Skeleton } from "@/components/ui/skeleton" +import { usePluginsQuery } from "@/features/plugin/queries" import { useAgentFiles } from "@/hooks/use-config" -import { usePluginsQuery } from "@/hooks/use-plugins" import { ENTITY_ACTIONS, type EntityActionId } from "@/lib/entity-actions" import { extractBody, formatDate, formatInstalls } from "@/lib/format" import { queryKeys } from "@/lib/query-keys" diff --git a/src/features/subagent/components/subagent-inspector.tsx b/src/features/subagent/components/subagent-inspector.tsx index b0caece..a878894 100644 --- a/src/features/subagent/components/subagent-inspector.tsx +++ b/src/features/subagent/components/subagent-inspector.tsx @@ -14,9 +14,9 @@ import { InspectorTitle, } from "@/components/ui/inspector" import { Separator } from "@/components/ui/separator" +import { usePluginsQuery } from "@/features/plugin/queries" import { useAgentFileDetailQuery } from "@/hooks/use-agent-file-detail" import { useAgentFiles } from "@/hooks/use-config" -import { usePluginsQuery } from "@/hooks/use-plugins" import { ENTITY_ACTIONS, type EntityActionId } from "@/lib/entity-actions" import { extractBody, formatDate } from "@/lib/format" import { queryKeys } from "@/lib/query-keys" diff --git a/src/hooks/use-claude-md-files.ts b/src/hooks/use-claude-md-files.ts deleted file mode 100644 index cd0afef..0000000 --- a/src/hooks/use-claude-md-files.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { useQuery } from "@tanstack/react-query" -import { useProjectContext } from "@/components/ProjectContext" -import { queryKeys } from "@/lib/query-keys" - -export function useClaudeMdFiles() { - const { activeProjectPath } = useProjectContext() - - return useQuery({ - queryKey: queryKeys.claudeMdFiles.byProject(activeProjectPath), - queryFn: async () => { - if (!activeProjectPath) return [] - const { scanClaudeMdFilesFn } = await import("@/server/projects") - return scanClaudeMdFilesFn({ data: { projectPath: activeProjectPath } }) - }, - enabled: !!activeProjectPath, - refetchOnWindowFocus: true, - refetchInterval: 30_000, - }) -} diff --git a/src/hooks/use-config.ts b/src/hooks/use-config.ts index aea40d8..4339636 100644 --- a/src/hooks/use-config.ts +++ b/src/hooks/use-config.ts @@ -159,7 +159,7 @@ export function useMemoryFiles() { queryKey: queryKeys.memory.byProject(activeProjectPath), queryFn: async () => { if (!activeProjectPath) return [] - const { getMemoryFilesFn } = await import("@/server/memory") + const { getMemoryFilesFn } = await import("@/features/memory/server") return getMemoryFilesFn({ data: { projectPath: activeProjectPath } }) }, enabled: !!activeProjectPath, diff --git a/src/services/overview-service.ts b/src/services/overview-service.ts index 6f10642..9bdbfcb 100644 --- a/src/services/overview-service.ts +++ b/src/services/overview-service.ts @@ -1,13 +1,13 @@ import path from "node:path" +import { getMcpServers } from "@/features/mcp/service" +import { getMemoryFiles } from "@/features/memory/service" +import { getPlugins } from "@/features/plugin/service" import { scanMdDirWithScope } from "@/services/agent-file-service" import { getClaudeMd, getGlobalConfigPath, getProjectConfigPath, } from "@/services/config-service" -import { getMcpServers } from "@/services/mcp-service" -import { getMemoryFiles } from "@/services/memory-service" -import { getPlugins } from "@/services/plugin-service" import type { AgentFile, Overview } from "@/shared/types" export async function getOverview(projectPath?: string): Promise { diff --git a/tests/integration/error-edge-cases.test.ts b/tests/integration/error-edge-cases.test.ts index cc0ca5b..7783567 100644 --- a/tests/integration/error-edge-cases.test.ts +++ b/tests/integration/error-edge-cases.test.ts @@ -2,11 +2,11 @@ import fs from "node:fs/promises" import os from "node:os" import path from "node:path" import { afterEach, beforeEach, describe, expect, it, vi } from "vitest" +import { getPlugins } from "@/features/plugin/service" import { validateItemName } from "@/server/validation" import { scanMdDir } from "@/services/agent-file-service" import { getClaudeMd } from "@/services/config-service" import { writeMarkdown } from "@/services/file-writer" -import { getPlugins } from "@/services/plugin-service" import { createTmpDir, removeTmpDir, writeFile } from "../helpers/test-utils" // ── 모킹: process.cwd() + os.homedir() ── diff --git a/tests/integration/feature-migration.test.ts b/tests/integration/feature-migration.test.ts new file mode 100644 index 0000000..e37c252 --- /dev/null +++ b/tests/integration/feature-migration.test.ts @@ -0,0 +1,107 @@ +import { describe, expect, it } from "vitest" + +describe("Feature directory migration integrity", () => { + // MCP + it("mcp/service exports getMcpServers", async () => { + const mod = await import("@/features/mcp/service") + expect(mod.getMcpServers).toBeTypeOf("function") + }) + + it("mcp/server exports server functions", async () => { + const mod = await import("@/features/mcp/server") + expect(mod.getMcpServersFn).toBeDefined() + expect(mod.addMcpServerFn).toBeDefined() + expect(mod.removeMcpServerFn).toBeDefined() + expect(mod.toggleMcpServerFn).toBeDefined() + expect(mod.getMcpStatusFn).toBeDefined() + }) + + it("mcp/config exports mcpConfig with correct type", async () => { + const mod = await import("@/features/mcp/config") + expect(mod.mcpConfig.type).toBe("mcp") + }) + + it("mcp/utils exports getMcpIconClass", async () => { + const mod = await import("@/features/mcp/utils") + expect(mod.getMcpIconClass).toBeTypeOf("function") + }) + + // Plugin + it("plugin/service exports getPlugins", async () => { + const mod = await import("@/features/plugin/service") + expect(mod.getPlugins).toBeTypeOf("function") + }) + + it("plugin/server exports server functions", async () => { + const mod = await import("@/features/plugin/server") + expect(mod.getPluginsFn).toBeDefined() + expect(mod.togglePluginFn).toBeDefined() + expect(mod.uninstallPluginFn).toBeDefined() + expect(mod.updatePluginFn).toBeDefined() + }) + + it("plugin/config exports pluginConfig with correct type", async () => { + const mod = await import("@/features/plugin/config") + expect(mod.pluginConfig.type).toBe("plugin") + }) + + it("plugin/constants exports PLUGIN_COMPONENT_META", async () => { + const mod = await import("@/features/plugin/constants") + expect(mod.PLUGIN_COMPONENT_META).toBeDefined() + expect(mod.SCOPE_ORDER).toBeDefined() + }) + + // File + it("file/service exports scanClaudeDir", async () => { + const mod = await import("@/features/file/service") + expect(mod.scanClaudeDir).toBeTypeOf("function") + }) + + it("file/server exports server functions", async () => { + const mod = await import("@/features/file/server") + expect(mod.getFileTreeFn).toBeDefined() + expect(mod.getFileContentFn).toBeDefined() + }) + + it("file/config exports fileConfig with correct type", async () => { + const mod = await import("@/features/file/config") + expect(mod.fileConfig.type).toBe("file") + }) + + // Memory + it("memory/service exports getMemoryFiles", async () => { + const mod = await import("@/features/memory/service") + expect(mod.getMemoryFiles).toBeTypeOf("function") + }) + + it("memory/server exports getMemoryFilesFn", async () => { + const mod = await import("@/features/memory/server") + expect(mod.getMemoryFilesFn).toBeDefined() + }) + + it("memory/config exports memoryConfig with correct type", async () => { + const mod = await import("@/features/memory/config") + expect(mod.memoryConfig.type).toBe("memory") + }) + + // Entity registry integrity + it("all entity configs are registered", async () => { + await import("@/config/entities") + const { getEntityConfig } = await import("@/config/entity-registry") + for (const type of [ + "skill", + "command", + "subagent", + "hook", + "mcp", + "plugin", + "file", + "memory", + ]) { + expect( + getEntityConfig(type), + `${type} should be registered`, + ).toBeDefined() + } + }) +}) From 61760105e345bae88809804e199a3f9f042dd468 Mon Sep 17 00:00:00 2001 From: Youngsup Oh Date: Fri, 20 Mar 2026 14:50:54 +0900 Subject: [PATCH 2/4] refactor: remove barrel exports and re-export files Remove 6 re-export files that only forwarded imports: - files-editor/api/files.{queries,functions}.ts - files-editor/constants.ts - config-editor/api/config.{queries,functions}.ts - config-editor/constants.ts Strip barrel exports from config/entities/index.ts (keep registration side-effect only). Update 7 consumer files to import directly from source modules. Delete feature-migration.test.ts (export existence checks are not meaningful integration tests). Co-Authored-By: Claude Opus 4.6 (1M context) --- src/components/board/BoardLayout.tsx | 15 +-- .../config-editor/api/config.functions.ts | 2 - .../config-editor/api/config.queries.ts | 2 - .../components/ConfigCategoryNav.tsx | 2 +- .../components/ConfigPageContent.tsx | 2 +- .../components/ConfigScopeTabs.tsx | 2 +- .../components/ConfigSettingsPanel.tsx | 2 +- .../config-editor/constants.test.ts | 2 +- src/components/config-editor/constants.ts | 2 - .../config-editor/context/ConfigContext.tsx | 2 +- .../files-editor/api/files.functions.ts | 2 - .../files-editor/api/files.queries.ts | 2 - .../components/FileViewerPanel.tsx | 2 +- src/components/files-editor/constants.ts | 2 - src/config/entities/index.ts | 10 -- tests/integration/feature-migration.test.ts | 107 ------------------ 16 files changed, 13 insertions(+), 145 deletions(-) delete mode 100644 src/components/config-editor/api/config.functions.ts delete mode 100644 src/components/config-editor/api/config.queries.ts delete mode 100644 src/components/config-editor/constants.ts delete mode 100644 src/components/files-editor/api/files.functions.ts delete mode 100644 src/components/files-editor/api/files.queries.ts delete mode 100644 src/components/files-editor/constants.ts delete mode 100644 tests/integration/feature-migration.test.ts diff --git a/src/components/board/BoardLayout.tsx b/src/components/board/BoardLayout.tsx index bef72d2..553c4e0 100644 --- a/src/components/board/BoardLayout.tsx +++ b/src/components/board/BoardLayout.tsx @@ -41,24 +41,21 @@ import { SheetTitle, } from "@/components/ui/sheet" import { Switch } from "@/components/ui/switch" -import { - commandConfig, - type HookItem, - hookConfig, - mcpConfig, - memoryConfig, - skillConfig, - subagentConfig, -} from "@/config/entities" import { getEntityConfig } from "@/config/entity-registry" import { AddHookDialog } from "@/features/hook/components/add-hook-dialog" +import type { HookItem } from "@/features/hook/config" +import { hookConfig } from "@/features/hook/config" import { useHooksQuery } from "@/features/hook/queries" import type { HookScope, HooksSettings } from "@/features/hook/types" import { getHookIcon } from "@/features/hook/utils" import { AddMcpDialog } from "@/features/mcp/components/add-mcp-dialog" +import { mcpConfig } from "@/features/mcp/config" import { useMcpMutations, useMcpQuery } from "@/features/mcp/queries" +import { memoryConfig } from "@/features/memory/config" import { usePluginsQuery } from "@/features/plugin/queries" +import { commandConfig, skillConfig } from "@/features/skill/config" import { AddSubagentDialog } from "@/features/subagent/components/add-subagent-dialog" +import { subagentConfig } from "@/features/subagent/config" import { useAgentFiles, useMemoryFiles } from "@/hooks/use-config" import { m } from "@/paraglide/messages" import type { BoardColumnId, Scope } from "@/shared/types" diff --git a/src/components/config-editor/api/config.functions.ts b/src/components/config-editor/api/config.functions.ts deleted file mode 100644 index 800b15d..0000000 --- a/src/components/config-editor/api/config.functions.ts +++ /dev/null @@ -1,2 +0,0 @@ -// Re-export from canonical location -export * from "@/server/config-settings" diff --git a/src/components/config-editor/api/config.queries.ts b/src/components/config-editor/api/config.queries.ts deleted file mode 100644 index 63c0450..0000000 --- a/src/components/config-editor/api/config.queries.ts +++ /dev/null @@ -1,2 +0,0 @@ -// Re-export from canonical location -export * from "@/hooks/use-config-settings" diff --git a/src/components/config-editor/components/ConfigCategoryNav.tsx b/src/components/config-editor/components/ConfigCategoryNav.tsx index a1c5422..59f8a7a 100644 --- a/src/components/config-editor/components/ConfigCategoryNav.tsx +++ b/src/components/config-editor/components/ConfigCategoryNav.tsx @@ -9,7 +9,7 @@ import { import type { ElementType } from "react" import { memo } from "react" import { ListItem } from "@/components/ui/list-item" -import { type CategoryId, CONFIG_CATEGORIES } from "../constants" +import { type CategoryId, CONFIG_CATEGORIES } from "@/lib/config-constants" const CATEGORY_ICONS: Record = { general: SettingsIcon, diff --git a/src/components/config-editor/components/ConfigPageContent.tsx b/src/components/config-editor/components/ConfigPageContent.tsx index f3e9d54..6eb72cc 100644 --- a/src/components/config-editor/components/ConfigPageContent.tsx +++ b/src/components/config-editor/components/ConfigPageContent.tsx @@ -3,8 +3,8 @@ import { ArrowLeftIcon, ExternalLink } from "lucide-react" import { useProjectContext } from "@/components/ProjectContext" import { Button } from "@/components/ui/button" import { Skeleton } from "@/components/ui/skeleton" +import { useConfigMutations, useConfigQuery } from "@/hooks/use-config-settings" import { m } from "@/paraglide/messages" -import { useConfigMutations, useConfigQuery } from "../api/config.queries" import { useConfigSelection } from "../context/ConfigContext" import { ConfigCategoryNav } from "./ConfigCategoryNav" import { ConfigScopeTabs } from "./ConfigScopeTabs" diff --git a/src/components/config-editor/components/ConfigScopeTabs.tsx b/src/components/config-editor/components/ConfigScopeTabs.tsx index a5b8df1..a1006e9 100644 --- a/src/components/config-editor/components/ConfigScopeTabs.tsx +++ b/src/components/config-editor/components/ConfigScopeTabs.tsx @@ -1,6 +1,6 @@ import { memo } from "react" +import type { ConfigScope } from "@/lib/config-constants" import { m } from "@/paraglide/messages" -import type { ConfigScope } from "../constants" interface ConfigScopeTabsProps { scope: ConfigScope diff --git a/src/components/config-editor/components/ConfigSettingsPanel.tsx b/src/components/config-editor/components/ConfigSettingsPanel.tsx index 929a629..b9f3eda 100644 --- a/src/components/config-editor/components/ConfigSettingsPanel.tsx +++ b/src/components/config-editor/components/ConfigSettingsPanel.tsx @@ -1,4 +1,4 @@ -import type { CategoryId } from "../constants" +import type { CategoryId } from "@/lib/config-constants" import { AuthSettings } from "./categories/AuthSettings" import { DisplaySettings } from "./categories/DisplaySettings" import { EnvironmentSettings } from "./categories/EnvironmentSettings" diff --git a/src/components/config-editor/constants.test.ts b/src/components/config-editor/constants.test.ts index d56e11b..476518f 100644 --- a/src/components/config-editor/constants.test.ts +++ b/src/components/config-editor/constants.test.ts @@ -3,7 +3,7 @@ import { type CategoryId, CONFIG_CATEGORIES, getCategoryById, -} from "./constants" +} from "@/lib/config-constants" describe("constants", () => { it("defines exactly 6 categories", () => { diff --git a/src/components/config-editor/constants.ts b/src/components/config-editor/constants.ts deleted file mode 100644 index 9cf691b..0000000 --- a/src/components/config-editor/constants.ts +++ /dev/null @@ -1,2 +0,0 @@ -// Re-export from canonical location -export * from "@/lib/config-constants" diff --git a/src/components/config-editor/context/ConfigContext.tsx b/src/components/config-editor/context/ConfigContext.tsx index b9b20a9..1219622 100644 --- a/src/components/config-editor/context/ConfigContext.tsx +++ b/src/components/config-editor/context/ConfigContext.tsx @@ -10,7 +10,7 @@ import { type ConfigScope, DEFAULT_CATEGORY, DEFAULT_SCOPE, -} from "../constants" +} from "@/lib/config-constants" export interface ConfigContextValue { scope: ConfigScope diff --git a/src/components/files-editor/api/files.functions.ts b/src/components/files-editor/api/files.functions.ts deleted file mode 100644 index a4ec050..0000000 --- a/src/components/files-editor/api/files.functions.ts +++ /dev/null @@ -1,2 +0,0 @@ -// Re-export from canonical location -export * from "@/features/file/server" diff --git a/src/components/files-editor/api/files.queries.ts b/src/components/files-editor/api/files.queries.ts deleted file mode 100644 index ce742c8..0000000 --- a/src/components/files-editor/api/files.queries.ts +++ /dev/null @@ -1,2 +0,0 @@ -// Re-export from canonical location -export * from "@/features/file/queries" diff --git a/src/components/files-editor/components/FileViewerPanel.tsx b/src/components/files-editor/components/FileViewerPanel.tsx index 06966e5..cb141df 100644 --- a/src/components/files-editor/components/FileViewerPanel.tsx +++ b/src/components/files-editor/components/FileViewerPanel.tsx @@ -10,8 +10,8 @@ import { DropdownMenuTrigger, } from "@/components/ui/dropdown-menu" import { Skeleton } from "@/components/ui/skeleton" +import { useFileContentQuery } from "@/features/file/queries" import { m } from "@/paraglide/messages" -import { useFileContentQuery } from "../api/files.queries" interface FileViewerPanelProps { filePath: string | null diff --git a/src/components/files-editor/constants.ts b/src/components/files-editor/constants.ts deleted file mode 100644 index 23a1e70..0000000 --- a/src/components/files-editor/constants.ts +++ /dev/null @@ -1,2 +0,0 @@ -// Re-export from canonical location -export * from "@/features/file/constants" diff --git a/src/config/entities/index.ts b/src/config/entities/index.ts index 154a65f..87133f4 100644 --- a/src/config/entities/index.ts +++ b/src/config/entities/index.ts @@ -15,13 +15,3 @@ registerEntity(mcpConfig) registerEntity(pluginConfig) registerEntity(memoryConfig) registerEntity(fileConfig) - -export type { FileItem } from "@/features/file/config" -export { fileConfig } from "@/features/file/config" -export type { HookItem } from "@/features/hook/config" -export { hookConfig } from "@/features/hook/config" -export { mcpConfig } from "@/features/mcp/config" -export { memoryConfig } from "@/features/memory/config" -export { pluginConfig } from "@/features/plugin/config" -export { commandConfig, skillConfig } from "@/features/skill/config" -export { subagentConfig } from "@/features/subagent/config" diff --git a/tests/integration/feature-migration.test.ts b/tests/integration/feature-migration.test.ts deleted file mode 100644 index e37c252..0000000 --- a/tests/integration/feature-migration.test.ts +++ /dev/null @@ -1,107 +0,0 @@ -import { describe, expect, it } from "vitest" - -describe("Feature directory migration integrity", () => { - // MCP - it("mcp/service exports getMcpServers", async () => { - const mod = await import("@/features/mcp/service") - expect(mod.getMcpServers).toBeTypeOf("function") - }) - - it("mcp/server exports server functions", async () => { - const mod = await import("@/features/mcp/server") - expect(mod.getMcpServersFn).toBeDefined() - expect(mod.addMcpServerFn).toBeDefined() - expect(mod.removeMcpServerFn).toBeDefined() - expect(mod.toggleMcpServerFn).toBeDefined() - expect(mod.getMcpStatusFn).toBeDefined() - }) - - it("mcp/config exports mcpConfig with correct type", async () => { - const mod = await import("@/features/mcp/config") - expect(mod.mcpConfig.type).toBe("mcp") - }) - - it("mcp/utils exports getMcpIconClass", async () => { - const mod = await import("@/features/mcp/utils") - expect(mod.getMcpIconClass).toBeTypeOf("function") - }) - - // Plugin - it("plugin/service exports getPlugins", async () => { - const mod = await import("@/features/plugin/service") - expect(mod.getPlugins).toBeTypeOf("function") - }) - - it("plugin/server exports server functions", async () => { - const mod = await import("@/features/plugin/server") - expect(mod.getPluginsFn).toBeDefined() - expect(mod.togglePluginFn).toBeDefined() - expect(mod.uninstallPluginFn).toBeDefined() - expect(mod.updatePluginFn).toBeDefined() - }) - - it("plugin/config exports pluginConfig with correct type", async () => { - const mod = await import("@/features/plugin/config") - expect(mod.pluginConfig.type).toBe("plugin") - }) - - it("plugin/constants exports PLUGIN_COMPONENT_META", async () => { - const mod = await import("@/features/plugin/constants") - expect(mod.PLUGIN_COMPONENT_META).toBeDefined() - expect(mod.SCOPE_ORDER).toBeDefined() - }) - - // File - it("file/service exports scanClaudeDir", async () => { - const mod = await import("@/features/file/service") - expect(mod.scanClaudeDir).toBeTypeOf("function") - }) - - it("file/server exports server functions", async () => { - const mod = await import("@/features/file/server") - expect(mod.getFileTreeFn).toBeDefined() - expect(mod.getFileContentFn).toBeDefined() - }) - - it("file/config exports fileConfig with correct type", async () => { - const mod = await import("@/features/file/config") - expect(mod.fileConfig.type).toBe("file") - }) - - // Memory - it("memory/service exports getMemoryFiles", async () => { - const mod = await import("@/features/memory/service") - expect(mod.getMemoryFiles).toBeTypeOf("function") - }) - - it("memory/server exports getMemoryFilesFn", async () => { - const mod = await import("@/features/memory/server") - expect(mod.getMemoryFilesFn).toBeDefined() - }) - - it("memory/config exports memoryConfig with correct type", async () => { - const mod = await import("@/features/memory/config") - expect(mod.memoryConfig.type).toBe("memory") - }) - - // Entity registry integrity - it("all entity configs are registered", async () => { - await import("@/config/entities") - const { getEntityConfig } = await import("@/config/entity-registry") - for (const type of [ - "skill", - "command", - "subagent", - "hook", - "mcp", - "plugin", - "file", - "memory", - ]) { - expect( - getEntityConfig(type), - `${type} should be registered`, - ).toBeDefined() - } - }) -}) From 9f95df07f8f364f18eb37622031a86788b96c173 Mon Sep 17 00:00:00 2001 From: Youngsup Oh Date: Fri, 20 Mar 2026 15:25:09 +0900 Subject: [PATCH 3/4] refactor(test): move integration tests to tests/integration/ mcp/service.test.ts and plugin/service.test.ts use real filesystem (createTmpDir/removeTmpDir), making them integration tests per TESTING.md rules. Move from co-located src/features/ to tests/integration/ where they belong. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../integration/mcp-service.test.ts | 8 ++------ .../integration/plugin-service.test.ts | 6 +----- 2 files changed, 3 insertions(+), 11 deletions(-) rename src/features/mcp/service.test.ts => tests/integration/mcp-service.test.ts (97%) rename src/features/plugin/service.test.ts => tests/integration/plugin-service.test.ts (99%) diff --git a/src/features/mcp/service.test.ts b/tests/integration/mcp-service.test.ts similarity index 97% rename from src/features/mcp/service.test.ts rename to tests/integration/mcp-service.test.ts index 9aeedf9..75e4ef1 100644 --- a/src/features/mcp/service.test.ts +++ b/tests/integration/mcp-service.test.ts @@ -1,12 +1,8 @@ import os from "node:os" import path from "node:path" import { afterEach, beforeEach, describe, expect, it, vi } from "vitest" -import { - createTmpDir, - removeTmpDir, - writeFile, -} from "../../../tests/helpers/test-utils" -import { getMcpServers, parseMcpList } from "./service" +import { getMcpServers, parseMcpList } from "@/features/mcp/service" +import { createTmpDir, removeTmpDir, writeFile } from "../helpers/test-utils" async function writeJson(filePath: string, data: unknown): Promise { await writeFile(filePath, JSON.stringify(data, null, 2)) diff --git a/src/features/plugin/service.test.ts b/tests/integration/plugin-service.test.ts similarity index 99% rename from src/features/plugin/service.test.ts rename to tests/integration/plugin-service.test.ts index 4d7d9e0..7025cf4 100644 --- a/src/features/plugin/service.test.ts +++ b/tests/integration/plugin-service.test.ts @@ -8,11 +8,7 @@ import { readPluginManifest, scanPluginComponents, } from "@/features/plugin/service" -import { - createTmpDir, - removeTmpDir, - writeFile, -} from "../../../tests/helpers/test-utils" +import { createTmpDir, removeTmpDir, writeFile } from "../helpers/test-utils" async function writeJson(filePath: string, data: unknown): Promise { await writeFile(filePath, JSON.stringify(data, null, 2)) From 67739f84fa5fa9b1a9d551a52c8d9297eeac5fe5 Mon Sep 17 00:00:00 2001 From: Youngsup Oh Date: Fri, 20 Mar 2026 15:34:57 +0900 Subject: [PATCH 4/4] refactor(test): rename plugin-mcp-servers.test.ts to service.test.ts Now that the integration test moved to tests/integration/, the co-located unit test can use the standard service.test.ts name matching its test target (mcp/service.ts). Co-Authored-By: Claude Opus 4.6 (1M context) --- src/features/mcp/{plugin-mcp-servers.test.ts => service.test.ts} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/features/mcp/{plugin-mcp-servers.test.ts => service.test.ts} (100%) diff --git a/src/features/mcp/plugin-mcp-servers.test.ts b/src/features/mcp/service.test.ts similarity index 100% rename from src/features/mcp/plugin-mcp-servers.test.ts rename to src/features/mcp/service.test.ts