@tienne/gestalt 0.68.0 → 0.69.0
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.
- package/CLAUDE.md +9 -2
- package/README.md +1 -0
- package/dist/package.json +2 -1
- package/dist/plugin/review-agents/quality-reviewer/AGENT.md +1 -1
- package/dist/plugin/review-agents/writing-reviewer/AGENT.md +2 -2
- package/dist/plugin/skills/local-pr/SKILL.md +201 -0
- package/dist/plugin/skills/pr/SKILL.md +73 -4
- package/dist/plugin/skills/review/SKILL.md +190 -18
- package/dist/plugin/skills/review-reply/SKILL.md +144 -7
- package/dist/src/cli/commands/pr.d.ts +133 -0
- package/dist/src/cli/commands/pr.d.ts.map +1 -0
- package/dist/src/cli/commands/pr.js +489 -0
- package/dist/src/cli/commands/pr.js.map +1 -0
- package/dist/src/cli/index.d.ts.map +1 -1
- package/dist/src/cli/index.js +85 -0
- package/dist/src/cli/index.js.map +1 -1
- package/dist/src/core/config.d.ts.map +1 -1
- package/dist/src/core/config.js +4 -3
- package/dist/src/core/config.js.map +1 -1
- package/dist/src/core/home.d.ts +20 -0
- package/dist/src/core/home.d.ts.map +1 -0
- package/dist/src/core/home.js +52 -0
- package/dist/src/core/home.js.map +1 -0
- package/dist/src/core/types.d.ts +38 -0
- package/dist/src/core/types.d.ts.map +1 -1
- package/dist/src/core/version.d.ts.map +1 -1
- package/dist/src/core/version.js +4 -7
- package/dist/src/core/version.js.map +1 -1
- package/dist/src/events/store.d.ts +32 -0
- package/dist/src/events/store.d.ts.map +1 -1
- package/dist/src/events/store.js +68 -3
- package/dist/src/events/store.js.map +1 -1
- package/dist/src/local-pr/engine.d.ts +176 -0
- package/dist/src/local-pr/engine.d.ts.map +1 -0
- package/dist/src/local-pr/engine.js +373 -0
- package/dist/src/local-pr/engine.js.map +1 -0
- package/dist/src/local-pr/git.d.ts +190 -0
- package/dist/src/local-pr/git.d.ts.map +1 -0
- package/dist/src/local-pr/git.js +580 -0
- package/dist/src/local-pr/git.js.map +1 -0
- package/dist/src/local-pr/index.d.ts +6 -0
- package/dist/src/local-pr/index.d.ts.map +1 -0
- package/dist/src/local-pr/index.js +8 -0
- package/dist/src/local-pr/index.js.map +1 -0
- package/dist/src/local-pr/policy.d.ts +77 -0
- package/dist/src/local-pr/policy.d.ts.map +1 -0
- package/dist/src/local-pr/policy.js +80 -0
- package/dist/src/local-pr/policy.js.map +1 -0
- package/dist/src/local-pr/registry.d.ts +79 -0
- package/dist/src/local-pr/registry.d.ts.map +1 -0
- package/dist/src/local-pr/registry.js +307 -0
- package/dist/src/local-pr/registry.js.map +1 -0
- package/dist/src/local-pr/repository.d.ts +67 -0
- package/dist/src/local-pr/repository.d.ts.map +1 -0
- package/dist/src/local-pr/repository.js +251 -0
- package/dist/src/local-pr/repository.js.map +1 -0
- package/dist/src/local-pr/types.d.ts +151 -0
- package/dist/src/local-pr/types.d.ts.map +1 -0
- package/dist/src/local-pr/types.js +9 -0
- package/dist/src/local-pr/types.js.map +1 -0
- package/dist/src/local-pr-web/engine.d.ts +24 -0
- package/dist/src/local-pr-web/engine.d.ts.map +1 -0
- package/dist/src/local-pr-web/engine.js +112 -0
- package/dist/src/local-pr-web/engine.js.map +1 -0
- package/dist/src/local-pr-web/html-generator.d.ts +24 -0
- package/dist/src/local-pr-web/html-generator.d.ts.map +1 -0
- package/dist/src/local-pr-web/html-generator.js +385 -0
- package/dist/src/local-pr-web/html-generator.js.map +1 -0
- package/dist/src/local-pr-web/index.d.ts +5 -0
- package/dist/src/local-pr-web/index.d.ts.map +1 -0
- package/dist/src/local-pr-web/index.js +5 -0
- package/dist/src/local-pr-web/index.js.map +1 -0
- package/dist/src/local-pr-web/server.d.ts +126 -0
- package/dist/src/local-pr-web/server.d.ts.map +1 -0
- package/dist/src/local-pr-web/server.js +394 -0
- package/dist/src/local-pr-web/server.js.map +1 -0
- package/dist/src/local-pr-web/types.d.ts +11 -0
- package/dist/src/local-pr-web/types.d.ts.map +1 -0
- package/dist/src/local-pr-web/types.js +2 -0
- package/dist/src/local-pr-web/types.js.map +1 -0
- package/dist/src/mcp/schemas.d.ts +79 -4
- package/dist/src/mcp/schemas.d.ts.map +1 -1
- package/dist/src/mcp/schemas.js +80 -2
- package/dist/src/mcp/schemas.js.map +1 -1
- package/dist/src/mcp/server.d.ts.map +1 -1
- package/dist/src/mcp/server.js +13 -2
- package/dist/src/mcp/server.js.map +1 -1
- package/dist/src/mcp/tools/pr.d.ts +5 -0
- package/dist/src/mcp/tools/pr.d.ts.map +1 -0
- package/dist/src/mcp/tools/pr.js +165 -0
- package/dist/src/mcp/tools/pr.js.map +1 -0
- package/dist/src/mcp/tools/review-passthrough.d.ts.map +1 -1
- package/dist/src/mcp/tools/review-passthrough.js +264 -15
- package/dist/src/mcp/tools/review-passthrough.js.map +1 -1
- package/dist/src/memory/user-profile-store.d.ts.map +1 -1
- package/dist/src/memory/user-profile-store.js +3 -4
- package/dist/src/memory/user-profile-store.js.map +1 -1
- package/dist/src/review/context-collector.d.ts +1 -1
- package/dist/src/review/context-collector.js +1 -1
- package/dist/src/review/passthrough-engine.d.ts +1 -0
- package/dist/src/review/passthrough-engine.d.ts.map +1 -1
- package/dist/src/review/passthrough-engine.js +7 -2
- package/dist/src/review/passthrough-engine.js.map +1 -1
- package/dist/src/spec/text-based-spec-generator.d.ts.map +1 -1
- package/dist/src/spec/text-based-spec-generator.js +5 -2
- package/dist/src/spec/text-based-spec-generator.js.map +1 -1
- package/package.json +2 -1
- package/plugin/.codex-plugin/plugin.json +1 -1
- package/plugin/review-agents/quality-reviewer/AGENT.md +1 -1
- package/plugin/review-agents/writing-reviewer/AGENT.md +2 -2
- package/plugin/skills/local-pr/SKILL.md +201 -0
- package/plugin/skills/pr/SKILL.md +73 -4
- package/plugin/skills/review/SKILL.md +190 -18
- package/plugin/skills/review-reply/SKILL.md +144 -7
|
@@ -0,0 +1,385 @@
|
|
|
1
|
+
import { threadsOf, unresolvedCount } from '../local-pr/policy.js';
|
|
2
|
+
/**
|
|
3
|
+
* 로컬 PR 웹 UI의 HTML을 만드는 순수 함수 모음.
|
|
4
|
+
*
|
|
5
|
+
* graph-viz/html-generator.ts와 같은 자리다 — 프레임워크 없이 문자열을 이어 붙여
|
|
6
|
+
* 자기완결적인 HTML을 만든다. 다른 점은 여기 들어가는 텍스트(제목, 코멘트 본문)는
|
|
7
|
+
* 전부 리뷰어가 입력한 신뢰할 수 없는 값이라는 것이다. escapeHtml을 거치지 않은
|
|
8
|
+
* 텍스트를 그대로 꽂으면 코멘트에 심은 `<script>`가 브라우저에서 그대로 실행된다.
|
|
9
|
+
*/
|
|
10
|
+
/** HTML 텍스트 노드/속성에 안전하게 꽂도록 이스케이프한다 */
|
|
11
|
+
export function escapeHtml(input) {
|
|
12
|
+
return input
|
|
13
|
+
.replace(/&/g, '&')
|
|
14
|
+
.replace(/</g, '<')
|
|
15
|
+
.replace(/>/g, '>')
|
|
16
|
+
.replace(/"/g, '"')
|
|
17
|
+
.replace(/'/g, ''');
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* `<script>` 태그 안에 JSON을 꽂을 때 쓴다.
|
|
21
|
+
*
|
|
22
|
+
* JSON.stringify는 `<`, `>`를 이스케이프하지 않는다. diff나 코멘트 본문에
|
|
23
|
+
* `</script><script>...` 가 들어 있으면 JSON.stringify만으로는 스크립트 태그를
|
|
24
|
+
* 조기 종료시켜 뒤에 오는 문자열이 그대로 코드로 실행된다. 유니코드 이스케이프로
|
|
25
|
+
* 그 시퀀스를 깨서 막는다.
|
|
26
|
+
*/
|
|
27
|
+
const SCRIPT_UNSAFE = {
|
|
28
|
+
'<': '\\u003c',
|
|
29
|
+
'>': '\\u003e',
|
|
30
|
+
'&': '\\u0026',
|
|
31
|
+
};
|
|
32
|
+
export function toEmbeddableJson(value) {
|
|
33
|
+
// 한 번만 훑는다. 패스마다 새 문자열이 생겨서 diff처럼 큰 값이면 원본의 몇 배를
|
|
34
|
+
// 순간적으로 잡는다
|
|
35
|
+
return JSON.stringify(value).replace(/[<>&]/g, (c) => SCRIPT_UNSAFE[c]);
|
|
36
|
+
}
|
|
37
|
+
function pageShell(title, body, extraHead = '') {
|
|
38
|
+
return `<!DOCTYPE html>
|
|
39
|
+
<html lang="ko">
|
|
40
|
+
<head>
|
|
41
|
+
<meta charset="UTF-8" />
|
|
42
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
43
|
+
<title>${escapeHtml(title)}</title>
|
|
44
|
+
<style>
|
|
45
|
+
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
|
46
|
+
body {
|
|
47
|
+
background: #0d1117;
|
|
48
|
+
color: #e6edf3;
|
|
49
|
+
font-family: 'Segoe UI', system-ui, -apple-system, sans-serif;
|
|
50
|
+
line-height: 1.5;
|
|
51
|
+
padding: 24px 32px 64px;
|
|
52
|
+
}
|
|
53
|
+
a { color: #58a6ff; text-decoration: none; }
|
|
54
|
+
a:hover { text-decoration: underline; }
|
|
55
|
+
/* 다크 배경 위 브라우저 기본 포커스 링은 거의 안 보인다. 키보드로 훑는 사람이
|
|
56
|
+
지금 어디 있는지 알아야 한다 */
|
|
57
|
+
a:focus-visible { outline: 2px solid #58a6ff; outline-offset: 2px; text-decoration: underline; }
|
|
58
|
+
h1 { font-size: 20px; margin-bottom: 16px; }
|
|
59
|
+
h2 { font-size: 15px; color: #8b949e; margin: 28px 0 10px; text-transform: uppercase; letter-spacing: 0.04em; }
|
|
60
|
+
table { width: 100%; border-collapse: collapse; }
|
|
61
|
+
th, td { text-align: left; padding: 10px 12px; border-bottom: 1px solid #30363d; font-size: 14px; }
|
|
62
|
+
th { color: #8b949e; font-weight: 600; font-size: 12px; text-transform: uppercase; }
|
|
63
|
+
tr:hover td, tr:focus-within td { background: #161b22; }
|
|
64
|
+
.badge {
|
|
65
|
+
display: inline-block;
|
|
66
|
+
padding: 2px 8px;
|
|
67
|
+
border-radius: 999px;
|
|
68
|
+
font-size: 11px;
|
|
69
|
+
font-weight: 600;
|
|
70
|
+
text-transform: uppercase;
|
|
71
|
+
}
|
|
72
|
+
.badge-open { background: #1f6feb33; color: #58a6ff; }
|
|
73
|
+
.badge-changes_requested { background: #f8514933; color: #ff7b72; }
|
|
74
|
+
.badge-merged { background: #23863633; color: #3fb950; }
|
|
75
|
+
.badge-closed { background: #21262d; color: #c9d1d9; }
|
|
76
|
+
.badge-approve { background: #23863633; color: #3fb950; }
|
|
77
|
+
.badge-request_changes { background: #f8514933; color: #ff7b72; }
|
|
78
|
+
.badge-comment { background: #21262d; color: #c9d1d9; }
|
|
79
|
+
.meta { color: #8b949e; font-size: 13px; }
|
|
80
|
+
.sr-only { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0; }
|
|
81
|
+
/* #484f58은 이 배경에서 약 2.2대 1이라 AA에 크게 못 미친다. 빈 상태를 알리는
|
|
82
|
+
유일한 문구가 가장 안 보이면 안 된다 */
|
|
83
|
+
.empty { color: #8b949e; padding: 40px 0; text-align: center; }
|
|
84
|
+
.card {
|
|
85
|
+
background: #161b22;
|
|
86
|
+
border: 1px solid #30363d;
|
|
87
|
+
border-radius: 8px;
|
|
88
|
+
padding: 16px 18px;
|
|
89
|
+
margin-bottom: 12px;
|
|
90
|
+
}
|
|
91
|
+
.card-header { display: flex; justify-content: space-between; align-items: baseline; margin-bottom: 6px; }
|
|
92
|
+
.body-text { white-space: pre-wrap; word-break: break-word; font-size: 13px; }
|
|
93
|
+
.thread { border-left: 2px solid #30363d; padding-left: 14px; margin-bottom: 18px; }
|
|
94
|
+
.comment + .comment { margin-top: 10px; padding-top: 10px; border-top: 1px dashed #30363d; }
|
|
95
|
+
.path { font-family: 'JetBrains Mono', 'Fira Code', monospace; color: #8b949e; font-size: 12px; }
|
|
96
|
+
#diff-container { margin-top: 8px; }
|
|
97
|
+
ul.repos { list-style: none; padding: 0; margin: 0 0 16px; display: flex; gap: 12px; flex-wrap: wrap; }
|
|
98
|
+
ul.repos li { font-size: 13px; }
|
|
99
|
+
</style>
|
|
100
|
+
${extraHead}
|
|
101
|
+
</head>
|
|
102
|
+
<body>
|
|
103
|
+
<main>
|
|
104
|
+
${body}
|
|
105
|
+
</main>
|
|
106
|
+
</body>
|
|
107
|
+
</html>`;
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* 배지 클래스는 아는 값에서만 고른다.
|
|
111
|
+
*
|
|
112
|
+
* escapeHtml은 공백을 안 건드리므로 값에 공백이 섞이면 class 자리에 다른 클래스가
|
|
113
|
+
* 함께 들어간다. 지금은 타입이 유니온이라 안 나지만 이 값은 디스크의 이벤트를
|
|
114
|
+
* 되감아 만든 것이고 타입은 런타임 보증이 아니다.
|
|
115
|
+
*/
|
|
116
|
+
const STATUS_CLASS = {
|
|
117
|
+
open: 'badge-open',
|
|
118
|
+
changes_requested: 'badge-changes_requested',
|
|
119
|
+
merged: 'badge-merged',
|
|
120
|
+
closed: 'badge-closed',
|
|
121
|
+
};
|
|
122
|
+
const VERDICT_CLASS = {
|
|
123
|
+
approve: 'badge-approve',
|
|
124
|
+
request_changes: 'badge-request_changes',
|
|
125
|
+
comment: 'badge-comment',
|
|
126
|
+
};
|
|
127
|
+
/** ISO 원문은 읽기 부담이 크다. datetime 속성에 원본을 남기고 화면에는 짧게 보인다 */
|
|
128
|
+
function formatTime(iso) {
|
|
129
|
+
const at = new Date(iso);
|
|
130
|
+
if (Number.isNaN(at.getTime()))
|
|
131
|
+
return iso;
|
|
132
|
+
return at.toISOString().replace('T', ' ').slice(0, 16);
|
|
133
|
+
}
|
|
134
|
+
function statusBadge(status) {
|
|
135
|
+
const cls = STATUS_CLASS[status] ?? 'badge-closed';
|
|
136
|
+
return `<span class="badge ${cls}">${escapeHtml(status)}</span>`;
|
|
137
|
+
}
|
|
138
|
+
function verdictBadge(verdict) {
|
|
139
|
+
const cls = VERDICT_CLASS[verdict] ?? 'badge-comment';
|
|
140
|
+
return `<span class="badge ${cls}">${escapeHtml(verdict)}</span>`;
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* diff 렌더러는 CDN에서 받는다. 버전을 고정하고 SRI를 붙인다.
|
|
144
|
+
*
|
|
145
|
+
* 이 스크립트는 로컬 PR 서버와 같은 오리진에서 돌아 PR 본문 전체에 닿는다. 버전을
|
|
146
|
+
* 안 묶으면 CDN이 오염되거나 새 major가 나가는 순간 리뷰 중인 비공개 소스가 남의
|
|
147
|
+
* 코드 손에 들어간다. integrity가 안 맞으면 브라우저가 실행을 막는다. 그때는 아래
|
|
148
|
+
* 부트스트랩이 원본 diff를 그대로 보여준다.
|
|
149
|
+
*
|
|
150
|
+
* highlight.js 테마를 함께 받는 이유는 highlightCode가 hljs 클래스를 붙이는데
|
|
151
|
+
* 짝이 되는 CSS가 없으면 토큰 색이 상속돼 흰 배경 위 흰 글자가 되기 때문이다.
|
|
152
|
+
*/
|
|
153
|
+
const DIFF2HTML_VERSION = '3.4.51';
|
|
154
|
+
const DIFF2HTML_CSS = `https://cdn.jsdelivr.net/npm/diff2html@${DIFF2HTML_VERSION}/bundles/css/diff2html.min.css`;
|
|
155
|
+
const DIFF2HTML_CSS_SRI = 'sha384-iBvSlI3tNrrSIy7s6mvLg+5B2Z/QXbR4L0Pzg1nRf8zkXrz5JF316MLm2igMIpi2';
|
|
156
|
+
const DIFF2HTML_JS = `https://cdn.jsdelivr.net/npm/diff2html@${DIFF2HTML_VERSION}/bundles/js/diff2html-ui.min.js`;
|
|
157
|
+
const DIFF2HTML_JS_SRI = 'sha384-ZfUgCQ5nDmqyzTrOvgM5FJEYXzdtWqL4LcBa1/+07S5i9JnJDmrrZfIoJQN5uWSh';
|
|
158
|
+
const HLJS_CSS = 'https://cdn.jsdelivr.net/npm/highlight.js@11.11.1/styles/github-dark.min.css';
|
|
159
|
+
const HLJS_CSS_SRI = 'sha384-wH75j6z1lH97ZOpMOInqhgKzFkAInZPPSPlZpYKYTOqsaizPvhQZmAtLcPKXpLyH';
|
|
160
|
+
/**
|
|
161
|
+
* HTML에 인라인으로 심을 diff의 상한.
|
|
162
|
+
*
|
|
163
|
+
* 문서 크기가 diff 크기를 그대로 따라간다. 브라우저가 그 전체를 한 번에 파싱해
|
|
164
|
+
* 메인 스레드를 잡는다. 넘는 만큼은 잘라내고 전체를 보는 법을 안내한다.
|
|
165
|
+
*/
|
|
166
|
+
const MAX_INLINE_DIFF_BYTES = 512 * 1024;
|
|
167
|
+
/**
|
|
168
|
+
* diff를 상한까지 자른다.
|
|
169
|
+
*
|
|
170
|
+
* 바이트로 잰다. `diff.length`는 UTF-16 코드 유닛이라 한글 diff에서는 문자당 2바이트가
|
|
171
|
+
* 넘어 실제 페이로드가 상한의 두 배를 넘긴다. Buffer로 자르면 멀티바이트 시퀀스가
|
|
172
|
+
* 중간에서 갈릴 수 있다. `toString`이 그 자리를 접어주고 남는 고아 서로게이트는
|
|
173
|
+
* 뒤에서 걷어낸다.
|
|
174
|
+
*
|
|
175
|
+
* 자를 때는 마지막 파일 경계까지만 준다. diff2html은 관대해서 잘린 hunk에도 예외를
|
|
176
|
+
* 안 내고 그냥 헤더가 약속한 줄 수보다 적게 그린다 — 리뷰어가 없는 변경을 봤다고
|
|
177
|
+
* 판단하는 자리라 파싱이 성립하는 자리에서 끊는다.
|
|
178
|
+
*/
|
|
179
|
+
function clipDiff(diff) {
|
|
180
|
+
// byteLength는 할당 없이 센다. Buffer.from으로 재면 자를 필요가 없는 대다수 경우에도
|
|
181
|
+
// diff 길이만큼 힙을 잡는다 — git 쪽 maxBuffer가 64MB라 그만큼까지 커진다
|
|
182
|
+
if (Buffer.byteLength(diff, 'utf-8') <= MAX_INLINE_DIFF_BYTES) {
|
|
183
|
+
return { shownDiff: diff, truncated: false };
|
|
184
|
+
}
|
|
185
|
+
let shown = Buffer.from(diff, 'utf-8')
|
|
186
|
+
.subarray(0, MAX_INLINE_DIFF_BYTES)
|
|
187
|
+
.toString('utf-8')
|
|
188
|
+
.replace(/[\uD800-\uDBFF]$/, '');
|
|
189
|
+
const lastFile = shown.lastIndexOf('\ndiff --git ');
|
|
190
|
+
if (lastFile > 0)
|
|
191
|
+
shown = shown.slice(0, lastFile + 1);
|
|
192
|
+
return { shownDiff: shown, truncated: true };
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* 레포 사이를 옮겨 다니는 줄.
|
|
196
|
+
*
|
|
197
|
+
* 레포가 하나뿐이어도 안내 한 줄은 남긴다. PR을 만들기만 하고 `pr serve`를 안 돌린
|
|
198
|
+
* 레포는 목록에 안 뜨는데, 아무 표시가 없으면 왜 없는지 알 길이 없다.
|
|
199
|
+
*/
|
|
200
|
+
function repoNavHtml(repos) {
|
|
201
|
+
const hint = '<p class="meta">여기 목록은 <code>gestalt pr serve</code>를 한 번이라도 돌린 레포만 보여요</p>';
|
|
202
|
+
if (repos.length <= 1)
|
|
203
|
+
return hint;
|
|
204
|
+
const items = repos
|
|
205
|
+
.map((r) => {
|
|
206
|
+
const label = `${escapeHtml(r.name)} <span class="meta">${r.openCount}</span>`;
|
|
207
|
+
return `<li>${r.active ? `<strong>${label}</strong>` : `<a href="/r/${escapeHtml(r.key)}">${label}</a>`}</li>`;
|
|
208
|
+
})
|
|
209
|
+
.join('\n ');
|
|
210
|
+
return `<nav aria-label="레포"><ul class="repos">\n ${items}\n </ul>\n ${hint}</nav>`;
|
|
211
|
+
}
|
|
212
|
+
/** `GET /r/:key` — PR 목록 페이지 */
|
|
213
|
+
export function generatePrListHtml(prs, repos = []) {
|
|
214
|
+
const base = repos.find((r) => r.active)?.key ?? '';
|
|
215
|
+
const rows = prs
|
|
216
|
+
.map((pr) => {
|
|
217
|
+
const round = pr.rounds[pr.rounds.length - 1];
|
|
218
|
+
const unresolved = unresolvedCount(pr);
|
|
219
|
+
return `<tr>
|
|
220
|
+
<td><a href="/r/${escapeHtml(base)}/prs/${encodeURIComponent(pr.id)}">${escapeHtml(pr.id)}</a></td>
|
|
221
|
+
<td><a href="/r/${escapeHtml(base)}/prs/${encodeURIComponent(pr.id)}">${escapeHtml(pr.title)}</a></td>
|
|
222
|
+
<td>${statusBadge(pr.status)}</td>
|
|
223
|
+
<td>${round.number}</td>
|
|
224
|
+
<td>${unresolved}</td>
|
|
225
|
+
<td class="meta">${escapeHtml(pr.author)}</td>
|
|
226
|
+
</tr>`;
|
|
227
|
+
})
|
|
228
|
+
.join('\n');
|
|
229
|
+
const body = `
|
|
230
|
+
${repoNavHtml(repos)}
|
|
231
|
+
<h1>로컬 PR</h1>
|
|
232
|
+
${prs.length === 0
|
|
233
|
+
? '<p class="empty">아직 PR이 없어요</p>'
|
|
234
|
+
: `<table>
|
|
235
|
+
<caption class="sr-only">로컬 PR 목록</caption>
|
|
236
|
+
<thead>
|
|
237
|
+
<tr><th scope="col">ID</th><th scope="col">제목</th><th scope="col">상태</th><th scope="col">라운드</th><th scope="col">미해결</th><th scope="col">작성자</th></tr>
|
|
238
|
+
</thead>
|
|
239
|
+
<tbody>
|
|
240
|
+
${rows}
|
|
241
|
+
</tbody>
|
|
242
|
+
</table>`}`;
|
|
243
|
+
return pageShell('로컬 PR 목록', body);
|
|
244
|
+
}
|
|
245
|
+
function renderRounds(pr) {
|
|
246
|
+
return pr.rounds
|
|
247
|
+
.map((round) => {
|
|
248
|
+
const reviews = pr.reviews.filter((r) => r.round === round.number);
|
|
249
|
+
const reviewItems = reviews.length === 0
|
|
250
|
+
? '<div class="meta">판정 없음</div>'
|
|
251
|
+
: reviews
|
|
252
|
+
.map((r) => `<div class="card">
|
|
253
|
+
<div class="card-header">
|
|
254
|
+
<strong>${escapeHtml(r.reviewer)}</strong>
|
|
255
|
+
${verdictBadge(r.verdict)}
|
|
256
|
+
</div>
|
|
257
|
+
<div class="body-text">${escapeHtml(r.summary)}</div>
|
|
258
|
+
</div>`)
|
|
259
|
+
.join('\n');
|
|
260
|
+
return `<h2>라운드 ${round.number} — ${round.verdict ? escapeHtml(round.verdict) : '진행 중'} (코멘트 ${round.commentCount})</h2>
|
|
261
|
+
${reviewItems}`;
|
|
262
|
+
})
|
|
263
|
+
.join('\n');
|
|
264
|
+
}
|
|
265
|
+
function renderThreads(pr) {
|
|
266
|
+
const threads = threadsOf(pr);
|
|
267
|
+
if (threads.length === 0)
|
|
268
|
+
return '<p class="empty">코멘트가 없어요</p>';
|
|
269
|
+
return threads
|
|
270
|
+
.map((thread) => {
|
|
271
|
+
// 배지는 policy의 open으로 찍는다. 첫 코멘트의 resolved로 찍으면 뿌리가 닫히고
|
|
272
|
+
// 답글이 열린 스레드에서 목록 페이지의 미해결 수와 갈린다
|
|
273
|
+
const resolved = thread.open ? '열림' : '해결';
|
|
274
|
+
// 자리는 스레드가 처음 붙은 코멘트에서 가져온다. 답글은 라인을 안 갖는다
|
|
275
|
+
const anchor = thread.all[0];
|
|
276
|
+
const at = anchor.line === null ? anchor.path : `${anchor.path}:${anchor.line}`;
|
|
277
|
+
const items = thread.all
|
|
278
|
+
.map((c) => `<div class="comment">
|
|
279
|
+
<div class="card-header">
|
|
280
|
+
<strong>${escapeHtml(c.author)}</strong>
|
|
281
|
+
<time class="meta" datetime="${escapeHtml(c.createdAt)}">${escapeHtml(formatTime(c.createdAt))}</time>
|
|
282
|
+
</div>
|
|
283
|
+
<div class="body-text">${escapeHtml(c.body)}</div>
|
|
284
|
+
</div>`)
|
|
285
|
+
.join('\n');
|
|
286
|
+
return `<div class="thread">
|
|
287
|
+
<div class="meta"><span class="path">${escapeHtml(at)}</span> — ${resolved}</div>
|
|
288
|
+
${items}
|
|
289
|
+
</div>`;
|
|
290
|
+
})
|
|
291
|
+
.join('\n');
|
|
292
|
+
}
|
|
293
|
+
/** `GET /prs/:id` — diff와 코멘트 스레드, 라운드별 판정 이력 */
|
|
294
|
+
export function generatePrDetailHtml(pr, diff, repoKey = '', repoName = '', nonce = '') {
|
|
295
|
+
// CSP를 붙이면 인라인 스크립트가 nonce 없이는 안 돈다. nonce를 안 주면 속성도 안
|
|
296
|
+
// 붙는다 — 헤더 없이 이 함수만 쓰는 자리(테스트)가 그대로 동작해야 한다
|
|
297
|
+
const nonceAttr = nonce ? ` nonce="${escapeHtml(nonce)}"` : '';
|
|
298
|
+
const extraHead = `
|
|
299
|
+
<link rel="stylesheet" href="${DIFF2HTML_CSS}" integrity="${DIFF2HTML_CSS_SRI}" crossorigin="anonymous" />
|
|
300
|
+
<link rel="stylesheet" href="${HLJS_CSS}" integrity="${HLJS_CSS_SRI}" crossorigin="anonymous" />
|
|
301
|
+
<script src="${DIFF2HTML_JS}" integrity="${DIFF2HTML_JS_SRI}" crossorigin="anonymous"></script>
|
|
302
|
+
`;
|
|
303
|
+
const { shownDiff, truncated } = clipDiff(diff);
|
|
304
|
+
const body = `
|
|
305
|
+
<nav aria-label="브레드크럼"><a href="/r/${escapeHtml(repoKey)}">← ${repoName ? `${escapeHtml(repoName)}의 PR 목록` : 'PR 목록'}</a></nav>
|
|
306
|
+
<h1>${escapeHtml(pr.title)} <span class="meta">#${escapeHtml(pr.id)}</span></h1>
|
|
307
|
+
<p class="meta">
|
|
308
|
+
${statusBadge(pr.status)} 작성 ${escapeHtml(pr.author)} ·
|
|
309
|
+
base ${escapeHtml(pr.baseRef ?? pr.baseSha.slice(0, 8))} →
|
|
310
|
+
head ${escapeHtml(pr.headRef ?? pr.headSha.slice(0, 8))}
|
|
311
|
+
</p>
|
|
312
|
+
${pr.body ? `<div class="card body-text">${escapeHtml(pr.body)}</div>` : ''}
|
|
313
|
+
|
|
314
|
+
<h2>Diff</h2>
|
|
315
|
+
<section id="diff-container" role="region" aria-label="변경 내용" aria-busy="true"></section>
|
|
316
|
+
|
|
317
|
+
<h2>라운드</h2>
|
|
318
|
+
${renderRounds(pr)}
|
|
319
|
+
|
|
320
|
+
<h2>코멘트</h2>
|
|
321
|
+
${renderThreads(pr)}
|
|
322
|
+
|
|
323
|
+
<script${nonceAttr}>
|
|
324
|
+
(function () {
|
|
325
|
+
var diffString = ${toEmbeddableJson(shownDiff)};
|
|
326
|
+
var truncated = ${truncated};
|
|
327
|
+
var target = document.getElementById('diff-container');
|
|
328
|
+
var addNote = function () {
|
|
329
|
+
if (!truncated) return;
|
|
330
|
+
var note = document.createElement('p');
|
|
331
|
+
note.className = 'empty';
|
|
332
|
+
note.setAttribute('role', 'status');
|
|
333
|
+
note.textContent = 'diff가 커서 앞부분만 보여드려요. 전체는 gestalt pr diff로 봐주세요.';
|
|
334
|
+
target.insertBefore(note, target.firstChild);
|
|
335
|
+
};
|
|
336
|
+
var done = function () {
|
|
337
|
+
addNote();
|
|
338
|
+
target.setAttribute('aria-busy', 'false');
|
|
339
|
+
};
|
|
340
|
+
|
|
341
|
+
if (!diffString) {
|
|
342
|
+
target.innerHTML = '<p class="empty">변경 없음</p>';
|
|
343
|
+
done();
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// 스크립트를 못 받아오면(오프라인, 막힌 사내망) 아무 말 없이 빈 상자가 남는다.
|
|
348
|
+
// 원본 diff라도 보여줘야 "변경이 없다"와 구분된다
|
|
349
|
+
if (typeof Diff2HtmlUI === 'undefined') {
|
|
350
|
+
var pre = document.createElement('pre');
|
|
351
|
+
pre.className = 'path';
|
|
352
|
+
pre.textContent = diffString;
|
|
353
|
+
target.innerHTML = '<p class="empty">diff 렌더러를 못 불러왔어요 — 원본을 그대로 보여드려요</p>';
|
|
354
|
+
target.appendChild(pre);
|
|
355
|
+
done();
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
try {
|
|
360
|
+
var ui = new Diff2HtmlUI(target, diffString, {
|
|
361
|
+
drawFileList: true,
|
|
362
|
+
matching: 'lines',
|
|
363
|
+
outputFormat: 'line-by-line',
|
|
364
|
+
// 라이브러리가 wrapper에 d2h-dark-color-scheme를 붙이고 --d2h-dark-* 변수를
|
|
365
|
+
// 켠다. 기본값이 light라 안 넘기면 다크 셸 안에 라이트 패널이 박힌다.
|
|
366
|
+
// CSS 변수를 밖에서 덮는 방법은 안 통한다 — --d2h-bg-color를 읽는 셀렉터는
|
|
367
|
+
// 줄 번호와 태그뿐이고 추가와 삭제 줄은 --d2h-ins-bg-color 쪽을 읽는다
|
|
368
|
+
colorScheme: 'dark',
|
|
369
|
+
});
|
|
370
|
+
ui.draw();
|
|
371
|
+
ui.highlightCode();
|
|
372
|
+
} catch (e) {
|
|
373
|
+
var raw = document.createElement('pre');
|
|
374
|
+
raw.className = 'path';
|
|
375
|
+
raw.textContent = diffString;
|
|
376
|
+
target.innerHTML = '';
|
|
377
|
+
target.appendChild(raw);
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
done();
|
|
381
|
+
})();
|
|
382
|
+
</script>`;
|
|
383
|
+
return pageShell(pr.title, body, extraHead);
|
|
384
|
+
}
|
|
385
|
+
//# sourceMappingURL=html-generator.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"html-generator.js","sourceRoot":"","sources":["../../../src/local-pr-web/html-generator.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export * from './types.js';
|
|
2
|
+
export { escapeHtml, toEmbeddableJson, generatePrListHtml, generatePrDetailHtml, } from './html-generator.js';
|
|
3
|
+
export { PrWebServer } from './server.js';
|
|
4
|
+
export { PrWebEngine, getPrWebEngine } from './engine.js';
|
|
5
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/local-pr-web/index.ts"],"names":[],"mappings":"AAAA,cAAc,YAAY,CAAC;AAC3B,OAAO,EACL,UAAU,EACV,gBAAgB,EAChB,kBAAkB,EAClB,oBAAoB,GACrB,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAC1C,OAAO,EAAE,WAAW,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC"}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export * from './types.js';
|
|
2
|
+
export { escapeHtml, toEmbeddableJson, generatePrListHtml, generatePrDetailHtml, } from './html-generator.js';
|
|
3
|
+
export { PrWebServer } from './server.js';
|
|
4
|
+
export { PrWebEngine, getPrWebEngine } from './engine.js';
|
|
5
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/local-pr-web/index.ts"],"names":[],"mappings":"AAAA,cAAc,YAAY,CAAC;AAC3B,OAAO,EACL,UAAU,EACV,gBAAgB,EAChB,kBAAkB,EAClB,oBAAoB,GACrB,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAC1C,OAAO,EAAE,WAAW,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC"}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import type { LocalPrEngine } from '../local-pr/engine.js';
|
|
2
|
+
import type { RegisteredRepo } from '../local-pr/registry.js';
|
|
3
|
+
/**
|
|
4
|
+
* 로컬 PR을 브라우저에서 읽는 HTTP 서버. 읽기 전용이다.
|
|
5
|
+
*
|
|
6
|
+
* graph-viz/server.ts와 같은 자리다. 다른 점은 데이터를 생성 시점에 한 번 굳히지
|
|
7
|
+
* 않고 매 요청마다 LocalPrEngine에서 다시 읽는다는 것 — PR 목록은 서버가 떠 있는
|
|
8
|
+
* 동안에도 다른 워커가 코멘트를 달거나 판정을 남기며 바뀐다.
|
|
9
|
+
*
|
|
10
|
+
* 레포 여럿을 한 서버가 보여준다. URL에 실리는 건 경로가 아니라 등록된 레포의
|
|
11
|
+
* 키다 — 요청이 경로를 지정할 수 있으면 인증 없는 이 서버가 이 머신의 아무 git
|
|
12
|
+
* 레포나 읽어주는 도구가 된다. 모르는 키는 404다.
|
|
13
|
+
*
|
|
14
|
+
* Routes:
|
|
15
|
+
* GET / → `pr serve`를 친 레포의 목록으로 302
|
|
16
|
+
* GET /r/:key → 그 레포의 PR 목록 HTML
|
|
17
|
+
* GET /r/:key/prs/:id → PR 상세 HTML (diff, 코멘트 스레드, 라운드별 판정)
|
|
18
|
+
* GET /api/repos → 등록된 레포 목록 JSON
|
|
19
|
+
* GET /api/r/:key/prs → PR 목록 JSON
|
|
20
|
+
* GET /api/r/:key/prs/:id → PR 상세 JSON
|
|
21
|
+
* * → 404
|
|
22
|
+
*/
|
|
23
|
+
export declare class PrWebServer {
|
|
24
|
+
private engines;
|
|
25
|
+
private primaryKey;
|
|
26
|
+
private server;
|
|
27
|
+
private sigintHandler;
|
|
28
|
+
private diffCache;
|
|
29
|
+
/**
|
|
30
|
+
* 레포 탭에 붙는 열린 PR 수. 마지막으로 센 값과 그 시각이다.
|
|
31
|
+
*
|
|
32
|
+
* 목록 페이지 한 번에 탭마다 `engine.list('open')`이 돌면 그 레포의 이벤트를
|
|
33
|
+
* 통째로 다시 접는다. 배지 숫자 하나 때문에 PR 전체를 복원하는 셈이다. 그 비용이
|
|
34
|
+
* 등록된 레포 수만큼 곱해진다.
|
|
35
|
+
*
|
|
36
|
+
* 이벤트 저장소에 집계 경로를 내는 길도 있었다. 안 골랐다 — `EventStore`는 로컬 PR
|
|
37
|
+
* 바깥에서도 쓰는 공용 모듈이라 배지 하나 때문에 조회 API를 늘리게 된다. 여기서는
|
|
38
|
+
* 짧은 TTL로 충분하다. 지금 보는 레포는 이 캐시를 아예 안 탄다 — 같은 요청이 이미
|
|
39
|
+
* 만든 목록에서 센다. 그래서 뒤처질 수 있는 건 남의 레포 배지뿐이다.
|
|
40
|
+
*/
|
|
41
|
+
private navCounts;
|
|
42
|
+
/** 남의 레포 배지를 붙잡아 두는 시간 */
|
|
43
|
+
private static readonly NAV_COUNT_TTL_MS;
|
|
44
|
+
/**
|
|
45
|
+
* @param engines 레포 키 → 엔진. 시작할 때 정해지고 요청이 못 늘린다
|
|
46
|
+
* @param primaryKey `pr serve`를 친 자리의 레포. `/`가 여기로 보낸다
|
|
47
|
+
*/
|
|
48
|
+
constructor(engines: Map<string, {
|
|
49
|
+
repo: RegisteredRepo;
|
|
50
|
+
engine: LocalPrEngine;
|
|
51
|
+
}>, primaryKey: string);
|
|
52
|
+
/** Start the server on the given port. Resolves when listening. */
|
|
53
|
+
start(port: number): Promise<void>;
|
|
54
|
+
/** Stop the server gracefully. Safe to call even if server was never started. */
|
|
55
|
+
stop(): Promise<void>;
|
|
56
|
+
/** Port the server is listening on, or undefined if not started. */
|
|
57
|
+
get port(): number | undefined;
|
|
58
|
+
/** 퍼센트 인코딩이 깨져 있으면 null. 부르는 쪽이 400으로 접는다 */
|
|
59
|
+
private static safeDecode;
|
|
60
|
+
private handleRequest;
|
|
61
|
+
/**
|
|
62
|
+
* 레포 탭 줄. 지금 보는 레포의 숫자는 `activePrs`에서 세고 나머지는 TTL 캐시를 탄다.
|
|
63
|
+
*
|
|
64
|
+
* @param activePrs 이 요청이 이미 만든 목록. 같은 레포를 두 번 접지 않으려고 받는다
|
|
65
|
+
*/
|
|
66
|
+
private repoNav;
|
|
67
|
+
private cachedOpenCount;
|
|
68
|
+
/**
|
|
69
|
+
* PR의 diff. 같은 sha 쌍이면 캐시에서 준다.
|
|
70
|
+
*
|
|
71
|
+
* 캐시 키에 레포를 함께 넣는다. 등록된 자리 둘이 같은 레포의 클론이면 sha 쌍이
|
|
72
|
+
* 겹친다. 지금은 두 자리의 diff가 어차피 같다. 그래도 캐시가 레포 경계를 넘어
|
|
73
|
+
* 값을 옮기는 구조 자체를 안 두는 편이 낫다. 한쪽에만 객체가 없는 경우처럼
|
|
74
|
+
* 같은 sha가 다른 결과를 내는 자리가 있다.
|
|
75
|
+
*/
|
|
76
|
+
private diffOf;
|
|
77
|
+
/**
|
|
78
|
+
* HTML을 내보낸다.
|
|
79
|
+
*
|
|
80
|
+
* CSP를 붙이는 이유는 이 화면이 리뷰어가 쓴 텍스트를 담고 같은 오리진에 CDN
|
|
81
|
+
* 스크립트를 끌어오기 때문이다. SRI가 CDN 변조는 막지만 이스케이프가 한 군데라도
|
|
82
|
+
* 빠지면 남는 방어선이 없다. 인라인 스크립트는 nonce로만 허용한다.
|
|
83
|
+
*
|
|
84
|
+
* `frame-ancestors`는 아무 페이지나 이 뷰어를 iframe으로 띄우는 것을 막는다.
|
|
85
|
+
* 키를 아는 쪽이 화면을 읽어가는 경로다.
|
|
86
|
+
*/
|
|
87
|
+
private sendHtml;
|
|
88
|
+
/**
|
|
89
|
+
* JSON 응답.
|
|
90
|
+
*
|
|
91
|
+
* CORS 헤더를 안 붙인다. 이 API를 부르는 건 같은 오리진에서 뜬 자기 페이지뿐이라
|
|
92
|
+
* 필요가 없다. 와일드카드를 열면 127.0.0.1 바인딩으로 얻은 격리가 풀린다 —
|
|
93
|
+
* 사용자가 열어둔 아무 웹페이지나 리뷰 중인 비공개 소스를 읽어갈 수 있다.
|
|
94
|
+
*/
|
|
95
|
+
private sendJson;
|
|
96
|
+
private notFound;
|
|
97
|
+
private registerSigintHandler;
|
|
98
|
+
private removeSigintHandler;
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* diff 캐시.
|
|
102
|
+
*
|
|
103
|
+
* diff는 base와 head 두 sha에 대해 불변이라 캐시 키가 공짜로 주어져 있다. 안 쓰면
|
|
104
|
+
* 새로고침마다 execFileSync로 git을 부르는데, 동기 spawn이라 그동안 이벤트 루프가
|
|
105
|
+
* 통째로 멈춰 다른 요청도 함께 기다린다.
|
|
106
|
+
*
|
|
107
|
+
* 개수와 바이트를 함께 묶는다. 개수만 묶으면 `git.diff`의 maxBuffer가 64MB라
|
|
108
|
+
* 32개가 이론상 2GB가 된다. 예산보다 큰 diff 하나는 아예 안 담는다 — 담아 봐야
|
|
109
|
+
* 남은 걸 전부 밀어내고도 예산을 넘긴다.
|
|
110
|
+
*
|
|
111
|
+
* 상한을 생성자로 받는 건 테스트가 작은 값으로 밀어내기를 확인하려고 그렇다.
|
|
112
|
+
*/
|
|
113
|
+
export declare class DiffCache {
|
|
114
|
+
private readonly maxEntries;
|
|
115
|
+
private readonly maxBytes;
|
|
116
|
+
private entries;
|
|
117
|
+
private bytes;
|
|
118
|
+
constructor(maxEntries?: number, maxBytes?: number);
|
|
119
|
+
get(key: string): string | undefined;
|
|
120
|
+
set(key: string, value: string): void;
|
|
121
|
+
/** 담고 있는 항목 수 */
|
|
122
|
+
get size(): number;
|
|
123
|
+
/** 담고 있는 바이트 합 */
|
|
124
|
+
get byteSize(): number;
|
|
125
|
+
}
|
|
126
|
+
//# sourceMappingURL=server.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../../src/local-pr-web/server.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAC3D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,yBAAyB,CAAC;AAc9D;;;;;;;;;;;;;;;;;;;GAmBG;AACH,qBAAa,WAAW;IA2BpB,OAAO,CAAC,OAAO;IACf,OAAO,CAAC,UAAU;IA3BpB,OAAO,CAAC,MAAM,CAAuB;IACrC,OAAO,CAAC,aAAa,CAA6B;IAClD,OAAO,CAAC,SAAS,CAAmB;IAEpC;;;;;;;;;;;OAWG;IACH,OAAO,CAAC,SAAS,CAAoD;IAErE,0BAA0B;IAC1B,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,gBAAgB,CAAS;IAEjD;;;OAGG;gBAEO,OAAO,EAAE,GAAG,CAAC,MAAM,EAAE;QAAE,IAAI,EAAE,cAAc,CAAC;QAAC,MAAM,EAAE,aAAa,CAAA;KAAE,CAAC,EACrE,UAAU,EAAE,MAAM;IAG5B,mEAAmE;IACnE,KAAK,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAiClC,iFAAiF;IACjF,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAmBrB,oEAAoE;IACpE,IAAI,IAAI,IAAI,MAAM,GAAG,SAAS,CAI7B;IAID,6CAA6C;IAC7C,OAAO,CAAC,MAAM,CAAC,UAAU;IAQzB,OAAO,CAAC,aAAa;IA0GrB;;;;OAIG;IACH,OAAO,CAAC,OAAO;IAef,OAAO,CAAC,eAAe;IAWvB;;;;;;;OAOG;IACH,OAAO,CAAC,MAAM;IAed;;;;;;;;;OASG;IACH,OAAO,CAAC,QAAQ;IAqBhB;;;;;;OAMG;IACH,OAAO,CAAC,QAAQ;IAShB,OAAO,CAAC,QAAQ;IAQhB,OAAO,CAAC,qBAAqB;IAW7B,OAAO,CAAC,mBAAmB;CAM5B;AAED;;;;;;;;;;;;GAYG;AACH,qBAAa,SAAS;IAKlB,OAAO,CAAC,QAAQ,CAAC,UAAU;IAC3B,OAAO,CAAC,QAAQ,CAAC,QAAQ;IAL3B,OAAO,CAAC,OAAO,CAAsD;IACrE,OAAO,CAAC,KAAK,CAAK;gBAGC,UAAU,SAAK,EACf,QAAQ,SAAkB;IAG7C,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS;IAIpC,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI;IAyBrC,iBAAiB;IACjB,IAAI,IAAI,IAAI,MAAM,CAEjB;IAED,kBAAkB;IAClB,IAAI,QAAQ,IAAI,MAAM,CAErB;CACF"}
|