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개).
This commit is contained in:
parent
eb9f61e907
commit
4e1e5f0073
15 changed files with 486 additions and 39 deletions
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@designpaca/core",
|
||||
"version": "0.1.0",
|
||||
"version": "0.2.0",
|
||||
"private": true,
|
||||
"description": "designpaca 설치 엔진 — 타깃 어댑터, 매니페스트, 드리프트 감지",
|
||||
"type": "module",
|
||||
|
|
|
|||
|
|
@ -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
129
packages/core/src/tools.ts
Normal 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);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue