designpaca/packages/cli/src/index.ts
Yun Chan 4e1e5f0073
All checks were successful
ci / build (push) Successful in 25s
release / release (push) Successful in 46s
feat: 브리프 인터뷰 단계와 브라우저 도구를 넣고 온보딩 말투를 고친다
세 가지를 고친다. 전부 실제 사용에서 드러난 것이다.

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개).
2026-08-21 03:05:06 +09:00

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