From ef5f722e20b38863f2aacb3d35c54e4f9b6992c4 Mon Sep 17 00:00:00 2001 From: Youngsup Oh Date: Thu, 19 Mar 2026 09:58:35 +0900 Subject: [PATCH 1/5] refactor(subagent): rename entity type "agent" to "subagent" and migrate to features/subagent MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Rename entity type "agent" → "subagent" to distinguish from AI agents (Claude Code, Cline, etc.) - "subagent" refers to .claude/agents/ subagent definition files - Migrate config, server, queries, components to src/features/subagent/ - Rename all entity icons, actions, board column, i18n keys accordingly - Add legacy "agents" column id migration in agentfiles-config - Add Agent vs Subagent terminology rules to CONVENTIONS.md - Keep hook handler type "agent" unchanged (Claude Code external format) Co-Authored-By: Claude Opus 4.6 (1M context) --- docs/ARCHITECTURE.md | 3 +- docs/CONVENTIONS.md | 8 ++++- docs/ROADMAP.md | 1 + messages/en/agents.json | 26 -------------- messages/en/common.json | 4 +-- messages/en/plugins.json | 4 +-- messages/en/subagents.json | 26 ++++++++++++++ messages/ko/agents.json | 26 -------------- messages/ko/common.json | 4 +-- messages/ko/plugins.json | 4 +-- messages/ko/subagents.json | 26 ++++++++++++++ src/components/board/BoardColumnSettings.tsx | 2 +- src/components/board/BoardLayout.tsx | 28 +++++++-------- src/components/board/PluginsPanel.tsx | 12 +++---- src/components/board/entity-inspector.tsx | 6 ++-- src/components/icons/entity-icons.tsx | 2 +- src/components/plugin/plugin-inspector.tsx | 6 ++-- src/config/entities/index.ts | 6 ++-- .../components/add-subagent-dialog.tsx} | 36 +++++++++---------- .../components/subagent-inspector.tsx} | 32 ++++++++--------- .../subagent/config.ts} | 6 ++-- .../subagent/queries.ts} | 17 +++++---- .../agents.ts => features/subagent/server.ts} | 2 +- src/lib/entity-actions.ts | 4 +-- src/lib/plugin-constants.ts | 8 ++--- src/server/scope-management.ts | 2 +- src/services/agent-file-service.test.ts | 6 ++-- src/services/agent-file-service.ts | 2 +- src/services/agentfiles-config.ts | 18 ++++++++-- src/services/overview-service.ts | 4 +-- src/services/plugin-service.test.ts | 10 +++--- src/services/plugin-service.ts | 4 +-- src/services/scope-management.ts | 5 +-- src/shared/agents.test.ts | 2 +- src/shared/agents.ts | 2 +- src/shared/types.ts | 10 +++--- tests/i18n/messages.test.ts | 1 + tests/integration/crud-flow.test.ts | 16 ++++----- tests/integration/error-edge-cases.test.ts | 7 ++-- tests/unit/board-config.test.ts | 4 +-- tests/unit/scope-management.test.ts | 2 +- 41 files changed, 212 insertions(+), 182 deletions(-) delete mode 100644 messages/en/agents.json create mode 100644 messages/en/subagents.json delete mode 100644 messages/ko/agents.json create mode 100644 messages/ko/subagents.json rename src/{components/board/AddAgentDialog.tsx => features/subagent/components/add-subagent-dialog.tsx} (76%) rename src/{components/agent/agent-inspector.tsx => features/subagent/components/subagent-inspector.tsx} (83%) rename src/{config/entities/agent-config.tsx => features/subagent/config.ts} (71%) rename src/{hooks/use-agents.ts => features/subagent/queries.ts} (84%) rename src/{server/agents.ts => features/subagent/server.ts} (94%) diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 8a0eac5..c7d8367 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -33,7 +33,7 @@ CLI 위임으로 Claude Code의 유효성 검증과 포맷 호환성을 보장 ### 프론트엔드 아키텍처: 엔티티 시스템 -대시보드는 **칸반 보드** 형태의 단일 페이지 앱이다. 7개 엔티티(Skill, Agent, Hook, MCP, Plugin, File, Memory)를 공통 패턴으로 처리한다. +대시보드는 **칸반 보드** 형태의 단일 페이지 앱이다. 7개 엔티티(Skill, Subagent, Hook, MCP, Plugin, File, Memory)를 공통 패턴으로 처리한다. ```text EntityConfig (설정 객체) 공통 UI Primitives 도메인별 Inspector @@ -68,6 +68,7 @@ src/ features/ ← 도메인별 feature 디렉토리 (타입, 서비스, 서버, 쿼리, 컴포넌트 co-locate) 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/) components/ ui/ ← shadcn primitives (Button, Sheet, ListItem, Inspector 등) memory/ ← MemoryInspector diff --git a/docs/CONVENTIONS.md b/docs/CONVENTIONS.md index e2cfde3..1867a9b 100644 --- a/docs/CONVENTIONS.md +++ b/docs/CONVENTIONS.md @@ -62,7 +62,7 @@ ${scope}:${event}:${matcher ?? ""}:${command ?? prompt ?? ""} ## 제품 컨벤션 - 순수 shadcn/ui — 커스텀 스타일, 색상 변경 없음 -- 엔티티 아이콘은 `ENTITY_ICONS`만 사용 — `skill: ScrollTextIcon`, `command: TerminalSquareIcon`, `agent: WorkflowIcon`, `mcp: ServerIcon`, `hook: ZapIcon`, `plugin: Plug2Icon`. lucide-react 직접 import 금지 +- 엔티티 아이콘은 `ENTITY_ICONS`만 사용 — `skill: ScrollTextIcon`, `command: TerminalSquareIcon`, `subagent: WorkflowIcon`, `mcp: ServerIcon`, `hook: ZapIcon`, `plugin: Plug2Icon`. lucide-react 직접 import 금지 - skills.sh를 설치 백엔드로 활용 (자체 마켓플레이스 구현 X) - 뷰어/모니터링 중심 — 수정(에디팅)은 IDE 위임 - Server Functions (`createServerFn`)으로 타입 안전한 서버 호출 @@ -72,6 +72,12 @@ ${scope}:${event}:${matcher ?? ""}:${command ?? prompt ?? ""} - 보안: 127.0.0.1 바인딩, 랜덤 토큰 인증, CORS 미설정 - 개발 서버 실행 시 Chrome 앱 모드(`--app`)로 열기 +## 용어 규칙: Agent vs Subagent + +- **Agent (AI Agent)**: Claude Code, Cline, Cursor 등 AI 코딩 에이전트. `AgentType`, `AgentConfig`, `shared/agents.ts` 등에서 사용. +- **Subagent**: `.claude/agents/` 디렉토리의 subagent definition 파일 (.md). 엔티티 타입 `"subagent"`, `subagentConfig`, `SubagentInspector` 등에서 사용. +- Hook handler type `"agent"`는 Claude Code 외부 포맷이므로 리네이밍하지 않음. + ## Commit Convention - Use English for all commit messages, PR titles, and issue comments diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index b452324..eb9baca 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -129,6 +129,7 @@ Claude Code가 자체 설정 관리 GUI를 추가하면, 그걸 쓰면 된다. a ## Shipped +- **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에서만). - **Inspector 아키텍처 단순화** (2026-03-18) — `DashboardDetailTarget` union / `toDetailTarget` 제거. Inspector가 `itemKey`만 받아 자체 fetch 및 액션 처리. `use-entity-action-handler.ts` 제거. diff --git a/messages/en/agents.json b/messages/en/agents.json deleted file mode 100644 index 82a6868..0000000 --- a/messages/en/agents.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "agents_title": "Agents", - "agents_empty_title": "No Agent Selected", - "agents_empty_desc": "Select an agent from the left panel to view its details.", - "agents_add_title": "Add Agent", - "agents_add_desc": "Creates a new agent file in {path}.", - "agents_add_name": "Name", - "agents_add_name_placeholder": "my-agent", - "agents_add_name_required": "Name is required", - "agents_add_name_max": "Name must be 64 characters or less", - "agents_add_name_pattern": "Only lowercase letters, numbers, and hyphens", - "agents_add_name_rule": "Lowercase letters, numbers, and hyphens only. Max 64 characters.", - "agents_add_description": "Description", - "agents_add_description_placeholder": "What does this agent do?", - "agents_toast_created": "Agent '{name}' created", - "agents_toast_create_failed": "Failed to create agent", - "agents_creating": "Creating...", - "agents_create": "Create", - "agents_detail_scope": "Scope", - "agents_detail_last_updated": "Last Updated", - "agents_detail_description": "Description", - "agents_detail_no_description": "No description", - "agents_detail_model": "Model", - "agents_detail_tools": "Tools", - "agents_detail_permission_mode": "Permission Mode" -} diff --git a/messages/en/common.json b/messages/en/common.json index e7cdb14..09a7e32 100644 --- a/messages/en/common.json +++ b/messages/en/common.json @@ -47,7 +47,7 @@ "board_no_plugins": "No plugins", "board_no_mcp": "No MCP servers", "board_no_skills": "No skills", - "board_no_agents": "No agents", + "board_no_subagents": "No agents", "board_no_hooks": "No hooks", "board_no_memory": "No memory files", "board_no_lsp": "No LSP servers", @@ -56,7 +56,7 @@ "board_col_plugins": "Plugins", "board_col_mcp": "MCP Servers", "board_col_skills": "Skills", - "board_col_agents": "Agents", + "board_col_subagents": "Agents", "board_col_hooks": "Hooks", "board_col_memory": "Memory", "board_col_lsp": "LSP Servers", diff --git a/messages/en/plugins.json b/messages/en/plugins.json index 1b405af..f527b3b 100644 --- a/messages/en/plugins.json +++ b/messages/en/plugins.json @@ -8,14 +8,14 @@ "plugin_show_more": "{count} more", "plugin_cat_commands": "Commands", "plugin_cat_skills": "Skills", - "plugin_cat_agents": "Agents", + "plugin_cat_subagents": "Agents", "plugin_cat_hooks": "Hooks", "plugin_cat_mcp_servers": "MCP Servers", "plugin_cat_lsp_servers": "LSP Servers", "plugin_cat_output_styles": "Output Styles", "plugin_cat_desc_commands": "Type / in the chat to use these shortcuts and run workflows as needed.", "plugin_cat_desc_skills": "Teach Claude team norms and domain expertise to automatically apply to related tasks.", - "plugin_cat_desc_agents": "Specialized AI assistants that can be invoked to handle specific types of tasks.", + "plugin_cat_desc_subagents": "Specialized AI assistants that can be invoked to handle specific types of tasks.", "plugin_cat_desc_hooks": "Automatically run shell commands before and after specific events like tool calls or notifications.", "plugin_cat_desc_mcp_servers": "Connect to external tools and data sources to extend Claude's capabilities.", "plugin_cat_desc_lsp_servers": "Provide language server features like auto-completion and go-to-definition during code editing.", diff --git a/messages/en/subagents.json b/messages/en/subagents.json new file mode 100644 index 0000000..6e347f6 --- /dev/null +++ b/messages/en/subagents.json @@ -0,0 +1,26 @@ +{ + "subagents_title": "Agents", + "subagents_empty_title": "No Agent Selected", + "subagents_empty_desc": "Select an agent from the left panel to view its details.", + "subagents_add_title": "Add Agent", + "subagents_add_desc": "Creates a new agent file in {path}.", + "subagents_add_name": "Name", + "subagents_add_name_placeholder": "my-agent", + "subagents_add_name_required": "Name is required", + "subagents_add_name_max": "Name must be 64 characters or less", + "subagents_add_name_pattern": "Only lowercase letters, numbers, and hyphens", + "subagents_add_name_rule": "Lowercase letters, numbers, and hyphens only. Max 64 characters.", + "subagents_add_description": "Description", + "subagents_add_description_placeholder": "What does this agent do?", + "subagents_toast_created": "Agent '{name}' created", + "subagents_toast_create_failed": "Failed to create agent", + "subagents_creating": "Creating...", + "subagents_create": "Create", + "subagents_detail_scope": "Scope", + "subagents_detail_last_updated": "Last Updated", + "subagents_detail_description": "Description", + "subagents_detail_no_description": "No description", + "subagents_detail_model": "Model", + "subagents_detail_tools": "Tools", + "subagents_detail_permission_mode": "Permission Mode" +} diff --git a/messages/ko/agents.json b/messages/ko/agents.json deleted file mode 100644 index e3f4547..0000000 --- a/messages/ko/agents.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "agents_title": "에이전트", - "agents_empty_title": "에이전트가 선택되지 않음", - "agents_empty_desc": "왼쪽 패널에서 에이전트를 선택하여 세부 정보를 확인하세요.", - "agents_add_title": "에이전트 추가", - "agents_add_desc": "{path}에 새 에이전트 파일을 생성합니다.", - "agents_add_name": "이름", - "agents_add_name_placeholder": "my-agent", - "agents_add_name_required": "이름은 필수입니다", - "agents_add_name_max": "이름은 64자 이하여야 합니다", - "agents_add_name_pattern": "소문자, 숫자, 하이픈만 사용 가능", - "agents_add_name_rule": "소문자, 숫자, 하이픈만 사용 가능. 최대 64자.", - "agents_add_description": "설명", - "agents_add_description_placeholder": "이 에이전트는 무엇을 하나요?", - "agents_toast_created": "에이전트 '{name}' 생성됨", - "agents_toast_create_failed": "에이전트 생성 실패", - "agents_creating": "생성 중...", - "agents_create": "생성", - "agents_detail_scope": "스코프", - "agents_detail_last_updated": "마지막 업데이트", - "agents_detail_description": "설명", - "agents_detail_no_description": "설명 없음", - "agents_detail_model": "모델", - "agents_detail_tools": "도구", - "agents_detail_permission_mode": "권한 모드" -} diff --git a/messages/ko/common.json b/messages/ko/common.json index 0349813..275da1a 100644 --- a/messages/ko/common.json +++ b/messages/ko/common.json @@ -47,7 +47,7 @@ "board_no_plugins": "플러그인 없음", "board_no_mcp": "MCP 서버 없음", "board_no_skills": "스킬 없음", - "board_no_agents": "에이전트 없음", + "board_no_subagents": "에이전트 없음", "board_no_hooks": "훅 없음", "board_no_memory": "메모리 파일 없음", "board_no_lsp": "LSP 서버 없음", @@ -56,7 +56,7 @@ "board_col_plugins": "플러그인", "board_col_mcp": "MCP 서버", "board_col_skills": "스킬", - "board_col_agents": "에이전트", + "board_col_subagents": "에이전트", "board_col_hooks": "훅", "board_col_memory": "메모리", "board_col_lsp": "LSP 서버", diff --git a/messages/ko/plugins.json b/messages/ko/plugins.json index 1308e45..7457482 100644 --- a/messages/ko/plugins.json +++ b/messages/ko/plugins.json @@ -8,14 +8,14 @@ "plugin_show_more": "{count}개 더 보기", "plugin_cat_commands": "명령", "plugin_cat_skills": "스킬", - "plugin_cat_agents": "에이전트", + "plugin_cat_subagents": "에이전트", "plugin_cat_hooks": "훅", "plugin_cat_mcp_servers": "MCP 서버", "plugin_cat_lsp_servers": "LSP 서버", "plugin_cat_output_styles": "출력 스타일", "plugin_cat_desc_commands": "채팅 창에서 /를 입력하여 이러한 바로가기를 사용하면 필요에 따라 워크플로를 실행할 수 있습니다.", "plugin_cat_desc_skills": "Claude에게 팀 규범과 도메인 전문 지식을 학습시켜 관련 작업에 자동으로 반영하세요.", - "plugin_cat_desc_agents": "특정 유형의 작업을 처리하기 위해 호출할 수 있는 전문 AI 어시스턴트입니다.", + "plugin_cat_desc_subagents": "특정 유형의 작업을 처리하기 위해 호출할 수 있는 전문 AI 어시스턴트입니다.", "plugin_cat_desc_hooks": "도구 호출이나 알림과 같은 특정 이벤트 전후에 셸 명령을 자동으로 실행합니다.", "plugin_cat_desc_mcp_servers": "외부 도구와 데이터 소스에 연결하여 Claude의 기능을 확장합니다.", "plugin_cat_desc_lsp_servers": "코드 편집 시 자동 완성, 정의 이동 등 언어 서버 기능을 제공합니다.", diff --git a/messages/ko/subagents.json b/messages/ko/subagents.json new file mode 100644 index 0000000..b869828 --- /dev/null +++ b/messages/ko/subagents.json @@ -0,0 +1,26 @@ +{ + "subagents_title": "에이전트", + "subagents_empty_title": "에이전트가 선택되지 않음", + "subagents_empty_desc": "왼쪽 패널에서 에이전트를 선택하여 세부 정보를 확인하세요.", + "subagents_add_title": "에이전트 추가", + "subagents_add_desc": "{path}에 새 에이전트 파일을 생성합니다.", + "subagents_add_name": "이름", + "subagents_add_name_placeholder": "my-agent", + "subagents_add_name_required": "이름은 필수입니다", + "subagents_add_name_max": "이름은 64자 이하여야 합니다", + "subagents_add_name_pattern": "소문자, 숫자, 하이픈만 사용 가능", + "subagents_add_name_rule": "소문자, 숫자, 하이픈만 사용 가능. 최대 64자.", + "subagents_add_description": "설명", + "subagents_add_description_placeholder": "이 에이전트는 무엇을 하나요?", + "subagents_toast_created": "에이전트 '{name}' 생성됨", + "subagents_toast_create_failed": "에이전트 생성 실패", + "subagents_creating": "생성 중...", + "subagents_create": "생성", + "subagents_detail_scope": "스코프", + "subagents_detail_last_updated": "마지막 업데이트", + "subagents_detail_description": "설명", + "subagents_detail_no_description": "설명 없음", + "subagents_detail_model": "모델", + "subagents_detail_tools": "도구", + "subagents_detail_permission_mode": "권한 모드" +} diff --git a/src/components/board/BoardColumnSettings.tsx b/src/components/board/BoardColumnSettings.tsx index a55b9f5..5cdcf9f 100644 --- a/src/components/board/BoardColumnSettings.tsx +++ b/src/components/board/BoardColumnSettings.tsx @@ -21,7 +21,7 @@ const COLUMN_LABELS: Record string> = { plugins: () => m.board_col_plugins(), mcp: () => m.board_col_mcp(), skills: () => m.board_col_skills(), - agents: () => m.board_col_agents(), + subagents: () => m.board_col_subagents(), hooks: () => m.board_col_hooks(), memory: () => m.board_col_memory(), lsp: () => m.board_col_lsp(), diff --git a/src/components/board/BoardLayout.tsx b/src/components/board/BoardLayout.tsx index 99f4acc..360efba 100644 --- a/src/components/board/BoardLayout.tsx +++ b/src/components/board/BoardLayout.tsx @@ -42,25 +42,25 @@ import { } from "@/components/ui/sheet" import { Switch } from "@/components/ui/switch" import { - agentConfig, 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 { useHooksQuery } from "@/features/hook/queries" import type { HookScope, HooksSettings } from "@/features/hook/types" import { getHookIcon } from "@/features/hook/utils" +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 { AddAgentDialog } from "./AddAgentDialog" import { AddMcpDialog } from "./AddMcpDialog" import { AddSkillDialog } from "./AddSkillDialog" import { BoardColumnSettings } from "./BoardColumnSettings" @@ -232,7 +232,7 @@ export function BoardLayout() { // ── Add-dialog state ── type AddDialogState = { - colId: "skills" | "agents" | "hooks" | "mcp" + colId: "skills" | "subagents" | "hooks" | "mcp" scope: Scope | HookScope } | null const [addDialog, setAddDialog] = useState(null) @@ -246,7 +246,7 @@ export function BoardLayout() { } = useAgentFiles("skill") const { query: { data: agents = [], isLoading: agentsLoading }, - } = useAgentFiles("agent") + } = useAgentFiles("subagent") const { data: globalHooks = {}, isLoading: globalHooksLoading } = useHooksQuery("user") const { data: projectHooks = {}, isLoading: projectHooksLoading } = @@ -283,7 +283,7 @@ export function BoardLayout() { items: skills.filter((s) => s.type === "command"), loading: skillsLoading, }, - agent: { items: agents, loading: agentsLoading }, + subagent: { items: agents, loading: agentsLoading }, hook: { items: hookItems, loading: globalHooksLoading || projectHooksLoading, @@ -359,9 +359,9 @@ export function BoardLayout() { color: "green", }, { - id: "agents", + id: "subagents", title: "Agents", - icon: ENTITY_ICONS.agent, + icon: ENTITY_ICONS.subagent, scopes: ["user", "project"], color: "orange", }, @@ -496,13 +496,13 @@ export function BoardLayout() { ) } - case "agents": + case "subagents": return ( ) @@ -580,7 +580,7 @@ export function BoardLayout() { )} ) : col.id === "skills" || - col.id === "agents" || + col.id === "subagents" || col.id === "hooks" || col.id === "mcp" ? ( diff --git a/src/components/agent/agent-inspector.tsx b/src/features/subagent/components/subagent-inspector.tsx similarity index 83% rename from src/components/agent/agent-inspector.tsx rename to src/features/subagent/components/subagent-inspector.tsx index 48bc01b..b0caece 100644 --- a/src/components/agent/agent-inspector.tsx +++ b/src/features/subagent/components/subagent-inspector.tsx @@ -23,21 +23,21 @@ import { queryKeys } from "@/lib/query-keys" import { m } from "@/paraglide/messages" import { getLocale } from "@/paraglide/runtime" -interface AgentInspectorProps { +interface SubagentInspectorProps { itemKey: string } -export function AgentInspector({ itemKey }: AgentInspectorProps) { - const { query: agentsQuery } = useAgentFiles("agent") - const agents = agentsQuery.data ?? [] +export function SubagentInspector({ itemKey }: SubagentInspectorProps) { + const { query: subagentsQuery } = useAgentFiles("subagent") + const subagents = subagentsQuery.data ?? [] const { activeProjectPath } = useProjectContext() const queryClient = useQueryClient() const { data: plugins = [] } = usePluginsQuery(activeProjectPath) const item = - agents.find((a) => a.path === itemKey) ?? + subagents.find((a) => a.path === itemKey) ?? plugins - .flatMap((p) => p.contents?.agents ?? []) + .flatMap((p) => p.contents?.subagents ?? []) .find((a) => a.path === itemKey) const { data: itemDetail, isLoading: detailLoading } = @@ -61,7 +61,7 @@ export function AgentInspector({ itemKey }: AgentInspectorProps) { await toast.promise( deleteItemFn({ data: { - type: "agent", + type: "subagent", name: item.name, scope: item.scope, projectPath: activeProjectPath, @@ -89,10 +89,10 @@ export function AgentInspector({ itemKey }: AgentInspectorProps) { return ( - + @@ -102,32 +102,32 @@ export function AgentInspector({ itemKey }: AgentInspectorProps) {
- + {item.scope} - + {formatDate(item.lastModified, getLocale())}
- + {item.frontmatter?.description ? ( String(item.frontmatter.description) ) : ( - {m.agents_detail_no_description()} + {m.subagents_detail_no_description()} )} {item.frontmatter?.model && ( - + {String(item.frontmatter.model)} @@ -135,7 +135,7 @@ export function AgentInspector({ itemKey }: AgentInspectorProps) { )} {item.frontmatter?.tools && ( - + {String(item.frontmatter.tools)} @@ -143,7 +143,7 @@ export function AgentInspector({ itemKey }: AgentInspectorProps) { )} {item.frontmatter?.permissionMode && ( - + {String(item.frontmatter.permissionMode)} diff --git a/src/config/entities/agent-config.tsx b/src/features/subagent/config.ts similarity index 71% rename from src/config/entities/agent-config.tsx rename to src/features/subagent/config.ts index a06796e..9ba67fc 100644 --- a/src/config/entities/agent-config.tsx +++ b/src/features/subagent/config.ts @@ -2,9 +2,9 @@ import type { EntityConfig } from "@/config/entity-registry" import { ENTITY_ACTIONS } from "@/lib/entity-actions" import type { AgentFile } from "@/shared/types" -export const agentConfig: EntityConfig = { - type: "agent", - actions: ENTITY_ACTIONS.agent.map((a) => a.id), +export const subagentConfig: EntityConfig = { + type: "subagent", + actions: ENTITY_ACTIONS.subagent.map((a) => a.id), getKey: (item) => item.path, getLabel: (item) => item.name, getDescription: (item) => item.frontmatter?.description, diff --git a/src/hooks/use-agents.ts b/src/features/subagent/queries.ts similarity index 84% rename from src/hooks/use-agents.ts rename to src/features/subagent/queries.ts index 62bb69f..8c57c89 100644 --- a/src/hooks/use-agents.ts +++ b/src/features/subagent/queries.ts @@ -2,20 +2,23 @@ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query" import { useProjectContext } from "@/components/ProjectContext" import { FREQUENT_REFETCH } from "@/hooks/use-config" import { queryKeys } from "@/lib/query-keys" -import { createAgentFn } from "@/server/agents" import type { Scope } from "@/shared/types" +import { createSubagentFn } from "./server" // ── Queries ────────────────────────────────────────────────────────────────── -export function useAgentsQuery() { +export function useSubagentsQuery() { const { activeProjectPath } = useProjectContext() return useQuery({ - queryKey: queryKeys.agentFiles.byTypeAndProject("agent", activeProjectPath), + queryKey: queryKeys.agentFiles.byTypeAndProject( + "subagent", + activeProjectPath, + ), queryFn: async () => { const { getItemsFn } = await import("@/server/items") return getItemsFn({ - data: { type: "agent", projectPath: activeProjectPath }, + data: { type: "subagent", projectPath: activeProjectPath }, }) }, ...FREQUENT_REFETCH, @@ -24,7 +27,7 @@ export function useAgentsQuery() { // ── Mutations ──────────────────────────────────────────────────────────────── -export function useAgentMutations() { +export function useSubagentMutations() { const { activeProjectPath } = useProjectContext() const queryClient = useQueryClient() @@ -39,7 +42,7 @@ export function useAgentMutations() { scope: Scope description?: string }) => - createAgentFn({ + createSubagentFn({ data: { ...params, projectPath: activeProjectPath ?? undefined }, }), onSuccess: invalidate, @@ -50,7 +53,7 @@ export function useAgentMutations() { const { deleteItemFn } = await import("@/server/items") return deleteItemFn({ data: { - type: "agent" as const, + type: "subagent" as const, ...params, projectPath: activeProjectPath ?? undefined, }, diff --git a/src/server/agents.ts b/src/features/subagent/server.ts similarity index 94% rename from src/server/agents.ts rename to src/features/subagent/server.ts index 1de0d61..0e1a39e 100644 --- a/src/server/agents.ts +++ b/src/features/subagent/server.ts @@ -2,7 +2,7 @@ import { createServerFn } from "@tanstack/react-start" import { z } from "zod" import { scopeSchema } from "@/shared/types" -export const createAgentFn = createServerFn({ method: "POST" }) +export const createSubagentFn = createServerFn({ method: "POST" }) .inputValidator( z.object({ name: z diff --git a/src/lib/entity-actions.ts b/src/lib/entity-actions.ts index 35a7540..4ff4cb9 100644 --- a/src/lib/entity-actions.ts +++ b/src/lib/entity-actions.ts @@ -21,7 +21,7 @@ export interface EntityAction { export type EntityActionType = | "skill" - | "agent" + | "subagent" | "hook" | "plugin" | "mcp" @@ -74,7 +74,7 @@ const deleteAction: EntityAction = { export const ENTITY_ACTIONS: Record = { skill: [openVscode, openCursor, openFolder, removeFromAgent, deleteAction], - agent: [openVscode, openCursor, edit, deleteAction], + subagent: [openVscode, openCursor, edit, deleteAction], hook: [openVscode, openCursor, edit, deleteAction], plugin: [openVscode, openCursor, deleteAction], mcp: [openVscode, openCursor, edit, deleteAction], diff --git a/src/lib/plugin-constants.ts b/src/lib/plugin-constants.ts index 35cbdbd..09197c2 100644 --- a/src/lib/plugin-constants.ts +++ b/src/lib/plugin-constants.ts @@ -38,10 +38,10 @@ export const PLUGIN_COMPONENT_META: Record< labelFn: () => m.plugin_cat_skills(), descriptionFn: () => m.plugin_cat_desc_skills(), }, - agents: { + subagents: { icon: Workflow, - labelFn: () => m.plugin_cat_agents(), - descriptionFn: () => m.plugin_cat_desc_agents(), + labelFn: () => m.plugin_cat_subagents(), + descriptionFn: () => m.plugin_cat_desc_subagents(), }, hooks: { icon: Zap, @@ -68,7 +68,7 @@ export const PLUGIN_COMPONENT_META: Record< export const PLUGIN_COMPONENT_ORDER: PluginComponentType[] = [ "commands", "skills", - "agents", + "subagents", "hooks", "mcpServers", "lspServers", diff --git a/src/server/scope-management.ts b/src/server/scope-management.ts index 2193b9d..a80c80e 100644 --- a/src/server/scope-management.ts +++ b/src/server/scope-management.ts @@ -4,7 +4,7 @@ import { z } from "zod" export const moveOrCopyEntityFn = createServerFn({ method: "POST" }) .inputValidator( z.object({ - type: z.enum(["skill", "agent"]), + type: z.enum(["skill", "subagent"]), name: z.string().min(1), from: z.enum(["user", "project"]), to: z.enum(["user", "project"]), diff --git a/src/services/agent-file-service.test.ts b/src/services/agent-file-service.test.ts index 86f20b9..ca33675 100644 --- a/src/services/agent-file-service.test.ts +++ b/src/services/agent-file-service.test.ts @@ -122,9 +122,9 @@ describe("scanMdDir", () => { "---\ndescription: My agent\n---\n", ) - const result = await scanMdDir(dir, "agent") + const result = await scanMdDir(dir, "subagent") - expect(result[0].type).toBe("agent") + expect(result[0].type).toBe("subagent") expect(result[0].name).toBe("my-agent") }) @@ -159,7 +159,7 @@ describe("getAgentFiles", () => { "# Project Agent", ) - const result = await getAgentFiles("agent") + const result = await getAgentFiles("subagent") expect(result).toHaveLength(2) const globalAgent = result.find((a) => a.name === "g-agent") diff --git a/src/services/agent-file-service.ts b/src/services/agent-file-service.ts index b66aef1..2972452 100644 --- a/src/services/agent-file-service.ts +++ b/src/services/agent-file-service.ts @@ -257,7 +257,7 @@ export async function getAgentFiles( ] } - const dirName = `${type}s` // 'agent' → 'agents', 'command' → 'commands' + const dirName = type === "subagent" ? "agents" : `${type}s` // 'subagent' → 'agents', 'command' → 'commands' const [globalFiles, projectFiles] = await Promise.all([ scanMdDirWithScope(path.join(globalBase, dirName), type, "user"), diff --git a/src/services/agentfiles-config.ts b/src/services/agentfiles-config.ts index f1bb438..b3fe61d 100644 --- a/src/services/agentfiles-config.ts +++ b/src/services/agentfiles-config.ts @@ -16,7 +16,7 @@ export const DEFAULT_BOARD_CONFIG: BoardConfig = { "plugins", "mcp", "skills", - "agents", + "subagents", "hooks", "memory", "lsp", @@ -29,14 +29,28 @@ const DEFAULT_CONFIG: AgentfilesConfig = { board: DEFAULT_BOARD_CONFIG, } +/** Migrate legacy "agents" column id to "subagents" */ +function migrateBoardConfig(board: BoardConfig): BoardConfig { + const migrate = (ids: BoardColumnId[]) => + ids.map((id) => (id === ("agents" as BoardColumnId) ? "subagents" : id)) + return { + columnOrder: migrate(board.columnOrder), + hiddenColumns: migrate(board.hiddenColumns), + } +} + export async function getAgentfilesConfig(): Promise { try { const content = await fs.readFile(CONFIG_PATH, "utf-8") const parsed = JSON.parse(content) + const board = migrateBoardConfig({ + ...DEFAULT_BOARD_CONFIG, + ...parsed.board, + }) return { ...DEFAULT_CONFIG, ...parsed, - board: { ...DEFAULT_BOARD_CONFIG, ...parsed.board }, + board, } } catch { return DEFAULT_CONFIG diff --git a/src/services/overview-service.ts b/src/services/overview-service.ts index 30b9cbf..6f10642 100644 --- a/src/services/overview-service.ts +++ b/src/services/overview-service.ts @@ -30,8 +30,8 @@ export async function getOverview(projectPath?: string): Promise { getClaudeMd("project", projectPath), getPlugins(projectPath), getMcpServers(projectPath), - scanMdDirWithScope(path.join(globalBase, "agents"), "agent", "user"), - scanMdDirWithScope(path.join(projectBase, "agents"), "agent", "project"), + scanMdDirWithScope(path.join(globalBase, "agents"), "subagent", "user"), + scanMdDirWithScope(path.join(projectBase, "agents"), "subagent", "project"), scanMdDirWithScope(path.join(globalBase, "commands"), "command", "user"), scanMdDirWithScope( path.join(projectBase, "commands"), diff --git a/src/services/plugin-service.test.ts b/src/services/plugin-service.test.ts index e9e0b03..34a875e 100644 --- a/src/services/plugin-service.test.ts +++ b/src/services/plugin-service.test.ts @@ -139,9 +139,9 @@ describe("scanPluginComponents", () => { const result = await scanPluginComponents(installPath) - expect(result.agents).toHaveLength(1) - expect(result.agents[0].name).toBe("reviewer") - expect(result.agents[0].type).toBe("agent") + expect(result.subagents).toHaveLength(1) + expect(result.subagents[0].name).toBe("reviewer") + expect(result.subagents[0].type).toBe("subagent") }) it("hooks/hooks.json에서 훅 설정 읽기", async () => { @@ -263,7 +263,7 @@ describe("scanPluginComponents", () => { expect(result.commands).toEqual([]) expect(result.skills).toEqual([]) - expect(result.agents).toEqual([]) + expect(result.subagents).toEqual([]) expect(result.hooks).toEqual({}) expect(result.mcpServers).toEqual([]) expect(result.lspServers).toEqual([]) @@ -278,7 +278,7 @@ describe("scanPluginComponents", () => { expect(result.commands).toEqual([]) expect(result.skills).toEqual([]) - expect(result.agents).toEqual([]) + expect(result.subagents).toEqual([]) expect(result.hooks).toEqual({}) expect(result.mcpServers).toEqual([]) expect(result.lspServers).toEqual([]) diff --git a/src/services/plugin-service.ts b/src/services/plugin-service.ts index bd47c99..1b42e10 100644 --- a/src/services/plugin-service.ts +++ b/src/services/plugin-service.ts @@ -316,7 +316,7 @@ export async function scanPluginComponents( ] = await Promise.all([ scanMdDir(path.join(installPath, "commands"), "command").catch(() => []), scanSkillsDir(path.join(installPath, "skills")).catch(() => []), - scanMdDir(path.join(installPath, "agents"), "agent").catch(() => []), + scanMdDir(path.join(installPath, "agents"), "subagent").catch(() => []), readHooksJson(path.join(installPath, "hooks", "hooks.json")), readMcpJson(path.join(installPath, ".mcp.json")), readLspJson(path.join(installPath, ".lsp.json")), @@ -325,7 +325,7 @@ export async function scanPluginComponents( return { commands, skills, - agents, + subagents: agents, hooks, mcpServers, lspServers, diff --git a/src/services/scope-management.ts b/src/services/scope-management.ts index c0a1b33..94aa6ac 100644 --- a/src/services/scope-management.ts +++ b/src/services/scope-management.ts @@ -8,7 +8,7 @@ import { const INVALID_NAME_PATTERN = /[/\\]|\.{2}/ interface MoveOrCopyParams { - type: "skill" | "agent" + type: "skill" | "subagent" name: string from: "user" | "project" to: "user" | "project" @@ -16,7 +16,8 @@ interface MoveOrCopyParams { projectPath?: string } -function getEntityDirName(type: "skill" | "agent"): string { +function getEntityDirName(type: "skill" | "subagent"): string { + if (type === "subagent") return "agents" return `${type}s` } diff --git a/src/shared/agents.test.ts b/src/shared/agents.test.ts index 5f46408..975ecdc 100644 --- a/src/shared/agents.test.ts +++ b/src/shared/agents.test.ts @@ -14,7 +14,7 @@ describe("agents", () => { expect(entities).toContain("plugin") expect(entities).toContain("mcp") expect(entities).toContain("hook") - expect(entities).toContain("agent") + expect(entities).toContain("subagent") }) it("returns undefined for unknown agent", () => { diff --git a/src/shared/agents.ts b/src/shared/agents.ts index 09e5d55..47ccf27 100644 --- a/src/shared/agents.ts +++ b/src/shared/agents.ts @@ -35,7 +35,7 @@ export const agents: Record = { globalSkillsDir: "~/.claude/skills", configDir: ".claude", globalConfigDir: "~/.claude", - entities: ["skill", "agent", "hook", "plugin", "mcp"], + entities: ["skill", "subagent", "hook", "plugin", "mcp"], }, cline: { name: "cline", diff --git a/src/shared/types.ts b/src/shared/types.ts index 2f1695a..081f162 100644 --- a/src/shared/types.ts +++ b/src/shared/types.ts @@ -3,12 +3,12 @@ import type { HooksSettings } from "@/features/hook/types" // ── Zod 스키마 ── export const scopeSchema = z.enum(["user", "project", "local", "managed"]) -export const agentFileTypeSchema = z.enum(["agent", "command", "skill"]) +export const agentFileTypeSchema = z.enum(["subagent", "command", "skill"]) // ── Entity Type ── export const entityTypeSchema = z.enum([ "skill", - "agent", + "subagent", "hook", "plugin", "mcp", @@ -132,7 +132,7 @@ export interface LspServer { export interface PluginComponents { commands: AgentFile[] skills: AgentFile[] - agents: AgentFile[] + subagents: AgentFile[] hooks: HooksSettings mcpServers: McpServer[] lspServers: LspServer[] @@ -191,7 +191,7 @@ export interface AgentFile { } size: number lastModified: string // ISO 8601 - type: "agent" | "command" | "skill" + type: "subagent" | "command" | "skill" isSymlink?: boolean symlinkTarget?: string isSkillDir?: boolean // true if .claude/skills//SKILL.md format @@ -305,7 +305,7 @@ export type BoardColumnId = | "plugins" | "mcp" | "skills" - | "agents" + | "subagents" | "hooks" | "memory" | "lsp" diff --git a/tests/i18n/messages.test.ts b/tests/i18n/messages.test.ts index b4beafe..2a2015c 100644 --- a/tests/i18n/messages.test.ts +++ b/tests/i18n/messages.test.ts @@ -54,6 +54,7 @@ describe("i18n messages", () => { const validPrefixes = [ "action", "agents", + "subagents", "app", "board", "claude", diff --git a/tests/integration/crud-flow.test.ts b/tests/integration/crud-flow.test.ts index 0c62efd..5b048de 100644 --- a/tests/integration/crud-flow.test.ts +++ b/tests/integration/crud-flow.test.ts @@ -96,17 +96,17 @@ describe("Agent/Command/Skill CRUD 흐름", () => { await writeMarkdown(agentPath, content) // Read - 목록에 포함 확인 - const listAfterCreate = await getAgentFiles("agent") + const listAfterCreate = await getAgentFiles("subagent") const found = listAfterCreate.find((a) => a.name === "my-agent") expect(found).toBeDefined() expect(found?.scope).toBe("user") - expect(found?.type).toBe("agent") + expect(found?.type).toBe("subagent") // Delete await deleteFile(agentPath) // Read - 목록에서 제외 확인 - const listAfterDelete = await getAgentFiles("agent") + const listAfterDelete = await getAgentFiles("subagent") const notFound = listAfterDelete.find((a) => a.name === "my-agent") expect(notFound).toBeUndefined() }) @@ -199,13 +199,13 @@ describe("Agent/Command/Skill CRUD 흐름", () => { writeMarkdown(agentC, "# Agent C"), ]) - const beforeDelete = await getAgentFiles("agent") + const beforeDelete = await getAgentFiles("subagent") expect(beforeDelete).toHaveLength(3) // agentB만 삭제 await deleteFile(agentB) - const afterDelete = await getAgentFiles("agent") + const afterDelete = await getAgentFiles("subagent") expect(afterDelete).toHaveLength(2) expect(afterDelete.find((a) => a.name === "agent-a")).toBeDefined() expect(afterDelete.find((a) => a.name === "agent-b")).toBeUndefined() @@ -226,7 +226,7 @@ describe("글로벌 + 프로젝트 혼합 및 충돌 감지", () => { writeMarkdown(projectPath, "# 프로젝트 에이전트"), ]) - const list = await getAgentFiles("agent") + const list = await getAgentFiles("subagent") expect(list).toHaveLength(2) const globalAgent = list.find((a) => a.scope === "user") @@ -385,7 +385,7 @@ describe("Frontmatter 파싱 통합 테스트", () => { await writeMarkdown(filePath, content) - const results = await scanMdDir(dir, "agent") + const results = await scanMdDir(dir, "subagent") expect(results).toHaveLength(1) expect(results[0].name).toBe("simple-agent") @@ -412,7 +412,7 @@ describe("Frontmatter 파싱 통합 테스트", () => { await writeMarkdown(agentPath, content) - const list = await getAgentFiles("agent") + const list = await getAgentFiles("subagent") const agent = list.find((a) => a.name === "advanced-agent") expect(agent).toBeDefined() diff --git a/tests/integration/error-edge-cases.test.ts b/tests/integration/error-edge-cases.test.ts index e126356..cc0ca5b 100644 --- a/tests/integration/error-edge-cases.test.ts +++ b/tests/integration/error-edge-cases.test.ts @@ -78,7 +78,10 @@ describe("존재하지 않는 리소스 처리", () => { }) it("scanMdDir - 존재하지 않는 디렉토리는 빈 배열 반환", async () => { - const result = await scanMdDir(path.join(tmpGlobal, "nonexistent"), "agent") + const result = await scanMdDir( + path.join(tmpGlobal, "nonexistent"), + "subagent", + ) expect(result).toEqual([]) }) }) @@ -131,7 +134,7 @@ describe("특수 문자 파일명 처리", () => { const dir = path.join(tmpGlobal, "agents") await writeFile(path.join(dir, "my agent.md"), "# My Agent") - const result = await scanMdDir(dir, "agent") + const result = await scanMdDir(dir, "subagent") expect(result).toHaveLength(1) expect(result[0].name).toBe("my agent") diff --git a/tests/unit/board-config.test.ts b/tests/unit/board-config.test.ts index d6fd4d8..79ae31c 100644 --- a/tests/unit/board-config.test.ts +++ b/tests/unit/board-config.test.ts @@ -43,7 +43,7 @@ describe("board-config", () => { "skills", "files", "plugins", - "agents", + "subagents", "hooks", "memory", "lsp", @@ -80,7 +80,7 @@ describe("board-config", () => { "skills", "files", "plugins", - "agents", + "subagents", "hooks", "memory", "lsp", diff --git a/tests/unit/scope-management.test.ts b/tests/unit/scope-management.test.ts index 8fd169b..daae44c 100644 --- a/tests/unit/scope-management.test.ts +++ b/tests/unit/scope-management.test.ts @@ -69,7 +69,7 @@ describe("scope-management", () => { it("copies an agent from project to user", async () => { await moveOrCopyEntity({ - type: "agent", + type: "subagent", name: "my-agent", from: "project", to: "user", From 092382a6ed31bf44c917314a31b5e70126520e39 Mon Sep 17 00:00:00 2001 From: Youngsup Oh Date: Thu, 19 Mar 2026 10:12:00 +0900 Subject: [PATCH 2/5] fix: rename agents variable to subagents in PluginsPanel Co-Authored-By: Claude Opus 4.6 (1M context) --- src/components/board/PluginsPanel.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/components/board/PluginsPanel.tsx b/src/components/board/PluginsPanel.tsx index b2b0d0a..356ce95 100644 --- a/src/components/board/PluginsPanel.tsx +++ b/src/components/board/PluginsPanel.tsx @@ -179,7 +179,7 @@ function PluginTreeItem({ }) { const contents = plugin.contents const skills = contents?.skills ?? [] - const agents = contents?.subagents ?? [] + const subagents = contents?.subagents ?? [] const mcpServers = contents?.mcpServers ?? [] const hookEntries = Object.entries(contents?.hooks ?? {}) @@ -299,14 +299,14 @@ function PluginTreeItem({ )} - {agents.length > 0 && ( + {subagents.length > 0 && ( <> - {agents.map((a) => { + {subagents.map((a) => { const t = { type: "subagent", key: a.path } const acts = ENTITY_ACTIONS.subagent.filter(openOnlyFilter) return ( From ae7f0ac698f5b2e753939ca67c286d4a26273d28 Mon Sep 17 00:00:00 2001 From: Youngsup Oh Date: Thu, 19 Mar 2026 10:14:41 +0900 Subject: [PATCH 3/5] fix: rename agents variable to subagents in plugin-service Co-Authored-By: Claude Opus 4.6 (1M context) --- src/services/plugin-service.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/services/plugin-service.ts b/src/services/plugin-service.ts index 1b42e10..950a975 100644 --- a/src/services/plugin-service.ts +++ b/src/services/plugin-service.ts @@ -308,7 +308,7 @@ export async function scanPluginComponents( const [ commands, skills, - agents, + subagents, hooks, mcpServers, lspServers, @@ -325,7 +325,7 @@ export async function scanPluginComponents( return { commands, skills, - subagents: agents, + subagents, hooks, mcpServers, lspServers, From 8c417aac7151192249a1ed73e5185c4e8b4985f5 Mon Sep 17 00:00:00 2001 From: Youngsup Oh Date: Thu, 19 Mar 2026 10:15:12 +0900 Subject: [PATCH 4/5] fix: remove stale "agents" prefix from i18n validation Co-Authored-By: Claude Opus 4.6 (1M context) --- tests/i18n/messages.test.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/i18n/messages.test.ts b/tests/i18n/messages.test.ts index 2a2015c..f0ee11b 100644 --- a/tests/i18n/messages.test.ts +++ b/tests/i18n/messages.test.ts @@ -53,7 +53,6 @@ describe("i18n messages", () => { it("keys should follow naming convention (prefix_name)", () => { const validPrefixes = [ "action", - "agents", "subagents", "app", "board", From 2f4606651dccc838ba46d31a76678a1f80f3c6b6 Mon Sep 17 00:00:00 2001 From: Youngsup Oh Date: Thu, 19 Mar 2026 10:19:25 +0900 Subject: [PATCH 5/5] refactor(test): split subagent tests from crud-flow into subagent-crud Co-Authored-By: Claude Opus 4.6 (1M context) --- tests/integration/crud-flow.test.ts | 114 +----------------- tests/integration/subagent-crud.test.ts | 153 ++++++++++++++++++++++++ 2 files changed, 155 insertions(+), 112 deletions(-) create mode 100644 tests/integration/subagent-crud.test.ts diff --git a/tests/integration/crud-flow.test.ts b/tests/integration/crud-flow.test.ts index 5b048de..4695732 100644 --- a/tests/integration/crud-flow.test.ts +++ b/tests/integration/crud-flow.test.ts @@ -85,32 +85,9 @@ describe("CLAUDE.md CRUD 흐름", () => { }) }) -// ── 2. Agent / Command / Skill CRUD ── - -describe("Agent/Command/Skill CRUD 흐름", () => { - it("Agent CRUD: writeMarkdown → getAgentFiles 목록 포함 → deleteFile → 제외", async () => { - const agentPath = path.join(tmpGlobal, ".claude", "agents", "my-agent.md") - const content = "# My Agent\n\n에이전트 설명입니다." - - // Create - await writeMarkdown(agentPath, content) - - // Read - 목록에 포함 확인 - const listAfterCreate = await getAgentFiles("subagent") - const found = listAfterCreate.find((a) => a.name === "my-agent") - expect(found).toBeDefined() - expect(found?.scope).toBe("user") - expect(found?.type).toBe("subagent") - - // Delete - await deleteFile(agentPath) - - // Read - 목록에서 제외 확인 - const listAfterDelete = await getAgentFiles("subagent") - const notFound = listAfterDelete.find((a) => a.name === "my-agent") - expect(notFound).toBeUndefined() - }) +// ── 2. Command / Skill CRUD ── +describe("Command/Skill CRUD 흐름", () => { it("Command CRUD: writeMarkdown → getAgentFiles 목록 포함 → deleteFile → 제외", async () => { const cmdPath = path.join(tmpGlobal, ".claude", "commands", "my-command.md") const content = "# My Command\n\n커맨드 설명입니다." @@ -187,55 +164,11 @@ describe("Agent/Command/Skill CRUD 흐름", () => { const notFound = listAfterDelete.find((s) => s.name === "my-skill") expect(notFound).toBeUndefined() }) - - it("여러 항목 생성 후 일부 삭제 → 나머지 유지", async () => { - const agentA = path.join(tmpGlobal, ".claude", "agents", "agent-a.md") - const agentB = path.join(tmpGlobal, ".claude", "agents", "agent-b.md") - const agentC = path.join(tmpProject, ".claude", "agents", "agent-c.md") - - await Promise.all([ - writeMarkdown(agentA, "# Agent A"), - writeMarkdown(agentB, "# Agent B"), - writeMarkdown(agentC, "# Agent C"), - ]) - - const beforeDelete = await getAgentFiles("subagent") - expect(beforeDelete).toHaveLength(3) - - // agentB만 삭제 - await deleteFile(agentB) - - const afterDelete = await getAgentFiles("subagent") - expect(afterDelete).toHaveLength(2) - expect(afterDelete.find((a) => a.name === "agent-a")).toBeDefined() - expect(afterDelete.find((a) => a.name === "agent-b")).toBeUndefined() - expect(afterDelete.find((a) => a.name === "agent-c")).toBeDefined() - }) }) // ── 3. 글로벌 + 프로젝트 혼합 (충돌 감지) ── describe("글로벌 + 프로젝트 혼합 및 충돌 감지", () => { - it("동일 이름 양쪽 생성 → getAgentFiles 양쪽 포함", async () => { - const name = "shared-agent" - const globalPath = path.join(tmpGlobal, ".claude", "agents", `${name}.md`) - const projectPath = path.join(tmpProject, ".claude", "agents", `${name}.md`) - - await Promise.all([ - writeMarkdown(globalPath, "# 글로벌 에이전트"), - writeMarkdown(projectPath, "# 프로젝트 에이전트"), - ]) - - const list = await getAgentFiles("subagent") - expect(list).toHaveLength(2) - - const globalAgent = list.find((a) => a.scope === "user") - const projectAgent = list.find((a) => a.scope === "project") - - expect(globalAgent?.name).toBe(name) - expect(projectAgent?.name).toBe(name) - }) - it("동일 이름 command 양쪽 생성 → getOverview conflictCount 반영", async () => { const globalCmd = path.join( tmpGlobal, @@ -378,49 +311,6 @@ describe("Frontmatter 파싱 통합 테스트", () => { expect(results[0].frontmatter?.version).toBe("1.0.0") }) - it("frontmatter 없는 md 작성 → scanMdDir → frontmatter undefined", async () => { - const dir = path.join(tmpProject, ".claude", "agents") - const filePath = path.join(dir, "simple-agent.md") - const content = "# Simple Agent\n\nfrontmatter 없는 에이전트" - - await writeMarkdown(filePath, content) - - const results = await scanMdDir(dir, "subagent") - - expect(results).toHaveLength(1) - expect(results[0].name).toBe("simple-agent") - expect(results[0].frontmatter).toBeUndefined() - }) - - it("여러 frontmatter 필드 작성 후 getAgentFiles 로 조회", async () => { - const agentPath = path.join( - tmpGlobal, - ".claude", - "agents", - "advanced-agent.md", - ) - const content = [ - "---", - "description: 고급 에이전트", - "author: test-user", - "tags:", - " - ai", - " - automation", - "---", - "# Advanced Agent", - ].join("\n") - - await writeMarkdown(agentPath, content) - - const list = await getAgentFiles("subagent") - const agent = list.find((a) => a.name === "advanced-agent") - - expect(agent).toBeDefined() - expect(agent?.frontmatter?.description).toBe("고급 에이전트") - expect(agent?.frontmatter?.author).toBe("test-user") - expect(agent?.frontmatter?.tags).toEqual(["ai", "automation"]) - }) - it("frontmatter Update: 내용 변경 후 재조회 → 새 frontmatter 반영", async () => { const dir = path.join(tmpGlobal, ".claude", "skills") const filePath = path.join(dir, "my-skill.md") diff --git a/tests/integration/subagent-crud.test.ts b/tests/integration/subagent-crud.test.ts new file mode 100644 index 0000000..0e802a8 --- /dev/null +++ b/tests/integration/subagent-crud.test.ts @@ -0,0 +1,153 @@ +/** + * 서비스 통합 테스트 - Subagent CRUD 흐름 + * + * .claude/agents/ 디렉토리를 사용하거나 getAgentFiles("subagent")를 호출하는 + * 테스트를 별도로 분리하여 관리한다. + */ + +import os from "node:os" +import path from "node:path" +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest" +import { getAgentFiles, scanMdDir } from "@/services/agent-file-service" +import { deleteFile, writeMarkdown } from "@/services/file-writer" +import { createTmpDir, removeTmpDir } from "../helpers/test-utils" + +// ── 모킹: process.cwd() + os.homedir() ── + +let tmpGlobal: string +let tmpProject: string + +beforeEach(async () => { + tmpGlobal = await createTmpDir("subagent-global") + tmpProject = await createTmpDir("subagent-project") + + vi.spyOn(os, "homedir").mockReturnValue(tmpGlobal) + vi.spyOn(process, "cwd").mockReturnValue(tmpProject) +}) + +afterEach(async () => { + vi.restoreAllMocks() + await Promise.all([removeTmpDir(tmpGlobal), removeTmpDir(tmpProject)]) +}) + +// ── 1. Subagent CRUD ── + +describe("Subagent CRUD 흐름", () => { + it("Agent CRUD: writeMarkdown → getAgentFiles 목록 포함 → deleteFile → 제외", async () => { + const agentPath = path.join(tmpGlobal, ".claude", "agents", "my-agent.md") + const content = "# My Agent\n\n에이전트 설명입니다." + + // Create + await writeMarkdown(agentPath, content) + + // Read - 목록에 포함 확인 + const listAfterCreate = await getAgentFiles("subagent") + const found = listAfterCreate.find((a) => a.name === "my-agent") + expect(found).toBeDefined() + expect(found?.scope).toBe("user") + expect(found?.type).toBe("subagent") + + // Delete + await deleteFile(agentPath) + + // Read - 목록에서 제외 확인 + const listAfterDelete = await getAgentFiles("subagent") + const notFound = listAfterDelete.find((a) => a.name === "my-agent") + expect(notFound).toBeUndefined() + }) + + it("여러 항목 생성 후 일부 삭제 → 나머지 유지", async () => { + const agentA = path.join(tmpGlobal, ".claude", "agents", "agent-a.md") + const agentB = path.join(tmpGlobal, ".claude", "agents", "agent-b.md") + const agentC = path.join(tmpProject, ".claude", "agents", "agent-c.md") + + await Promise.all([ + writeMarkdown(agentA, "# Agent A"), + writeMarkdown(agentB, "# Agent B"), + writeMarkdown(agentC, "# Agent C"), + ]) + + const beforeDelete = await getAgentFiles("subagent") + expect(beforeDelete).toHaveLength(3) + + // agentB만 삭제 + await deleteFile(agentB) + + const afterDelete = await getAgentFiles("subagent") + expect(afterDelete).toHaveLength(2) + expect(afterDelete.find((a) => a.name === "agent-a")).toBeDefined() + expect(afterDelete.find((a) => a.name === "agent-b")).toBeUndefined() + expect(afterDelete.find((a) => a.name === "agent-c")).toBeDefined() + }) +}) + +// ── 2. 글로벌 + 프로젝트 혼합 ── + +describe("글로벌 + 프로젝트 혼합", () => { + it("동일 이름 양쪽 생성 → getAgentFiles 양쪽 포함", async () => { + const name = "shared-agent" + const globalPath = path.join(tmpGlobal, ".claude", "agents", `${name}.md`) + const projectPath = path.join(tmpProject, ".claude", "agents", `${name}.md`) + + await Promise.all([ + writeMarkdown(globalPath, "# 글로벌 에이전트"), + writeMarkdown(projectPath, "# 프로젝트 에이전트"), + ]) + + const list = await getAgentFiles("subagent") + expect(list).toHaveLength(2) + + const globalAgent = list.find((a) => a.scope === "user") + const projectAgent = list.find((a) => a.scope === "project") + + expect(globalAgent?.name).toBe(name) + expect(projectAgent?.name).toBe(name) + }) +}) + +// ── 3. Frontmatter 파싱 ── + +describe("Frontmatter 파싱", () => { + it("frontmatter 없는 md 작성 → scanMdDir → frontmatter undefined", async () => { + const dir = path.join(tmpProject, ".claude", "agents") + const filePath = path.join(dir, "simple-agent.md") + const content = "# Simple Agent\n\nfrontmatter 없는 에이전트" + + await writeMarkdown(filePath, content) + + const results = await scanMdDir(dir, "subagent") + + expect(results).toHaveLength(1) + expect(results[0].name).toBe("simple-agent") + expect(results[0].frontmatter).toBeUndefined() + }) + + it("여러 frontmatter 필드 작성 후 getAgentFiles 로 조회", async () => { + const agentPath = path.join( + tmpGlobal, + ".claude", + "agents", + "advanced-agent.md", + ) + const content = [ + "---", + "description: 고급 에이전트", + "author: test-user", + "tags:", + " - ai", + " - automation", + "---", + "# Advanced Agent", + ].join("\n") + + await writeMarkdown(agentPath, content) + + const list = await getAgentFiles("subagent") + const agent = list.find((a) => a.name === "advanced-agent") + + expect(agent).toBeDefined() + expect(agent?.frontmatter?.description).toBe("고급 에이전트") + expect(agent?.frontmatter?.author).toBe("test-user") + expect(agent?.frontmatter?.tags).toEqual(["ai", "automation"]) + }) +})