세 가지를 고친다. 전부 실제 사용에서 드러난 것이다.
1) 0단계에서 가정으로 채우던 것을 인터뷰로 바꾼다
"꽃집 사이트 만들어보자"를 받고 업종 성격·목표 행동·톤·이름을 혼자 정했다.
물어보니 넷 중 넷이 달랐다(일상 구독 → 하이엔드 스튜디오, 문의 하나 → 넷 다,
톤 미정 → 에디토리얼, 이름 지어냄 → 목요일의 화원).
그대로 갔으면 레퍼런스 세 개를 전부 틀린 방향에서 골랐다.
SKILL.md 0단계에 질문 도구로 한 번에 묻는 절차를 넣었다. 무엇을 묻고
무엇을 묻지 않는지, 답이 모순될 때 어떻게 정리하는지까지 적었다.
2) 레퍼런스가 막히면 브라우저를 띄운다
Aesop(403) → Kinto(404) → Hasami(DNS) → MUJI(타임아웃)로 네 번 왕복하고
톤 레퍼런스를 하나도 못 얻었다. 좋은 레퍼런스일수록 봇을 막는다.
galleries.md 에 headed 브라우저로 직접 열어 스크린샷과 실측값을 받는
방법을 넣고, 접근 실패 2회면 바로 전환하도록 규칙을 세웠다.
Playwright 를 선택 의존성으로 잡았다(core/tools.ts).
- 온보딩 마지막에 설치 여부를 묻는다. 건너뛰어도 스킬은 동작한다
- `designpaca tools` 로 상태 확인, `--yes` 로 설치
- 수백 MB 라 --yes 없이는 상태만 보여준다
- MCP 서버는 설치만 하고 등록 명령은 안내만 한다(에이전트 설정을
대신 건드리지 않는다)
3) 온보딩 말투
"이제 브리프를 던져라", "설치해라", "건너뛴다" — 사용자를 향한 문구가
명령조였다. 스킬 문서의 단정한 반말은 의도지만 CLI UI 는 다르다.
전부 존댓말로 바꾸고, 각 단계가 왜 필요한지 설명을 붙였다.
명령조가 다시 섞이지 않도록 검사하는 테스트를 넣었다(테스트 22 → 25개).
184 lines
6.1 KiB
TypeScript
184 lines
6.1 KiB
TypeScript
import { ADAPTERS, type Scope, type TargetId } from "@designpaca/core";
|
|
import { getVersion } from "./skill.ts";
|
|
import { checkForUpdate } from "./update-check.ts";
|
|
import { printNextSteps, printOutcomes, runInstall } from "./commands/install.ts";
|
|
import { runDoctor } from "./commands/doctor.ts";
|
|
import { runTools } from "./commands/tools.ts";
|
|
import { runUpdate } from "./commands/update.ts";
|
|
import { runUninstall } from "./commands/uninstall.ts";
|
|
import { runList } from "./commands/list.ts";
|
|
import { onboard } from "./tui/onboard.ts";
|
|
import { accent, bad, bold, dim, warn } from "./ui.ts";
|
|
|
|
const VALID_TARGETS = ADAPTERS.map((a) => a.id) as string[];
|
|
|
|
interface Args {
|
|
command: string;
|
|
targets: TargetId[];
|
|
scope: Scope;
|
|
yes: boolean;
|
|
force: boolean;
|
|
dryRun: boolean;
|
|
help: boolean;
|
|
version: boolean;
|
|
}
|
|
|
|
function parse(argv: string[]): Args {
|
|
const out: Args = {
|
|
command: "",
|
|
targets: [],
|
|
scope: "user",
|
|
yes: false,
|
|
force: false,
|
|
dryRun: false,
|
|
help: false,
|
|
version: false,
|
|
};
|
|
|
|
for (let i = 0; i < argv.length; i++) {
|
|
const a = argv[i] as string;
|
|
if (a === "--help" || a === "-h") out.help = true;
|
|
else if (a === "--version" || a === "-v") out.version = true;
|
|
else if (a === "--yes" || a === "-y") out.yes = true;
|
|
else if (a === "--force" || a === "-f") out.force = true;
|
|
else if (a === "--dry-run") out.dryRun = true;
|
|
else if (a === "--no-update-check") process.env["DESIGNPACA_NO_UPDATE_CHECK"] = "1";
|
|
else if (a === "--target" || a === "-t") {
|
|
const v = argv[++i];
|
|
if (v) out.targets.push(...(v.split(",") as TargetId[]));
|
|
} else if (a.startsWith("--target=")) {
|
|
out.targets.push(...(a.slice(9).split(",") as TargetId[]));
|
|
} else if (a === "--scope" || a === "-s") {
|
|
const v = argv[++i];
|
|
if (v) out.scope = v as Scope;
|
|
} else if (a.startsWith("--scope=")) {
|
|
out.scope = a.slice(8) as Scope;
|
|
} else if (!a.startsWith("-") && !out.command) {
|
|
out.command = a;
|
|
}
|
|
}
|
|
return out;
|
|
}
|
|
|
|
function usage(version: string): string {
|
|
return `
|
|
${accent("designpaca")} ${dim(`v${version}`)} — 웹 디자인 파이프라인 스킬 설치기
|
|
|
|
${bold("사용법")}
|
|
npx designpaca 대화형 온보딩 (권장)
|
|
npx designpaca install 설치
|
|
npx designpaca update 최신 스킬로 갱신
|
|
npx designpaca uninstall 제거
|
|
npx designpaca doctor 설치 상태 진단
|
|
npx designpaca list 설치 가능 대상과 현재 설치 목록
|
|
npx designpaca tools 브라우저 도구 상태 확인 ${dim("(--yes 로 설치)")}
|
|
|
|
${bold("옵션")}
|
|
-t, --target <id[,id]> 설치 대상: ${VALID_TARGETS.join(", ")}
|
|
-s, --scope <범위> user | project ${dim("(기본: user)")}
|
|
-y, --yes 확인 없이 진행합니다
|
|
-f, --force 직접 수정한 파일도 덮어씁니다 ${dim("(.orig 로 백업)")}
|
|
--dry-run 쓰지 않고 계획만 보여줍니다
|
|
--no-update-check 새 버전 확인을 건너뜁니다
|
|
-h, --help 이 도움말
|
|
-v, --version 버전
|
|
|
|
${bold("예시")}
|
|
${dim("npx designpaca install -t claude-code,codex -s user -y")}
|
|
${dim("npx designpaca install -t cursor -s project")}
|
|
${dim("npx designpaca update --force")}
|
|
${dim("npx designpaca tools --yes")}
|
|
`.trimStart();
|
|
}
|
|
|
|
async function main(): Promise<number> {
|
|
const args = parse(process.argv.slice(2));
|
|
const version = await getVersion();
|
|
|
|
if (args.version) {
|
|
console.log(version);
|
|
return 0;
|
|
}
|
|
if (args.help) {
|
|
console.log(usage(version));
|
|
return 0;
|
|
}
|
|
|
|
const bad_ = args.targets.filter((t) => !VALID_TARGETS.includes(t));
|
|
if (bad_.length > 0) {
|
|
console.error(bad(`알 수 없는 대상: ${bad_.join(", ")}`));
|
|
console.error(dim(` 가능한 값: ${VALID_TARGETS.join(", ")}`));
|
|
return 2;
|
|
}
|
|
if (args.scope !== "user" && args.scope !== "project") {
|
|
console.error(bad(`알 수 없는 범위: ${args.scope} (user | project)`));
|
|
return 2;
|
|
}
|
|
|
|
// 대화형으로 쓸 수 있는 환경이면 인자 없이 실행했을 때 온보딩으로 보낸다
|
|
const interactive = process.stdout.isTTY && !args.yes;
|
|
if (!args.command || args.command === "onboard") {
|
|
if (interactive) return onboard();
|
|
console.log(usage(version));
|
|
return 0;
|
|
}
|
|
|
|
let code = 0;
|
|
switch (args.command) {
|
|
case "install": {
|
|
const targets = args.targets.length > 0 ? args.targets : (["claude-code"] as TargetId[]);
|
|
if (interactive && args.targets.length === 0) return onboard();
|
|
const outcomes = await runInstall({
|
|
targets,
|
|
scope: args.scope,
|
|
force: args.force,
|
|
dryRun: args.dryRun,
|
|
});
|
|
printOutcomes(outcomes, args.dryRun);
|
|
if (!args.dryRun) printNextSteps(targets);
|
|
code = outcomes.some((o) => o.blocked) ? 1 : 0;
|
|
break;
|
|
}
|
|
case "update":
|
|
code = await runUpdate({ force: args.force });
|
|
break;
|
|
case "uninstall":
|
|
case "remove":
|
|
code = await runUninstall({ targets: args.targets, force: args.force });
|
|
break;
|
|
case "doctor":
|
|
code = await runDoctor();
|
|
break;
|
|
case "tools":
|
|
// 수백 MB 를 받는 일이라 --yes 없이는 상태만 보여준다
|
|
code = await runTools({ install: args.yes });
|
|
break;
|
|
case "list":
|
|
case "ls":
|
|
code = await runList();
|
|
break;
|
|
default:
|
|
console.error(bad(`알 수 없는 명령입니다: ${args.command}`));
|
|
console.log(usage(version));
|
|
return 2;
|
|
}
|
|
|
|
// 명령을 마친 뒤에만 알린다 — 시작을 지연시키지 않는다
|
|
const latest = await checkForUpdate(version);
|
|
if (latest) {
|
|
console.log(
|
|
"\n" + warn(`새 버전 v${latest} 이 나왔습니다 — ${dim("npx designpaca@latest update")}`),
|
|
);
|
|
}
|
|
return code;
|
|
}
|
|
|
|
main()
|
|
.then((code) => {
|
|
process.exitCode = code;
|
|
})
|
|
.catch((err: unknown) => {
|
|
console.error(bad(err instanceof Error ? err.message : String(err)));
|
|
if (process.env["DESIGNPACA_DEBUG"] === "1" && err instanceof Error) console.error(err.stack);
|
|
process.exitCode = 1;
|
|
});
|