feat(core): 어떤 하네스에서든 스킬을 발견하는 타깃 체계로 바꾼다
Some checks are pending
ci / build (push) Waiting to run

- 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 로 수정
This commit is contained in:
Yun Chan 2026-08-21 12:24:34 +09:00
parent 30dc6687dd
commit 8d02820fa5
14 changed files with 379 additions and 136 deletions

View file

@ -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 가 프로젝트와 홈에서 이 경로를 읽고, 같은 규약을 채택하는
* 하네스가 계속 늘고 있다. 어떤 도구를 쓰는지와 무관하게 **한 번 깔면 다 같이 잡는 자리**다.
*
* 경로:
* 워크스페이스 <root>/.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")),
};
},
};

View file

@ -7,17 +7,17 @@ import { anyExists, scopeRoot, skillDirActions } from "./common.ts";
* Antigravity 와 `.agents/skills` 를 읽는 도구들.
*
* `.agents/skills/` 는 특정 제품의 경로가 아니라 **중립 경로**다.
* Antigravity 가 여기서 스킬을 읽고, 같은 규약을 쓰는 다른 하네스도 늘고 있다.
* Antigravity 가 여기서 스킬을 읽고, 같은 규약을 쓰는 다른 하네스도 늘고 있다
* (중립 경로 단독 설치는 `agents` 타깃이 담당한다).
* SKILL.md 구조(프론트매터 + references/)가 Claude Code 와 같아서
* 파일을 그대로 복사하면 된다 — 변환이 필요 없다.
*
* 경로(공식 문서 기준):
* 워크스페이스 <root>/.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 {

View file

@ -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")),
};
},
};

View file

@ -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")),
};
},
};

View file

@ -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")),
};
},
};

View file

@ -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,
};

View file

@ -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")),
};
},
};

View file

@ -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")),
};
},
};