@ahnseungchan/device-farm 0.0.0-stage → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/LICENSE +21 -0
  3. package/README.md +297 -2
  4. package/dist/bin.js +79 -0
  5. package/dist/chaos-CIB7G5X5.js +453 -0
  6. package/dist/chunk-3GCH6TN3.js +86 -0
  7. package/dist/chunk-4UOXXMXL.js +74 -0
  8. package/dist/chunk-7OJDY276.js +16 -0
  9. package/dist/chunk-AVECSGEH.js +1462 -0
  10. package/dist/chunk-AXQNUPGC.js +416 -0
  11. package/dist/chunk-F4TMB4WY.js +54 -0
  12. package/dist/chunk-F6PZOBU6.js +167 -0
  13. package/dist/chunk-JD2L6T7E.js +56 -0
  14. package/dist/chunk-JZDYF3HL.js +169 -0
  15. package/dist/chunk-NJNTEWXE.js +134 -0
  16. package/dist/chunk-NLX3V7ZJ.js +134 -0
  17. package/dist/chunk-OUNLQI2N.js +34 -0
  18. package/dist/chunk-P2M55S53.js +110 -0
  19. package/dist/chunk-PLMSQ5KF.js +173 -0
  20. package/dist/chunk-PYZWMVJ6.js +302 -0
  21. package/dist/chunk-PZZQWRSU.js +283 -0
  22. package/dist/chunk-Q6CXF3NJ.js +102 -0
  23. package/dist/chunk-S6GHOP4B.js +111 -0
  24. package/dist/chunk-SWW42KTA.js +223 -0
  25. package/dist/chunk-UAHWEUDI.js +68 -0
  26. package/dist/chunk-VGPCTMUA.js +258 -0
  27. package/dist/chunk-WTTCWFOX.js +161 -0
  28. package/dist/chunk-ZE2GGXFO.js +169 -0
  29. package/dist/device-explore-PHS6SVEJ.js +64 -0
  30. package/dist/device-run-XPNRYXEL.js +60 -0
  31. package/dist/explore-allowlist-F44YQDVK.js +44 -0
  32. package/dist/index.d.ts +192 -0
  33. package/dist/index.js +24 -0
  34. package/dist/init-QFFEOJWY.js +29 -0
  35. package/dist/policy-check-AUWGFNHZ.js +75 -0
  36. package/dist/policy-watch-QPHI2P7Z.js +66 -0
  37. package/dist/qa-run-5LLQMLAM.js +59 -0
  38. package/dist/record-EGHVQFR7.js +152 -0
  39. package/dist/serve-XT32MAEN.js +1687 -0
  40. package/dist/server-3NM7ODUG.js +251 -0
  41. package/dist/templates/env.example +48 -0
  42. package/dist/templates/farm.config.ts +104 -0
  43. package/dist/web/assets/index-BCuSKs27.js +17 -0
  44. package/dist/web/assets/index-C3is0x-E.css +1 -0
  45. package/dist/web/favicon.svg +1 -0
  46. package/dist/web/index.html +14 -0
  47. package/package.json +95 -4
