feat: 브리프 인터뷰 단계와 브라우저 도구를 넣고 온보딩 말투를 고친다
All checks were successful
ci / build (push) Successful in 25s
release / release (push) Successful in 46s

세 가지를 고친다. 전부 실제 사용에서 드러난 것이다.

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개).
This commit is contained in:
Yun Chan 2026-08-21 03:05:06 +09:00
parent eb9f61e907
commit 4e1e5f0073
15 changed files with 486 additions and 39 deletions

View file

@ -1,6 +1,6 @@
{
"name": "@designpaca/core",
"version": "0.1.0",
"version": "0.2.0",
"private": true,
"description": "designpaca 설치 엔진 — 타깃 어댑터, 매니페스트, 드리프트 감지",
"type": "module",

View file

@ -5,4 +5,5 @@ export * from "./marker.ts";
export * from "./manifest.ts";
export * from "./skill-source.ts";
export * from "./installer.ts";
export * from "./tools.ts";
export { ADAPTERS, getAdapter, MARKER } from "./targets/index.ts";

129
packages/core/src/tools.ts Normal file
View file

@ -0,0 +1,129 @@
/**
* .
*
* . (403)
* SPA , .
* .
* headed .
*
* ** **. .
*/
import { execFile } from "node:child_process";
import { promisify } from "node:util";
import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
const exec = promisify(execFile);
export interface OptionalTool {
id: string;
label: string;
/** 이게 없으면 무엇을 못 하는가 — 고르는 사람이 판단할 수 있게 */
why: string;
/** 설치 크기 어림값. 수백 MB 를 말없이 받게 하지 않는다 */
size: string;
detect(): Promise<boolean>;
install(): Promise<void>;
/** 설치 후 사람이 직접 해야 하는 것이 있으면 여기에 */
followUp?: string;
}
/** npm 전역 루트. Windows 경로에 공백이 있어도 안전하게 얻는다 */
async function npmRoot(): Promise<string | null> {
try {
const { stdout } = await exec("npm", ["root", "-g"], { shell: true });
return stdout.trim() || null;
} catch {
return null;
}
}
async function exists(p: string): Promise<boolean> {
try {
await fs.access(p);
return true;
} catch {
return false;
}
}
/**
* Playwright .
*
* .
* .
*/
const playwright: OptionalTool = {
id: "playwright",
label: "Playwright (headed 브라우저)",
why: "봇 차단(403)·SPA 갤러리를 직접 열어 스크린샷과 실측값을 받는다. 5단계 실렌더 검사에도 쓴다",
size: "약 180MB (크로미움 포함)",
async detect() {
const root = await npmRoot();
if (!root || !(await exists(path.join(root, "playwright")))) return false;
// 브라우저 바이너리까지 확인한다 — 라이브러리만 있으면 첫 실행에서 실패한다
const cacheDir =
process.platform === "win32"
? path.join(os.homedir(), "AppData", "Local", "ms-playwright")
: process.platform === "darwin"
? path.join(os.homedir(), "Library", "Caches", "ms-playwright")
: path.join(os.homedir(), ".cache", "ms-playwright");
try {
const entries = await fs.readdir(cacheDir);
return entries.some((e) => e.startsWith("chromium"));
} catch {
return false;
}
},
async install() {
await exec("npm", ["install", "-g", "playwright"], { shell: true, maxBuffer: 1 << 24 });
await exec("npx", ["--yes", "playwright", "install", "chromium"], {
shell: true,
maxBuffer: 1 << 24,
});
},
};
/**
* Playwright MCP Claude Code .
*
* ** .** MCP
* , . .
*/
const playwrightMcp: OptionalTool = {
id: "playwright-mcp",
label: "Playwright MCP 서버",
why: "에이전트가 브라우저를 직접 조작한다. 스크립트를 매번 쓰지 않아도 된다",
size: "약 2MB",
followUp: "claude mcp add playwright -- npx @playwright/mcp@latest",
async detect() {
const root = await npmRoot();
if (!root) return false;
return exists(path.join(root, "@playwright", "mcp"));
},
async install() {
await exec("npm", ["install", "-g", "@playwright/mcp"], { shell: true, maxBuffer: 1 << 24 });
},
};
export const OPTIONAL_TOOLS: OptionalTool[] = [playwright, playwrightMcp];
export interface ToolStatus {
tool: OptionalTool;
installed: boolean;
}
export async function checkTools(): Promise<ToolStatus[]> {
return Promise.all(
OPTIONAL_TOOLS.map(async (tool) => ({ tool, installed: await tool.detect() })),
);
}
export function getTool(id: string): OptionalTool | undefined {
return OPTIONAL_TOOLS.find((t) => t.id === id);
}