@letrquan/book 0.2.0 → 0.3.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/CHANGELOG.md +2747 -40
- package/README.md +132 -1311
- package/dist/additional-roots-H7UNAEHS.js +31 -0
- package/dist/{app-MOXXWEOY.js → app-GE4OHJOC.js} +5740 -4045
- package/dist/app-GE4OHJOC.js.map +1 -0
- package/dist/{chunk-5GDZ22YP.js → chunk-224FJZLH.js} +99 -108
- package/dist/chunk-224FJZLH.js.map +1 -0
- package/dist/chunk-2AAOF5B5.js +28 -0
- package/dist/chunk-2AAOF5B5.js.map +1 -0
- package/dist/{chunk-CAGOB2N4.js → chunk-4AS7LKKQ.js} +6 -4
- package/dist/chunk-4AS7LKKQ.js.map +1 -0
- package/dist/chunk-525CAZPY.js +120 -0
- package/dist/chunk-525CAZPY.js.map +1 -0
- package/dist/{chunk-URLQAXTR.js → chunk-56SIEMDX.js} +2 -2
- package/dist/chunk-56SIEMDX.js.map +1 -0
- package/dist/{chunk-XIFFTATU.js → chunk-5YHTKWJN.js} +5420 -4031
- package/dist/chunk-5YHTKWJN.js.map +1 -0
- package/dist/chunk-62QL5QUP.js +283 -0
- package/dist/chunk-62QL5QUP.js.map +1 -0
- package/dist/{chunk-KWHQ2XXL.js → chunk-67UZEWWF.js} +3 -3
- package/dist/chunk-67UZEWWF.js.map +1 -0
- package/dist/chunk-6OQQOMZR.js +785 -0
- package/dist/chunk-6OQQOMZR.js.map +1 -0
- package/dist/{chunk-GTVB4HEM.js → chunk-AQEQX4FF.js} +344 -315
- package/dist/chunk-AQEQX4FF.js.map +1 -0
- package/dist/chunk-C7ADUNPR.js +273 -0
- package/dist/chunk-C7ADUNPR.js.map +1 -0
- package/dist/{chunk-E75IPST5.js → chunk-EFJ2NH6E.js} +12 -58
- package/dist/chunk-EFJ2NH6E.js.map +1 -0
- package/dist/{chunk-5KLVH3PY.js → chunk-HQI7NYGX.js} +5 -3
- package/dist/chunk-HQI7NYGX.js.map +1 -0
- package/dist/chunk-IME7R2UI.js +6541 -0
- package/dist/chunk-IME7R2UI.js.map +1 -0
- package/dist/chunk-J4F2QLM2.js +273 -0
- package/dist/chunk-J4F2QLM2.js.map +1 -0
- package/dist/{chunk-MAQXCUR4.js → chunk-J62VZYOB.js} +444 -112
- package/dist/chunk-J62VZYOB.js.map +1 -0
- package/dist/chunk-JTPOI3KU.js +826 -0
- package/dist/chunk-JTPOI3KU.js.map +1 -0
- package/dist/{chunk-WKBO5R4O.js → chunk-KBQS35OV.js} +2 -2
- package/dist/chunk-KBQS35OV.js.map +1 -0
- package/dist/chunk-KXAYUIKP.js +90 -0
- package/dist/chunk-KXAYUIKP.js.map +1 -0
- package/dist/{chunk-VZQXX3WW.js → chunk-QYBD22H5.js} +1 -1
- package/dist/chunk-QYBD22H5.js.map +1 -0
- package/dist/{chunk-V4BOYR52.js → chunk-RFJ6Q6LE.js} +763 -198
- package/dist/chunk-RFJ6Q6LE.js.map +1 -0
- package/dist/{chunk-7N4J557C.js → chunk-RT5SATF7.js} +1 -1
- package/dist/chunk-RT5SATF7.js.map +1 -0
- package/dist/{chunk-C7D4NJ2D.js → chunk-S4JMID4G.js} +68 -26
- package/dist/chunk-S4JMID4G.js.map +1 -0
- package/dist/{chunk-ANSJPYGI.js → chunk-SOW4WRQP.js} +3 -3
- package/dist/chunk-SOW4WRQP.js.map +1 -0
- package/dist/{chunk-J7MIZ5CA.js → chunk-TLX3H2CL.js} +518 -79
- package/dist/chunk-TLX3H2CL.js.map +1 -0
- package/dist/{chunk-DGMQEI6Y.js → chunk-VPNLCMVR.js} +22 -9
- package/dist/chunk-VPNLCMVR.js.map +1 -0
- package/dist/{chunk-BTZZUCLI.js → chunk-W3XFHEF4.js} +173 -29
- package/dist/chunk-W3XFHEF4.js.map +1 -0
- package/dist/{chunk-574C6MQR.js → chunk-X4NJT436.js} +130 -60
- package/dist/chunk-X4NJT436.js.map +1 -0
- package/dist/chunk-X66I2NQO.js +137 -0
- package/dist/chunk-X66I2NQO.js.map +1 -0
- package/dist/chunk-XHAO3K7I.js +18 -0
- package/dist/chunk-XHAO3K7I.js.map +1 -0
- package/dist/{chunk-3U5IBM24.js → chunk-Y2VHN7TC.js} +2 -2
- package/dist/chunk-Y2VHN7TC.js.map +1 -0
- package/dist/chunk-YC6W4EJH.js +947 -0
- package/dist/chunk-YC6W4EJH.js.map +1 -0
- package/dist/{chunk-24AXE6SP.js → chunk-ZPSZZUWF.js} +1 -1
- package/dist/chunk-ZPSZZUWF.js.map +1 -0
- package/dist/{command-approvals-6Y57VCWZ.js → command-approvals-VBIU6BLM.js} +6 -5
- package/dist/context-CBOBDY5Q.js +38 -0
- package/dist/{hook-approvals-CTUFKEJT.js → hook-approvals-ZVT4V7IM.js} +5 -5
- package/dist/index.js +996 -155
- package/dist/index.js.map +1 -1
- package/dist/{interactive-assets-CCAAWVKH.js → interactive-assets-T4I4SEHU.js} +13 -9
- package/dist/interactive-assets-T4I4SEHU.js.map +1 -0
- package/dist/job-runner.js +159 -111
- package/dist/job-runner.js.map +1 -1
- package/dist/job-supervisor.d.ts +2 -0
- package/dist/job-supervisor.js +112 -0
- package/dist/job-supervisor.js.map +1 -0
- package/dist/loader-S77OVTHH.js +18 -0
- package/dist/{mcp-approvals-WNPH65BK.js → mcp-approvals-HJ2VDYMC.js} +5 -5
- package/dist/{mcp-config-YIJIES2C.js → mcp-config-RNB5LEVE.js} +3 -3
- package/dist/memory-store-DZUT5HUH.js +75 -0
- package/dist/{model-window-store-YFVNXCIP.js → model-window-store-MTSQ5JDJ.js} +8 -4
- package/dist/openai-compatible-2OB54YST.js +14 -0
- package/dist/{permission-approvals-76PVREFD.js → permission-approvals-YLV6UC5W.js} +5 -5
- package/dist/permissions-DHOKF3W2.js +45 -0
- package/dist/sandbox-WRYTZH3D.js +41 -0
- package/dist/{scrollback-LUKLIBVI.js → scrollback-35ME2GFX.js} +32 -22
- package/dist/scrollback-35ME2GFX.js.map +1 -0
- package/dist/sdk.d.ts +2219 -2361
- package/dist/sdk.js +53 -38
- package/dist/sdk.js.map +1 -1
- package/dist/settings-loader-JUNPGAAM.js +35 -0
- package/dist/settings-loader-JUNPGAAM.js.map +1 -0
- package/dist/{shell-selection-V6QAYXPX.js → shell-selection-CDHGJ4GC.js} +2 -2
- package/dist/shell-selection-CDHGJ4GC.js.map +1 -0
- package/package.json +23 -22
- package/dist/app-MOXXWEOY.js.map +0 -1
- package/dist/chunk-24AXE6SP.js.map +0 -1
- package/dist/chunk-3U5IBM24.js.map +0 -1
- package/dist/chunk-574C6MQR.js.map +0 -1
- package/dist/chunk-5GDZ22YP.js.map +0 -1
- package/dist/chunk-5KLVH3PY.js.map +0 -1
- package/dist/chunk-7N4J557C.js.map +0 -1
- package/dist/chunk-A7JLBK2Y.js +0 -3506
- package/dist/chunk-A7JLBK2Y.js.map +0 -1
- package/dist/chunk-ANSJPYGI.js.map +0 -1
- package/dist/chunk-BTZZUCLI.js.map +0 -1
- package/dist/chunk-C7D4NJ2D.js.map +0 -1
- package/dist/chunk-CAGOB2N4.js.map +0 -1
- package/dist/chunk-DGMQEI6Y.js.map +0 -1
- package/dist/chunk-E75IPST5.js.map +0 -1
- package/dist/chunk-EYGD6X2T.js +0 -180
- package/dist/chunk-EYGD6X2T.js.map +0 -1
- package/dist/chunk-GTVB4HEM.js.map +0 -1
- package/dist/chunk-J7MIZ5CA.js.map +0 -1
- package/dist/chunk-KWHQ2XXL.js.map +0 -1
- package/dist/chunk-L6LK2QIQ.js +0 -657
- package/dist/chunk-L6LK2QIQ.js.map +0 -1
- package/dist/chunk-MAQXCUR4.js.map +0 -1
- package/dist/chunk-S4XL7HOM.js +0 -402
- package/dist/chunk-S4XL7HOM.js.map +0 -1
- package/dist/chunk-URLQAXTR.js.map +0 -1
- package/dist/chunk-V4BOYR52.js.map +0 -1
- package/dist/chunk-VZQXX3WW.js.map +0 -1
- package/dist/chunk-WKBO5R4O.js.map +0 -1
- package/dist/chunk-XIFFTATU.js.map +0 -1
- package/dist/context-ZC5IFEAX.js +0 -25
- package/dist/interactive-assets-CCAAWVKH.js.map +0 -1
- package/dist/loader-4IDS2RIE.js +0 -15
- package/dist/openai-compatible-RHZUBHV6.js +0 -10
- package/dist/permissions-CHCKTFQQ.js +0 -28
- package/dist/sandbox-CNVAQDA7.js +0 -23
- package/dist/scrollback-LUKLIBVI.js.map +0 -1
- package/dist/settings-loader-M2QP5TX2.js +0 -21
- package/patches/ink+6.8.0.patch +0 -13
- package/scripts/apply-ink-patch.mjs +0 -29
- package/scripts/ink-patch.mjs +0 -59
- package/scripts/verify-ink-patch.mjs +0 -11
- /package/dist/{command-approvals-6Y57VCWZ.js.map → additional-roots-H7UNAEHS.js.map} +0 -0
- /package/dist/{context-ZC5IFEAX.js.map → command-approvals-VBIU6BLM.js.map} +0 -0
- /package/dist/{hook-approvals-CTUFKEJT.js.map → context-CBOBDY5Q.js.map} +0 -0
- /package/dist/{loader-4IDS2RIE.js.map → hook-approvals-ZVT4V7IM.js.map} +0 -0
- /package/dist/{mcp-approvals-WNPH65BK.js.map → loader-S77OVTHH.js.map} +0 -0
- /package/dist/{mcp-config-YIJIES2C.js.map → mcp-approvals-HJ2VDYMC.js.map} +0 -0
- /package/dist/{model-window-store-YFVNXCIP.js.map → mcp-config-RNB5LEVE.js.map} +0 -0
- /package/dist/{openai-compatible-RHZUBHV6.js.map → memory-store-DZUT5HUH.js.map} +0 -0
- /package/dist/{permission-approvals-76PVREFD.js.map → model-window-store-MTSQ5JDJ.js.map} +0 -0
- /package/dist/{permissions-CHCKTFQQ.js.map → openai-compatible-2OB54YST.js.map} +0 -0
- /package/dist/{sandbox-CNVAQDA7.js.map → permission-approvals-YLV6UC5W.js.map} +0 -0
- /package/dist/{settings-loader-M2QP5TX2.js.map → permissions-DHOKF3W2.js.map} +0 -0
- /package/dist/{shell-selection-V6QAYXPX.js.map → sandbox-WRYTZH3D.js.map} +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/clock.ts"],"sourcesContent":["/**\n * Time that measures, separated from time that labels.\n *\n * `Date.now()` is the settable wall clock. NTP steps it, a VM resumes with it\n * wrong and then corrects, an operator fixes a drifted host, DST changes the\n * offset. None of that matters over a five-minute chat, and all of it is\n * near-certain over the multi-day runs Book is built for — which is why every\n * *duration* decision here reads a monotonic source instead.\n *\n * What the two are for:\n *\n * - **`monotonicNowMs()`** — \"how long has this been going?\" Retry budgets,\n * flush deadlines, kill timeouts, elapsed run time. It only ever moves\n * forward, at a rate nothing can adjust, and its origin is arbitrary, so a\n * reading is meaningless on its own and comparable only to another reading\n * *from the same process*.\n * - **`wallNowMs()`** — \"when did this happen?\" Anything written to a file, put\n * on screen, or compared against a stamp another process wrote. Still\n * `Date.now()`; the name exists so the choice is visible at the call site\n * rather than implied by its absence.\n *\n * **The origin is per-process, and that is the whole limit.** Two processes'\n * monotonic clocks share no zero, so a monotonic reading cannot be persisted,\n * sent, or compared across a process boundary. Cross-process liveness — the\n * background-shell heartbeat, the run-status file `book status` reads, the\n * retention sweeps — therefore stays on the wall clock by necessity, not by\n * oversight. A backwards step there reads as \"unexpectedly fresh\" and a forward\n * step can declare a live run stale; fixing that needs a sequence counter in the\n * file, not a better clock. See `MILESTONES.md`.\n *\n * Injected rather than imported directly at the decision sites, per the\n * module-level-mutable-state rule: a caller that needs to control time passes a\n * `Clock`, and tests do exactly that instead of reaching for fake timers.\n *\n * One trap worth knowing when writing those tests: vitest's default\n * `vi.useFakeTimers()` fakes `performance` too, so the monotonic clock stops\n * with the fake wall clock and a test cannot tell the two apart. Use\n * `vi.useFakeTimers({ toFake: ['Date'] })` to move the wall clock while the\n * monotonic one keeps its own time — that divergence is the thing under test.\n */\n\n/** A monotonic reading, in milliseconds. Only comparable within one process. */\nexport type MonotonicMs = number;\n\nexport interface Clock {\n /** Milliseconds since an arbitrary fixed origin. Never goes backwards. */\n monotonicNowMs(): MonotonicMs;\n /** Milliseconds since the Unix epoch. Settable; use only for labels. */\n wallNowMs(): number;\n}\n\n/**\n * `performance.now()` is monotonic in Node — it reads `uv_hrtime`, which is\n * unaffected by `settimeofday` — and is millisecond-resolution floating point,\n * which is finer than any deadline here needs.\n */\nexport const systemClock: Clock = {\n monotonicNowMs: () => performance.now(),\n wallNowMs: () => Date.now(),\n};\n\n/**\n * A clock a test drives by hand. Both hands start at the given values and move\n * only when told, so a test can advance one without the other — which is the\n * only way to show a duration decision is immune to a wall-clock step.\n */\nexport function createTestClock(options?: { monotonicMs?: number; wallMs?: number }): Clock & {\n advanceMonotonic(ms: number): void;\n setWall(ms: number): void;\n} {\n let monotonic = options?.monotonicMs ?? 0;\n let wall = options?.wallMs ?? 1_700_000_000_000;\n return {\n monotonicNowMs: () => monotonic,\n wallNowMs: () => wall,\n advanceMonotonic(ms: number) {\n // Guard the invariant the production clock has for free: a test that\n // rewinds this is testing something that cannot happen.\n if (ms < 0) throw new Error('monotonic time cannot move backwards');\n monotonic += ms;\n },\n setWall(ms: number) {\n wall = ms;\n },\n };\n}\n"],"mappings":";AAwDO,IAAM,cAAqB;AAAA,EAChC,gBAAgB,MAAM,YAAY,IAAI;AAAA,EACtC,WAAW,MAAM,KAAK,IAAI;AAC5B;","names":[]}
|
|
@@ -1,7 +1,35 @@
|
|
|
1
|
-
// src/cli/ink-
|
|
1
|
+
// src/cli/ink-renderer.ts
|
|
2
|
+
import { readFileSync } from "fs";
|
|
2
3
|
import { createRequire } from "module";
|
|
3
4
|
import { dirname, join } from "path";
|
|
4
5
|
import { pathToFileURL } from "url";
|
|
6
|
+
var INK_TRAILING_NEWLINE_FIX = "ansiEscapes.cursorUp(previousLines.length - 1)";
|
|
7
|
+
function inkBuildDir(from = import.meta.url) {
|
|
8
|
+
const require2 = createRequire(from);
|
|
9
|
+
return dirname(require2.resolve("ink"));
|
|
10
|
+
}
|
|
11
|
+
function inkLogUpdatePath(from) {
|
|
12
|
+
return join(inkBuildDir(from), "log-update.js");
|
|
13
|
+
}
|
|
14
|
+
function hasInkTrailingNewlineFix(from) {
|
|
15
|
+
try {
|
|
16
|
+
return readFileSync(inkLogUpdatePath(from), "utf8").includes(INK_TRAILING_NEWLINE_FIX);
|
|
17
|
+
} catch {
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
async function wrapInkLogUpdate(wrap) {
|
|
22
|
+
try {
|
|
23
|
+
const module = await import(pathToFileURL(inkLogUpdatePath()).href);
|
|
24
|
+
const logUpdate = module.default;
|
|
25
|
+
logUpdate.create = wrap(logUpdate.create);
|
|
26
|
+
return true;
|
|
27
|
+
} catch {
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// src/cli/ink-scroll-renderer.ts
|
|
5
33
|
var pendingHint = null;
|
|
6
34
|
var rendererInstalled = false;
|
|
7
35
|
function isInkScrollRendererEnabled(value) {
|
|
@@ -96,6 +124,11 @@ function createScrollAwareRenderer(stream, options, createBase) {
|
|
|
96
124
|
previousFrame = "";
|
|
97
125
|
base.done();
|
|
98
126
|
};
|
|
127
|
+
render.reset = () => {
|
|
128
|
+
pendingHint = null;
|
|
129
|
+
previousFrame = "";
|
|
130
|
+
base.reset?.();
|
|
131
|
+
};
|
|
99
132
|
render.sync = (output) => {
|
|
100
133
|
pendingHint = null;
|
|
101
134
|
previousFrame = output;
|
|
@@ -114,22 +147,12 @@ async function installInkScrollRenderer(enabled) {
|
|
|
114
147
|
if (rendererInstalled) return;
|
|
115
148
|
if (!(enabled ?? isInkScrollRendererEnabled(process.env.BOOK_SCROLL_RENDERER))) return;
|
|
116
149
|
rendererInstalled = true;
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
const logUpdateUrl = pathToFileURL(join(dirname(inkEntry), "log-update.js")).href;
|
|
121
|
-
const module = await import(logUpdateUrl);
|
|
122
|
-
const logUpdate = module.default;
|
|
123
|
-
const createBase = logUpdate.create;
|
|
124
|
-
logUpdate.create = (stream, options) => createScrollAwareRenderer(stream, options, createBase);
|
|
125
|
-
} catch {
|
|
126
|
-
}
|
|
150
|
+
await wrapInkLogUpdate(
|
|
151
|
+
(createBase) => (stream, options) => createScrollAwareRenderer(stream, options, createBase)
|
|
152
|
+
);
|
|
127
153
|
}
|
|
128
154
|
|
|
129
155
|
// src/cli/frame-buffer.ts
|
|
130
|
-
import { createRequire as createRequire2 } from "module";
|
|
131
|
-
import { dirname as dirname2, join as join2 } from "path";
|
|
132
|
-
import { pathToFileURL as pathToFileURL2 } from "url";
|
|
133
156
|
var latestFrame = "";
|
|
134
157
|
var lastCursorPosition = null;
|
|
135
158
|
var captureInstalled = false;
|
|
@@ -178,6 +201,10 @@ function createFrameCapturingRenderer(stream, options, createBase) {
|
|
|
178
201
|
lastCursorPosition = null;
|
|
179
202
|
base.done();
|
|
180
203
|
};
|
|
204
|
+
render.reset = () => {
|
|
205
|
+
latestFrame = "";
|
|
206
|
+
base.reset?.();
|
|
207
|
+
};
|
|
181
208
|
render.sync = (output) => {
|
|
182
209
|
latestFrame = output;
|
|
183
210
|
base.sync(output);
|
|
@@ -194,24 +221,39 @@ function createFrameCapturingRenderer(stream, options, createBase) {
|
|
|
194
221
|
async function installFrameCapture() {
|
|
195
222
|
if (captureInstalled) return;
|
|
196
223
|
captureInstalled = true;
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
224
|
+
await wrapInkLogUpdate(
|
|
225
|
+
(createBase) => (stream, options) => createFrameCapturingRenderer(stream, options, createBase)
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// src/cli/tui-renderer-mode.ts
|
|
230
|
+
function resolveTuiRendererMode(value = process.env.BOOK_TUI_RENDERER, context = {}) {
|
|
231
|
+
if (context.isTTY === false || context.screenReader) return "safe";
|
|
232
|
+
if (context.incrementalRendererFixed === false) return "safe";
|
|
233
|
+
if (value === "incremental") return "incremental";
|
|
234
|
+
if (value === "experimental-scroll") return "experimental-scroll";
|
|
235
|
+
if (value === void 0) return context.platform === "win32" ? "safe" : "incremental";
|
|
236
|
+
return "safe";
|
|
237
|
+
}
|
|
238
|
+
function isCiEnvironment(env = process.env) {
|
|
239
|
+
const set = (key) => key in env && env[key] !== "0" && env[key] !== "false";
|
|
240
|
+
return set("CI") || set("CONTINUOUS_INTEGRATION");
|
|
241
|
+
}
|
|
242
|
+
function resolveInkMaxFps(platform = process.platform) {
|
|
243
|
+
return platform === "win32" ? 72 : 60;
|
|
207
244
|
}
|
|
208
245
|
|
|
209
246
|
export {
|
|
247
|
+
inkBuildDir,
|
|
248
|
+
hasInkTrailingNewlineFix,
|
|
210
249
|
setTranscriptScrollHint,
|
|
211
250
|
installInkScrollRenderer,
|
|
212
251
|
getFrameLines,
|
|
213
252
|
getLastCursorPosition,
|
|
214
253
|
subscribeToFrameUpdates,
|
|
215
|
-
installFrameCapture
|
|
254
|
+
installFrameCapture,
|
|
255
|
+
resolveTuiRendererMode,
|
|
256
|
+
isCiEnvironment,
|
|
257
|
+
resolveInkMaxFps
|
|
216
258
|
};
|
|
217
|
-
//# sourceMappingURL=chunk-
|
|
259
|
+
//# sourceMappingURL=chunk-S4JMID4G.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/cli/ink-renderer.ts","../src/cli/ink-scroll-renderer.ts","../src/cli/frame-buffer.ts","../src/cli/tui-renderer-mode.ts"],"sourcesContent":["import { readFileSync } from 'node:fs';\nimport { createRequire } from 'node:module';\nimport { dirname, join } from 'node:path';\nimport { pathToFileURL } from 'node:url';\n\n/**\n * The Ink release whose renderer Book's TUI was last verified against. A dependency bump changes\n * `package.json` but not this constant, so the contract test and `npm run verify:ink` fail until\n * someone re-verifies the TUI (docs/guide/development.md, \"Upgrading Ink\") and updates it.\n */\nexport const VERIFIED_INK_VERSION = '7.1.1';\n\n/**\n * The incremental renderer's cursor rewind with Ink's trailing-newline fix (upstream issue 909,\n * shipped in Ink 7.0.0). Without it, a frame that ends in a newline rewinds one row short and\n * later updates land below their rows.\n */\nconst INK_TRAILING_NEWLINE_FIX = 'ansiEscapes.cursorUp(previousLines.length - 1)';\n\n/** The subset of a writable terminal stream Ink's renderer writes through. */\nexport interface InkStream {\n isTTY?: boolean;\n rows?: number;\n write: (data: string) => unknown;\n}\n\n/** Ink's private log-update renderer. Ink 7 added `reset()`; every wrapper must forward it. */\nexport interface LogUpdateRenderer {\n (output: string): boolean | void;\n clear: () => void;\n done: () => void;\n reset?: () => void;\n sync: (output: string) => void;\n setCursorPosition: (position: unknown) => void;\n isCursorDirty: () => boolean;\n willRender: (output: string) => boolean;\n}\n\nexport type CreateLogUpdate = (\n stream: InkStream,\n options?: { incremental?: boolean },\n) => LogUpdateRenderer;\n\n/** Ink's build directory, where its private modules live. Throws when Ink cannot be resolved. */\nexport function inkBuildDir(from: string | URL = import.meta.url): string {\n const require = createRequire(from);\n return dirname(require.resolve('ink'));\n}\n\n/**\n * Ink's private log-update module: the renderer the TUI wraps for frame capture and scroll hints,\n * and the file whose trailing-newline fix decides the renderer mode. Throws when Ink cannot be\n * resolved. `from` is the module Ink is resolved from.\n */\nexport function inkLogUpdatePath(from?: string | URL): string {\n return join(inkBuildDir(from), 'log-update.js');\n}\n\n/** The installed Ink version, or undefined when Ink cannot be resolved. `from` is the module Ink is resolved from. */\nexport function installedInkVersion(from?: string | URL): string | undefined {\n try {\n const manifest = JSON.parse(\n readFileSync(join(inkBuildDir(from), '..', 'package.json'), 'utf8'),\n ) as {\n version?: unknown;\n };\n return typeof manifest.version === 'string' ? manifest.version : undefined;\n } catch {\n return undefined;\n }\n}\n\n/** Incremental rendering is safe only when Ink's renderer carries the trailing-newline fix. `from` is the module Ink is resolved from. */\nexport function hasInkTrailingNewlineFix(from?: string | URL): boolean {\n try {\n return readFileSync(inkLogUpdatePath(from), 'utf8').includes(INK_TRAILING_NEWLINE_FIX);\n } catch {\n return false;\n }\n}\n\n/**\n * Replaces Ink's private `logUpdate.create` with `wrap(original)`, so every renderer Ink creates\n * afterwards goes through the wrapper. Returns false, leaving Ink's renderer in place, when Ink's\n * module layout has changed.\n */\nexport async function wrapInkLogUpdate(\n wrap: (createBase: CreateLogUpdate) => CreateLogUpdate,\n): Promise<boolean> {\n try {\n const module = (await import(pathToFileURL(inkLogUpdatePath()).href)) as {\n default: { create: CreateLogUpdate };\n };\n const logUpdate = module.default;\n logUpdate.create = wrap(logUpdate.create);\n return true;\n } catch {\n return false;\n }\n}\n","import {\n wrapInkLogUpdate,\n type CreateLogUpdate,\n type InkStream,\n type LogUpdateRenderer,\n} from './ink-renderer.js';\n\nexport interface TranscriptScrollHint {\n top: number;\n bottom: number;\n delta: number;\n}\n\nlet pendingHint: TranscriptScrollHint | null = null;\nlet rendererInstalled = false;\n\nexport function isInkScrollRendererEnabled(value?: string): boolean {\n return value === 'on';\n}\n\nexport function setTranscriptScrollHint(hint: TranscriptScrollHint): void {\n if (!Number.isFinite(hint.delta) || hint.delta === 0) return;\n if (\n pendingHint &&\n pendingHint.top === hint.top &&\n pendingHint.bottom === hint.bottom &&\n Math.sign(pendingHint.delta) === Math.sign(hint.delta)\n ) {\n pendingHint = { ...pendingHint, delta: pendingHint.delta + hint.delta };\n return;\n }\n pendingHint = { ...hint };\n}\n\nfunction splitFrame(frame: string): string[] {\n return frame.endsWith('\\n') ? frame.slice(0, -1).split('\\n') : frame.split('\\n');\n}\n\nfunction getFrameEndRow(frame: string, visibleLineCount: number): number {\n return visibleLineCount + (frame.endsWith('\\n') ? 1 : 0);\n}\n\nfunction cursorTo(row: number): string {\n return `\\x1b[${Math.max(1, Math.floor(row))};1H`;\n}\n\nfunction setScrollRegion(top: number, bottom: number): string {\n return `\\x1b[${Math.max(1, Math.floor(top))};${Math.max(1, Math.floor(bottom))}r`;\n}\n\nexport function isPureVerticalShift(\n previousLines: readonly string[],\n nextLines: readonly string[],\n hint: TranscriptScrollHint,\n): boolean {\n if (previousLines.length !== nextLines.length) return false;\n const top = Math.floor(hint.top) - 1;\n const bottom = Math.floor(hint.bottom);\n const delta = Math.trunc(hint.delta);\n if (top < 0 || bottom > nextLines.length || bottom - top < 2) return false;\n if (Math.abs(delta) >= bottom - top) return false;\n\n for (let row = 0; row < top; row++) {\n if (previousLines[row] !== nextLines[row]) return false;\n }\n for (let row = bottom; row < nextLines.length; row++) {\n if (previousLines[row] !== nextLines[row]) return false;\n }\n\n if (delta > 0) {\n for (let row = top; row < bottom - delta; row++) {\n if (nextLines[row] !== previousLines[row + delta]) return false;\n }\n } else {\n const distance = Math.abs(delta);\n for (let row = top + distance; row < bottom; row++) {\n if (nextLines[row] !== previousLines[row - distance]) return false;\n }\n }\n return true;\n}\n\nfunction writeExposedRows(\n stream: InkStream,\n lines: readonly string[],\n top: number,\n bottom: number,\n delta: number,\n): void {\n const distance = Math.abs(delta);\n const first = delta > 0 ? bottom - distance : top;\n for (let offset = 0; offset < distance; offset++) {\n const row = first + offset;\n stream.write(cursorTo(row + 1) + (lines[row] ?? '') + '\\x1b[K');\n }\n}\n\nfunction createScrollAwareRenderer(\n stream: InkStream,\n options: { incremental?: boolean } | undefined,\n createBase: CreateLogUpdate,\n): LogUpdateRenderer {\n const base = createBase(stream, options);\n let previousFrame = '';\n\n const render = ((output: string) => {\n const hint = pendingHint;\n pendingHint = null;\n const previousLines = splitFrame(previousFrame);\n const nextLines = splitFrame(output);\n const terminalRows = stream.rows ?? 0;\n const frameEndRow = getFrameEndRow(output, nextLines.length);\n const canScroll =\n Boolean(stream.isTTY) &&\n terminalRows > 0 &&\n frameEndRow <= terminalRows &&\n previousFrame.length > 0 &&\n hint !== null &&\n hint.bottom <= nextLines.length &&\n isPureVerticalShift(previousLines, nextLines, hint);\n\n if (canScroll && hint) {\n const top = Math.floor(hint.top);\n const bottom = Math.floor(hint.bottom);\n const delta = Math.trunc(hint.delta);\n stream.write('\\x1b[?25l' + setScrollRegion(top, bottom));\n stream.write(`\\x1b[${Math.abs(delta)}${delta > 0 ? 'S' : 'T'}`);\n writeExposedRows(stream, nextLines, top - 1, bottom, delta);\n // Ink's sync() positions the input cursor relative to the frame bottom.\n // Re-anchor there first so its cursor suffix cannot repaint the prompt on\n // a transcript row after a terminal-native scroll.\n stream.write(setScrollRegion(1, terminalRows) + cursorTo(frameEndRow));\n previousFrame = output;\n base.sync(output);\n return true;\n }\n\n previousFrame = output;\n return base(output);\n }) as LogUpdateRenderer;\n\n render.clear = () => {\n pendingHint = null;\n previousFrame = '';\n base.clear();\n };\n render.done = () => {\n pendingHint = null;\n previousFrame = '';\n base.done();\n };\n render.reset = () => {\n pendingHint = null;\n previousFrame = '';\n base.reset?.();\n };\n render.sync = (output: string) => {\n pendingHint = null;\n previousFrame = output;\n base.sync(output);\n };\n render.setCursorPosition = (position: unknown) => base.setCursorPosition(position);\n render.isCursorDirty = () => base.isCursorDirty();\n render.willRender = (output: string) => {\n const shouldRender = base.willRender(output);\n if (!shouldRender) pendingHint = null;\n return shouldRender;\n };\n return render;\n}\n\nexport async function installInkScrollRenderer(enabled?: boolean): Promise<void> {\n if (rendererInstalled) return;\n // DECSTBM/CSI S/T rendering remains available for profiling, but Windows\n // ConPTY can corrupt fixed footer rows after a later incremental repaint.\n if (!(enabled ?? isInkScrollRendererEnabled(process.env.BOOK_SCROLL_RENDERER))) return;\n rendererInstalled = true;\n\n // Keep Ink's built-in renderer when its internal module layout changes:\n // wrapInkLogUpdate reports false and leaves Ink's own renderer in place.\n await wrapInkLogUpdate(\n (createBase) => (stream, options) => createScrollAwareRenderer(stream, options, createBase),\n );\n}\n","import {\n wrapInkLogUpdate,\n type CreateLogUpdate,\n type InkStream,\n type LogUpdateRenderer,\n} from './ink-renderer.js';\n\nexport interface FrameCursorPosition {\n x: number;\n y: number;\n}\n\nlet latestFrame = '';\nlet lastCursorPosition: FrameCursorPosition | null = null;\nlet captureInstalled = false;\nconst frameUpdateListeners = new Set<() => void>();\n\nfunction notifyFrameUpdate(): void {\n for (const listener of frameUpdateListeners) {\n try {\n listener();\n } catch {\n // A terminal overlay must never break Ink's renderer.\n }\n }\n}\n\nfunction splitFrame(frame: string): string[] {\n return frame.endsWith('\\n') ? frame.slice(0, -1).split('\\n') : frame.split('\\n');\n}\n\nfunction toCursorPosition(position: unknown): FrameCursorPosition | null {\n if (!position || typeof position !== 'object') return null;\n const candidate = position as { x?: unknown; y?: unknown };\n if (typeof candidate.x !== 'number' || typeof candidate.y !== 'number') return null;\n return { x: candidate.x, y: candidate.y };\n}\n\n/** Lines of the most recently rendered full-screen frame. */\nexport function getFrameLines(): string[] {\n return latestFrame.length === 0 ? [] : splitFrame(latestFrame);\n}\n\n/** Last cursor position Ink asked the renderer to restore, if any. */\nexport function getLastCursorPosition(): FrameCursorPosition | null {\n return lastCursorPosition;\n}\n\n/** Subscribe to completed Ink frame writes, used to preserve terminal overlays. */\nexport function subscribeToFrameUpdates(listener: () => void): () => void {\n frameUpdateListeners.add(listener);\n return () => frameUpdateListeners.delete(listener);\n}\n\n/** Override the captured snapshot. Used by focused selection tests. */\nexport function setFrameSnapshotForTesting(\n frame: string | null,\n cursor: FrameCursorPosition | null = null,\n): void {\n latestFrame = frame ?? '';\n lastCursorPosition = cursor;\n}\n\nexport function createFrameCapturingRenderer(\n stream: InkStream,\n options: { incremental?: boolean } | undefined,\n createBase: CreateLogUpdate,\n): LogUpdateRenderer {\n const base = createBase(stream, options);\n\n const render = ((output: string) => {\n latestFrame = output;\n const rendered = base(output);\n if (rendered !== false) notifyFrameUpdate();\n return rendered;\n }) as LogUpdateRenderer;\n\n render.clear = () => {\n latestFrame = '';\n base.clear();\n };\n render.done = () => {\n latestFrame = '';\n lastCursorPosition = null;\n base.done();\n };\n // Ink 7 resets its renderer's memory of the last frame after handing the terminal to a child.\n render.reset = () => {\n latestFrame = '';\n base.reset?.();\n };\n render.sync = (output: string) => {\n latestFrame = output;\n base.sync(output);\n notifyFrameUpdate();\n };\n render.setCursorPosition = (position: unknown) => {\n lastCursorPosition = toCursorPosition(position);\n base.setCursorPosition(position);\n };\n render.isCursorDirty = () => base.isCursorDirty();\n render.willRender = (output: string) => base.willRender(output);\n return render;\n}\n\n/** Capture Ink's latest full frame so mouse selection can extract visible text. */\nexport async function installFrameCapture(): Promise<void> {\n if (captureInstalled) return;\n captureInstalled = true;\n\n // Keep the TUI usable if Ink changes its private renderer layout: wrapInkLogUpdate reports\n // false and leaves Ink's own renderer in place.\n await wrapInkLogUpdate(\n (createBase) => (stream, options) => createFrameCapturingRenderer(stream, options, createBase),\n );\n}\n","export type TuiRendererMode = 'safe' | 'incremental' | 'experimental-scroll';\n\ninterface TuiRendererContext {\n isTTY?: boolean;\n screenReader?: boolean;\n incrementalRendererFixed?: boolean;\n platform?: NodeJS.Platform;\n}\n\nexport function resolveTuiRendererMode(\n value = process.env.BOOK_TUI_RENDERER,\n context: TuiRendererContext = {},\n): TuiRendererMode {\n if (context.isTTY === false || context.screenReader) return 'safe';\n if (context.incrementalRendererFixed === false) return 'safe';\n if (value === 'incremental') return 'incremental';\n if (value === 'experimental-scroll') return 'experimental-scroll';\n if (value === undefined) return context.platform === 'win32' ? 'safe' : 'incremental';\n return 'safe';\n}\n\n/**\n * Ink 6 rendered live frames unless it detected CI; Ink 7 also requires a TTY stdout. Book runs its\n * TUI on a non-TTY stdout when a Windows build is launched from WSL, so it passes Ink 6's rule to\n * `render({ interactive })` itself. Same test as Ink's `is-in-ci`.\n */\nexport function isCiEnvironment(env: NodeJS.ProcessEnv = process.env): boolean {\n const set = (key: string) => key in env && env[key] !== '0' && env[key] !== 'false';\n return set('CI') || set('CONTINUOUS_INTEGRATION');\n}\n\n/**\n * The frame cap handed to Ink's `render({ maxFps })`. Ink throttles renders to\n * `ceil(1000 / maxFps)` ms, and on Windows libuv timers fire only on the 15.6 ms system tick:\n * the 17 ms that 60 fps asks for waits two ticks (31 ms) and caps the TUI at about 32 fps, so a\n * transcript scrolled with the wheel or a trackpad moves in visible steps. 72 fps asks for\n * 14 ms, which one tick serves. Elsewhere timers are precise and 60 fps means 60.\n */\nexport function resolveInkMaxFps(platform: NodeJS.Platform = process.platform): number {\n return platform === 'win32' ? 72 : 60;\n}\n"],"mappings":"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","names":["require","splitFrame"]}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import {
|
|
2
2
|
updateWorkspaceTrust
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-VPNLCMVR.js";
|
|
4
4
|
import {
|
|
5
5
|
HOOK_EVENTS
|
|
6
|
-
} from "./chunk-
|
|
6
|
+
} from "./chunk-X4NJT436.js";
|
|
7
7
|
|
|
8
8
|
// src/hook-approvals.ts
|
|
9
9
|
import { createHash } from "crypto";
|
|
@@ -81,4 +81,4 @@ export {
|
|
|
81
81
|
describeDeclaredHook,
|
|
82
82
|
persistProjectHookChoice
|
|
83
83
|
};
|
|
84
|
-
//# sourceMappingURL=chunk-
|
|
84
|
+
//# sourceMappingURL=chunk-SOW4WRQP.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/hook-approvals.ts"],"sourcesContent":["/**\n * Trust decisions for hook entries declared by a repository.\n *\n * A hook entry is a shell command the repository can get Book to execute at any\n * lifecycle event — on every prompt, around every tool call, at session start.\n * Once merged into resolved settings an entry carries no provenance, so nothing\n * downstream can tell a repository's hook from the user's own.\n *\n * Project-declared hooks therefore require a one-time per-workspace decision,\n * keyed by a fingerprint of the full entry (event, matcher, command, env) and\n * recorded in the user-global trust store, outside the workspace, where nothing\n * the repository ships can reach it (see `workspace-trust.ts`). Any change to\n * an entry produces a new fingerprint and reverts it to untrusted.\n *\n * User-global and local-layer hooks need no gate: those layers were written by\n * or for the user.\n */\nimport { createHash } from 'crypto';\nimport { HOOK_EVENTS, type HookEntry, type HookEvent, type ProjectHookChoice } from './settings.js';\nimport { updateWorkspaceTrust } from './workspace-trust.js';\n\n/** Every project-declared hook entry, paired with the event that triggers it. */\nexport interface DeclaredHook {\n event: HookEvent;\n entry: HookEntry;\n}\n\n/** Stable digest of everything the user is asked to trust about a hook. */\nexport function hookFingerprint(event: HookEvent, entry: HookEntry): string {\n const sortedEnv = Object.fromEntries(\n Object.entries(entry.env ?? {}).sort(([a], [b]) => a.localeCompare(b)),\n );\n const canonical = JSON.stringify({\n event,\n matcher: entry.matcher ?? null,\n command: entry.command,\n env: sortedEnv,\n });\n return createHash('sha256').update(canonical).digest('hex').slice(0, 16);\n}\n\n/** Flatten a settings document into its declared hook entries, event paired. */\nexport function collectDeclaredHooks(\n settings:\n | {\n hooks?: Partial<Record<HookEvent, HookEntry[]>> & { projectEntries?: unknown };\n }\n | null\n | undefined,\n): DeclaredHook[] {\n const declared: DeclaredHook[] = [];\n for (const event of HOOK_EVENTS) {\n for (const entry of settings?.hooks?.[event] ?? []) {\n declared.push({ event, entry });\n }\n }\n return declared;\n}\n\nexport type ProjectHookState = ProjectHookChoice | 'unknown';\n\nexport function evaluateProjectHook(\n store: Record<string, ProjectHookChoice> | undefined,\n fingerprint: string,\n): ProjectHookState {\n return store?.[fingerprint] ?? 'unknown';\n}\n\nexport interface ProjectHookPartition {\n /** Approved by the user: these join the resolved hooks. */\n approved: DeclaredHook[];\n /** No decision recorded: withheld until the user makes one. */\n pending: DeclaredHook[];\n /** Explicitly refused: withheld, and not re-offered. */\n rejected: DeclaredHook[];\n}\n\nexport function partitionProjectHooks(\n hooks: readonly DeclaredHook[],\n store: Record<string, ProjectHookChoice> | undefined,\n): ProjectHookPartition {\n const partition: ProjectHookPartition = { approved: [], pending: [], rejected: [] };\n for (const hook of hooks) {\n const state = evaluateProjectHook(store, hookFingerprint(hook.event, hook.entry));\n if (state === 'approved') partition.approved.push(hook);\n else if (state === 'rejected') partition.rejected.push(hook);\n else partition.pending.push(hook);\n }\n return partition;\n}\n\n/**\n * Render repository-authored text for a terminal that is about to ask the user\n * to trust it.\n *\n * A hook command and its environment values are written by the party being\n * gated. Printed raw, a value carrying newlines or ANSI escapes can forge\n * further lines of the report — including an `[x] …` line for a hook that is\n * nothing of the kind. Control characters become visible escapes, and an\n * over-long value is truncated so one entry cannot push the rest of the\n * disclosure off screen.\n */\nexport function renderUntrustedText(value: string, maxLength = 200): string {\n const escaped = value.replace(/[\\u0000-\\u001f\\u007f-\\u009f]/g, (character) => {\n if (character === '\\n') return '\\\\n';\n if (character === '\\r') return '\\\\r';\n if (character === '\\t') return '\\\\t';\n return `\\\\x${character.codePointAt(0)!.toString(16).padStart(2, '0')}`;\n });\n return escaped.length > maxLength ? `${escaped.slice(0, maxLength)}…` : escaped;\n}\n\n/**\n * Everything the fingerprint covers, rendered for a trust decision.\n *\n * The fingerprint digests event, matcher, command *and* env, so a disclosure\n * showing only the command understates what approval grants: `npm test` with\n * `NODE_OPTIONS=--require ./payload.js` reads as harmless and is not. Two\n * entries differing only in matcher or env are also indistinguishable without\n * these lines.\n */\nexport function describeDeclaredHook(hook: DeclaredHook): { headline: string; details: string[] } {\n const details = [`fingerprint: ${hookFingerprint(hook.event, hook.entry)}`];\n if (hook.entry.matcher !== undefined) {\n details.push(`matcher: ${renderUntrustedText(hook.entry.matcher)}`);\n }\n for (const [key, value] of Object.entries(hook.entry.env ?? {}).sort(([a], [b]) =>\n a.localeCompare(b),\n )) {\n details.push(`env: ${renderUntrustedText(key, 64)}=${renderUntrustedText(value)}`);\n }\n return { headline: `${hook.event}: ${renderUntrustedText(hook.entry.command)}`, details };\n}\n\n/**\n * Record an approve/reject decision for one hook, leaving every other recorded\n * decision — in this workspace and in every other — untouched.\n */\nexport function persistProjectHookChoice(\n workspace: string,\n fingerprint: string,\n choice: ProjectHookChoice,\n options: { trustStorePath?: string } = {},\n): { ok: boolean; error?: string } {\n return updateWorkspaceTrust(\n workspace,\n (trust) => {\n trust.hookEntries[fingerprint] = choice;\n },\n options.trustStorePath,\n );\n}\n"],"mappings":";;;;;;;;AAiBA,SAAS,kBAAkB;AAWpB,SAAS,gBAAgB,OAAkB,OAA0B;AAC1E,QAAM,YAAY,OAAO;AAAA,IACvB,OAAO,QAAQ,MAAM,OAAO,CAAC,CAAC,EAAE,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,MAAM,EAAE,cAAc,CAAC,CAAC;AAAA,EACvE;AACA,QAAM,YAAY,KAAK,UAAU;AAAA,IAC/B;AAAA,IACA,SAAS,MAAM,WAAW;AAAA,IAC1B,SAAS,MAAM;AAAA,IACf,KAAK;AAAA,EACP,CAAC;AACD,SAAO,WAAW,QAAQ,EAAE,OAAO,SAAS,EAAE,OAAO,KAAK,EAAE,MAAM,GAAG,EAAE;AACzE;AAGO,SAAS,qBACd,UAMgB;AAChB,QAAM,WAA2B,CAAC;AAClC,aAAW,SAAS,aAAa;AAC/B,eAAW,SAAS,UAAU,QAAQ,KAAK,KAAK,CAAC,GAAG;AAClD,eAAS,KAAK,EAAE,OAAO,MAAM,CAAC;AAAA,IAChC;AAAA,EACF;AACA,SAAO;AACT;AAIO,SAAS,oBACd,OACA,aACkB;AAClB,SAAO,QAAQ,WAAW,KAAK;AACjC;AAWO,SAAS,sBACd,OACA,OACsB;AACtB,QAAM,YAAkC,EAAE,UAAU,CAAC,GAAG,SAAS,CAAC,GAAG,UAAU,CAAC,EAAE;AAClF,aAAW,QAAQ,OAAO;AACxB,UAAM,QAAQ,oBAAoB,OAAO,gBAAgB,KAAK,OAAO,KAAK,KAAK,CAAC;AAChF,QAAI,UAAU,WAAY,WAAU,SAAS,KAAK,IAAI;AAAA,aAC7C,UAAU,WAAY,WAAU,SAAS,KAAK,IAAI;AAAA,QACtD,WAAU,QAAQ,KAAK,IAAI;AAAA,EAClC;AACA,SAAO;AACT;AAaO,SAAS,oBAAoB,OAAe,YAAY,KAAa;AAC1E,QAAM,UAAU,MAAM,QAAQ,iCAAiC,CAAC,cAAc;AAC5E,QAAI,cAAc,KAAM,QAAO;AAC/B,QAAI,cAAc,KAAM,QAAO;AAC/B,QAAI,cAAc,IAAM,QAAO;AAC/B,WAAO,MAAM,UAAU,YAAY,CAAC,EAAG,SAAS,EAAE,EAAE,SAAS,GAAG,GAAG,CAAC;AAAA,EACtE,CAAC;AACD,SAAO,QAAQ,SAAS,YAAY,GAAG,QAAQ,MAAM,GAAG,SAAS,CAAC,WAAM;AAC1E;AAWO,SAAS,qBAAqB,MAA6D;AAChG,QAAM,UAAU,CAAC,gBAAgB,gBAAgB,KAAK,OAAO,KAAK,KAAK,CAAC,EAAE;AAC1E,MAAI,KAAK,MAAM,YAAY,QAAW;AACpC,YAAQ,KAAK,gBAAgB,oBAAoB,KAAK,MAAM,OAAO,CAAC,EAAE;AAAA,EACxE;AACA,aAAW,CAAC,KAAK,KAAK,KAAK,OAAO,QAAQ,KAAK,MAAM,OAAO,CAAC,CAAC,EAAE;AAAA,IAAK,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,MAC3E,EAAE,cAAc,CAAC;AAAA,EACnB,GAAG;AACD,YAAQ,KAAK,gBAAgB,oBAAoB,KAAK,EAAE,CAAC,IAAI,oBAAoB,KAAK,CAAC,EAAE;AAAA,EAC3F;AACA,SAAO,EAAE,UAAU,GAAG,KAAK,KAAK,KAAK,oBAAoB,KAAK,MAAM,OAAO,CAAC,IAAI,QAAQ;AAC1F;AAMO,SAAS,yBACd,WACA,aACA,QACA,UAAuC,CAAC,GACP;AACjC,SAAO;AAAA,IACL;AAAA,IACA,CAAC,UAAU;AACT,YAAM,YAAY,WAAW,IAAI;AAAA,IACnC;AAAA,IACA,QAAQ;AAAA,EACV;AACF;","names":[]}
|