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,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` 로 바로잡는다.

View file

@ -0,0 +1,6 @@
---
"designpaca": minor
"@designpaca/core": minor
---
ZCode 타깃 추가 — `~/.zcode/skills/designpaca/` (전역) 또는 `.zcode/skills/designpaca/` (프로젝트) 에 스킬을 설치한다. SKILL.md 규약이 Claude Code 와 같아 변환 없이 그대로 복사된다.

View file

@ -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 <id[,id]>` | `claude-code` `codex` `cursor` `agents-md` |
| `-t, --target <id[,id]>` | `claude-code` `codex` `cursor` `zcode` `agents` `antigravity` `gemini-cli` `windsurf` `copilot` `agents-md` |
| `-s, --scope <범위>` | `user` (전역) \| `project` |
| `-y, --yes` | 확인 없이 진행 |
| `-f, --force` | 직접 수정한 파일도 덮어쓴다 (`.orig` 백업) |

View file

@ -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(

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

View file

@ -2,9 +2,13 @@
export type TargetId =
| "claude-code"
| "codex"
| "antigravity"
| "cursor"
| "zcode"
| "agents"
| "antigravity"
| "gemini-cli"
| "windsurf"
| "copilot"
| "agents-md";
/** 설치 범위 — user: 홈 디렉터리 전역, project: 현재 프로젝트 */

View file

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