package/CHANGELOG.md ADDED
@@ -0,0 +1,27 @@
1
+ # Changelog
2
+
3
+ `@ahnseungchan/device-farm`의 바뀐 점을 적는다.
4
+
5
+ ## [0.1.1] - 2026-11-11
6
+
7
+ ### Docs
8
+
9
+ - README를 WebViewKit 형식으로 개편: 가운데 정렬 로고·배지·바로가기, 구조 다이어그램(SVG), Why 비교표, 긴 내용은 접기(`<details>`), Contributing
10
+ - 로고·구조 다이어그램을 `docs/assets/`에 추가(npm에서도 보이도록 GitHub 절대 URL로 링크, 패키지에는 넣지 않음)
11
+
12
+ ## [0.1.0] - 2026-11-11
13
+
14
+ ### Added
15
+
16
+ - npm 패키지로 첫 배포. `npm i -D @ahnseungchan/device-farm` 후 `npx device-farm <명령>`으로 쓴다
17
+ - `init`: 작업 폴더에 `farm.config.ts`·`.env.example` 생성(있으면 건너뜀)
18
+ - `dev [--port]`: 미리 빌드한 화면을 vite preview로 열고 기기 API·스트림을 붙인다
19
+ - `check`·`explore`·`chaos`·`record`·`qa`·`policy-watch`·`policy-check`·`explore-allowlist`: 기존 CLI
20
+ - `mcp`: Claude Code용 MCP 서버(stdio). 결과·`.env`·설정은 작업 폴더 기준
21
+ - 패키지 루트에서 `defineAppConfig`·`AppConfigInput`·`AppConfig` 타입과 데이터 QA 도우미(`equalWon`·`formatWon`·`numberAt`·`textHas`·`pick`)를 내보낸다
22
+ - 빌드: tsup으로 워크스페이스를 `dist/`에 묶고, 화면은 `vite build`, 설정 타입은 `dist/index.d.ts` 하나
23
+
24
+ ### Changed
25
+
26
+ - `explore/allowlist.json`·`explore/form-values.json`을 작업 폴더에서 읽는다. 없으면 빈 허용 목록(모든 쓰기 요청 차단)·빈 입력값
27
+ - MCP 서버는 `FARM_ROOT`가 있으면 그 폴더를 결과·`.env` 기준으로 쓴다
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Ahnseungc
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,298 @@
1
- # Temporary Holding Version
1
+ <p align="center">
2
+ <img src="https://raw.githubusercontent.com/Ahnseungc/device-farm/main/docs/assets/logo.svg?sanitize=true" width="96" alt="device-farm 로고: 초록 바탕의 폰에서 새싹이 자란다" />
3
+ </p>
2
4
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
5
+ <h1 align="center">device-farm</h1>
6
+
7
+ <p align="center">
8
+ <b>WebView로 만든 모바일 앱을 실기기에서 QA 자동화하는 디바이스팜.</b><br />
9
+ 내 프로젝트에 설치하고 <code>farm.config.ts</code> 하나로 점검할 앱을 정한다.
10
+ </p>
11
+
12
+ <p align="center">
13
+ <a href="https://www.npmjs.com/package/@ahnseungchan/device-farm"><img src="https://img.shields.io/npm/v/@ahnseungchan/device-farm" alt="npm" /></a>
14
+ <a href="https://github.com/Ahnseungc/device-farm/blob/main/LICENSE"><img src="https://img.shields.io/badge/license-MIT-blue" alt="license" /></a>
15
+ <a href="https://nodejs.org/"><img src="https://img.shields.io/node/v/@ahnseungchan/device-farm" alt="node" /></a>
16
+ </p>
17
+
18
+ <p align="center">
19
+ <a href="#설치">설치</a> ·
20
+ <a href="#quick-start">Quick start</a> ·
21
+ <a href="#cli">CLI</a> ·
22
+ <a href="#claude-code-mcp">Claude Code</a> ·
23
+ <a href="#안전">안전</a> ·
24
+ <a href="https://github.com/Ahnseungc/device-farm/blob/main/CHANGELOG.md">Changelog</a>
25
+ </p>
26
+
27
+ <p align="center">
28
+ <img src="https://raw.githubusercontent.com/Ahnseungc/device-farm/main/docs/assets/architecture.svg?sanitize=true" width="760" alt="device-farm 구조: farm.config.ts를 읽은 웹 화면·CLI·MCP가 Android(adb+CDP)·iOS(Appium) 기기를 조작하고 결과를 test-results에 남긴다" />
29
+ </p>
30
+
31
+ ## Why
32
+
33
+ 손으로 하는 QA는 기기 수와 화면 수에 비례해 늘어납니다. device-farm은 그 반복을 실기기 위에서 자동으로 돌립니다.
34
+
35
+ | 손으로 하는 QA | device-farm |
36
+ | ------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------- |
37
+ | 새 빌드가 나오면 기기마다 깔고 화면을 하나씩 열어 본다 | Firebase App Distribution·TestFlight 새 빌드를 받아 연결된 기기에 깔고, 고정 주소 화면을 딥링크로 열어 기준 화면과 비교 |
38
+ | 버튼을 다 눌러 봤는지 기억에 의존한다 | 화면의 버튼을 하나씩 눌러 반응(이동·팝업·반응 없음·오류)을 남기고, 결과가 **화면 지도**가 된다 |
39
+ | 시나리오 테스트를 코드로 짜고 고친다 | 사람이 기기를 움직이면 녹화해 플로우 후보로 올리고, 확정한 것만 돈다 |
40
+ | 뒤로·연타·백그라운드 같은 엣지 케이스는 운에 맡긴다 | 시드 고정 카오스 탐색으로 재현 가능한 무작위 조작 |
41
+ | 정책 문서·피그마 문구와 화면을 눈으로 대조한다 | Confluence 정책·피그마 문구를 화면 글자와 맞춰 본다 |
42
+ | 탐색 자동화가 실제 결제·탈퇴를 누를까 무섭다 | 쓰기 요청 가드와 금지 버튼 목록. 가드를 못 걸면 시작하지 않는다 |
43
+
44
+ 점검할 앱은 코드에 없습니다. 패키지 id·웹 주소·하단 탭·금지어·시나리오처럼 앱마다 다른 값은 전부 `farm.config.ts`로 **주입**합니다.
45
+ Android는 adb + Chrome DevTools Protocol(Playwright), iOS는 Appium(XCUITest·WebDriverAgent)으로 앱 안 WebView를 직접 조작합니다.
46
+
47
+ ## Features
48
+
49
+ - **빌드 점검**: 새 빌드 감시 → 설치 → 고정 주소 화면 점검. 앱 오류 문구·로그인 튕김·빈 화면·기준 화면 픽셀 비교로 판정.
50
+ - **버튼 눌러보기·화면 지도**: 버튼별 반응을 기록하고 화면 간 이동을 지도로 쌓는다. 지도 블록을 이어 붙여 플로우를 만든다.
51
+ - **플로우**: 홈에서 버튼·링크만 눌러 가는 시나리오. 녹화, 화면 지도 조립, PRD로 LLM 후보 생성(선택).
52
+ - **데이터 QA**: 화면이 부르는 GET API를 다시 불러 합계·표시 값을 규칙으로 맞춰 본다(LLM 없음).
53
+ - **탐색·카오스 탐색**: 너비 우선 탐색과 시드 고정 카오스(뒤로·연타·로딩 중 뒤로·백그라운드·재시작).
54
+ - **정책·피그마 대조**: Confluence 정책 바뀜 감지, 규칙 뽑기, 피그마 문구 대조.
55
+ - **원격 조작**: 웹 화면에서 기기 화면을 보며(Android H.264, iOS MJPEG) 직접 누른다. 조작 중인 기기는 잠근다.
56
+ - **Claude Code MCP**: Claude가 MCP 도구로 기기를 탐색하고, 찾은 경로를 플로우 후보로 올린다.
57
+
58
+ ## 설치
59
+
60
+ ```bash
61
+ npm install -D @ahnseungchan/device-farm
62
+ ```
63
+
64
+ | 준비물 | 언제 필요 |
65
+ | -------------------------------------------------------------------------- | -------------------------- |
66
+ | macOS, Node.js 22 이상 | 항상 |
67
+ | `adb`(Android platform-tools), USB 디버깅을 켠 기기 | Android |
68
+ | WebView 디버깅을 켠 dev 빌드: `WebView.setWebContentsDebuggingEnabled(true)` | Android |
69
+ | Xcode, Appium + XCUITest 드라이버(WebDriverAgent), `isInspectable = true`인 빌드 | iOS |
70
+ | Firebase 서비스 계정, App Store Connect API 키 | 새 빌드 자동 받기(선택) |
71
+ | Confluence API 토큰, Figma 토큰 | 정책·피그마 대조(선택) |
72
+
73
+ 호스트 맥·기기 준비: [docs/operations.md](https://github.com/Ahnseungc/device-farm/blob/main/docs/operations.md)
74
+
75
+ <details>
76
+ <summary><b>점검할 앱이 맞춰야 할 것</b></summary>
77
+
78
+ 1. **WebView 디버깅**이 켜진 빌드. 디바이스팜은 이 디버깅 소켓으로 들어간다.
79
+ 2. **경로형 딥링크**: `<scheme>://<웹 경로>`를 받으면 그 웹 경로를 WebView로 연다.
80
+ 3. **하단 탭이 웹 링크**(`a[href]` 또는 `role=link`)이고, 화면 상단 뒤로 버튼에 이름(`aria-label`)이 있다.
81
+ 4. 점검용 계정으로 로그인해 둔다. 로그인 화면으로 튕기면 「로그인 필요」로 남는다.
82
+
83
+ </details>
84
+
85
+ ## Quick start
86
+
87
+ ```bash
88
+ npx device-farm init # farm.config.ts, .env.example 생성(이미 있으면 건너뜀)
89
+ npx device-farm dev # http://127.0.0.1:5173 (--port 로 변경)
90
+ ```
91
+
92
+ `init`이 만든 `farm.config.ts`를 내 앱 값으로 고칩니다. 필수 값만 남기면 이 정도입니다.
93
+
94
+ ```ts
95
+ // farm.config.ts
96
+ import { defineAppConfig } from "@ahnseungchan/device-farm";
97
+
98
+ export default defineAppConfig({
99
+ name: "My App",
100
+ appIds: { dev: "com.example.app.dev", prod: "com.example.app" },
101
+ web: { dev: "https://dev.app.example.com", prod: "https://app.example.com" },
102
+ homePath: "/home",
103
+ homeTabName: "홈",
104
+ tabPaths: { 검색: "/search", 마이: "/my" },
105
+ deepLink: { scheme: "myapp", homeHost: "home" },
106
+ });
107
+ ```
108
+
109
+ 토큰이 필요하면 `.env.example`을 `.env`로 복사해 채우고, `.gitignore`에 `.env`와 `test-results/`를 넣어 두세요.
110
+ 결과(실행 기록·스크린샷·기준 화면·화면 지도)는 전부 작업 폴더의 `test-results/`에 쌓입니다.
111
+
112
+ <details>
113
+ <summary><b>데이터 QA 규칙 예시</b></summary>
114
+
115
+ ```ts
116
+ import { defineAppConfig, equalWon, formatWon, numberAt, textHas } from "@ahnseungchan/device-farm";
117
+
118
+ export default defineAppConfig({
119
+ // ...필수 값
120
+ apiOrigin: "https://api-dev.app.example.com",
121
+ qa: {
122
+ sessionPath: "/api/auth/session",
123
+ screens: [
124
+ {
125
+ path: "/cart",
126
+ name: "장바구니",
127
+ apis: { cart: "/v1/users/{userId}/cart" },
128
+ rules: [
129
+ {
130
+ id: "total-shown",
131
+ title: "받은 합계가 화면에 그대로 보인다",
132
+ check: (context) => {
133
+ const shown = `${formatWon(numberAt(context, "cart", "totalWon"))}원`;
134
+ return textHas(context.text, shown) ? null : `화면에 없음: 합계 「${shown}」`;
135
+ },
136
+ },
137
+ ],
138
+ },
139
+ ],
140
+ },
141
+ });
142
+ ```
143
+
144
+ `npx device-farm qa --device <id> [--path /cart]`로 돌립니다. 전체 예시는 `init`이 만든 `farm.config.ts`에 있습니다.
145
+
146
+ </details>
147
+
148
+ ## 앱 설정
149
+
150
+ `defineAppConfig()`가 빠진 값을 기본값으로 채웁니다. 타입은 `AppConfigInput`·`AppConfig`로 패키지에서 가져올 수 있습니다.
151
+ 설정 파일은 `FARM_CONFIG` → 작업 폴더 `farm.config.ts` → `farm.config.example.ts` 순으로 찾고, 비밀 값은 작업 폴더 `.env`에서 읽습니다.
152
+
153
+ | 필드 | 필수 | 설명 |
154
+ | ------------------------------------ | ---- | ---------------------------------------------------------------------------- |
155
+ | `name` | ✓ | 앱 이름. 화면 제목·프롬프트에 쓴다 |
156
+ | `appIds` | ✓ | `{ dev, prod }` Android 패키지 = iOS 번들 id. 다르면 `iosBundleIds` |
157
+ | `homePath`·`homeTabName`·`tabPaths` | ✓ | 홈 경로, 홈 탭 이름, 하단 탭 이름 → 경로 |
158
+ | `web` | | `{ dev, prod }` WebView 웹 주소. 비면 `FARM_WEB_ORIGIN_DEV`·`FARM_WEB_ORIGIN_PROD` |
159
+ | `apiOrigin` | | API 호스트(웹과 다를 때). 가드가 내부로 보고 데이터 QA가 부른다 |
160
+ | `deepLink` | | `{ scheme, homeHost }` 경로형 딥링크 |
161
+ | `guard` | | 기본 금지어·금지 API에 **더할** 값, 늘 통과시킬 `pushTokenRequests` |
162
+ | `scenarios`·`excludedScenarioPaths` | | 고정 주소 화면 점검 목록과 뺄 경로 |
163
+
164
+ <details>
165
+ <summary><b>전체 필드</b></summary>
166
+
167
+ | 필드 | 설명 |
168
+ | --------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
169
+ | `inPageBackNames` | 화면 상단 뒤로 버튼 이름. 기본 `["뒤로가기", "Back"]` |
170
+ | `loginPaths`·`appErrorText` | 로그인 화면 경로, 앱 오류 문구. 기본 `["/login"]`, `"Application error"` |
171
+ | `android.mainActivity` | WebView 액티비티. 다른 네이티브 화면이 위에 있으면 뒤로 키로 닫는다 |
172
+ | `webview` | `stackResetScript`(WebView 스택 닫기 브리지), `sheetCloseName`(시트 닫기 버튼) |
173
+ | `guard` | `extraForbiddenButtonWords`·`extraForbiddenClickNames`·`extraForbiddenApiPatterns`, `pushTokenRequests`, 누르지 않을 `avoidedNamePattern`·`noPressPathPattern` |
174
+ | `qa` | `{ screens, sessionPath }` 데이터 QA 화면·규칙, 로그인 세션 경로 |
175
+ | `policy.confluence` | `{ cloudId, spaceKey, titleQuery, siteUrl }` 정책 정본 |
176
+ | `figma` | `{ frontRepo, ref, appDir }` 화면 → 피그마 노드를 뽑을 프론트 체크아웃 |
177
+ | `install` | `sources`(플랫폼·빌드별 `firebase`/`testflight`), `firebase`(앱 id), `testflightDevAppName` |
178
+ | `ios` | `{ teamId, wdaBundleId }` WDA 서명. 비면 `IOS_TEAM_ID`·`IOS_WDA_BUNDLE_ID` |
179
+ | `access.allowedDomains` | 화면에 들어올 수 있는 Tailscale 계정 도메인. 비면 이 맥에서만 |
180
+
181
+ 데이터 QA 도우미 `equalWon`·`formatWon`·`numberAt`·`textHas`·`pick`도 패키지 루트에서 내보냅니다. 환경 변수 목록은 `.env.example`에 있습니다.
182
+
183
+ </details>
184
+
185
+ ## CLI
186
+
187
+ 모든 명령은 작업 폴더(내 프로젝트) 기준으로 설정·`.env`를 읽고 결과를 씁니다. `npx device-farm <명령> --help`로 옵션을 볼 수 있습니다.
188
+
189
+ | 명령 | 하는 일 |
190
+ | ----------------------------------------- | --------------------------------------------------------------------- |
191
+ | `device-farm init` | `farm.config.ts`·`.env.example` 생성 |
192
+ | `device-farm dev [--port 5173]` | 화면 + API 서버(127.0.0.1). 새 빌드 감시·정책 감시가 함께 돈다 |
193
+ | `device-farm check --build dev\|prod --version 2.1.5` | 떠 있는 서버에 빌드 점검을 시키고 끝날 때까지 진행을 출력 |
194
+ | `device-farm explore` | 탐색 점검(서버 필요) |
195
+ | `device-farm chaos --device <serial>` | 카오스 탐색(Android dev) |
196
+ | `device-farm record --device <serial>` | 사람 조작 녹화 → 화면 지도·플로우 후보 |
197
+ | `device-farm qa --device <id>` | 데이터 QA만 |
198
+ | `device-farm policy-watch` | 정책 바뀜 감지·규칙 뽑기·피그마 문구 |
199
+ | `device-farm policy-check --device <id>` | 정책·피그마 대조를 기기 한 대에서 |
200
+ | `device-farm explore-allowlist` | OpenAPI(`FARM_OPENAPI_URL`)의 쓰기 API를 허용 목록에 deny로 추가 |
201
+ | `device-farm mcp` | Claude Code용 MCP 서버(stdio) |
202
+
203
+ <details>
204
+ <summary><b>명령별 옵션</b></summary>
205
+
206
+ ```bash
207
+ device-farm dev [--port 5173]
208
+ device-farm check --build dev|prod --version 2.1.5 [--devices all|id,id] [--port 5173]
209
+ device-farm explore [--devices all|id,id] [--build dev] [--port 5173]
210
+ device-farm chaos --device <Android serial> [--seed 42] [--episodes 3] [--steps 50] [--minutes 60] [--no-guard]
211
+ device-farm record --device <Android serial> [--seconds 300] [--name "플로우 이름"]
212
+ device-farm qa --device <id> [--path /cart]
213
+ device-farm policy-watch [--extract <pageId>] [--figma]
214
+ device-farm policy-check --device <id> [--path /my/orders]
215
+ ```
216
+
217
+ `check`·`explore`는 떠 있는 `dev` 서버에 작업을 시킵니다. 포트를 바꿨다면 같은 `--port`를 넘기세요.
218
+ 탐색 허용 목록 `explore/allowlist.json`, 입력칸 테스트 값 `explore/form-values.json`, 정책 등록 `policy/sources.json`은 작업 폴더에 두고 커밋합니다. 허용 목록이 없으면 모든 쓰기 요청을 막습니다.
219
+
220
+ </details>
221
+
222
+ ## Claude Code (MCP)
223
+
224
+ ```bash
225
+ # 내 프로젝트 폴더에서
226
+ claude mcp add --scope project device-farm -- npx device-farm mcp
227
+ ```
228
+
229
+ - 도구 흐름: `device_start` → `device_look` / `device_press` / `device_go_home` / `device_report_issue` → `device_finish`.
230
+ - 끝나면 화면 지도에 합치고, 홈부터 다시 돌 수 있는 경로면 플로우 후보로 올린다. 보고서는 `test-results/agent/<시각>/report.json`.
231
+ - 탐색은 Android dev 빌드에서 쓰기 요청 가드를 켠 채로만 돈다. 사용자의 Claude Code 계정으로 돌고 API 키는 필요 없다.
232
+
233
+ 이 저장소는 Claude Code 플러그인(`.claude-plugin`, `skills/run-check`·`chaos-explore`·`policy-figma-check`)으로도 쓸 수 있습니다. 저장소를 받아 `npm install`한 뒤 플러그인으로 추가하세요.
234
+
235
+ ## 안전
236
+
237
+ - 탐색·버튼 눌러보기·카오스·MCP 탐색은 **Android dev 빌드**에서, 쓰기 요청 가드를 켠 채로 돈다. 가드를 못 걸면 시작하지 않는다(fail closed).
238
+ - 가드 판단 순서: 읽기(GET·HEAD·OPTIONS) 통과 → `pushTokenRequests` 통과 → 금지 경로 차단 → 허용 목록 allow 통과 → deny·모름 차단.
239
+ - 기본 금지 버튼(구매·결제·매도·매수·송금·출금·탈퇴·주문·환불·해지·삭제·로그아웃 등)은 목록에서 빠지고, 눌러도 거절한다. 앱 설정은 금지어를 더할 뿐 빼지 못한다. 비밀번호·PIN 화면은 누르지 않고 나가기만 한다.
240
+ - `chaos --no-guard`는 쓰기 요청을 막지 않는다. **dev 서버에서만** 쓴다. 금지 버튼은 그대로 누르지 않는다.
241
+ - 화면 서버는 127.0.0.1에만 열린다. 밖에서는 `tailscale serve`로만 들어오고, `access.allowedDomains`로 계정 도메인을 제한한다.
242
+
243
+ ## FAQ
244
+
245
+ <details>
246
+ <summary><b>기기 없이 화면만 열어 볼 수 있나요?</b></summary>
247
+
248
+ 네. `npx device-farm init && npx device-farm dev`면 예시 설정으로 화면이 열립니다. 기기 목록은 비어 있고 점검은 돌지 않습니다.
249
+
250
+ </details>
251
+
252
+ <details>
253
+ <summary><b>iOS도 탐색·카오스가 되나요?</b></summary>
254
+
255
+ 빌드 점검·원격 조작은 iOS(Appium)를 지원합니다. 탐색·카오스·녹화·MCP 탐색은 지금 Android dev 빌드만 지원합니다.
256
+
257
+ </details>
258
+
259
+ <details>
260
+ <summary><b><code>check</code>가 서버에 연결할 수 없다고 합니다.</b></summary>
261
+
262
+ `check`·`explore`는 떠 있는 `device-farm dev` 서버에 작업을 시킵니다. 다른 터미널에서 `dev`를 먼저 켜고, 포트를 바꿨다면 같은 `--port`를 넘기세요.
263
+
264
+ </details>
265
+
266
+ <details>
267
+ <summary><b>LLM 키가 꼭 필요한가요?</b></summary>
268
+
269
+ 아니요. 점검·탐색·카오스·데이터 QA는 규칙으로만 판정합니다. PRD로 플로우 후보를 만들 때(`FLOW_LLM_*`)와 정책 판정에만 씁니다.
270
+
271
+ </details>
272
+
273
+ ## 문서
274
+
275
+ - [운영: 호스트 맥과 기기 준비](https://github.com/Ahnseungc/device-farm/blob/main/docs/operations.md)
276
+ - [설계: 빌드 점검·기준 화면·플로우·화면 지도](https://github.com/Ahnseungc/device-farm/blob/main/docs/design-device-farm.md)
277
+ - [설계: 탐색 점검과 쓰기 요청 가드](https://github.com/Ahnseungc/device-farm/blob/main/docs/design-explore.md)
278
+ - [설계: 카오스 탐색](https://github.com/Ahnseungc/device-farm/blob/main/docs/design-chaos.md)
279
+ - [설계: 정책·피그마 대조](https://github.com/Ahnseungc/device-farm/blob/main/docs/design-policy-figma.md)
280
+
281
+ ## Contributing
282
+
283
+ 이슈와 PR 환영합니다. 재현 방법이 있는 버그 리포트, 문서 보완 모두 도움이 됩니다.
284
+
285
+ ```bash
286
+ git clone https://github.com/Ahnseungc/device-farm.git && cd device-farm
287
+ npm install
288
+ npm run typecheck && npm test && npm run build
289
+ npm run dev # 화면 개발(vite dev). 저장소 루트 farm.config.ts → farm.config.example.ts
290
+ ```
291
+
292
+ npm workspaces 모노레포입니다. 의존 방향은 `core ← devices ← engine ← server ← apps`이고 `tools/boundaries.test.ts`가 지킵니다. 배포 패키지는 워크스페이스를 `dist/`에 묶어 냅니다(tsup + vite build).
293
+
294
+ 릴리스: `package.json`·`CHANGELOG.md`의 버전을 올리고 `npm run publish:npm`(`prepublishOnly`가 typecheck·test·build를 돈다), `v<version>` 태그를 push합니다.
295
+
296
+ ## License
297
+
298
+ [MIT](https://github.com/Ahnseungc/device-farm/blob/main/LICENSE)
package/dist/bin.js ADDED
@@ -0,0 +1,79 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ defaultPort
4
+ } from "./chunk-4UOXXMXL.js";
5
+
6
+ // apps/cli/src/bin.ts
7
+ import { register } from "tsx/esm/api";
8
+ var commands = {
9
+ init: { usage: "init", summary: "farm.config.ts\xB7.env.example\uC744 \uB9CC\uB4E0\uB2E4(\uC788\uC73C\uBA74 \uAC74\uB108\uB700)", run: async () => (await import("./init-QFFEOJWY.js")).init() },
10
+ dev: { usage: "dev [--port 5173]", summary: "\uB514\uBC14\uC774\uC2A4\uD31C \uD654\uBA74\xB7API \uC11C\uBC84\uB97C \uC5F0\uB2E4(127.0.0.1)", run: runDev },
11
+ check: {
12
+ usage: "check --build dev|prod --version 2.1.5 [--devices all|id,id] [--port 5173]",
13
+ summary: "\uAE30\uC900 \uD654\uBA74 \uBE44\uAD50\xB7\uBC84\uD2BC \uB20C\uB7EC\uBCF4\uAE30 \uC810\uAC80(dev \uC11C\uBC84\uAC00 \uB5A0 \uC788\uC5B4\uC57C \uD55C\uB2E4)",
14
+ run: () => import("./device-run-XPNRYXEL.js")
15
+ },
16
+ explore: { usage: "explore [--devices all|id,id] [--build dev] [--port 5173]", summary: "\uD654\uBA74 \uD0D0\uC0C9(dev \uC11C\uBC84\uAC00 \uB5A0 \uC788\uC5B4\uC57C \uD55C\uB2E4)", run: () => import("./device-explore-PHS6SVEJ.js") },
17
+ chaos: {
18
+ usage: "chaos --device <Android serial> [--seed 42] [--episodes 3] [--steps 50] [--minutes 60] [--no-guard]",
19
+ summary: "\uCE74\uC624\uC2A4 \uD0D0\uC0C9. --no-guard\uB294 \uC4F0\uAE30 \uC694\uCCAD\uC744 \uB9C9\uC9C0 \uC54A\uB294\uB2E4(dev \uC11C\uBC84 \uC804\uC6A9)",
20
+ run: () => import("./chaos-CIB7G5X5.js")
21
+ },
22
+ record: { usage: 'record --device <Android serial> [--seconds 300] [--name "\uD50C\uB85C\uC6B0 \uC774\uB984"]', summary: "\uC0AC\uB78C \uC870\uC791\uC744 \uB179\uD654\uD574 \uD654\uBA74 \uC9C0\uB3C4\xB7\uD50C\uB85C\uC6B0 \uD6C4\uBCF4\uB85C", run: () => import("./record-EGHVQFR7.js") },
23
+ qa: { usage: "qa --device <id> [--path /home]", summary: "\uD654\uBA74 \uB370\uC774\uD130 QA(API \uC751\uB2F5 \u2194 \uD654\uBA74 \uAE00\uC790)", run: () => import("./qa-run-5LLQMLAM.js") },
24
+ "policy-watch": { usage: "policy-watch [--extract <pageId>] [--figma]", summary: "Confluence \uC815\uCC45 \uBC14\uB01C \uAC10\uC9C0\xB7\uADDC\uCE59 \uBF51\uAE30\xB7\uD53C\uADF8\uB9C8 \uCE90\uC2DC", run: () => import("./policy-watch-QPHI2P7Z.js") },
25
+ "policy-check": { usage: "policy-check --device <id> [--path /my/orders]", summary: "\uC815\uCC45\xB7\uD53C\uADF8\uB9C8 \uB300\uC870\uB97C \uAE30\uAE30 \uD55C \uB300\uC5D0\uC11C", run: () => import("./policy-check-AUWGFNHZ.js") },
26
+ "explore-allowlist": { usage: "explore-allowlist", summary: "FARM_OPENAPI_URL \uC2A4\uD399\uC758 \uC4F0\uAE30 API\uB97C explore/allowlist.json\uC5D0 deny\uB85C \uCD94\uAC00", run: () => import("./explore-allowlist-F44YQDVK.js") },
27
+ mcp: { usage: "mcp", summary: "Claude Code\uC6A9 MCP \uC11C\uBC84(stdio)", run: runMcp }
28
+ };
29
+ function usage() {
30
+ const width = Math.max(...Object.keys(commands).map((name) => name.length));
31
+ const lines = Object.entries(commands).map(([name, command]) => ` ${name.padEnd(width)} ${command.summary}`);
32
+ return `\uC0AC\uC6A9\uBC95: device-farm <\uBA85\uB839> [\uC635\uC158]
33
+
34
+ \uBA85\uB839
35
+ ${lines.join("\n")}
36
+
37
+ \uBA85\uB839\uBCC4 \uC635\uC158: device-farm <\uBA85\uB839> --help
38
+ \uC571 \uC124\uC815: FARM_CONFIG \u2192 ./farm.config.ts \u2192 ./farm.config.example.ts, \uBE44\uBC00 \uAC12: ./.env`;
39
+ }
40
+ async function runDev() {
41
+ const index = process.argv.indexOf("--port");
42
+ const raw = index >= 0 ? process.argv[index + 1] : process.env.FARM_PORT;
43
+ const port = raw ? Number(raw) : defaultPort;
44
+ if (!Number.isInteger(port) || port < 1 || port > 65535) throw new Error(`--port\uB294 1~65535 \uC815\uC218\uC5EC\uC57C \uD569\uB2C8\uB2E4: ${raw}`);
45
+ process.env.FARM_PORT = String(port);
46
+ await (await import("./serve-XT32MAEN.js")).serve(port);
47
+ }
48
+ async function runMcp() {
49
+ process.env.FARM_ROOT ??= process.cwd();
50
+ await import("./server-3NM7ODUG.js");
51
+ }
52
+ async function main() {
53
+ const [name, ...rest] = process.argv.slice(2);
54
+ if (!name || name === "--help" || name === "-h" || name === "help") {
55
+ console.log(usage());
56
+ return;
57
+ }
58
+ const command = commands[name];
59
+ if (!command) {
60
+ console.error(`\uC54C \uC218 \uC5C6\uB294 \uBA85\uB839\uC785\uB2C8\uB2E4: ${name}
61
+
62
+ ${usage()}`);
63
+ process.exitCode = 1;
64
+ return;
65
+ }
66
+ if (rest.includes("--help") || rest.includes("-h")) {
67
+ console.log(`\uC0AC\uC6A9\uBC95: device-farm ${command.usage}
68
+
69
+ ${command.summary}`);
70
+ return;
71
+ }
72
+ process.argv.splice(2, 1);
73
+ register();
74
+ await command.run();
75
+ }
76
+ main().catch((error) => {
77
+ console.error(error instanceof Error ? error.message : error);
78
+ process.exitCode = 1;
79
+ });