@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
@@ -0,0 +1,161 @@
1
+ import {
2
+ policyRoot,
3
+ readJudgeCache,
4
+ readScreenHashes,
5
+ writeJudgeCache,
6
+ writeScreenHashes
7
+ } from "./chunk-VGPCTMUA.js";
8
+ import {
9
+ compareCopyRules,
10
+ diffFigmaCopy,
11
+ docsMismatchChecks,
12
+ figmaChecks,
13
+ isIgnored,
14
+ rulesForScreen,
15
+ screenTextHash,
16
+ situationalRules
17
+ } from "./chunk-UAHWEUDI.js";
18
+ import {
19
+ classifyWithFigma,
20
+ findingLabels,
21
+ parseJsonObject,
22
+ screenMatches,
23
+ shortHash
24
+ } from "./chunk-F6PZOBU6.js";
25
+ import {
26
+ samePath,
27
+ sleep
28
+ } from "./chunk-NLX3V7ZJ.js";
29
+
30
+ // packages/engine/src/policy/screen-check.ts
31
+ import { mkdirSync, writeFileSync } from "fs";
32
+ import path from "path";
33
+
34
+ // packages/core/src/policy/judge.ts
35
+ var judgeKey = (path2, screenHash, rules, figmaHash) => `${path2}|${screenHash}|${shortHash(rules.map((rule) => `${rule.id}:${rule.pageHash}`).sort().join(","))}|${figmaHash}`;
36
+ var isSensitivePath = (path2) => /(account|bank|certif|identity|verify|auth|kyc|password|pin)/i.test(path2);
37
+ var maxScreenChars = 6e3;
38
+ var maxFigmaChars = 6e3;
39
+ function judgePrompt(path2, text, rules, screenshotFile, figmaTexts = [], appName = "\uB300\uC0C1") {
40
+ return [
41
+ `${appName} \uC571 \uD654\uBA74 ${path2}\uC774 \uC544\uB798 \uC815\uCC45 \uADDC\uCE59\uC5D0 \uB9DE\uB294\uC9C0 \uD310\uC815\uD574 JSON \uD558\uB098\uB85C\uB9CC \uB2F5\uD558\uC138\uC694.`,
42
+ screenshotFile ? `\uD654\uBA74 \uC2A4\uD06C\uB9B0\uC0F7: ${screenshotFile} (Read \uB3C4\uAD6C\uB85C \uC5F4\uC5B4 \uBCF4\uC138\uC694)` : "\uC2A4\uD06C\uB9B0\uC0F7\uC740 \uC5C6\uC2B5\uB2C8\uB2E4(\uAC1C\uC778\uC815\uBCF4 \uD654\uBA74). \uD654\uBA74 \uAE00\uC790\uB85C\uB9CC \uD310\uC815\uD558\uC138\uC694.",
43
+ "",
44
+ "\uD310\uC815 \uAE30\uC900:",
45
+ '- "match": \uD654\uBA74\uC774 \uADDC\uCE59\uB300\uB85C\uB2E4.',
46
+ '- "differ": \uD654\uBA74\uC774 \uADDC\uCE59\uACFC \uB2E4\uB974\uB2E4. reason\uC5D0 \uD654\uBA74\uC5D0\uC11C \uBCF8 \uAC83\uC744 \uC801\uB294\uB2E4.',
47
+ '- "unknown": \uC9C0\uAE08 \uD654\uBA74 \uC0C1\uD0DC\uB85C\uB294 \uC54C \uC218 \uC5C6\uB2E4(\uADF8 \uC0C1\uD0DC\uAC00 \uC544\uB2D8, \uB370\uC774\uD130 \uC5C6\uC74C \uB4F1). \uC560\uB9E4\uD558\uBA74 unknown.',
48
+ "",
49
+ "differ\uC774\uBA74 \uC544\uB798 \uD53C\uADF8\uB9C8 \uB514\uC790\uC778 \uBB38\uAD6C\uB97C \uBCF4\uACE0 figma\uC5D0 \uD53C\uADF8\uB9C8\uAC00 \uC5B4\uB290 \uCABD\uACFC \uAC19\uC740\uC9C0 \uC801\uB294\uB2E4:",
50
+ '- "policy": \uD53C\uADF8\uB9C8\uAC00 \uC815\uCC45\uB300\uB85C\uB2E4(\uC571\uB9CC \uB2E4\uB974\uB2E4). "app": \uD53C\uADF8\uB9C8\uAC00 \uC571 \uD654\uBA74\uACFC \uAC19\uB2E4(\uC815\uCC45\uB9CC \uB2E4\uB974\uB2E4). "neither": \uC14B \uB2E4 \uB2E4\uB974\uB2E4. "unknown": \uD53C\uADF8\uB9C8\uB85C \uC54C \uC218 \uC5C6\uB2E4.',
51
+ "",
52
+ 'reason\uC740 \uD55C\uAD6D\uC5B4 \uD55C \uC904\uB85C \uC4F4\uB2E4. \uD615\uC2DD: {"verdicts": [{"ruleId": "...", "result": "differ", "reason": "\uD55C \uC904", "figma": "app"}]}',
53
+ "",
54
+ "\uADDC\uCE59:",
55
+ ...rules.map((rule) => `- ${rule.id}: ${rule.statement} (\uADFC\uAC70: ${rule.evidence})`),
56
+ "",
57
+ "\uD654\uBA74 \uAE00\uC790:",
58
+ text.slice(0, maxScreenChars),
59
+ "",
60
+ "\uD53C\uADF8\uB9C8 \uB514\uC790\uC778 \uBB38\uAD6C(\uC774 \uD654\uBA74\uC5D0 \uC5F0\uACB0\uB41C \uB178\uB4DC\uB4E4, \uC5EC\uB7EC \uC0C1\uD0DC\uAC00 \uC11E\uC5EC \uC788\uB2E4):",
61
+ figmaTexts.length > 0 ? figmaTexts.join("\n").slice(0, maxFigmaChars) : "(\uC5C6\uC74C)"
62
+ ].join("\n");
63
+ }
64
+ var results = ["match", "differ", "unknown"];
65
+ var sides = ["policy", "app", "neither", "unknown"];
66
+ function parseVerdicts(text, rules) {
67
+ const parsed = parseJsonObject(text);
68
+ const ids = new Set(rules.map((rule) => rule.id));
69
+ const list = Array.isArray(parsed.verdicts) ? parsed.verdicts : [];
70
+ return list.flatMap((item) => {
71
+ const value = item;
72
+ if (typeof value.ruleId !== "string" || !ids.has(value.ruleId) || !results.includes(value.result)) return [];
73
+ const figma = sides.includes(value.figma) ? value.figma : "unknown";
74
+ return [{ ruleId: value.ruleId, result: value.result, reason: typeof value.reason === "string" ? value.reason : "", figma }];
75
+ });
76
+ }
77
+ function verdictChecks(rules, verdicts, context) {
78
+ const byId = new Map(verdicts.map((verdict) => [verdict.ruleId, verdict]));
79
+ return rules.map((rule) => {
80
+ const verdict = byId.get(rule.id);
81
+ const base = { id: rule.id, title: rule.statement, ignoreKey: rule.id, ignoreHash: rule.pageHash, pageId: rule.pageId };
82
+ if (!verdict || verdict.result === "unknown") return { ...base, ok: true, message: `\uD310\uB2E8 \uBD88\uAC00${verdict?.reason ? `: ${verdict.reason}` : ""}` };
83
+ if (verdict.result === "match") return { ...base, ok: true };
84
+ const figmaAgrees = verdict.figma === "policy" ? true : verdict.figma === "app" ? false : null;
85
+ const finding = classifyWithFigma(rule, figmaAgrees, context.screenChanged, context.now);
86
+ return { ...base, ok: false, finding, ignored: isIgnored(context.ignores, rule.id, rule.pageHash), message: `[${findingLabels[finding]}] ${verdict.reason} \xB7 \u300C${rule.pageTitle}\u300D` };
87
+ });
88
+ }
89
+
90
+ // packages/engine/src/policy/screen-check.ts
91
+ var policySettleMs = 4e3;
92
+ var policyOpenAttempts = 2;
93
+ function policyScreens(pages, knownPaths, figmaCache, frontRoutes = {}) {
94
+ const concrete = (pattern) => pattern.includes("*") ? knownPaths.filter((known) => screenMatches(pattern, known)) : [pattern];
95
+ const paths = new Set(pages.flatMap((page) => [...page.screens, ...page.rules.flatMap((rule) => rule.screens)].flatMap(concrete)));
96
+ const allRules = pages.flatMap((page) => page.rules);
97
+ return [...paths].sort().map((screen) => {
98
+ const refs = /* @__PURE__ */ new Set([
99
+ ...pages.filter((page) => page.screens.some((pattern) => screenMatches(pattern, screen))).flatMap((page) => page.figma),
100
+ ...Object.entries(frontRoutes).flatMap(([route, routeRefs]) => screenMatches(route, screen) ? routeRefs : [])
101
+ ]);
102
+ const figmaTexts = [...refs].sort().flatMap((ref) => figmaCache[ref] ? [figmaCache[ref]] : []);
103
+ const texts = figmaTexts.flatMap((item) => item.texts);
104
+ return { path: screen, rules: rulesForScreen(allRules, screen), figma: texts.length > 0 ? { hash: shortHash(figmaTexts.map((item) => item.hash).join(",")), texts } : null };
105
+ });
106
+ }
107
+ async function judge(screen, text, screenHash, screenChanged, driver, deps) {
108
+ const root = deps.root ?? policyRoot;
109
+ const behaviors = screen.rules.filter((rule) => rule.kind === "behavior");
110
+ const context = { ignores: deps.ignores, screenChanged, now: deps.now?.() ?? Date.now() };
111
+ if (behaviors.length === 0) return { checks: [], cached: false };
112
+ const key = judgeKey(screen.path, screenHash, behaviors, screen.figma?.hash ?? "");
113
+ const cache = readJudgeCache(root);
114
+ if (cache[key]) return { checks: verdictChecks(behaviors, cache[key], context), cached: true };
115
+ if (!deps.ask) return { checks: [], cached: false, judgeError: "Claude\uB97C \uC4F8 \uC218 \uC5C6\uC5B4 \uD654\uBA74 \uB3D9\uC791 \uADDC\uCE59\uC744 \uD310\uC815\uD558\uC9C0 \uC54A\uC558\uC2B5\uB2C8\uB2E4." };
116
+ let screenshotFile = null;
117
+ if (!isSensitivePath(screen.path)) {
118
+ screenshotFile = path.join(root, "judge-shots", `${shortHash(key)}.png`);
119
+ mkdirSync(path.dirname(screenshotFile), { recursive: true });
120
+ writeFileSync(screenshotFile, await driver.screenshot());
121
+ }
122
+ const prompt = judgePrompt(screen.path, text, behaviors, screenshotFile, screen.figma?.texts ?? [], driver.app.name);
123
+ const verdicts = parseVerdicts(await deps.ask(prompt, { allowRead: Boolean(screenshotFile) }), behaviors);
124
+ writeJudgeCache({ ...readJudgeCache(root), [key]: verdicts }, root);
125
+ return { checks: verdictChecks(behaviors, verdicts, context), cached: false };
126
+ }
127
+ async function checkPolicyScreen(driver, screen, deps) {
128
+ const root = deps.root ?? policyRoot;
129
+ let opened = "";
130
+ for (let attempt = 0; attempt < policyOpenAttempts && !samePath(screen.path, opened); attempt += 1) {
131
+ await driver.navigate(screen.path);
132
+ await sleep(deps.settleMs ?? policySettleMs);
133
+ opened = await driver.currentPath();
134
+ }
135
+ if (!samePath(screen.path, opened)) {
136
+ return { path: screen.path, situational: 0, cached: false, checks: [{ id: "open", title: "\uD654\uBA74 \uC5F4\uAE30", ok: false, message: `${screen.path} \uB300\uC2E0 ${opened} \uD654\uBA74\uC774 \uC5F4\uB824 \uC788\uC2B5\uB2C8\uB2E4.`, ignoreKey: "open", ignoreHash: "" }] };
137
+ }
138
+ const text = await driver.evaluate("document.body.innerText");
139
+ const screenHash = screenTextHash(text);
140
+ const hashes = readScreenHashes(root);
141
+ const screenChanged = hashes[screen.path] !== void 0 && hashes[screen.path] !== screenHash;
142
+ writeScreenHashes({ ...hashes, [screen.path]: screenHash }, root);
143
+ const now = deps.now?.() ?? Date.now();
144
+ const checks = compareCopyRules(screen.rules, { path: screen.path, text, ignores: deps.ignores, screenChanged, now, figmaTexts: screen.figma?.texts ?? null });
145
+ if (screen.figma) {
146
+ checks.push(...figmaChecks(diffFigmaCopy(screen.figma.texts, text), screen.figma.hash, screen.path, deps.ignores));
147
+ checks.push(...docsMismatchChecks(screen.rules, screen.figma.texts, text, deps.ignores));
148
+ }
149
+ const situational = situationalRules(screen.rules, screen.path).length;
150
+ try {
151
+ const judged = await judge(screen, text, screenHash, screenChanged, driver, deps);
152
+ return { path: screen.path, checks: [...checks, ...judged.checks], situational, cached: judged.cached, judgeError: judged.judgeError };
153
+ } catch (error) {
154
+ return { path: screen.path, checks, situational, cached: false, judgeError: error instanceof Error ? error.message : String(error) };
155
+ }
156
+ }
157
+
158
+ export {
159
+ policyScreens,
160
+ checkPolicyScreen
161
+ };
@@ -0,0 +1,169 @@
1
+ import {
2
+ formatNetworkLog
3
+ } from "./chunk-AVECSGEH.js";
4
+ import {
5
+ getIosSession
6
+ } from "./chunk-S6GHOP4B.js";
7
+ import {
8
+ execFileText
9
+ } from "./chunk-AXQNUPGC.js";
10
+
11
+ // packages/devices/src/logs/collect.ts
12
+ import { spawn } from "child_process";
13
+ import { createInterface } from "readline";
14
+ var safariPollMs = 2e3;
15
+ var httpErrorStatus = 400;
16
+ var androidCrashMarker = "FATAL EXCEPTION";
17
+ var iosCrashMarker = "Terminating app due to uncaught exception";
18
+ function safeUrl(url) {
19
+ try {
20
+ const parsed = new URL(url);
21
+ return `${parsed.origin}${parsed.pathname}`;
22
+ } catch {
23
+ return url.split(/[?#]/)[0];
24
+ }
25
+ }
26
+ function logcatLineToEntry(line) {
27
+ const text = line.trim();
28
+ if (!text) return null;
29
+ return { source: text.includes(androidCrashMarker) ? "crash" : "system", text };
30
+ }
31
+ function syslogLineToEntry(line) {
32
+ const text = line.trim();
33
+ if (!text) return null;
34
+ return { source: text.includes(iosCrashMarker) ? "crash" : "system", text };
35
+ }
36
+ function consoleToEntry(type, text) {
37
+ return type === "error" ? { source: "console", text } : null;
38
+ }
39
+ function responseToEntry(status, method, url) {
40
+ if (status < httpErrorStatus) return null;
41
+ return { source: "network", text: formatNetworkLog(method || "UNKNOWN", status, safeUrl(url)) };
42
+ }
43
+ function requestFailedToEntry(method, url, errorText) {
44
+ return { source: "network", text: formatNetworkLog(method, 0, safeUrl(url), errorText) };
45
+ }
46
+ function isRecord(value) {
47
+ return typeof value === "object" && value !== null && !Array.isArray(value);
48
+ }
49
+ function parseMessage(entry) {
50
+ if (!isRecord(entry) || typeof entry.message !== "string") return null;
51
+ try {
52
+ const parsed = JSON.parse(entry.message);
53
+ return isRecord(parsed) ? parsed : null;
54
+ } catch {
55
+ return null;
56
+ }
57
+ }
58
+ var severeLevels = ["error", "SEVERE"];
59
+ function safariConsoleToEntry(entry) {
60
+ const message = parseMessage(entry);
61
+ if (!message || typeof message.text !== "string") return null;
62
+ const outerLevel = isRecord(entry) ? entry.level : void 0;
63
+ const isError = severeLevels.some((level) => level === message.level || level === outerLevel);
64
+ return isError ? { source: "console", text: message.text } : null;
65
+ }
66
+ function responseOf(params) {
67
+ const response = params.response;
68
+ if (!isRecord(response) || typeof response.status !== "number" || typeof response.url !== "string") return null;
69
+ return responseToEntry(response.status, "", response.url);
70
+ }
71
+ function safariNetworkToEntry(entry) {
72
+ const message = parseMessage(entry);
73
+ if (!message || !isRecord(message.params)) return null;
74
+ if (message.method === "Network.responseReceived") return responseOf(message.params);
75
+ if (message.method === "Network.loadingFailed") {
76
+ const errorText = typeof message.params.errorText === "string" ? message.params.errorText : "";
77
+ return { source: "network", text: formatNetworkLog("UNKNOWN", 0, "-", errorText) };
78
+ }
79
+ return null;
80
+ }
81
+ var defaultSpawn = (command, args) => spawn(command, args, { stdio: ["ignore", "pipe", "ignore"] });
82
+ function pipeLines(child, ring, toEntry, label) {
83
+ const lines = createInterface({ input: child.stdout });
84
+ lines.on("line", (line) => {
85
+ const entry = toEntry(line);
86
+ if (entry) ring.push(entry);
87
+ });
88
+ child.on("error", (error) => ring.push({ source: "system", text: `${label} \uC2E4\uD589 \uC2E4\uD328: ${error.message}` }));
89
+ return async () => {
90
+ lines.close();
91
+ child.kill("SIGTERM");
92
+ };
93
+ }
94
+ function isAndroidDriver(driver) {
95
+ return "context" in driver;
96
+ }
97
+ function watchPage(driver, ring) {
98
+ const page = driver.context;
99
+ const push = (entry) => entry && ring.push(entry);
100
+ const onConsole = (message) => push(consoleToEntry(message.type(), message.text()));
101
+ const onResponse = (response) => push(responseToEntry(response.status(), response.request().method(), response.url()));
102
+ const onFailed = (request) => push(requestFailedToEntry(request.method(), request.url(), request.failure()?.errorText ?? ""));
103
+ page.on("console", onConsole);
104
+ page.on("response", onResponse);
105
+ page.on("requestfailed", onFailed);
106
+ return async () => {
107
+ page.off("console", onConsole);
108
+ page.off("response", onResponse);
109
+ page.off("requestfailed", onFailed);
110
+ };
111
+ }
112
+ async function appPid(serial, appId, exec) {
113
+ const pid = (await exec("adb", ["-s", serial, "shell", "pidof", appId])).trim().split(/\s+/)[0];
114
+ if (!pid) throw new Error(`${appId} \uC571\uC774 \uC2E4\uD589 \uC911\uC774 \uC544\uB2D9\uB2C8\uB2E4.`);
115
+ return pid;
116
+ }
117
+ async function startAndroid(device, appId, driver, ring, deps) {
118
+ const pid = await appPid(device.id, appId, deps.exec ?? execFileText);
119
+ const spawnProcess = deps.spawnProcess ?? defaultSpawn;
120
+ const stops = [pipeLines(spawnProcess("adb", ["-s", device.id, "logcat", `--pid=${pid}`]), ring, logcatLineToEntry, "logcat")];
121
+ if (isAndroidDriver(driver)) stops.push(watchPage(driver, ring));
122
+ return stops;
123
+ }
124
+ function pollSafariLogs(session, ring, pollMs) {
125
+ let lastError = "";
126
+ let polling = false;
127
+ const pull = async (type, toEntry) => {
128
+ const entries = await session.getLogs(type);
129
+ for (const entry of entries) {
130
+ const line = toEntry(entry);
131
+ if (line) ring.push(line);
132
+ }
133
+ };
134
+ const poll = async () => {
135
+ if (polling) return;
136
+ polling = true;
137
+ try {
138
+ await pull("safariConsole", safariConsoleToEntry);
139
+ await pull("safariNetwork", safariNetworkToEntry);
140
+ lastError = "";
141
+ } catch (error) {
142
+ const text = `safari \uB85C\uADF8 \uC218\uC9D1 \uC2E4\uD328: ${error instanceof Error ? error.message : String(error)}`;
143
+ if (text !== lastError) ring.push({ source: "system", text });
144
+ lastError = text;
145
+ } finally {
146
+ polling = false;
147
+ }
148
+ };
149
+ const timer = setInterval(() => void poll(), pollMs);
150
+ return async () => clearInterval(timer);
151
+ }
152
+ async function startIos(device, appId, ring, deps) {
153
+ const session = deps.iosSession ?? await getIosSession(device.id, appId);
154
+ const spawnProcess = deps.spawnProcess ?? defaultSpawn;
155
+ return [
156
+ pipeLines(spawnProcess("idevicesyslog", ["-u", device.id, "-p", "Runner"]), ring, syslogLineToEntry, "idevicesyslog"),
157
+ pollSafariLogs(session, ring, deps.pollMs ?? safariPollMs)
158
+ ];
159
+ }
160
+ async function startCollectors(device, appId, driver, ring, deps = {}) {
161
+ const stops = device.platform === "android" ? await startAndroid(device, appId, driver, ring, deps) : await startIos(device, appId, ring, deps);
162
+ return async () => {
163
+ await Promise.all(stops.map((stop) => stop()));
164
+ };
165
+ }
166
+
167
+ export {
168
+ startCollectors
169
+ };
@@ -0,0 +1,64 @@
1
+ import {
2
+ CliError,
3
+ exitCodes,
4
+ exploreExitCode,
5
+ formatExploreRun,
6
+ formatExploreSummary,
7
+ isExploreFinished,
8
+ isRecord,
9
+ requestJson,
10
+ summarizeExplore
11
+ } from "./chunk-P2M55S53.js";
12
+ import {
13
+ parseExploreArgs
14
+ } from "./chunk-4UOXXMXL.js";
15
+
16
+ // apps/cli/src/device-explore.ts
17
+ import { setTimeout as wait } from "timers/promises";
18
+ var pollIntervalMs = 2e3;
19
+ async function startExplore(port, body) {
20
+ const result = await requestJson(port, "/api/devices/explore", {
21
+ method: "POST",
22
+ headers: { "content-type": "application/json" },
23
+ body: JSON.stringify(body)
24
+ });
25
+ if (!isRecord(result) || typeof result.runId !== "string") throw new CliError("\uB514\uBC14\uC774\uC2A4\uD31C \uC11C\uBC84\uAC00 runId\uB97C \uC8FC\uC9C0 \uC54A\uC558\uC2B5\uB2C8\uB2E4.");
26
+ return result.runId;
27
+ }
28
+ async function readRuns(port, runId) {
29
+ const result = await requestJson(port, "/api/devices/explore/latest");
30
+ if (!Array.isArray(result)) return [];
31
+ return result.filter((run) => isRecord(run) && run.runId === runId);
32
+ }
33
+ async function followExplore(port, runId) {
34
+ const printed = /* @__PURE__ */ new Map();
35
+ for (; ; ) {
36
+ const runs = await readRuns(port, runId);
37
+ for (const run of runs) {
38
+ const line = formatExploreRun(run);
39
+ if (printed.get(run.deviceId) === line) continue;
40
+ printed.set(run.deviceId, line);
41
+ console.log(line);
42
+ }
43
+ if (isExploreFinished(runs)) return runs;
44
+ await wait(pollIntervalMs);
45
+ }
46
+ }
47
+ async function main() {
48
+ const args = parseExploreArgs(process.argv.slice(2));
49
+ if (!args.ok) throw new CliError(args.error);
50
+ const { devices, port, build } = args.value;
51
+ const runId = await startExplore(port, build ? { devices, build } : { devices });
52
+ console.log(`\uD0D0\uC0C9 \uC2DC\uC791 (${runId})`);
53
+ const runs = await followExplore(port, runId);
54
+ console.log(formatExploreSummary(summarizeExplore(runs)));
55
+ const code = exploreExitCode(runs);
56
+ console.log(`exit ${code}`);
57
+ return code;
58
+ }
59
+ main().then((code) => {
60
+ process.exitCode = code;
61
+ }).catch((error) => {
62
+ console.error(error instanceof Error ? error.message : error);
63
+ process.exitCode = exitCodes.failed;
64
+ });
@@ -0,0 +1,60 @@
1
+ import {
2
+ CliError,
3
+ changedLines,
4
+ exitCodes,
5
+ isRecord,
6
+ isRunFinished,
7
+ noDevicesMessage,
8
+ requestJson,
9
+ runExitCode
10
+ } from "./chunk-P2M55S53.js";
11
+ import {
12
+ parseRunArgs
13
+ } from "./chunk-4UOXXMXL.js";
14
+
15
+ // apps/cli/src/device-run.ts
16
+ import { setTimeout as wait } from "timers/promises";
17
+ var pollIntervalMs = 2e3;
18
+ async function startRun(port, body) {
19
+ const result = await requestJson(port, "/api/devices/run", {
20
+ method: "POST",
21
+ headers: { "content-type": "application/json" },
22
+ body: JSON.stringify(body)
23
+ });
24
+ if (!isRecord(result) || typeof result.runId !== "string") throw new CliError("\uB514\uBC14\uC774\uC2A4\uD31C \uC11C\uBC84\uAC00 runId\uB97C \uC8FC\uC9C0 \uC54A\uC558\uC2B5\uB2C8\uB2E4.");
25
+ return result.runId;
26
+ }
27
+ async function readLatestRun(port) {
28
+ const result = await requestJson(port, "/api/devices/runs/latest");
29
+ if (!isRecord(result) || typeof result.runId !== "string") return null;
30
+ return result;
31
+ }
32
+ async function followRun(port, runId) {
33
+ const printed = /* @__PURE__ */ new Map();
34
+ for (; ; ) {
35
+ const run = await readLatestRun(port);
36
+ if (run?.runId === runId) {
37
+ for (const line of changedLines(run, printed)) console.log(line);
38
+ if (isRunFinished(run)) return run;
39
+ }
40
+ await wait(pollIntervalMs);
41
+ }
42
+ }
43
+ async function main() {
44
+ const args = parseRunArgs(process.argv.slice(2));
45
+ if (!args.ok) throw new CliError(args.error);
46
+ const { kind, version, devices, port } = args.value;
47
+ const runId = await startRun(port, { kind, version, devices });
48
+ console.log(`\uAE30\uAE30 \uC810\uAC80 \uC2DC\uC791 (${runId}) \xB7 ${kind} ${version}`);
49
+ const run = await followRun(port, runId);
50
+ if (run.outcomes.length === 0) console.log(noDevicesMessage);
51
+ const code = runExitCode(run);
52
+ console.log(`exit ${code}`);
53
+ return code;
54
+ }
55
+ main().then((code) => {
56
+ process.exitCode = code;
57
+ }).catch((error) => {
58
+ console.error(error instanceof Error ? error.message : error);
59
+ process.exitCode = exitCodes.failed;
60
+ });
@@ -0,0 +1,44 @@
1
+ import {
2
+ addMissingAsDeny,
3
+ defaultAllowlistFile,
4
+ isMutatingMethod,
5
+ loadAllowlist
6
+ } from "./chunk-Q6CXF3NJ.js";
7
+
8
+ // apps/cli/src/explore-allowlist.ts
9
+ import { mkdir, writeFile } from "fs/promises";
10
+ import { dirname } from "path";
11
+ function collectWriteOperations(spec) {
12
+ if (typeof spec !== "object" || spec === null || !("paths" in spec)) throw new Error("OpenAPI \uC2A4\uD399\uC5D0 paths\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4.");
13
+ const { paths } = spec;
14
+ if (typeof paths !== "object" || paths === null) throw new Error("OpenAPI paths\uAC00 \uAC1D\uCCB4\uAC00 \uC544\uB2D9\uB2C8\uB2E4.");
15
+ const operations = [];
16
+ for (const [path, item] of Object.entries(paths)) {
17
+ if (typeof item !== "object" || item === null) continue;
18
+ for (const method of Object.keys(item)) {
19
+ const upper = method.toUpperCase();
20
+ if (isMutatingMethod(upper)) operations.push({ method: upper, path });
21
+ }
22
+ }
23
+ return operations;
24
+ }
25
+ async function fetchSpec(url) {
26
+ const response = await fetch(url);
27
+ if (!response.ok) throw new Error(`OpenAPI \uC2A4\uD399\uC744 \uBABB \uBC1B\uC558\uC2B5\uB2C8\uB2E4: ${response.status} ${url}`);
28
+ return response.json();
29
+ }
30
+ async function main() {
31
+ const url = process.env.FARM_OPENAPI_URL;
32
+ if (!url) throw new Error("FARM_OPENAPI_URL\uC774 \uBE44\uC5B4 \uC788\uC2B5\uB2C8\uB2E4.");
33
+ const operations = collectWriteOperations(await fetchSpec(url));
34
+ const current = await loadAllowlist();
35
+ const { allowlist, added } = addMissingAsDeny(current, operations);
36
+ await mkdir(dirname(defaultAllowlistFile), { recursive: true });
37
+ await writeFile(defaultAllowlistFile, `${JSON.stringify(allowlist, null, 2)}
38
+ `);
39
+ console.log(`\uC4F0\uAE30 API ${operations.length}\uAC1C \uC911 ${added}\uAC1C\uB97C deny\uB85C \uCD94\uAC00\uD588\uC2B5\uB2C8\uB2E4. (${defaultAllowlistFile})`);
40
+ }
41
+ main().catch((error) => {
42
+ console.error(error instanceof Error ? error.message : error);
43
+ process.exitCode = 1;
44
+ });