From 8d02820fa5f4653ed7eb9da1c9cfce8b016a2694 Mon Sep 17 00:00:00 2001 From: Yun Chan Date: Fri, 21 Aug 2026 12:24:34 +0900 Subject: [PATCH] =?UTF-8?q?feat(core):=20=EC=96=B4=EB=96=A4=20=ED=95=98?= =?UTF-8?q?=EB=84=A4=EC=8A=A4=EC=97=90=EC=84=9C=EB=93=A0=20=EC=8A=A4?= =?UTF-8?q?=ED=82=AC=EC=9D=84=20=EB=B0=9C=EA=B2=AC=ED=95=98=EB=8A=94=20?= =?UTF-8?q?=ED=83=80=EA=B9=83=20=EC=B2=B4=EA=B3=84=EB=A1=9C=20=EB=B0=94?= =?UTF-8?q?=EA=BE=BC=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ZCode(~/.zcode/skills) 타깃 추가 - 중립 경로 agents(~/.agents/skills · .agents/skills) 타깃 추가 - gemini-cli(.gemini/skills)·copilot(~/.copilot/skills · .github/skills) 타깃 추가 - Cursor·Windsurf 를 네이티브 스킬 경로로 전환 — 규칙 파일 12,000자 상한 때문에 Windsurf 는 실제 스킬이 아예 설치되지 못했었다. 전역 설치도 가능해진다 - Antigravity 전역 경로를 공식 문서대로 ~/.gemini/antigravity-cli/skills 로 수정 --- .changeset/universal-skill-targets.md | 7 ++ .changeset/zcode-target.md | 6 ++ packages/cli/README.md | 18 +++- packages/cli/src/commands/install.ts | 29 +++++- packages/core/src/targets/agents.ts | 41 ++++++++ packages/core/src/targets/antigravity.ts | 18 ++-- packages/core/src/targets/copilot.ts | 42 ++++++++ packages/core/src/targets/cursor.ts | 58 ++++------- packages/core/src/targets/gemini-cli.ts | 41 ++++++++ packages/core/src/targets/index.ts | 32 +++++- packages/core/src/targets/windsurf.ts | 60 ++++-------- packages/core/src/targets/zcode.ts | 37 +++++++ packages/core/src/types.ts | 6 +- packages/core/test/installer.test.ts | 120 ++++++++++++++++------- 14 files changed, 379 insertions(+), 136 deletions(-) create mode 100644 .changeset/universal-skill-targets.md create mode 100644 .changeset/zcode-target.md create mode 100644 packages/core/src/targets/agents.ts create mode 100644 packages/core/src/targets/copilot.ts create mode 100644 packages/core/src/targets/gemini-cli.ts create mode 100644 packages/core/src/targets/zcode.ts diff --git a/.changeset/universal-skill-targets.md b/.changeset/universal-skill-targets.md new file mode 100644 index 0000000..2479fd2 --- /dev/null +++ b/.changeset/universal-skill-targets.md @@ -0,0 +1,7 @@ +--- +"designpaca": minor +"@designpaca/core": minor +"@designpaca/skill": minor +--- + +어떤 하네스 에이전트든 스킬을 발견하게 만든다. 중립 경로 `agents` 타깃(`~/.agents/skills` · `.agents/skills`)을 새로 추가한다 — Cursor · Antigravity · ZCode 등 Agent Skills 규약을 읽는 하네스가 함께 잡는 자리다. `gemini-cli`(`.gemini/skills`)와 `copilot`(`.github/skills` · `~/.copilot/skills`) 타깃을 추가한다. Cursor 는 구식 `.cursor/rules/*.mdc` 변환을 버리고 네이티브 `.cursor/skills` 로 옮기며 전역 설치를 지원한다. Windsurf 도 규칙 파일(12,000자 상한 때문에 실제 스킬이 아예 설치되지 못했다)에서 `.windsurf/skills` · `~/.codeium/windsurf/skills` 네이티브 경로로 옮긴다. Antigravity 전역 경로를 공식 마이그레이션 문서대로 `~/.gemini/antigravity-cli/skills` 로 바로잡는다. diff --git a/.changeset/zcode-target.md b/.changeset/zcode-target.md new file mode 100644 index 0000000..011f5ce --- /dev/null +++ b/.changeset/zcode-target.md @@ -0,0 +1,6 @@ +--- +"designpaca": minor +"@designpaca/core": minor +--- + +ZCode 타깃 추가 — `~/.zcode/skills/designpaca/` (전역) 또는 `.zcode/skills/designpaca/` (프로젝트) 에 스킬을 설치한다. SKILL.md 규약이 Claude Code 와 같아 변환 없이 그대로 복사된다. diff --git a/packages/cli/README.md b/packages/cli/README.md index 19ea25d..18d8cba 100644 --- a/packages/cli/README.md +++ b/packages/cli/README.md @@ -1,6 +1,8 @@ # designpaca -**웹 디자인 파이프라인 스킬을 Claude Code · Codex · Cursor 에 한 줄로 설치한다.** +**웹 디자인 파이프라인 스킬을 어떤 에이전트 하네스에든 한 줄로 설치한다.** + +Agent Skills 규약(SKILL.md)을 그대로 따르므로, 규약을 읽는 하네스라면 종류를 가리지 않는다. ```bash npx designpaca @@ -53,11 +55,19 @@ npx designpaca install -t claude-code,codex -s user -y npx designpaca install -t cursor -s project ``` +모든 타깃이 같은 SKILL.md 를 쓴다. 중립 경로(`agents`)는 이 규약을 읽는 하네스라면 무엇이든 잡는다. + | 대상 | 설치 위치 | |---|---| | `claude-code` | `~/.claude/skills/designpaca/` (또는 프로젝트 `.claude/`) | -| `codex` | `~/.codex/skills/designpaca/` | -| `cursor` | `.cursor/rules/designpaca.mdc` (프로젝트 단위) | +| `codex` | `~/.codex/skills/designpaca/` (또는 프로젝트 `.codex/`) | +| `cursor` | `~/.cursor/skills/designpaca/` (또는 프로젝트 `.cursor/`) | +| `zcode` | `~/.zcode/skills/designpaca/` (또는 프로젝트 `.zcode/`) | +| `agents` | `~/.agents/skills/designpaca/` (또는 프로젝트 `.agents/`) — **중립 경로** | +| `antigravity` | 전역 `~/.gemini/antigravity-cli/skills/` · 워크스페이스 `.agents/skills/` | +| `gemini-cli` | `~/.gemini/skills/designpaca/` (또는 프로젝트 `.gemini/`) | +| `windsurf` | `~/.codeium/windsurf/skills/designpaca/` (또는 프로젝트 `.windsurf/`) | +| `copilot` | `~/.copilot/skills/designpaca/` (또는 프로젝트 `.github/skills/`) | | `agents-md` | `AGENTS.md` 에 블록 주입 | ## 사용 @@ -95,7 +105,7 @@ npx designpaca update --force | 옵션 | 설명 | |---|---| -| `-t, --target ` | `claude-code` `codex` `cursor` `agents-md` | +| `-t, --target ` | `claude-code` `codex` `cursor` `zcode` `agents` `antigravity` `gemini-cli` `windsurf` `copilot` `agents-md` | | `-s, --scope <범위>` | `user` (전역) \| `project` | | `-y, --yes` | 확인 없이 진행 | | `-f, --force` | 직접 수정한 파일도 덮어쓴다 (`.orig` 백업) | diff --git a/packages/cli/src/commands/install.ts b/packages/cli/src/commands/install.ts index fa64382..34ed3b4 100644 --- a/packages/cli/src/commands/install.ts +++ b/packages/cli/src/commands/install.ts @@ -75,11 +75,36 @@ export function printNextSteps(targets: TargetId[]): void { if (targets.includes("codex")) { console.log(info(`Codex CLI 에서 designpaca 스킬이 자동으로 잡힌다`)); } + if (targets.includes("zcode")) { + console.log(info(`ZCode 는 새 세션부터 designpaca 스킬이 자동으로 잡힌다`)); + } if (targets.includes("cursor")) { - console.log(info(`Cursor 는 ${dim(".cursor/rules/designpaca.mdc")} 를 프로젝트 열 때 읽는다`)); + console.log(info(`Cursor 는 새 창을 열 때 ${dim(".cursor/skills/designpaca")} 스킬을 읽는다`)); + } + if (targets.includes("agents")) { + console.log( + info( + `${dim(".agents/skills")} 는 중립 경로다 — Cursor · Antigravity · ZCode 등 규약을 읽는 하네스가 함께 잡는다`, + ), + ); + } + if (targets.includes("antigravity")) { + console.log( + info(`Antigravity 는 전역 ${dim("~/.gemini/antigravity-cli/skills")} 를 읽는다 (워크스페이스는 .agents/skills)`), + ); + } + if (targets.includes("gemini-cli")) { + console.log(info(`Gemini CLI 는 ${dim("~/.gemini/skills")} 에서 스킬을 읽는다`)); } if (targets.includes("windsurf")) { - console.log(info(`Windsurf 는 ${dim(".windsurf/rules/designpaca.md")} 를 읽는다`)); + console.log( + info(`Windsurf(Cascade) 는 ${dim(".windsurf/skills/designpaca")} 스킬을 읽는다`), + ); + } + if (targets.includes("copilot")) { + console.log( + info(`GitHub Copilot 은 ${dim(".github/skills")} (~/.copilot/skills) 의 SKILL.md 를 읽는다`), + ); } if (targets.includes("agents-md")) { console.log( diff --git a/packages/core/src/targets/agents.ts b/packages/core/src/targets/agents.ts new file mode 100644 index 0000000..e964e76 --- /dev/null +++ b/packages/core/src/targets/agents.ts @@ -0,0 +1,41 @@ +import path from "node:path"; +import { exists } from "../fsx.ts"; +import type { TargetAdapter, TargetContext } from "../types.ts"; +import { anyExists, scopeRoot, skillDirActions } from "./common.ts"; + +/** + * 중립 경로 `.agents/skills` — 특정 제품의 경로가 아니라 Agent Skills 규약 자체의 홈이다. + * + * "스펙은 통일됐는데 경로가 갈렸다"가 현재 생태계의 문제라면, 이 디렉터리가 그 답이다. + * Cursor · Antigravity · ZCode 가 프로젝트와 홈에서 이 경로를 읽고, 같은 규약을 채택하는 + * 하네스가 계속 늘고 있다. 어떤 도구를 쓰는지와 무관하게 **한 번 깔면 다 같이 잡는 자리**다. + * + * 경로: + * 워크스페이스 /.agents/skills/designpaca/ + * 전역 ~/.agents/skills/designpaca/ + */ +export const agents: TargetAdapter = { + id: "agents", + label: "중립 경로 (.agents)", + hint: "~/.agents/skills/designpaca — 이 규약을 읽는 하네스라면 무엇이든 잡는다", + scopes: ["user", "project"], + + async detect(ctx: TargetContext) { + return anyExists([path.join(ctx.home, ".agents"), path.join(ctx.cwd, ".agents")]); + }, + + async plan(ctx: TargetContext) { + const root = scopeRoot( + ctx, + [".agents", "skills", "designpaca"], + [".agents", "skills", "designpaca"], + ); + return { + target: this.id, + scope: ctx.scope, + root, + actions: skillDirActions(root, ctx.skill), + alreadyInstalled: await exists(path.join(root, "SKILL.md")), + }; + }, +}; diff --git a/packages/core/src/targets/antigravity.ts b/packages/core/src/targets/antigravity.ts index f7388dd..494e46f 100644 --- a/packages/core/src/targets/antigravity.ts +++ b/packages/core/src/targets/antigravity.ts @@ -7,17 +7,17 @@ import { anyExists, scopeRoot, skillDirActions } from "./common.ts"; * Antigravity 와 `.agents/skills` 를 읽는 도구들. * * `.agents/skills/` 는 특정 제품의 경로가 아니라 **중립 경로**다. - * Antigravity 가 여기서 스킬을 읽고, 같은 규약을 쓰는 다른 하네스도 늘고 있다. + * Antigravity 가 여기서 스킬을 읽고, 같은 규약을 쓰는 다른 하네스도 늘고 있다 + * (중립 경로 단독 설치는 `agents` 타깃이 담당한다). * SKILL.md 구조(프론트매터 + references/)가 Claude Code 와 같아서 * 파일을 그대로 복사하면 된다 — 변환이 필요 없다. * * 경로(공식 문서 기준): * 워크스페이스 /.agents/skills/<이름>/ - * 전역 ~/.gemini/config/skills/<이름>/ + * 전역 ~/.gemini/antigravity-cli/skills/<이름>/ * - * 구버전은 `.agent/skills`(단수)를 읽었고 지금도 하위호환으로 지원된다. - * 새로 설치하는 쪽은 복수형이 맞으므로 복수형에만 쓴다 — 두 곳에 같은 파일을 - * 깔면 스킬이 두 번 잡힌다. + * 전역 경로는 Gemini CLI → Antigravity 마이그레이션 문서가 정리한 현재 위치다. + * 예전 문서의 ~/.gemini/config/skills · ~/.gemini/skills 는 레거시다. * * 프론트매터 요구사항도 확인했다: `description` 필수, `name` 선택(없으면 폴더명). * 우리 SKILL.md 는 둘 다 갖고 있다. @@ -25,23 +25,21 @@ import { anyExists, scopeRoot, skillDirActions } from "./common.ts"; export const antigravity: TargetAdapter = { id: "antigravity", label: "Antigravity", - hint: ".agents/skills/designpaca — 중립 경로, 같은 규약을 쓰는 도구가 함께 읽는다", + hint: "전역 ~/.gemini/antigravity-cli/skills — 워크스페이스는 중립 경로 타깃과 같다", scopes: ["user", "project"], async detect(ctx: TargetContext) { return anyExists([ - path.join(ctx.home, ".gemini", "config", "skills"), path.join(ctx.home, ".gemini"), + path.join(ctx.home, ".gemini", "antigravity-cli"), path.join(ctx.cwd, ".agents"), - // 폴더 이름이 바뀌기 전에 만든 프로젝트 - path.join(ctx.cwd, ".agent"), ]); }, async plan(ctx: TargetContext) { const root = scopeRoot( ctx, - [".gemini", "config", "skills", "designpaca"], + [".gemini", "antigravity-cli", "skills", "designpaca"], [".agents", "skills", "designpaca"], ); return { diff --git a/packages/core/src/targets/copilot.ts b/packages/core/src/targets/copilot.ts new file mode 100644 index 0000000..b8057ab --- /dev/null +++ b/packages/core/src/targets/copilot.ts @@ -0,0 +1,42 @@ +import path from "node:path"; +import { exists } from "../fsx.ts"; +import type { TargetAdapter, TargetContext } from "../types.ts"; +import { anyExists, scopeRoot, skillDirActions } from "./common.ts"; + +/** + * GitHub Copilot — ~/.copilot/skills/designpaca/ (전역) 또는 .github/skills/designpaca/ (프로젝트). + * Copilot 코딩 에이전트는 2025-12 부터 Agent Skills 규약을 지원한다. + * + * detect 는 보수적으로 잡는다 — `.github/` 은 Copilot 을 안 쓰는 저장소에도 흔하니까. + * Copilot 사용 흔적(~/.copilot, 기존 .github/skills, copilot-instructions.md)이 있을 때만 + * 기본 선택에 올린다. 명시적으로 `-t copilot` 을 주면 언제든 설치할 수 있다. + */ +export const copilot: TargetAdapter = { + id: "copilot", + label: "GitHub Copilot", + hint: "~/.copilot/skills/designpaca — SKILL.md 그대로", + scopes: ["user", "project"], + + async detect(ctx: TargetContext) { + return anyExists([ + path.join(ctx.home, ".copilot"), + path.join(ctx.cwd, ".github", "skills"), + path.join(ctx.cwd, ".github", "copilot-instructions.md"), + ]); + }, + + async plan(ctx: TargetContext) { + const root = scopeRoot( + ctx, + [".copilot", "skills", "designpaca"], + [".github", "skills", "designpaca"], + ); + return { + target: this.id, + scope: ctx.scope, + root, + actions: skillDirActions(root, ctx.skill), + alreadyInstalled: await exists(path.join(root, "SKILL.md")), + }; + }, +}; diff --git a/packages/core/src/targets/cursor.ts b/packages/core/src/targets/cursor.ts index f320638..6a4d20d 100644 --- a/packages/core/src/targets/cursor.ts +++ b/packages/core/src/targets/cursor.ts @@ -1,57 +1,41 @@ import path from "node:path"; import { exists } from "../fsx.ts"; -import type { FileAction, TargetAdapter, TargetContext } from "../types.ts"; -import { anyExists, referenceActions, rewriteRefPaths } from "./common.ts"; - -/** 본문과 references 가 함께 놓이는 루트 (본문 경로 재작성 기준) */ -const REF_PREFIX = ".cursor/rules/designpaca"; +import type { TargetAdapter, TargetContext } from "../types.ts"; +import { anyExists, scopeRoot, skillDirActions } from "./common.ts"; /** - * Cursor — .cursor/rules/designpaca.mdc. - * .mdc 프론트매터는 SKILL.md 와 필드가 다르다(description/globs/alwaysApply)므로 변환한다. + * Cursor — ~/.cursor/skills/designpaca/ (전역) 또는 .cursor/skills/designpaca/ (프로젝트). * - * Windsurf 는 이 파일을 읽지 않는다(.windsurf/rules/*.md 를 쓴다) — 별도 어댑터로 분리했다. + * Cursor 는 Agent Skills 규약을 네이티브로 읽는다. 시작 시 자기 경로(.cursor/skills)와 + * 중립 경로(.agents/skills)를 살피고, 호환 경로(.claude/skills · .codex/skills)까지 함께 + * 읽는다 — 그래서 SKILL.md 를 변환 없이 그대로 복사하면 끝난다. + * + * 예전엔 .cursor/rules/designpaca.mdc 로 변환했지만 그 접근은 버렸다. 규칙 파일은 + * 프론트매터가 다르고 본문이 통째로 프롬프트에 들어가는 규약이라 설명만 띄우고 + * 본문·references 는 필요할 때 여는 스킬 방식이 맞다. 이제 전역 설치도 된다. */ export const cursor: TargetAdapter = { id: "cursor", label: "Cursor", - hint: ".cursor/rules/designpaca.mdc — 프로젝트 단위", - // Cursor 의 전역 규칙은 파일이 아니라 앱 설정(User Rules)이라 프로젝트 범위만 지원한다 - scopes: ["project"], + hint: "~/.cursor/skills/designpaca — SKILL.md 그대로 (전역 지원)", + scopes: ["user", "project"], async detect(ctx: TargetContext) { - return anyExists([path.join(ctx.cwd, ".cursor"), path.join(ctx.home, ".cursor")]); + return anyExists([path.join(ctx.home, ".cursor"), path.join(ctx.cwd, ".cursor")]); }, async plan(ctx: TargetContext) { - const rulesDir = path.join(ctx.cwd, ".cursor", "rules"); - const mdc = path.join(rulesDir, "designpaca.mdc"); - const refRoot = path.join(rulesDir, "designpaca"); - - const header = [ - "---", - `description: ${ctx.skill.description}`, - "globs:", - "alwaysApply: false", - "---", - "", - ].join("\n"); - - const actions: FileAction[] = [ - { - kind: "write", - path: mdc, - content: header + rewriteRefPaths(ctx.skill.body, REF_PREFIX), - }, - ...referenceActions(refRoot, ctx.skill), - ]; - + const root = scopeRoot( + ctx, + [".cursor", "skills", "designpaca"], + [".cursor", "skills", "designpaca"], + ); return { target: this.id, scope: ctx.scope, - root: rulesDir, - actions, - alreadyInstalled: await exists(mdc), + root, + actions: skillDirActions(root, ctx.skill), + alreadyInstalled: await exists(path.join(root, "SKILL.md")), }; }, }; diff --git a/packages/core/src/targets/gemini-cli.ts b/packages/core/src/targets/gemini-cli.ts new file mode 100644 index 0000000..9abaf26 --- /dev/null +++ b/packages/core/src/targets/gemini-cli.ts @@ -0,0 +1,41 @@ +import path from "node:path"; +import { exists } from "../fsx.ts"; +import type { TargetAdapter, TargetContext } from "../types.ts"; +import { anyExists, scopeRoot, skillDirActions } from "./common.ts"; + +/** + * Gemini CLI — ~/.gemini/skills/designpaca/ (전역) 또는 .gemini/skills/designpaca/ (프로젝트). + * Agent Skills 규약(SKILL.md)을 Claude 와 같은 형태로 그대로 읽는다. + * + * Google 을 Gemini CLI 에서 Antigravity 로 옮기고 있지만 마이그레이션이 끝난 사용자만 + * Antigravity 경로(~/.gemini/antigravity-cli/skills)를 쓴다 — 아직 Gemini CLI 를 쓰는 + * 사람을 위해 이 타깃을 따로 둔다. 둘 다 쓰면 스킬이 두 번 잡히니 하나만 고른다. + */ +export const geminiCli: TargetAdapter = { + id: "gemini-cli", + label: "Gemini CLI", + hint: "~/.gemini/skills/designpaca — SKILL.md 그대로 (Antigravity 전환 전)", + scopes: ["user", "project"], + + async detect(ctx: TargetContext) { + return anyExists([ + path.join(ctx.home, ".gemini"), + path.join(ctx.cwd, ".gemini"), + ]); + }, + + async plan(ctx: TargetContext) { + const root = scopeRoot( + ctx, + [".gemini", "skills", "designpaca"], + [".gemini", "skills", "designpaca"], + ); + return { + target: this.id, + scope: ctx.scope, + root, + actions: skillDirActions(root, ctx.skill), + alreadyInstalled: await exists(path.join(root, "SKILL.md")), + }; + }, +}; diff --git a/packages/core/src/targets/index.ts b/packages/core/src/targets/index.ts index b0531f0..b41fb7a 100644 --- a/packages/core/src/targets/index.ts +++ b/packages/core/src/targets/index.ts @@ -1,12 +1,27 @@ import type { TargetAdapter, TargetId } from "../types.ts"; import { claudeCode } from "./claude-code.ts"; import { codex } from "./codex.ts"; -import { antigravity } from "./antigravity.ts"; import { cursor } from "./cursor.ts"; +import { zcode } from "./zcode.ts"; +import { agents } from "./agents.ts"; +import { antigravity } from "./antigravity.ts"; +import { geminiCli } from "./gemini-cli.ts"; import { windsurf } from "./windsurf.ts"; +import { copilot } from "./copilot.ts"; import { agentsMd } from "./agents-md.ts"; -export const ADAPTERS: TargetAdapter[] = [claudeCode, codex, antigravity, cursor, windsurf, agentsMd]; +export const ADAPTERS: TargetAdapter[] = [ + claudeCode, + codex, + cursor, + zcode, + agents, + antigravity, + geminiCli, + windsurf, + copilot, + agentsMd, +]; export function getAdapter(id: TargetId): TargetAdapter { const a = ADAPTERS.find((x) => x.id === id); @@ -15,4 +30,15 @@ export function getAdapter(id: TargetId): TargetAdapter { } export { MARKER, rewriteRefPaths } from "./common.ts"; -export { claudeCode, codex, antigravity, cursor, windsurf, agentsMd }; +export { + claudeCode, + codex, + cursor, + zcode, + agents, + antigravity, + geminiCli, + windsurf, + copilot, + agentsMd, +}; diff --git a/packages/core/src/targets/windsurf.ts b/packages/core/src/targets/windsurf.ts index a4ca7e6..e92d7ca 100644 --- a/packages/core/src/targets/windsurf.ts +++ b/packages/core/src/targets/windsurf.ts @@ -1,66 +1,42 @@ import path from "node:path"; import { exists } from "../fsx.ts"; -import type { FileAction, TargetAdapter, TargetContext } from "../types.ts"; -import { anyExists, referenceActions, rewriteRefPaths } from "./common.ts"; - -const REF_PREFIX = ".windsurf/rules/designpaca"; - -/** Windsurf 규칙 파일의 하드 상한. 넘으면 잘려서 조용히 망가진다. */ -const WINDSURF_CHAR_LIMIT = 12_000; +import type { TargetAdapter, TargetContext } from "../types.ts"; +import { anyExists, scopeRoot, skillDirActions } from "./common.ts"; /** - * Windsurf — .windsurf/rules/designpaca.md. - * Cursor 와 경로·프론트매터가 모두 다르다(trigger/globs, .mdc 아님). + * Windsurf — ~/.codeium/windsurf/skills/designpaca/ (전역) 또는 .windsurf/skills/designpaca/ (프로젝트). + * Cascade 가 Agent Skills 규약(SKILL.md)을 네이티브로 읽는다. * - * trigger 는 model_decision 을 쓴다. always_on 으로 두면 디자인 스킬이 모든 메시지의 - * 시스템 프롬프트에 상주한다. + * 예전 .windsurf/rules/designpaca.md 방식은 규칙 파일에 12,000자 상한이 있어서 + * 25KB 짜리 실제 스킬이 아예 설치되지 못했다 — 네이티브 스킬 경로로 옮기며 풀렸다. + * 스킬은 name/description 만 띄우고 본문은 필요할 때 열기 때문에 크기 제약이 없다. */ export const windsurf: TargetAdapter = { id: "windsurf", label: "Windsurf", - hint: ".windsurf/rules/designpaca.md — 프로젝트 단위", - scopes: ["project"], + hint: "~/.codeium/windsurf/skills/designpaca — SKILL.md 그대로 (전역 지원)", + scopes: ["user", "project"], async detect(ctx: TargetContext) { return anyExists([ path.join(ctx.cwd, ".windsurf"), - path.join(ctx.cwd, ".windsurfrules"), path.join(ctx.home, ".windsurf"), + path.join(ctx.home, ".codeium"), ]); }, async plan(ctx: TargetContext) { - const rulesDir = path.join(ctx.cwd, ".windsurf", "rules"); - const rule = path.join(rulesDir, "designpaca.md"); - const refRoot = path.join(rulesDir, "designpaca"); - - const header = [ - "---", - "trigger: model_decision", - `description: ${ctx.skill.description}`, - "---", - "", - ].join("\n"); - - const body = header + rewriteRefPaths(ctx.skill.body, REF_PREFIX); - - const actions: FileAction[] = [ - { kind: "write", path: rule, content: body }, - ...referenceActions(refRoot, ctx.skill), - ]; - + const root = scopeRoot( + ctx, + [".codeium", "windsurf", "skills", "designpaca"], + [".windsurf", "skills", "designpaca"], + ); return { target: this.id, scope: ctx.scope, - root: rulesDir, - actions, - alreadyInstalled: await exists(rule), - // 상한을 넘으면 설치는 되지만 Windsurf 가 뒷부분을 버린다. 조용히 깨지느니 막는다. - ...(body.length > WINDSURF_CHAR_LIMIT - ? { - blocked: `규칙 본문이 ${body.length}자로 Windsurf 상한(${WINDSURF_CHAR_LIMIT}자)을 넘는다. SKILL.md 를 줄여야 한다.`, - } - : {}), + root, + actions: skillDirActions(root, ctx.skill), + alreadyInstalled: await exists(path.join(root, "SKILL.md")), }; }, }; diff --git a/packages/core/src/targets/zcode.ts b/packages/core/src/targets/zcode.ts new file mode 100644 index 0000000..d37c89d --- /dev/null +++ b/packages/core/src/targets/zcode.ts @@ -0,0 +1,37 @@ +import path from "node:path"; +import { exists } from "../fsx.ts"; +import type { TargetAdapter, TargetContext } from "../types.ts"; +import { anyExists, scopeRoot, skillDirActions } from "./common.ts"; + +/** + * ZCode — ~/.zcode/skills/designpaca/ (전역) 또는 .zcode/skills/designpaca/ (프로젝트). + * ZCode 도 Claude Code 와 같은 SKILL.md 규약을 그대로 읽으므로 변환이 없다. + * + * ZCode 는 중립 경로인 .agents/skills 도 함께 읽지만 그쪽은 agents 타깃이 쓴다. + * 같은 스킬을 두 곳에 깔면 하나가 다른 하나를 섀도잉할 뿐이므로 + * ZCode 전용 경로에만 쓴다. + */ +export const zcode: TargetAdapter = { + id: "zcode", + label: "ZCode", + hint: "~/.zcode/skills/designpaca — SKILL.md 그대로", + scopes: ["user", "project"], + + async detect(ctx: TargetContext) { + return anyExists([ + path.join(ctx.home, ".zcode"), + path.join(ctx.cwd, ".zcode"), + ]); + }, + + async plan(ctx: TargetContext) { + const root = scopeRoot(ctx, [".zcode", "skills", "designpaca"], [".zcode", "skills", "designpaca"]); + return { + target: this.id, + scope: ctx.scope, + root, + actions: skillDirActions(root, ctx.skill), + alreadyInstalled: await exists(path.join(root, "SKILL.md")), + }; + }, +}; diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index ee87fd3..38af630 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -2,9 +2,13 @@ export type TargetId = | "claude-code" | "codex" - | "antigravity" | "cursor" + | "zcode" + | "agents" + | "antigravity" + | "gemini-cli" | "windsurf" + | "copilot" | "agents-md"; /** 설치 범위 — user: 홈 디렉터리 전역, project: 현재 프로젝트 */ diff --git a/packages/core/test/installer.test.ts b/packages/core/test/installer.test.ts index 1c7d74c..7ed59e1 100644 --- a/packages/core/test/installer.test.ts +++ b/packages/core/test/installer.test.ts @@ -118,57 +118,49 @@ test("agents-md: 포인터만 주입하고 본문은 별도 디렉터리에 푼 assert.ok(!after2.includes("designpaca:start")); }); -test("cursor 는 user 범위를 거부한다", async () => { - const plan = await planInstall("cursor", "user", skill, env); - assert.ok(plan.blocked); - await assert.rejects(() => applyPlan(plan, skill.version, { env })); -}); +test("cursor: 네이티브 스킬 경로에 SKILL.md 그대로 싣는다 (전역·프로젝트)", async () => { + const user = await planInstall("cursor", "user", skill, env); + assert.equal(user.root, path.join(home, ".cursor", "skills", "designpaca")); + const proj = await planInstall("cursor", "project", skill, env); + assert.equal(proj.root, path.join(cwd, ".cursor", "skills", "designpaca")); -test("cursor: .mdc 프론트매터로 변환하고 참조 경로를 재작성한다", async () => { - const plan = await planInstall("cursor", "project", skill, env); - const res = await applyPlan(plan, skill.version, { env }); - - const mdc = await fs.readFile(path.join(cwd, ".cursor", "rules", "designpaca.mdc"), "utf8"); - assert.ok(mdc.startsWith("---\ndescription: 테스트용")); - assert.ok(mdc.includes("alwaysApply: false")); - assert.ok(mdc.includes("# 본문")); - // 본문과 references 가 다른 디렉터리에 놓이므로 경로가 재작성돼야 한다 - assert.ok( - mdc.includes(".cursor/rules/designpaca/references/tokens.md"), - "참조 경로가 재작성되지 않았다", - ); - await fs.access(path.join(cwd, ".cursor", "rules", "designpaca", "references", "tokens.md")); + const res = await applyPlan(proj, skill.version, { env }); + const md = await fs.readFile(path.join(proj.root, "SKILL.md"), "utf8"); + assert.match(md, /^---\n/, "프론트매터가 그대로 있어야 한다"); + assert.match(md, /`references\/tokens\.md`/, "참조 경로를 재작성하면 안 된다"); + await fs.access(path.join(proj.root, "references", "tokens.md")); await removeInstall(res.record, { env }); }); -test("windsurf: Cursor 와 다른 경로·프론트매터를 쓴다", async () => { - const plan = await planInstall("windsurf", "project", skill, env); - const res = await applyPlan(plan, skill.version, { env }); +test("windsurf: 네이티브 스킬 경로에 싣는다 — 본문 크기 제한이 없다", async () => { + const proj = await planInstall("windsurf", "project", skill, env); + assert.equal(proj.root, path.join(cwd, ".windsurf", "skills", "designpaca")); + const user = await planInstall("windsurf", "user", skill, env); + assert.equal(user.root, path.join(home, ".codeium", "windsurf", "skills", "designpaca")); - const rule = await fs.readFile(path.join(cwd, ".windsurf", "rules", "designpaca.md"), "utf8"); - // always_on 이면 디자인 스킬이 모든 메시지의 시스템 프롬프트에 상주한다 - assert.ok(rule.includes("trigger: model_decision")); - assert.ok(!rule.includes("alwaysApply")); - assert.ok(rule.includes(".windsurf/rules/designpaca/references/tokens.md")); - - await removeInstall(res.record, { env }); -}); - -test("windsurf: 12,000자 상한을 넘으면 설치를 막는다", async () => { + // 예전 규칙 파일 방식은 12,000자 상한 때문에 실제 스킬(본문 25KB)이 막혔다. + // 네이티브 스킬은 본문을 통째로 프롬프트에 넣지 않으므로 큰 본문도 설치돼야 한다. const huge: SkillSource = { ...skill, body: "가".repeat(13_000) }; - const plan = await planInstall("windsurf", "project", huge, env); - assert.ok(plan.blocked, "상한 초과인데 막지 않았다"); - assert.match(plan.blocked, /상한/); + const hugePlan = await planInstall("windsurf", "project", huge, env); + assert.ok(!hugePlan.blocked, "본문이 크다고 막히면 안 된다"); + + const res = await applyPlan(proj, skill.version, { env }); + const md = await fs.readFile(path.join(proj.root, "SKILL.md"), "utf8"); + assert.match(md, /`references\/tokens\.md`/, "참조 경로를 재작성하면 안 된다"); + await fs.access(path.join(proj.root, "references", "tokens.md")); + + await removeInstall(res.record, { env }); }); -test("antigravity: 프로젝트는 .agents/skills, 전역은 ~/.gemini/config/skills", async () => { +test("antigravity: 프로젝트는 .agents/skills, 전역은 ~/.gemini/antigravity-cli/skills", async () => { // 두 경로가 다른 제품 규약이라 하나만 맞으면 다른 쪽에서 스킬이 안 잡힌다. const proj = await planInstall("antigravity", "project", skill, env); assert.equal(proj.root, path.join(cwd, ".agents", "skills", "designpaca")); + // 전역은 공식 마이그레이션 문서가 정한 현재 경로다 (~/.gemini/config/skills 는 레거시). const user = await planInstall("antigravity", "user", skill, env); - assert.equal(user.root, path.join(home, ".gemini", "config", "skills", "designpaca")); + assert.equal(user.root, path.join(home, ".gemini", "antigravity-cli", "skills", "designpaca")); }); test("antigravity: SKILL.md 를 변환 없이 그대로 싣는다", async () => { @@ -187,3 +179,57 @@ test("antigravity: SKILL.md 를 변환 없이 그대로 싣는다", async () => await removeInstall(res.record, { env }); }); + +test("agents: 중립 경로 — 전역 ~/.agents/skills, 프로젝트 .agents/skills", async () => { + const user = await planInstall("agents", "user", skill, env); + assert.equal(user.root, path.join(home, ".agents", "skills", "designpaca")); + const proj = await planInstall("agents", "project", skill, env); + assert.equal(proj.root, path.join(cwd, ".agents", "skills", "designpaca")); + + const res = await applyPlan(proj, skill.version, { env }); + const md = await fs.readFile(path.join(proj.root, "SKILL.md"), "utf8"); + assert.match(md, /^---\n/, "프론트매터가 있어야 한다"); + assert.match(md, /`references\/tokens\.md`/, "참조 경로를 재작성하면 안 된다"); + await fs.access(path.join(proj.root, "references", "tokens.md")); + + await removeInstall(res.record, { env }); + await assert.rejects(() => fs.access(proj.root)); +}); + +test("gemini-cli: 전역 ~/.gemini/skills, 프로젝트 .gemini/skills", async () => { + const user = await planInstall("gemini-cli", "user", skill, env); + assert.equal(user.root, path.join(home, ".gemini", "skills", "designpaca")); + const proj = await planInstall("gemini-cli", "project", skill, env); + assert.equal(proj.root, path.join(cwd, ".gemini", "skills", "designpaca")); +}); + +test("copilot: 전역 ~/.copilot/skills, 프로젝트 .github/skills", async () => { + const user = await planInstall("copilot", "user", skill, env); + assert.equal(user.root, path.join(home, ".copilot", "skills", "designpaca")); + const proj = await planInstall("copilot", "project", skill, env); + assert.equal(proj.root, path.join(cwd, ".github", "skills", "designpaca")); +}); + +test("zcode: 전역은 ~/.zcode/skills, 프로젝트는 .zcode/skills", async () => { + // ZCode 는 .zcode/skills 와 .agents/skills 를 모두 읽지만 중립 경로 쪽은 + // agents 타깃이 쓰는 영역이라 zcode 타깃은 건드리지 않는다. + const user = await planInstall("zcode", "user", skill, env); + assert.equal(user.root, path.join(home, ".zcode", "skills", "designpaca")); + + const proj = await planInstall("zcode", "project", skill, env); + assert.equal(proj.root, path.join(cwd, ".zcode", "skills", "designpaca")); +}); + +test("zcode: SKILL.md 를 변환 없이 그대로 싣는다", async () => { + const plan = await planInstall("zcode", "user", skill, env); + const res = await applyPlan(plan, skill.version, { env }); + + const md = await fs.readFile(path.join(plan.root, "SKILL.md"), "utf8"); + assert.match(md, /^---\n/, "프론트매터가 있어야 한다"); + assert.match(md, /`references\/tokens\.md`/, "참조 경로를 재작성하면 안 된다"); + await fs.access(path.join(plan.root, "references", "tokens.md")); + await fs.access(path.join(plan.root, ".designpaca_version")); + + await removeInstall(res.record, { env }); + await assert.rejects(() => fs.access(plan.root)); +});