- 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:
parent
30dc6687dd
commit
8d02820fa5
14 changed files with 379 additions and 136 deletions
41
packages/core/src/targets/agents.ts
Normal file
41
packages/core/src/targets/agents.ts
Normal 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")),
|
||||
};
|
||||
},
|
||||
};
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
42
packages/core/src/targets/copilot.ts
Normal file
42
packages/core/src/targets/copilot.ts
Normal 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")),
|
||||
};
|
||||
},
|
||||
};
|
||||
|
|
@ -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")),
|
||||
};
|
||||
},
|
||||
};
|
||||
|
|
|
|||
41
packages/core/src/targets/gemini-cli.ts
Normal file
41
packages/core/src/targets/gemini-cli.ts
Normal 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")),
|
||||
};
|
||||
},
|
||||
};
|
||||
|
|
@ -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,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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")),
|
||||
};
|
||||
},
|
||||
};
|
||||
|
|
|
|||
37
packages/core/src/targets/zcode.ts
Normal file
37
packages/core/src/targets/zcode.ts
Normal 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")),
|
||||
};
|
||||
},
|
||||
};
|
||||
|
|
@ -2,9 +2,13 @@
|
|||
export type TargetId =
|
||||
| "claude-code"
|
||||
| "codex"
|
||||
| "antigravity"
|
||||
| "cursor"
|
||||
| "zcode"
|
||||
| "agents"
|
||||
| "antigravity"
|
||||
| "gemini-cli"
|
||||
| "windsurf"
|
||||
| "copilot"
|
||||
| "agents-md";
|
||||
|
||||
/** 설치 범위 — user: 홈 디렉터리 전역, project: 현재 프로젝트 */
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue