@junyoung-kim/reins 0.1.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/README.md +229 -0
- package/dist/cli.mjs +8598 -0
- package/dist/cli.mjs.map +1 -0
- package/package.json +57 -0
package/dist/cli.mjs.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/cli.tsx","../../../packages/core/src/lib/logger.ts","../../../packages/core/src/managers/ssh-manager.ts","../../../packages/core/src/managers/pty-manager.ts","../../../packages/core/src/managers/scrollback-buffer.ts","../../../packages/core/src/managers/scrollback-persister.ts","../../../packages/shared/src/constants.ts","../../../packages/core/src/managers/grid-emulator.ts","../../../node_modules/.pnpm/@xterm+addon-serialize@0.14.0/node_modules/src/common/Color.ts","../../../node_modules/.pnpm/@xterm+addon-serialize@0.14.0/node_modules/src/browser/Types.ts","../../../node_modules/.pnpm/@xterm+addon-serialize@0.14.0/node_modules/@xterm/addon-serialize/src/SerializeAddon.ts","../../../packages/core/src/protocol/binary-protocol.ts","../../../packages/core/src/managers/pty-lifecycle.ts","../../../packages/core/src/managers/authority-resolver.ts","../../../packages/core/src/managers/git-command.ts","../../../packages/core/src/managers/agent-detector.ts","../../../packages/core/src/managers/agent-sessions.ts","../../../packages/shared/src/utils.ts","../../../packages/core/src/multiplexer/none.ts","../../../packages/core/src/multiplexer/index.ts","../../../packages/core/src/managers/ws-manager.ts","../../../packages/shared/src/relay-config.ts","../../../packages/core/src/managers/pair-url.ts","../../../packages/core/src/managers/agent-runner.ts","../../../packages/core/src/managers/cockpit-parser-utils.ts","../../../packages/core/src/managers/cockpit-parser.ts","../../../packages/core/src/managers/file-upload-receiver.ts","../../../packages/core/src/managers/fcm-token-store.ts","../../../packages/core/src/create-agent-core.ts","../../../packages/core/src/managers/hook-receiver.ts","../../../packages/core/src/managers/claude-hooks.ts","../src/config/json-store.ts","../src/config/json-file.ts","../src/config/load-env.ts","../package.json","../src/identity.ts","../src/service/index.ts","../src/pairing.ts","../src/service/platform.ts","../src/service/systemd.ts","../src/service/unit-template.ts","../src/state.ts","../src/ui/App.tsx","../src/ui/screens/InfoScreen.tsx","../src/ui/constants.ts","../src/ui/pretty-url.ts","../src/ui/screens/MachinesScreen.tsx","../src/ui/components/ConfirmDialog.tsx","../src/ui/screens/MachineFormScreen.tsx","../src/config/validate.ts","../src/ui/components/Field.tsx","../src/ui/components/SelectField.tsx","../src/ui/useFieldFocus.ts","../src/ui/screens/machine-payload.ts","../src/ui/screens/MainScreen.tsx","../src/ui/qr.ts","../src/ui/scroll-window.ts","../src/ui/screens/OptionsScreen.tsx","../src/ui/ManagerApp.tsx","../src/ui/log-tail.ts"],"sourcesContent":["import os from 'node:os'\nimport path from 'node:path'\n\nimport { createAgentCore, setConsoleLogging } from '@arva/core'\nimport { render } from 'ink'\nimport React from 'react'\n\nimport { HeadlessConfigStore } from './config/json-store'\nimport { loadDotEnv } from './config/load-env'\nimport { identity } from './identity'\nimport { isServiceActive, runServiceCommand } from './service'\nimport { HeadlessStore } from './state'\nimport { App } from './ui/App'\nimport { ManagerApp } from './ui/ManagerApp'\n\n/**\n * `reins` 엔트리. 세 경로로 분기한다:\n *\n * 1. `reins service <install|uninstall|status>` — systemd user 서비스 등록만 수행(코어/TUI 없음).\n * 2. 인자 없음 + **서비스 가동 중** — 단일 인스턴스 가드. 두 번째 코어를 띄우지 않고 관리/상태 화면\n * ({@link ManagerApp})만 렌더한다(같은 install_id 이중 relay 접속 footgun 방지).\n * 3. 인자 없음 + 서비스 미가동 — 기존 풀 TUI. createAgentCore 가 로컬 머신 등록 + 로컬 WS 서버 +\n * relay 자동 접속까지 기동하고, App 이 상태 store 를 sink 로 렌더한다.\n *\n * 시크릿: relay Bearer 는 `MAIN_VITE_RELAY_AGENT_SECRET`(또는 `VIBE_RELAY_AGENT_SECRET`) env 로\n * 오버라이드하고, 없으면 빌드 시 박은 기본값으로 fallback 한다(npm 설치 직후 무설정 접속 — json-store 참조).\n */\n// relay 시크릿/URL 을 `.env` → process.env 로(코어·config 가 읽기 전에). 셸 env 가 있으면 우선.\nconst loadedEnvFiles = loadDotEnv()\n\n// Ink TUI 는 stdout 으로 화면을 그린다 → core 로거의 console transport 가 stdout 으로 새지 않게 끈다\n// (서비스 서브커맨드의 직접 console.log 는 electron-log 가 아니라 영향 없음).\nsetConsoleLogging(false)\n\nconst config = new HeadlessConfigStore()\nconst args = process.argv.slice(2)\n\nif (args[0] === 'service') {\n // 1) 서비스 등록 — 코어/TUI 없이 종료코드 반환.\n process.exit(runServiceCommand(args[1], config))\n}\n\n// 풀스크린 TUI 진입 — alternate screen buffer. Ink 는 출력이 터미널 높이를 넘으면 이전 프레임(예:\n// 큰 QR)의 스크롤된 줄을 erase 하지 못해 잔상이 남는다(`c` 로 QR 을 꺼도 지워지지 않음). alt screen 은\n// 스크롤이 없어 erase 가 viewport 안에서 정확해진다. non-TTY(파이프)면 적용하지 않고, 모든 종료\n// 경로에서 leave 를 보장하기 위해 process.on('exit') 에 복원을 건다(Ink unmount 후 원래 화면 복귀).\nif (process.stdout.isTTY) {\n process.stdout.write('\\x1b[?1049h')\n process.on('exit', () => process.stdout.write('\\x1b[?1049l'))\n}\n\nif (isServiceActive()) {\n // 2) 서비스 가동 중 — 관리/상태 화면(코어 미기동).\n const { waitUntilExit } = render(<ManagerApp config={config} />)\n // SIGTERM 시 'exit' 가 돌아 alt screen 이 복원되도록 명시 종료(코어가 없어 즉시 exit 안전).\n // 풀 TUI 분기는 자체 SIGTERM→finish(cleanup) 를 갖는다 — 여기 핸들러를 공용 블록에 두면\n // 그 경로에 두 번째 SIGTERM 핸들러가 생겨 cleanup 을 건너뛰는 race 가 나므로 분기 안에 둔다.\n process.on('SIGTERM', () => process.exit(0))\n void waitUntilExit().finally(() => process.exit(0))\n} else {\n // 3) 기본 — 풀 TUI(코어 기동).\n const store = new HeadlessStore()\n const logDir = path.join(os.homedir(), '.ai_remote_vibe_agent', 'logs')\n\n const relayTarget = config.getRelayUrls().join(', ')\n for (const f of loadedEnvFiles) store.log(`[boot] .env loaded: ${f}`)\n // 어떤 relay 로 붙는지 부팅 즉시 표시 — env/.env 반영 여부 한눈에(연결 실패 진단 1순위).\n store.log(`[boot] relay target: ${relayTarget}`)\n store.log('[boot] starting agent core…')\n\n const core = createAgentCore({\n config,\n identity,\n logDir,\n onHostMessage: msg => store.apply(msg),\n })\n\n // Sessions 카드 시드 — 코어가 등록한 머신(로컬 1개)을 첫 페인트에 표시. 이후 폰이 머신을 추가/삭제하면\n // 코어의 machine_list broadcast 가 store 를 라이브 갱신한다.\n store.apply({ type: 'machine_list', machines: core.ssh.getMachines() })\n\n // 부팅 직후 1회 QR 요청(연결 전이면 LAN fallback). 연결되면 App 이 relay QR 로 재요청.\n core.ws.requestQr()\n\n const { waitUntilExit } = render(<App store={store} core={core} config={config} />)\n\n // 단일 teardown 경로. Ink 가 Ctrl-C / q(exit) / unmount 시 waitUntilExit 를 resolve 하므로 그\n // 지점에서만 cleanup + exit(cleanup 은 createAgentCore 가 재진입 가드). SIGINT 는 Ink 처리.\n const finish = (code: number): void => {\n core.cleanup()\n process.exit(code)\n }\n process.on('SIGTERM', () => finish(0))\n\n void waitUntilExit()\n .then(() => finish(0))\n .catch(() => finish(1))\n}\n","// electron-log/node: Electron 런타임에 의존하지 않는 노드 변형. 본 factory 가 packages/core\r\n// (Phase A4)로 이동해 헤드리스(plain Node)에서도 동작하려면 default `electron-log`(내부에서\r\n// `require('electron')`)가 아니라 `/node` 엔트리여야 한다 — core 의 no-electron CI 가드 통과 조건.\r\n// `.js` 명시 필수: electron-log 5.x 는 exports 맵이 없어 ESM(tsup 헤드리스 번들)에서 확장자 자동\r\n// 추가가 안 된다(`electron-log/node` → ERR_MODULE_NOT_FOUND). CJS(데스크탑)·ESM 양쪽에서 동일.\r\nimport log from 'electron-log/node.js'\r\nimport path from 'node:path'\r\nimport os from 'node:os'\r\n\r\n/**\r\n * scoped 로그 파일이 기록될 디렉터리. 호스트 앱이 시작 시 {@link configureLogDir} 로 주입한다.\r\n *\r\n * 기본값은 비-Electron(헤드리스) 안전 경로(`~/.ai_remote_vibe_agent/logs`). Electron 데스크탑은\r\n * `index.ts` 가 `app.getPath('logs')`(= 기존 `userData/logs`)로 덮어써 로그 위치를 그대로 유지한다\r\n * (무회귀). electron-log/node 는 Electron 의 app 경로를 모르므로 디렉터리를 명시 주입해야 한다.\r\n */\r\nlet scopedLogDir = path.join(os.homedir(), '.ai_remote_vibe_agent', 'logs')\r\n\r\n/**\r\n * scoped 로그 디렉터리를 설정한다. 호스트 앱이 부팅 초반(첫 로그 기록 전)에 1회 호출.\r\n *\r\n * createLogger 는 모듈 top-level 에서 import 시점에 실행되지만, 파일 경로는 `resolvePathFn` 으로\r\n * **lazy** 해석되므로(첫 로그 write 시점) 본 주입이 그보다 늦어도 안전하다.\r\n */\r\nexport function configureLogDir(dir: string): void {\r\n scopedLogDir = dir\r\n}\r\n\r\n/**\r\n * 지금까지 {@link createLogger} 로 만든 모든 scoped logger. {@link setConsoleLogging} 가\r\n * console transport 를 일괄 토글하려면 인스턴스 핸들을 모아둬야 한다(로거는 모듈 top-level 에서\r\n * import 시점에 생성되므로, 호스트가 끄기 전에 이미 다 만들어져 있다).\r\n */\r\nconst loggers: Array<ReturnType<typeof log.create>> = []\r\n\r\n/** console transport 활성 여부. 신규 로거가 생성될 때 이 값을 따른다. */\r\nlet consoleEnabled = true\r\n\r\n/**\r\n * 모든(이미 생성된·앞으로 생성될) scoped logger 의 console transport 를 일괄 on/off.\r\n *\r\n * **왜**: 헤드리스 TUI(Ink)는 **stdout 을 화면 렌더에 쓴다**. core 로그가 console transport 로\r\n * stdout 에 새면 TUI 위로 로그가 끼어들어 화면이 계속 밀려 올라간다. 헤드리스 cli 는 부팅 시\r\n * 1회 `setConsoleLogging(false)` 로 끈다 — file transport(`logs/*.log`)와 TUI 자체 verbose 뷰는\r\n * 그대로 유지되므로 디버깅 정보는 손실 없다. 데스크탑(Electron)은 호출하지 않아 기존 동작을 유지한다.\r\n *\r\n * 재활성화 레벨은 electron-log 기본값 `'silly'`(console.js 기본).\r\n */\r\nexport function setConsoleLogging(enabled: boolean): void {\r\n consoleEnabled = enabled\r\n for (const scoped of loggers) {\r\n scoped.transports.console.level = enabled ? 'silly' : false\r\n }\r\n}\r\n\r\n/**\r\n * Per-component scoped logger factory.\r\n *\r\n * 각 component 마다 electron-log 의 별도 인스턴스 + 별도 파일에 로그를 분리.\r\n * 디버깅 시 한 component 만 골라서 보기 위함 (예: `tail -f logs/git.log`).\r\n *\r\n * **파일 위치**: `{configureLogDir 로 주입된 디렉터리}/{component}.log`\r\n * - 데스크탑: `app.getPath('logs')` = 기존 `userData/logs/{component}.log` (변동 없음)\r\n * - 헤드리스: `~/.ai_remote_vibe_agent/logs/{component}.log`\r\n *\r\n * **포맷**: `[YYYY-MM-DD HH:MM:SS.ms] [level] [component] message`\r\n *\r\n * **사용 예**:\r\n * ```ts\r\n * import { createLogger } from '@arva/core' // 코어 내부에선 상대경로 '../lib/logger'\r\n * const log = createLogger('ws')\r\n * log.info('session_attach machine=...') // → logs/ws.log\r\n * ```\r\n *\r\n * **주의 (WHY)**:\r\n * - 데스크탑 default logger (`index.ts` 의 `import log from 'electron-log'`)는 `logs/main.log`\r\n * 에 main 프로세스 로그를 모은다. 본 factory 는 그것과 별개 인스턴스(`.create`)라 default 를\r\n * 건드리지 않고 component 별 파일로만 분기한다.\r\n * - `logId` 가 같으면 같은 인스턴스 재사용 가능하지만 transports 설정 한 번이면\r\n * 충분하므로 caller 가 모듈 top-level 에서 한 번만 호출하면 됨.\r\n */\r\nexport function createLogger(component: string) {\r\n const scoped = log.create({ logId: component })\r\n\r\n // 파일 transport — {scopedLogDir}/{component}.log. resolvePathFn 은 write 시점 lazy 평가라\r\n // import 순서와 무관하게 configureLogDir 주입 결과가 반영된다.\r\n scoped.transports.file.fileName = `${component}.log`\r\n scoped.transports.file.resolvePathFn = () =>\r\n path.join(scopedLogDir, `${component}.log`)\r\n scoped.transports.file.format =\r\n '[{y}-{m}-{d} {h}:{i}:{s}.{ms}] [{level}] [{logId}] {text}'\r\n scoped.transports.file.maxSize = 5 * 1024 * 1024 // 5MB rotation\r\n\r\n // Console transport — 개발 환경에서 가독성 우선 (날짜 제외, 시간만)\r\n scoped.transports.console.format =\r\n '[{h}:{i}:{s}.{ms}] [{level}] [{logId}] {text}'\r\n\r\n // 레지스트리에 등록하고 현재 console 정책을 반영. 헤드리스가 setConsoleLogging(false) 를\r\n // 먼저 호출한 뒤 새 로거가 만들어져도 console 이 켜지지 않도록(stdout 오염 방지).\r\n loggers.push(scoped)\r\n if (!consoleEnabled) scoped.transports.console.level = false\r\n\r\n return scoped\r\n}\r\n","import { Client } from 'ssh2'\nimport type { ClientChannel, ConnectConfig } from 'ssh2'\nimport fs from 'fs'\nimport os from 'os'\nimport path from 'path'\nimport crypto from 'crypto'\nimport { StringDecoder } from 'string_decoder'\n\nimport simpleGit from 'simple-git'\nimport type { BranchSummary, StatusResult } from 'simple-git'\nimport pLimit from 'p-limit'\n\nimport { createNodePty } from './pty-manager'\nimport { PtyScrollbackBuffer } from './scrollback-buffer'\nimport { ScrollbackPersister } from './scrollback-persister'\nimport { PtyLifecycleManager } from './pty-lifecycle'\nimport { AuthorityResolver } from './authority-resolver'\nimport {\n buildGitCommand,\n classifyDeleteError,\n validateGitRef,\n} from './git-command'\nimport { detectRunningAgent } from './agent-detector'\nimport {\n listAgentSessions,\n mergeRunningSessions,\n readSessionTranscript,\n scheduleSessionRescan,\n} from './agent-sessions'\nimport { SCROLLBACK_BUFFER_BYTES_DEFAULT } from '@arva/shared/constants'\nimport { normalizeCwd } from '@arva/shared/utils'\nimport type {\n Machine,\n BroadcastFn,\n ConnectionState,\n MultiplexerSetting,\n ServerMessage,\n PtyProcess,\n GitAction,\n GitStatusData,\n GitBranchData,\n GitFileData,\n GitCommitData,\n RunningAgentKind,\n} from '@arva/shared/types'\nimport type { SSHManagerConnectOptions } from './interfaces/ssh-manager.interface'\nimport { createSessionManager, type SessionManager } from '../multiplexer'\nimport { createLogger } from '../lib/logger'\n\n// Per-component scoped loggers — git 작업 노이즈 (5s 폴링) 와 ssh/pty 이벤트\n// 를 별도 파일로 분리. `tail -f logs/git.log` / `tail -f logs/ssh.log` 가능.\nconst gitLog = createLogger('git')\nconst sshLog = createLogger('ssh')\n\nconst homeDir = os.homedir()\n\n/**\n * 경로가 실제로 존재하는 디렉토리인지 검사한다.\n *\n * PTY spawn 의 cwd 검증용. 없는 cwd 를 ConPTY/CreateProcess 에 넘기면 Windows 에서\n * `CreateProcess` 가 error 267 (ERROR_DIRECTORY, \"디렉토리 이름이 잘못됨\") 로 spawn\n * 자체를 실패시킨다 — localCwd 가 다른 OS 의 경로이거나 삭제된 폴더인 경우 발생.\n * 사전에 걸러 homeDir 로 fallback 하기 위함. statSync throw(ENOENT 등) 는 \"없음\" 으로 간주.\n */\nexport function isExistingDir(p: string): boolean {\n try {\n return fs.statSync(p).isDirectory()\n } catch {\n return false\n }\n}\n\n// ─── Persistence ──────────────────────────────────────────────────────────────\n\nconst DATA_DIR = path.join(homeDir, '.claude_code_agent')\nconst MACHINES_FILE = path.join(DATA_DIR, 'machines.json')\nconst KEYRING_FILE = path.join(DATA_DIR, 'keyring.key')\nconst ALGORITHM = 'aes-256-gcm'\n\n/**\n * Scrollback 디스크 persist state dir 위치 (v1.5.0).\n * Windows: %LOCALAPPDATA%/ai-rva/scrollback/\n * POSIX: $XDG_STATE_HOME/ai-rva/scrollback/, fallback ~/.local/state/ai-rva/scrollback/\n */\nfunction resolveScrollbackStateDir(): string {\n const baseDir =\n process.platform === 'win32'\n ? (process.env.LOCALAPPDATA ?? path.join(homeDir, 'AppData', 'Local'))\n : (process.env.XDG_STATE_HOME ?? path.join(homeDir, '.local', 'state'))\n return path.join(baseDir, 'ai-rva', 'scrollback')\n}\n\ntype PersistedMachine = Omit<\n Machine,\n 'status' | 'password' | 'passwordLost'\n> & {\n encryptedPassword?: string\n}\n\nlet _keyCache: Buffer | null = null\n\nfunction loadOrCreateKey(): Buffer {\n if (_keyCache) return _keyCache\n try {\n _keyCache = fs.readFileSync(KEYRING_FILE)\n return _keyCache\n } catch {\n if (!fs.existsSync(DATA_DIR)) fs.mkdirSync(DATA_DIR, { recursive: true })\n const key = crypto.randomBytes(32)\n fs.writeFileSync(KEYRING_FILE, key, { mode: 0o600 })\n _keyCache = key\n return key\n }\n}\n\nfunction encryptPassword(password: string): string {\n const key = loadOrCreateKey()\n const iv = crypto.randomBytes(12)\n const cipher = crypto.createCipheriv(ALGORITHM, key, iv)\n const encrypted = Buffer.concat([\n cipher.update(password, 'utf8'),\n cipher.final(),\n ])\n const tag = cipher.getAuthTag()\n return Buffer.concat([iv, tag, encrypted]).toString('base64')\n}\n\nfunction decryptPassword(\n encoded: string,\n machineId: string\n): string | undefined {\n try {\n const key = loadOrCreateKey()\n const buf = Buffer.from(encoded, 'base64')\n const iv = buf.subarray(0, 12)\n const tag = buf.subarray(12, 28)\n const ciphertext = buf.subarray(28)\n const decipher = crypto.createDecipheriv(ALGORITHM, key, iv)\n decipher.setAuthTag(tag)\n return decipher.update(ciphertext).toString('utf8') + decipher.final('utf8')\n } catch {\n sshLog.warn(\n `[ssh-manager] password decrypt failed for ${machineId} — keyring.key 손실 또는 파일 손상?`\n )\n return undefined\n }\n}\n\nfunction loadPersistedMachines(): Machine[] | null {\n try {\n const raw = fs.readFileSync(MACHINES_FILE, 'utf-8')\n const parsed: PersistedMachine[] = JSON.parse(raw)\n if (!Array.isArray(parsed)) return null\n return parsed.map(({ encryptedPassword, ...rest }) => {\n const password = encryptedPassword\n ? decryptPassword(encryptedPassword, rest.id)\n : undefined\n const isLocalHost = rest.host === '127.0.0.1' || rest.host === 'localhost'\n const machineType = rest.machineType ?? (isLocalHost ? 'local' : 'ssh')\n return {\n ...rest,\n machineType,\n status: 'disconnected' as const,\n ...(encryptedPassword && !password ? { passwordLost: true } : {}),\n ...(password ? { password } : {}),\n }\n })\n } catch {\n return null\n }\n}\n\nfunction savePersistedMachines(machines: Machine[]): void {\n try {\n if (!fs.existsSync(DATA_DIR)) {\n fs.mkdirSync(DATA_DIR, { recursive: true })\n }\n const toSave: PersistedMachine[] = machines.map(\n ({ status: _s, password, passwordLost: _pl, ...rest }) => ({\n ...rest,\n ...(password ? { encryptedPassword: encryptPassword(password) } : {}),\n })\n )\n fs.writeFileSync(MACHINES_FILE, JSON.stringify(toSave, null, 2), 'utf-8')\n } catch (err) {\n sshLog.error('[ssh-manager] Failed to save machines:', err)\n }\n}\n\nconst SSH_KEY_PATHS = [\n path.join(homeDir, '.ssh', 'id_ed25519'),\n path.join(homeDir, '.ssh', 'id_ecdsa'),\n path.join(homeDir, '.ssh', 'id_rsa'),\n]\n\nconst SSH_KEY_EXCLUDE = new Set([\n 'known_hosts',\n 'known_hosts.old',\n 'config',\n 'authorized_keys',\n])\n\nfunction findPrivateKey(): Buffer | null {\n for (const keyPath of SSH_KEY_PATHS) {\n try {\n return fs.readFileSync(keyPath)\n } catch {\n // try next\n }\n }\n return null\n}\n\ntype ActiveSession = {\n client: Client\n stream: ClientChannel | null\n}\n\n// git status 폴링 주기 (status timer 의 5초 tick 단위). 12 tick = 60초.\n// 탐지(detectAndBroadcastRunningAgent)는 매 tick 이지만 git 은 로그/네트워크 절감 위해\n// 1분에 1회. 모바일 git 패널 반영도 최대 60초 지연되는 trade-off (사용자 요청 2026-06-01).\nconst GIT_STATUS_EVERY_N_TICKS = 12\n\n// [Cockpit 신선도] 빈 세션 목록 재스캔 지연(ms). 방금 시작한 claude 가 `.jsonl` 을 flush\n// 하는 타이밍 창(보통 첫 턴 직후 수백 ms~수 초)을 덮는다. 유한 3회 — surface 되면 조기 중단.\nconst AGENT_SESSIONS_RESCAN_DELAYS_MS = [800, 2500, 6000]\n\n// PTY 그리드 경계 — node-pty crash 방지 + 비현실적인 값 차단.\n// NaN/0/negative → MIN, oversize → MAX.\nexport const PTY_MIN_COLS = 20\nexport const PTY_MAX_COLS = 500\nexport const PTY_MIN_ROWS = 5\nexport const PTY_MAX_ROWS = 200\n\nexport function clampCols(v: number): number {\n const n = Math.round(v)\n if (!Number.isFinite(n) || n <= 0) return PTY_MIN_COLS\n return Math.min(Math.max(n, PTY_MIN_COLS), PTY_MAX_COLS)\n}\n\nexport function clampRows(v: number): number {\n const n = Math.round(v)\n if (!Number.isFinite(n) || n <= 0) return PTY_MIN_ROWS\n return Math.min(Math.max(n, PTY_MIN_ROWS), PTY_MAX_ROWS)\n}\n\n/**\n * spawn cue 의 cols/rows 가 정상 viewport 측정값일 가능성이 있는지 판정.\n *\n * client (renderer/mobile) 의 첫 terminal_resize 가 spawn cue 로 사용되는데,\n * renderer 측 layout pending 시점의 measurement 가 비정상값 (예: 20×5, 80×10) 으로\n * 도착하면 inner-shell PTY 가 그 size 로 spawn 되어 footer/status-line 위치\n * stuck 회귀 (psmux race, task 13) + spawn 직후 PTY exit 회귀 (task 13 v2,\n * 2026-05-09 dogfood 의 PTY spawn loop).\n *\n * 임계값:\n * - cols >= 40: 정상 PC 화면의 최소 너비\n * - rows >= 24: classic VT terminal default (80×24). PowerShell / cmd.exe 가\n * 안정적으로 prompt + history 표시할 수 있는 최소 grid. 10 으로 두면 mobile 의\n * layout pending 첫 측정값 80×10 이 통과해 PTY 가 spawn 직후 exit 하는 회귀.\n *\n * clamp 의 PTY_MIN (20×5) 보다 큰 임계 — clamp 는 PTY 자체의 hard bound 이고,\n * 본 함수는 \"spawn cue 로 신뢰할 만한가\" 판단.\n */\nexport function isSpawnCueValid(cols: number, rows: number): boolean {\n return cols >= 40 && rows >= 24\n}\n\n/**\n * spawn 시점의 cols/rows 결정. stored cue 가 [isSpawnCueValid] 를 통과하면 그대로,\n * 아니면 fallback. log 마킹용 source 도 함께 반환.\n */\nexport function resolveSpawnSize(\n stored: { cols: number; rows: number } | undefined,\n fallbackCols: number,\n fallbackRows: number\n): {\n cols: number\n rows: number\n source: 'client' | 'fallback' | 'fallback-invalid-cue'\n} {\n if (!stored)\n return { cols: fallbackCols, rows: fallbackRows, source: 'fallback' }\n if (!isSpawnCueValid(stored.cols, stored.rows)) {\n return {\n cols: fallbackCols,\n rows: fallbackRows,\n source: 'fallback-invalid-cue',\n }\n }\n return { cols: stored.cols, rows: stored.rows, source: 'client' }\n}\n\nexport function mapSimpleGitState(\n index: string,\n workingDir: string\n): GitFileData['state'] {\n const char = index !== ' ' && index !== '?' ? index : workingDir\n if (char === 'A') return 'added'\n if (char === 'D') return 'deleted'\n if (char === '?' || workingDir === '?') return 'untracked'\n if (char === 'R') return 'renamed'\n if (char === 'U') return 'conflicted'\n return 'modified'\n}\n\n/**\n * Machine 의 multiplexer 설정 fallback resolver.\n *\n * `machine.multiplexer` 가 undefined (미설정/구버전 machine) 일 때 'none' 으로 떨어진다.\n * 멀티플렉서 제거(2026-06-23) 후 createSessionManager 는 setting 과 무관하게 항상 None 이라\n * 실질 분기 영향은 없으나, 저장된 값 해석 일관성을 위해 유지(회귀 가드 테스트 존재).\n */\nexport function resolveMultiplexerSetting(\n machine: Pick<Machine, 'multiplexer'>\n): MultiplexerSetting {\n return machine.multiplexer ?? 'none'\n}\n\nexport class SSHManager {\n private machines: Machine[] = []\n private sessions = new Map<string, ActiveSession>()\n private localSessions = new Map<string, PtyProcess>()\n private reconnectAttempts = new Map<string, number>()\n private ptySizes = new Map<string, { cols: number; rows: number }>()\n // 첫 spawn 직전 클라이언트의 cols/rows를 기다리는 resolver 맵.\n // resize()가 도착하면 ptySizes를 채운 뒤 resolver를 호출해 spawn을 깨운다.\n private pendingFirstResize = new Map<string, () => void>()\n private pendingDisconnects = new Set<string>()\n // ── PTY 권위 모델 (Phase 1, 20260519 기획안) ────────────────────────────────\n // 60s TTL 휴리스틱(mobileAuthorityUntil) 을 명시적 handover 모달 + grace timer 로\n // 대체. AuthorityResolver 가 머신별 상태기계 (no-mobile / awaiting-handover /\n // mobile-leading / mobile-rejected / grace-pending) 를 관리하고, resize() 는 source\n // 가 현재 leader 와 일치할 때만 통과시킨다.\n private lastDesktopSize = new Map<string, { cols: number; rows: number }>()\n public readonly authority: AuthorityResolver\n /**\n * [Reclaim guard 20260606] 에이전트 turn-busy 조회 provider. HookReceiver(claude_state)가\n * index.ts 에서 늦게 생성되므로 생성자 시점엔 미지정 — `setSessionBusyProvider` 로 후주입.\n * AuthorityResolver 의 `isSessionBusy` dep 클로저가 매 tick 본 필드를 읽는다(late-bound).\n */\n private sessionBusyProvider: ((machineId: string) => boolean) | null = null\n /**\n * [Cockpit 신선도 20260614] running 콕핏 세션 id provider. AgentRunner 가 ws-manager 에서\n * 늦게 생성되므로 생성자 시점엔 미지정 — `setActiveSessionProvider` 로 후주입(sessionBusyProvider\n * 와 동일 패턴). `broadcastAgentSessions` 가 디스크 flush 전 running 장수명 세션을 목록에 합류시킬 때 읽는다.\n */\n private activeSessionProvider: ((machineId: string) => string[]) | null = null\n // ── Multiplexer SessionManager cache (Phase 1, docs/plans/0001-...) ────────\n // setting 별 1개. machine 의 multiplexer 값이 같으면 매니저 공유.\n private sessionManagers = new Map<MultiplexerSetting, SessionManager>()\n // PTY 영속화 (Approach A) — 머신별 scrollback ring buffer.\n // PTY 의 모든 stdout/stderr 가 들어옴. ws-manager 가 attach 시 청크 분할해서 송신.\n // restartPty() 시 폐기. cleanup() 에서 전체 폐기.\n private scrollbacks = new Map<string, PtyScrollbackBuffer>()\n // v1.5.0 — scrollback 디스크 persist + restore-on-launch.\n // 앱 종료 시 buffers → 디스크 JSON, 다음 launch 의 첫 attach 시 deserialize 복원.\n // 평문 저장 (CHANGELOG privacy note). 암호화는 v1.6+ 후속.\n private persister: ScrollbackPersister\n // restartPty() 더블 클릭 방지 lock. CRITICAL GAP #5 fix.\n // 200ms 후 자동 release 로 debounce 효과. connect() 가 async 라 promise 기반은 복잡함.\n private restartingMachines = new Set<string>()\n private broadcast: BroadcastFn\n // PTY lifecycle policy (count cap, idle timer, pty_status broadcast).\n // ssh-manager 의 lifecycle 책임을 추출 — CLAUDE.md 10-메서드 규칙 + 미래 E1\n // daemon 분리 시 자연 boundary. 자세한 설계는 docs/adr/0001-pty-persistence.md.\n public readonly lifecycle: PtyLifecycleManager\n /**\n * A3 (Sub-Part 1F-ii-b) — GRID_DIFF (0x05) binary frame broadcast 콜백.\n *\n * ws-manager 가 [setBinaryBroadcast] 로 inject. cap=`grid.diff` 인 client 에만\n * fan-out 책임은 ws-manager 가짐 (capability filter).\n *\n * 본 콜백이 set 되어 있으면 PTY data flush 시점 (16ms 디바운스 후) 에\n * `lifecycle.flushDiff(machineId, frame => binaryBroadcast(machineId, frame))` 호출.\n * undefined 면 silent skip — emulator 는 grid 만 누적, 송신 0 (예: production mobile\n * 이 모두 cap=pty.data 인 경우).\n */\n private binaryBroadcast?: (machineId: string, frame: Buffer) => void\n private _destroyed = false\n private cwdMap = new Map<string, string>()\n private statusTimers = new Map<string, ReturnType<typeof setInterval>>()\n private gitLimiters = new Map<string, ReturnType<typeof pLimit>>()\n // [Cockpit 신선도] 머신별 빈-세션 재스캔 취소 핸들. 새 agent.sessions.list 요청·disconnect·\n // cleanup 시 직전 스케줄을 취소해 중복 broadcast / stale 타이머를 막는다.\n private agentSessionsRescanCancel = new Map<string, () => void>()\n\n /**\n * task 10 C7 (2026-05-10) — session_attach 시점에 mobile 측 viewport size 를\n * 첫 PTY spawn cue 로 주입한다. 내부적으로 [ptySizes] 에 그대로 set 해서\n * [resolveSpawnSize] 의 stored cue 경로를 그대로 활용한다 (별 hint Map 불필요).\n *\n * 호출처: ws-manager 의 handleSessionAttach (PTY 가 아직 없는 경우만).\n * PTY 가 이미 살아있으면 호출자가 [resize] 를 직접 호출 — hint path 와 분리.\n *\n * Validity guard: cols/rows 가 [isSpawnCueValid] (cols>=40, rows>=24) 미달이면\n * resolveSpawnSize 가 fallback 으로 자동 대체 → set 자체는 무해.\n */\n setSpawnSizeHint(machineId: string, cols: number, rows: number): void {\n this.ptySizes.set(machineId, { cols, rows })\n }\n\n /**\n * 현재 머신의 PTY 폭/높이를 반환한다 (없으면 undefined).\n *\n * 호출처: ws-manager 의 handleSessionAttach — mobile resize *직전* 에 호출해\n * scrollback 이 **생성된 당시의 폭**을 얻어 session_replay 에 동봉한다. 모바일은 그 폭으로\n * raw replay 를 먼저 그린 뒤 viewport 폭으로 reflow → 재접속 시 스크롤백이 좁은 폭으로\n * 재-wrap 되어 좌측으로 붕괴하던 버그(20260620) 회피. (ring buffer 자체는 raw 바이트만\n * 보관해 per-line 폭을 모르므로, attach 직전 현재 폭을 tail 의 생성 폭 근사로 사용.)\n */\n getPtySize(machineId: string): { cols: number; rows: number } | undefined {\n return this.ptySizes.get(machineId)\n }\n\n /**\n * A3 (Sub-Part 1F-ii-b) — GRID_DIFF binary broadcast 콜백 inject.\n *\n * ws-manager constructor 가 인스턴스화 직후 호출:\n * ```ts\n * ssh.setBinaryBroadcast((mid, frame) => this.broadcastGridFrame(mid, frame))\n * ```\n * 한 번만 set — 중복 호출 시 마지막 콜백이 승.\n */\n setBinaryBroadcast(fn: (machineId: string, frame: Buffer) => void): void {\n this.binaryBroadcast = fn\n }\n\n constructor(broadcast: BroadcastFn) {\n this.broadcast = broadcast\n this.lifecycle = new PtyLifecycleManager({\n currentPtyCount: () =>\n this.sessions.size +\n this.localSessions.size +\n this.pendingFirstResize.size,\n evictMachine: id => this.disconnect(id),\n isRestarting: id => this.restartingMachines.has(id),\n // pty-lifecycle 은 항상 renderer + mobile 양쪽 fan-out (skipRenderer 옵션 사용 안 함)\n broadcast: msg => this.broadcast(msg),\n })\n // Phase 1 — AuthorityResolver 위임. resize() 의 source 가드, mobile attach/detach\n // 시 ws-manager 가 호출, expireGrace 가 deps.applyResize 로 desktop-restore 트리거.\n this.authority = new AuthorityResolver({\n broadcast: this.broadcast,\n applyResize: (mid, cols, rows, source) =>\n this.resize(mid, cols, rows, source),\n getLastDesktopSize: mid => this.lastDesktopSize.get(mid),\n // [Reclaim guard 20260606] handed_off watchdog 가 응답 스트리밍/에이전트 턴 중 회수를\n // 보류하도록 PTY 출력 활동 + busy 신호 주입. getLastPtyActivity 는 lifecycle 의\n // lastActivity(출력/입력으로 갱신) 재사용. isSessionBusy 는 후주입 provider(late-bound).\n getLastPtyActivity: mid => this.lifecycle.getLastActivity(mid),\n isSessionBusy: mid => this.sessionBusyProvider?.(mid) ?? false,\n })\n this.persister = new ScrollbackPersister(resolveScrollbackStateDir())\n const saved = loadPersistedMachines()\n if (saved && saved.length > 0) {\n this.machines = saved\n }\n\n const hasLocal = this.machines.some(m => m.machineType === 'local')\n if (!hasLocal) {\n this.addMachine({\n name: 'localhost',\n host: '127.0.0.1',\n port: 22,\n username: os.userInfo().username,\n machineType: 'local',\n })\n }\n\n // v1.5.0 — orphan scrollback 파일 정리. UI 에서 삭제됐는데 디스크에 남은 backlog 제거.\n // 동기 호출 — 시작 비용 작음 (readdir + 비교 + unlink). 실패는 swallow.\n try {\n const known = new Set(this.machines.map(m => m.id))\n this.persister.cleanupStale(known)\n } catch {\n // ignore — orphan 정리 실패해도 라이브 동작에 영향 없음.\n }\n }\n\n /**\n * [Reclaim guard 20260606] 에이전트 turn-busy provider 후주입.\n *\n * index.ts 가 HookReceiver(claude_state) 생성 직후 호출 — `mid => hookReceiver.isAgentBusy(mid)`.\n * HookReceiver 가 비활성(`CLAUDE_HOOKS_DISABLED`/초기화 실패)이면 미호출 → provider=null →\n * AuthorityResolver 는 busy=false 로 graceful degrade(PTY 게이트 + 절대 상한만으로 동작).\n */\n setSessionBusyProvider(fn: (machineId: string) => boolean): void {\n this.sessionBusyProvider = fn\n }\n\n /**\n * [Cockpit 신선도 20260614] running 콕핏 세션 id provider 후주입.\n *\n * ws-manager 가 AgentRunner 생성 직후 호출 — `mid => agentRunner.getActiveSessionIds(mid)`.\n * 미주입(null)이면 [broadcastAgentSessions] 는 디스크 스캔 결과를 그대로 broadcast(graceful degrade).\n */\n setActiveSessionProvider(fn: (machineId: string) => string[]): void {\n this.activeSessionProvider = fn\n }\n\n getMachines(): Machine[] {\n return this.machines\n }\n\n broadcastMachines(): void {\n this.broadcast({ type: 'machine_list', machines: this.machines })\n }\n\n broadcastSshKeys(): void {\n this.broadcast({ type: 'ssh_keys', keys: this.listAvailableKeys() })\n }\n\n listAvailableKeys(): string[] {\n try {\n const dir = path.join(homeDir, '.ssh')\n return fs\n .readdirSync(dir)\n .filter(f => !f.endsWith('.pub') && !SSH_KEY_EXCLUDE.has(f))\n .map(f => path.join(dir, f))\n } catch {\n return []\n }\n }\n\n editMachine(data: Omit<Machine, 'status'>): Machine {\n let updated: Machine | undefined\n\n this.machines = this.machines.map(item => {\n if (item.id === data.id) {\n updated = { ...item, ...data }\n return updated\n }\n return item\n })\n\n if (!updated) {\n throw new Error('not found Machine')\n }\n\n savePersistedMachines(this.machines)\n return updated\n }\n\n addMachine(data: Omit<Machine, 'id' | 'status'>): Machine {\n const machine: Machine = {\n ...data,\n id: crypto.randomUUID(),\n status: 'disconnected',\n }\n this.machines.push(machine)\n savePersistedMachines(this.machines)\n return machine\n }\n\n removeMachine(id: string): void {\n this.disconnect(id)\n this.machines = this.machines.filter(m => m.id !== id)\n this.ptySizes.delete(id)\n // disconnect() 가 lifecycle.onDisconnect 호출하지만, removeMachine 은 의도가\n // 다르므로 explicit cleanup 도 추가 (defensive — 추후 disconnect chain 변경 대비).\n this.lifecycle.onRemoveMachine(id)\n // Phase 1 [Review P1] — authority resolver state + grace/pending timer 정리.\n // lastDesktopSize 도 같이 — 머신 삭제 후 잔존하면 다음 same-id 머신 시 stale.\n this.authority.removeMachine(id)\n this.lastDesktopSize.delete(id)\n savePersistedMachines(this.machines)\n }\n\n connect(\n machineId: string,\n { forceReConnect = false }: SSHManagerConnectOptions = {}\n ): void {\n const machine = this.machines.find(m => m.id === machineId)\n\n if (!machine) return\n\n if (machine.status === 'connected' && forceReConnect === false) {\n return\n }\n\n // PTY count cap 검사 + (필요 시) LRU eviction. **첫 await 전 동기**.\n // 같은 tick 동시 connect 호출은 pendingFirstResize.set 이 await 전이라 보호.\n // Approach A 의 영속 PTY 가 32개 cap 초과하지 않도록.\n this.lifecycle.onConnect(machineId)\n\n if (machine.machineType === 'local') {\n // connectLocal은 spawn-deferring 때문에 async — fire-and-forget으로 호출.\n void this.connectLocal(machineId, machine)\n return\n }\n\n // Approach A — PTY 영속화: 기존 SSH 세션이 살아있으면 재사용한다.\n // 모바일이 머신을 다시 선택했을 때 자식 프로세스 (claude/npm/vim/...) 가 죽지\n // 않고 그대로 살아남도록. ws-manager 가 attach 시 scrollback 을 replay 함.\n if (this.sessions.has(machineId)) {\n // 이미 connected 상태로 보고 종료. 호출자(ws-manager)가 attach replay 송신.\n this.updateStatus(machineId, 'connected')\n return\n }\n\n const connectOptions: ConnectConfig = {\n host: machine.host,\n port: machine.port,\n username: machine.username,\n hostVerifier: () => true,\n }\n\n if (machine.authMethod === 'password') {\n if (!machine.password) {\n this.updateStatus(machineId, 'error', 'Password is required')\n return\n }\n connectOptions.password = machine.password\n } else {\n if (machine.privateKeyPath) {\n try {\n connectOptions.privateKey = fs.readFileSync(machine.privateKeyPath)\n } catch {\n this.updateStatus(\n machineId,\n 'error',\n `Key file not found: ${machine.privateKeyPath}`\n )\n return\n }\n } else {\n const privateKey = findPrivateKey()\n if (!privateKey) {\n this.updateStatus(\n machineId,\n 'error',\n 'SSH key not found. Select a key or use password auth.'\n )\n return\n }\n connectOptions.privateKey = privateKey\n }\n }\n\n this.updateStatus(machineId, 'connecting')\n const client = new Client()\n this.sessions.set(machineId, { client, stream: null })\n\n client.on('ready', async () => {\n // spawn-deferring: 클라이언트의 첫 terminal_resize를 spawn cue로 사용해\n // shell이 모바일 측정 cols/rows로 직접 spawn되도록 한다 (race fix).\n // machine.spawnDeferTimeoutMs (default 600ms) 안에 도착 안 하면 fallback. 재연결이면 ptySizes 캐시로 즉시 진행.\n await this.waitForFirstResize(\n machineId,\n machine.spawnDeferTimeoutMs ?? 600\n )\n // 대기 중 disconnect 또는 새 connect로 client 교체 가능 — 정체성 확인 후 종료.\n const currentSession = this.sessions.get(machineId)\n if (!currentSession || currentSession.client !== client) return\n if (this.pendingDisconnects.has(machineId)) {\n this.pendingDisconnects.delete(machineId)\n return\n }\n const stored = this.ptySizes.get(machineId)\n // F2 — spawn cue sanity guard (task 13). resolveSpawnSize 가 비정상 cue (cols<40\n // 또는 rows<10) 를 fallback 으로 대체. 자세한 동기는 resolveSpawnSize doc 참조.\n const {\n cols: spawnCols,\n rows: spawnRows,\n source,\n } = resolveSpawnSize(\n stored,\n machine.spawnFallbackCols ?? 80,\n machine.spawnFallbackRows ?? 24\n )\n if (source === 'fallback-invalid-cue') {\n sshLog.warn(\n `[pty] spawn cue 비정상 ssh machine=${machineId} stored=${stored?.cols}x${stored?.rows} — fallback ${spawnCols}x${spawnRows}`\n )\n }\n sshLog.info(\n `[pty] spawn ssh machine=${machineId} ${spawnCols}x${spawnRows} source=${source}`\n )\n\n client.shell(\n { term: 'xterm-256color', cols: spawnCols, rows: spawnRows },\n (err, stream) => {\n if (err) {\n this.updateStatus(machineId, 'error', err.message)\n this.sessions.delete(machineId)\n return\n }\n\n const session = this.sessions.get(machineId)\n if (session) session.stream = stream\n\n this.updateStatus(machineId, 'connected')\n this.startStatusTimer(machineId)\n\n // shell이 spawnCols/Rows로 직접 spawn되므로 spawn 직후 setWindow 불필요.\n // ack는 클라이언트에 spawn 사이즈를 통지하기 위해 1회 보낸다.\n this.broadcast({\n type: 'terminal_resize_ack',\n machineId,\n cols: spawnCols,\n rows: spawnRows,\n })\n\n // ── 16ms 배치 처리 (IPC 성능) ──────────────────────────────────────\n // ⚠️ 반드시 연결 setup 내부에 선언 — 모듈 레벨이면 모든 머신이 버퍼 공유 (버그)\n let _buffer = ''\n let _flushTimer: ReturnType<typeof setTimeout> | null = null\n\n // Stateful UTF-8 decoder — chunk 경계에서 잘린 멀티바이트(한글 받침,\n // 이모지, ANSI 시퀀스 후속 byte)를 다음 chunk와 합쳐 안전하게 디코드한다.\n // stdout/stderr가 인터리브되면 시퀀스가 섞일 수 있어 각각 분리 인스턴스.\n const stdoutDecoder = new StringDecoder('utf8')\n const stderrDecoder = new StringDecoder('utf8')\n\n // Approach A — scrollback ring buffer. 머신별 1개. 첫 attach 까지 누적되며\n // 5MB 도달 시 head 부터 자동 drop. ws-manager.handleAttach() 가 청크 분할 송신.\n const scrollback = this.getOrCreateScrollback(machineId)\n\n const flushData = () => {\n this.broadcast({ type: 'terminal_data', machineId, data: _buffer })\n // A3 (Sub-Part 1F-ii-b): emulator 의 dirty cell → GRID_DIFF (0x05) broadcast.\n // 16ms 디바운스 안에서 1회만 — 효율적. flushDiff 가 dirty 0 이면 silent skip.\n // capability filter (cap=grid.diff client 만) 는 ws-manager 의 broadcastGridFrame 책임.\n if (this.binaryBroadcast) {\n this.lifecycle.flushDiff(machineId, frame =>\n this.binaryBroadcast!(machineId, frame)\n )\n }\n _buffer = ''\n _flushTimer = null\n }\n\n stream.on('data', (data: Buffer) => {\n const str = stdoutDecoder.write(data)\n this.parseOsc7(str, machineId)\n scrollback.append(str)\n this.persister.scheduleFlush(machineId, scrollback)\n this.lifecycle.onDataActivity(machineId)\n // A3 (Sub-Part 1F-i): emulator append — grid 누적. broadcast 자체는 본 turn 에서 미발사\n // (cap.ack 가 항상 'pty.data' fallback 이라 cap=grid.diff client 0). silent.\n this.lifecycle.onPtyData(machineId, str)\n _buffer += str\n if (!_flushTimer) _flushTimer = setTimeout(flushData, 16)\n })\n\n stream.stderr.on('data', (data: Buffer) => {\n const str = stderrDecoder.write(data)\n scrollback.append(str)\n this.persister.scheduleFlush(machineId, scrollback)\n this.lifecycle.onDataActivity(machineId)\n // A3 (Sub-Part 1F-i): stderr 도 ANSI sequence 가질 수 있어 emulator 에 fed.\n this.lifecycle.onPtyData(machineId, str)\n _buffer += str\n if (!_flushTimer) _flushTimer = setTimeout(flushData, 16)\n })\n\n stream.on('close', () => {\n // PTY 자연 종료 — pty_status('dead', natural) broadcast.\n // F2 (task 13 v2) — exit 진단 log. local 측과 동일.\n sshLog.info(`[pty] exit ssh machine=${machineId}`)\n // [A2 spike 20260523] mobile 권위 상태에서 PTY 죽으면 viewer_mode='error'\n // broadcast — desktop UI 의 rose-tone \"Connection lost — phone offline too\"\n // (Design Fix-E D2-δ row 9). leader=mobile 아니면 resolver 내부에서 no-op.\n this.authority.handleAgentCrash(machineId)\n this.lifecycle.onPtyExit(machineId)\n // decoder 잔여 byte를 마지막으로 flush\n const tail = stdoutDecoder.end() + stderrDecoder.end()\n if (tail) _buffer += tail\n if (_buffer || _flushTimer) {\n if (_flushTimer) {\n clearTimeout(_flushTimer)\n _flushTimer = null\n }\n if (_buffer)\n this.broadcast({\n type: 'terminal_data',\n machineId,\n data: _buffer,\n })\n _buffer = ''\n }\n this.clearStatusTimer(machineId)\n this.updateStatus(machineId, 'disconnected')\n this.sessions.delete(machineId)\n })\n }\n )\n })\n\n client.on('error', err => {\n this.updateStatus(machineId, 'error', err.message)\n this.sessions.delete(machineId)\n })\n\n client.connect(connectOptions)\n }\n\n private async connectLocal(\n machineId: string,\n machine: Machine\n ): Promise<void> {\n // Approach A — PTY 영속화: 기존 PTY 가 살아있으면 kill 하지 않고 재사용.\n // 사용자 우려의 핵심 해결: claude/npm/vim/dev-server 등 자식 프로세스가 SIGHUP 으로\n // 죽지 않도록. 호출자(ws-manager)가 attach 직후 scrollback replay 송신.\n if (this.localSessions.has(machineId)) {\n this.updateStatus(machineId, 'connected')\n return\n }\n\n this.updateStatus(machineId, 'connecting')\n\n try {\n const userShell = machine.localShell\n const envShell = process.env.SHELL\n const shell =\n userShell && userShell.length > 0\n ? userShell\n : envShell && envShell.length > 0\n ? envShell\n : process.platform === 'win32'\n ? (process.env.COMSPEC ?? 'cmd.exe')\n : '/bin/zsh'\n\n const env: Record<string, string> = {\n HOME: homeDir,\n LOGNAME: os.userInfo().username,\n USER: os.userInfo().username,\n SHELL: shell,\n ...(process.env as Record<string, string>),\n TERM: 'xterm-256color',\n COLORTERM: 'truecolor',\n }\n if (process.platform === 'win32') {\n const shellLower = shell.toLowerCase()\n if (\n shellLower.includes('bash') ||\n shellLower === 'wsl' ||\n shellLower === 'wsl.exe'\n ) {\n const gitBashBin = 'C:\\\\Program Files\\\\Git\\\\usr\\\\bin'\n env.PATH = `${gitBashBin};${env.PATH || env.Path || ''}`\n }\n } else {\n const defaultPaths = '/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin'\n env.PATH = `${env.PATH || ''}:${defaultPaths}`\n }\n\n // spawn-deferring: 클라이언트의 첫 terminal_resize를 기다린다 (machine.spawnDeferTimeoutMs ?? 600 cap).\n // ptySizes 캐시가 있으면 즉시 진행. cmd.exe가 처음부터 모바일 cols로 출력하도록.\n await this.waitForFirstResize(\n machineId,\n machine.spawnDeferTimeoutMs ?? 600\n )\n // 대기 중 disconnect 가능 — pending 표시되어 있으면 종료.\n if (this.pendingDisconnects.has(machineId)) {\n this.pendingDisconnects.delete(machineId)\n return\n }\n const stored = this.ptySizes.get(machineId)\n // F2 — spawn cue sanity guard (task 13). 자세한 동기는 resolveSpawnSize doc 참조.\n const {\n cols: spawnCols,\n rows: spawnRows,\n source: spawnSource,\n } = resolveSpawnSize(\n stored,\n machine.spawnFallbackCols ?? 80,\n machine.spawnFallbackRows ?? 24\n )\n if (spawnSource === 'fallback-invalid-cue') {\n sshLog.warn(\n `[pty] spawn cue 비정상 local machine=${machineId} stored=${stored?.cols}x${stored?.rows} — fallback ${spawnCols}x${spawnRows}`\n )\n }\n\n // ── SessionManager 위임 ───────────────────────────────────────────────\n // 멀티플렉서 제거(2026-06-23) 후 sessionMgr 는 항상 NoneSessionManager — hint 를\n // 그대로 SpawnSpec 으로 변환(직접 spawn). attachOrCreate 직접 호출.\n // cwd 존재 검증 — 없는 디렉토리를 ConPTY/CreateProcess 에 넘기면 Windows 에서\n // error 267 (ERROR_DIRECTORY) 로 spawn 자체가 실패한다 (localCwd 가 다른 OS 의\n // 경로이거나 삭제된 폴더인 경우). 존재하지 않으면 homeDir 로 fallback 하고, 연결 직후\n // 터미널 첫 줄에 경고를 남겨 사용자가 지정한 cwd 가 무시됐음을 알 수 있게 한다.\n const requestedCwd = machine.localCwd?.trim()\n let effectiveCwd = requestedCwd || homeDir\n let cwdWarningLine: string | null = null\n if (requestedCwd && !isExistingDir(requestedCwd)) {\n effectiveCwd = homeDir\n sshLog.warn(\n `[pty] localCwd 없음 machine=${machineId} cwd=${requestedCwd} → homeDir(${homeDir}) fallback`\n )\n cwdWarningLine =\n `\\r\\n\\x1b[33m⚠ 작업 디렉토리 '${requestedCwd}' 없음\\x1b[0m\\r\\n` +\n `\\x1b[33m → 홈(${homeDir})에서 연결합니다.\\x1b[0m\\r\\n`\n }\n\n const sessionMgr = this.getSessionManager(machine)\n const hint = {\n shell,\n args: machine.args,\n cwd: effectiveCwd,\n env,\n cols: spawnCols,\n rows: spawnRows,\n }\n const spec = await sessionMgr.attachOrCreate(machineId, hint)\n sshLog.info(\n `[pty] spawn local machine=${machineId} ${spawnCols}x${spawnRows} mux=${sessionMgr.kind} source=${spawnSource}`\n )\n\n const proc = createNodePty({\n shell: spec.shell,\n args: spec.args,\n cols: spec.cols,\n rows: spec.rows,\n cwd: spec.cwd,\n env: spec.env,\n })\n ;(proc as { _spawnAt?: number })._spawnAt = Date.now()\n\n this.localSessions.set(machineId, proc)\n this.reconnectAttempts.delete(machineId)\n this.updateStatus(machineId, 'connected')\n this.startStatusTimer(machineId)\n\n // spawn이 spawnCols/Rows로 직접 시작되므로 사후 resize 불필요. ack는 1회 통지.\n this.broadcast({\n type: 'terminal_resize_ack',\n machineId,\n cols: spawnCols,\n rows: spawnRows,\n })\n\n // ── 16ms 배치 처리 ────────────────────────────────────────────────────\n // ⚠️ 반드시 연결 setup 내부에 선언 — 모듈 레벨이면 모든 머신이 버퍼 공유 (버그)\n let _buffer = ''\n let _flushTimer: ReturnType<typeof setTimeout> | null = null\n\n // Approach A — scrollback ring buffer (머신별 1개, 5MB cap).\n const scrollback = this.getOrCreateScrollback(machineId)\n\n // cwd fallback 경고를 터미널 첫 줄에 주입한다. PTY onData 는 async 로 들어오므로\n // 동기 broadcast 가 항상 먼저 정렬된다. scrollback 에도 넣어 재접속/모바일 attach\n // 시 backlog 에 보존.\n if (cwdWarningLine) {\n scrollback.append(cwdWarningLine)\n this.broadcast({ type: 'terminal_data', machineId, data: cwdWarningLine })\n }\n\n const flushData = () => {\n this.broadcast({ type: 'terminal_data', machineId, data: _buffer })\n // A3 (Sub-Part 1F-ii-b): local PTY path — SSH path 와 동일 emitter 패턴.\n if (this.binaryBroadcast) {\n this.lifecycle.flushDiff(machineId, frame =>\n this.binaryBroadcast!(machineId, frame)\n )\n }\n _buffer = ''\n _flushTimer = null\n }\n\n proc.onData((data: string) => {\n this.parseOsc7(data, machineId)\n scrollback.append(data)\n this.persister.scheduleFlush(machineId, scrollback)\n this.lifecycle.onDataActivity(machineId)\n // A3 (Sub-Part 1F-i): emulator append — grid 누적 (local PTY path).\n this.lifecycle.onPtyData(machineId, data)\n _buffer += data\n if (!_flushTimer) _flushTimer = setTimeout(flushData, 16)\n })\n\n proc.onExit((_code: number) => {\n // PTY 자연 종료 — pty_status('dead', natural) broadcast.\n // F2 (task 13 v2) — exit 진단용 명시 log. spawn cycle 회귀 (PTY 가 ~5초 안에\n // 죽으면서 mobile 의 connect msg 가 매번 새 spawn 트리거하는 loop) 의 root cause\n // 추적. uptime + exit code 로 cmd.exe / psmux 자체 종료 vs SIGHUP / 외부 kill 구분.\n const spawnAt = (proc as { _spawnAt?: number })._spawnAt ?? Date.now()\n const uptimeMs = Date.now() - spawnAt\n sshLog.info(\n `[pty] exit local machine=${machineId} code=${_code} uptime=${uptimeMs}ms`\n )\n // [A2 spike 20260523] mobile 권위 상태에서 PTY 죽으면 viewer_mode='error'\n // broadcast — desktop UI 의 rose-tone error placeholder (Design D2-δ row 9).\n this.authority.handleAgentCrash(machineId)\n this.lifecycle.onPtyExit(machineId)\n if (_flushTimer) {\n clearTimeout(_flushTimer)\n _flushTimer = null\n }\n this.clearStatusTimer(machineId)\n this.localSessions.delete(machineId)\n\n // v1.5.0 — C2 shell-exit orphan fix (carry-over from v1.3.4 P-LR).\n // 사용자가 inner shell 에서 `exit` 시 bridged psmux/tmux 세션도 kill.\n // 안 하면 sleep 86400 dummy 가 영구 살아남음 (v1.3.4 production 누적 확인).\n // findCachedSessionManagerFor 는 어떤 매니저든 캐시된 것 반환 — psmux/tmux 양쪽 호환.\n const sessionMgr = this.findCachedSessionManagerFor(machineId)\n if (sessionMgr) {\n try {\n void Promise.resolve(sessionMgr.kill(machineId)).catch(\n () => undefined\n )\n } catch {\n // kill sync throw — 무해.\n }\n }\n\n if (this.pendingDisconnects.has(machineId)) {\n this.pendingDisconnects.delete(machineId)\n return\n }\n\n const currentMachine = this.machines.find(m => m.id === machineId)\n if (!currentMachine) return\n\n const attempts = (this.reconnectAttempts.get(machineId) ?? 0) + 1\n if (attempts >= 3) {\n this.reconnectAttempts.delete(machineId)\n this.updateStatus(\n machineId,\n 'error',\n `PTY 비정상 종료 (${shell}) — 3회 재시도 실패`\n )\n return\n }\n this.reconnectAttempts.set(machineId, attempts)\n this.updateStatus(machineId, 'disconnected')\n setTimeout(() => {\n const m = this.machines.find(m => m.id === machineId)\n if (m) void this.connectLocal(machineId, m)\n }, 2000)\n })\n } catch (err) {\n const userShell = machine.localShell\n const envShell = process.env.SHELL\n const failedShell =\n userShell ||\n envShell ||\n (process.platform === 'win32' ? 'cmd.exe' : '/bin/zsh')\n sshLog.error(`Local PTY 실패 (${failedShell}): ${(err as Error).message}`)\n this.updateStatus(\n machineId,\n 'error',\n `Local PTY 실패 (${failedShell}): ${(err as Error).message}`\n )\n }\n }\n\n disconnect(machineId: string): void {\n this.clearStatusTimer(machineId)\n // pty_status('dead', 'natural') broadcast FIRST — 모바일이 dead 먼저 받아\n // UI 전환, 그 후 connection_status('disconnected'). lifecycle metadata cleanup 도 같이.\n this.lifecycle.onDisconnect(machineId, 'natural')\n\n // [Cockpit 신선도] 진행 중인 빈-세션 재스캔 타이머 해제 (머신이 사라졌으므로 무의미).\n this.cancelAgentSessionsRescan(machineId)\n\n // pendingDisconnects는 in-flight spawn coroutine을 abort하기 위한 플래그.\n // 실제로 abort할 대상이 있을 때만 세팅 — 무조건 add하면 다음 connect의\n // post-await 가드가 이 플래그를 보고 spawn을 abort해 \"connecting\" 멈춤.\n const wake = this.pendingFirstResize.get(machineId)\n const localProc = this.localSessions.get(machineId)\n const session = this.sessions.get(machineId)\n const hasInFlight =\n wake !== undefined || localProc !== undefined || session !== undefined\n\n if (hasInFlight) {\n this.pendingDisconnects.add(machineId)\n }\n if (wake) wake() // 200ms timeout 대기 즉시 해제\n\n // 사용자 명시 disconnect — PTY 와 scrollback 모두 폐기. session_detach (모바일이\n // 화면을 떠남) 와 다른 의미. 머신 자체를 끄려는 사용자 의도이므로 진짜 종료.\n // v1.5.0: 디스크 persist 파일도 함께 제거 (다음 launch 에서 빈 backlog).\n this.scrollbacks.delete(machineId)\n try {\n this.persister.remove(machineId)\n } catch {\n // remove 실패 — 무해.\n }\n\n if (localProc) {\n localProc.kill()\n this.localSessions.delete(machineId)\n this.updateStatus(machineId, 'disconnected')\n return\n }\n\n if (session) {\n session.stream?.end()\n session.client.end()\n this.sessions.delete(machineId)\n }\n this.updateStatus(machineId, 'disconnected')\n }\n\n // ─── PTY persistence (Approach A) — public API for ws-manager ────────────────\n\n /** 머신 ID 의 PTY 가 살아있는지. ws-manager 가 attach 전에 spawn 필요 여부 판단. */\n hasSession(machineId: string): boolean {\n return this.localSessions.has(machineId) || this.sessions.has(machineId)\n }\n\n /**\n * 현재 살아있는 PTY (local + SSH) 총합.\n *\n * 업데이트 install 직전 mobile-safe gate (autoplan E1) 에서 사용 — count > 0 이면\n * 사용자에게 confirmation 다이얼로그를 띄워 quitAndInstall 를 보류한다.\n *\n * `pendingFirstResize` 는 제외 — 아직 spawn 전 단계이고, 사용자에게는 \"활성 세션\"\n * 으로 인지되지 않는다. 모바일 attach 도중인 race 윈도우는 본 gate 가 그 시점의\n * snapshot 만 보기 때문에 false negative 가능하지만, 다음 update 트리거에서 다시\n * 잡힌다 (1시간 폴링).\n */\n getActivePtyCount(): number {\n return this.sessions.size + this.localSessions.size\n }\n\n /** 머신 ID 의 scrollback. 없으면 null. ws-manager 가 attach 시 청크 분할 송신. */\n getScrollback(machineId: string): PtyScrollbackBuffer | null {\n return this.scrollbacks.get(machineId) ?? null\n }\n\n /**\n * PTY 강제 재시작 (E4). 진행중인 자식 프로세스 모두 SIGHUP, scrollback 비움, 새 spawn.\n * 사용자가 모바일에서 명시적으로 호출. 200ms debounce lock 으로 더블 클릭 방어.\n */\n restartPty(machineId: string): void {\n if (this.restartingMachines.has(machineId)) {\n sshLog.info(\n `[ssh-manager] restartPty ignored (in-flight) machine=${machineId}`\n )\n return\n }\n this.restartingMachines.add(machineId)\n try {\n sshLog.info(`[ssh-manager] restartPty machine=${machineId}`)\n const machine = this.machines.find(m => m.id === machineId)\n if (!machine) return\n\n // 기존 PTY kill\n const localProc = this.localSessions.get(machineId)\n if (localProc) {\n localProc.kill()\n this.localSessions.delete(machineId)\n }\n const session = this.sessions.get(machineId)\n if (session) {\n session.stream?.end()\n session.client.end()\n this.sessions.delete(machineId)\n }\n // scrollback 비움 — 새 PTY 의 출력만 남도록\n // v1.5.0: 디스크 파일도 제거 (restart 는 backlog 명시 reset 의도).\n this.scrollbacks.delete(machineId)\n try {\n this.persister.remove(machineId)\n } catch {\n // ignore\n }\n // lifecycle metadata reset (lastAttach=now, attachCount+1) + dead broadcast\n // 후 새 connect() 가 onConnect 호출해 새 metadata 생성.\n this.lifecycle.onRestartPty(machineId)\n\n // status reset 후 재spawn — connect() 의 already-connected fastpath 를 우회\n machine.status = 'disconnected'\n this.connect(machineId, { forceReConnect: true })\n } finally {\n // connect() 가 async 라서 즉시 release 하면 race. 200ms 후 release 로 debounce.\n setTimeout(() => this.restartingMachines.delete(machineId), 200)\n }\n }\n\n /**\n * 머신별 scrollback 을 lazily 생성. PTY 출력 callback 에서 호출.\n *\n * v1.5.0: 첫 생성 시 디스크에서 restore 시도 — 있으면 deserialize, 없으면 새 buffer.\n * 디스크 재생성된 buffer 는 이후 append 흐름에서 자연스럽게 누적된다 (mobile attach 시\n * snapshotChunks 가 옛 backlog + 새 출력 모두 포함).\n */\n private getOrCreateScrollback(machineId: string): PtyScrollbackBuffer {\n let buf = this.scrollbacks.get(machineId)\n if (!buf) {\n // v1.7.8 — per-machine cap. Machine.ringBufferBytes 명시 시 우선, 미설정 시\n // default 1MB (SCROLLBACK_BUFFER_BYTES_DEFAULT). machine 조회 실패 (race\n // 또는 LRU eviction 직후) 시 default fallback — fail-open.\n // cross-repo with `claude_code_mobile` PR #48 의 overlay 노출 시간 비례 단축.\n const machine = this.machines.find(m => m.id === machineId)\n const cap = machine?.ringBufferBytes ?? SCROLLBACK_BUFFER_BYTES_DEFAULT\n\n const snapshot = this.persister.restoreSync(machineId)\n if (snapshot) {\n try {\n buf = PtyScrollbackBuffer.deserialize(cap, snapshot)\n } catch (err) {\n // 손상된 snapshot — 무시하고 새 buffer (caller 가 backlog 빈 상태로 시작).\n sshLog.warn(\n `[ssh-manager] scrollback restore failed for ${machineId}: ${(err as Error).message}`\n )\n buf = new PtyScrollbackBuffer(cap)\n }\n } else {\n buf = new PtyScrollbackBuffer(cap)\n }\n this.scrollbacks.set(machineId, buf)\n }\n return buf\n }\n\n sendInput(machineId: string, data: string): void {\n const localProc = this.localSessions.get(machineId)\n if (localProc) {\n localProc.write(data)\n this.lifecycle.onUserInput(machineId)\n return\n }\n\n const session = this.sessions.get(machineId)\n if (session?.stream) {\n session.stream.write(data)\n this.lifecycle.onUserInput(machineId)\n }\n }\n\n resize(\n machineId: string,\n cols: number,\n rows: number,\n source: 'desktop' | 'mobile' | 'desktop-restore' = 'desktop'\n ): void {\n const safeCols = clampCols(cols)\n const safeRows = clampRows(rows)\n\n // Phase 1 (20260519) — TTL 휴리스틱 제거, AuthorityResolver 위임.\n // invariant: source 가 현재 leader 와 일치할 때만 적용. 그 외 drop. handover 협상\n // 중이면 모든 source drop (사이즈 깜빡임 차단). desktop-restore 는 무조건 통과.\n if (!this.authority.allowsResize(machineId, source)) {\n if (process.env.NODE_ENV === 'development') {\n sshLog.info(\n `[pty-resize] dropped by authority resolver machine=${machineId} source=${source} ${safeCols}x${safeRows}`\n )\n }\n return\n }\n\n // desktop source 면 lastDesktopSize 갱신 — grace expire 시 복귀 사이즈로 사용.\n // desktop-restore 는 이미 lastDesktopSize 가 source 인 경로라 재기록 불필요.\n if (source === 'desktop') {\n this.lastDesktopSize.set(machineId, { cols: safeCols, rows: safeRows })\n }\n\n const prev = this.ptySizes.get(machineId)\n // prev=undefined (첫 resize) 면 isNoop=false → 정상 spawn 경로. 의도된 동작.\n const isNoop = prev?.cols === safeCols && prev?.rows === safeRows\n\n if (process.env.NODE_ENV === 'development') {\n sshLog.info('[pty-resize]', {\n machineId,\n source,\n raw: { cols, rows },\n safe: { cols: safeCols, rows: safeRows },\n prev,\n isNoop,\n })\n }\n\n // ptySizes 갱신 — non-noop 만. wake 보다 먼저 update 해서 spawn awaiter 가\n // 새 ptySizes 를 읽도록 (microtask 순서 의존 없는 robust 패턴).\n if (!isNoop) {\n this.ptySizes.set(machineId, { cols: safeCols, rows: safeRows })\n sshLog.info(\n `[pty-resize] machine=${machineId} source=${source} ${safeCols}x${safeRows} (prev=${prev?.cols ?? '-'}x${prev?.rows ?? '-'})`\n )\n // [attach frame-snapshot 20260610] GridEmulator 를 적용된 PTY 사이즈로 동기화 — 안 하면\n // emulator 가 onConnect 기본(80×24)에 고정돼 serializeAnsi()(FRAME_SNAPSHOT)·GRID_SNAPSHOT\n // 이 잘못된 geometry 로 직렬화돼 모바일 화면이 깨진다(review C1). lifecycle.onResize 는\n // emulator 미존재 시 no-op 이라 cap=pty.data 경로에서도 안전.\n this.lifecycle.onResize(machineId, safeCols, safeRows)\n }\n\n // spawn-deferring: 첫 resize 도착 시 spawn 코루틴을 깨운다. noop 여도 wake 해야\n // 재연결/탭 복귀 시 cached 사이즈와 동일한 첫 resize 메시지에 코루틴이 막혀\n // timeout 까지 가는 회귀 방지 (spawn-deferring 정합성).\n const wake = this.pendingFirstResize.get(machineId)\n if (wake) wake()\n\n if (isNoop) {\n // D-1 — 동일 사이즈 echo: setWindow / mgr.resize / broadcast 모두 skip.\n // psmux refresh-client 무한 누적 차단 → freeze 증상 즉시 사라짐.\n // 동일 사이즈 broadcast 도 불필요 (renderer/mobile 모두 이미 그 사이즈).\n return\n }\n\n const localProc = this.localSessions.get(machineId)\n if (localProc) {\n localProc.resize(safeCols, safeRows)\n } else {\n const session = this.sessions.get(machineId)\n session?.stream?.setWindow(safeRows, safeCols, 0, 0)\n }\n\n // SessionManager 측에도 sync — 멀티플렉서 제거 후 NoneSessionManager.resize 는 no-op\n // (인터페이스 보존: 향후 multiplexer 재도입 시 alt-screen redraw 훅 자리).\n const mgr = this.findCachedSessionManagerFor(machineId)\n if (mgr) {\n // fire-and-forget — resize 호출 자체가 throw 흡수 처리되므로 unhandled rejection 위험 0.\n void mgr.resize(machineId, safeCols, safeRows, source).catch(() => {\n /* noop */\n })\n }\n\n // Phase 1 — 모든 viewer 동일 사이즈 sync. AuthorityResolver 가 source 필터로\n // invariant 보장하므로 renderer self-echo 도 안전 (renderer 의 resizeTerminal 안\n // noop guard 가 동일 사이즈 echo 차단). source-aware (skipRenderer) 분기 제거.\n this.authority.recordAppliedSize(machineId, safeCols, safeRows)\n this.broadcast({\n type: 'terminal_resize_ack',\n machineId,\n cols: safeCols,\n rows: safeRows,\n })\n }\n\n /**\n * 캐시된 SessionManager 중 본 머신이 사용 중인 것 반환. resize 시점은 connectLocal 의\n * getSessionManager() 가 이미 cache 를 채운 상태라 여기서는 lookup 만.\n *\n * Note: 같은 setting 의 매니저를 여러 머신이 공유하므로, 머신별 정확한 매니저 lookup 은\n * machine.multiplexer 를 모르면 불가능. 다만 `auto` 가 default 이므로 cache 의 첫 매니저로\n * 안전 fallback — Phase 1 단계에서는 한 사용자 환경의 multiplexer 설정이 동일한 게 일반적.\n */\n private findCachedSessionManagerFor(\n _machineId: string\n ): SessionManager | undefined {\n if (this.sessionManagers.size === 0) return undefined\n return this.sessionManagers.values().next().value\n }\n\n /**\n * machine 의 multiplexer setting 별 SessionManager 를 lazy 로 만들어 반환(setting 동일 시 공유).\n * 멀티플렉서 제거(2026-06-23) 후 createSessionManager 는 항상 NoneSessionManager 를 반환한다.\n */\n private getSessionManager(machine: Machine): SessionManager {\n const setting = resolveMultiplexerSetting(machine)\n const mouse = machine.multiplexerMouse ?? false\n const cacheKey = `${setting}:mouse=${mouse}` as MultiplexerSetting\n const cached = this.sessionManagers.get(cacheKey)\n if (cached) return cached\n const mgr = createSessionManager(setting)\n this.sessionManagers.set(cacheKey, mgr)\n return mgr\n }\n\n /// 첫 spawn 직전 클라이언트의 첫 terminal_resize 메시지를 기다린다.\n /// - ptySizes에 이미 값이 있으면 즉시 resolve (재연결 fast path).\n /// - timeoutMs 안에 안 오면 resolve (호출부가 fallback 80x24 사용).\n /// 동일 machineId의 중복 호출은 마지막 호출만 유효 (이전 resolver는 cleanup).\n private waitForFirstResize(\n machineId: string,\n timeoutMs = 600\n ): Promise<void> {\n if (this.ptySizes.has(machineId)) return Promise.resolve()\n const prev = this.pendingFirstResize.get(machineId)\n if (prev) prev() // resolve any stale waiter\n return new Promise<void>(resolve => {\n let done = false\n const finish = () => {\n if (done) return\n done = true\n this.pendingFirstResize.delete(machineId)\n clearTimeout(timer)\n resolve()\n }\n const timer = setTimeout(finish, timeoutMs)\n this.pendingFirstResize.set(machineId, finish)\n })\n }\n\n runInitCommands(machineId: string): void {\n const machine = this.machines.find(m => m.id === machineId)\n if (!machine) return\n if (machine.initCommands?.length) {\n this.sendInput(\n machineId,\n machine.initCommands.map(command => command.trim()).join(' && ') + '\\r'\n )\n }\n }\n\n // ── [Cockpit] running-agent 탐지 + interactive PTY agent 종료 ────────────────\n\n /**\n * 머신의 interactive PTY foreground 에서 도는 agent 를 탐지해 [Machine.runningAgent]\n * 를 갱신하고, 값이 바뀌었으면 `machine_list` 를 broadcast 한다.\n *\n * 로컬 PTY 만 탐지 가능 (SSH 원격은 pid 가 원격 호스트 것이라 OS 조회 불가) — local\n * session 이 없으면 'none' 으로 두고 즉시 반환. `agent.pid` 자식 트리를 OS 로 조회하는\n * [detectRunningAgent] 위임. 폴링(startStatusTimer 5s)과 cockpit 진입 직전에 호출된다.\n */\n /**\n * 머신의 현재 탐지된 foreground agent 를 반환한다(없으면 'none').\n *\n * `detectAndBroadcastRunningAgent` 가 갱신한 `Machine.runningAgent` 의 동기 조회.\n * 콕핏→터미널 핸드오프 주입 직전 **main 측 권위 가드**로 쓴다 — renderer 의 runningAgent 는\n * 5s 폴링이라 stale 일 수 있어, 살아있는 claude 프롬프트에 `claude --resume` 이 박히는 것을\n * main 이 최종 차단한다(호출 전 detect 를 await 해 최신화할 것).\n */\n getRunningAgent(machineId: string): RunningAgentKind {\n return this.machines.find(m => m.id === machineId)?.runningAgent ?? 'none'\n }\n\n async detectAndBroadcastRunningAgent(machineId: string): Promise<void> {\n const proc = this.localSessions.get(machineId)\n const machine = this.machines.find(m => m.id === machineId)\n if (!machine) return\n\n let detected: RunningAgentKind = 'none'\n if (proc && typeof proc.pid === 'number') {\n detected = await detectRunningAgent(proc.pid)\n }\n if (machine.runningAgent !== detected) {\n sshLog.info(\n `[cockpit] runningAgent changed machine=${machineId} ${machine.runningAgent ?? 'none'} → ${detected} — broadcasting`\n )\n machine.runningAgent = detected\n this.broadcastMachines()\n // [Cockpit 신선도] claude 가 새로 떴으면(→claude 전환) 세션 목록을 자동 갱신한다 —\n // 모바일이 재요청하지 않아도 방금 시작한 세션이 picker 에 나타난다. broadcastAgentSessions\n // 가 flush 전이면 bounded 재스캔까지 예약하므로 타이밍 창도 덮는다.\n if (detected === 'claude') {\n this.broadcastAgentSessions(machineId)\n }\n }\n }\n\n /**\n * 사용자 \"닫고 새로 열기\" — interactive PTY 의 foreground agent 에 SIGINT(Ctrl-C)\n * 를 주입한다. PTY 자체는 유지(shell 복귀) — agent.run 으로 띄운 별도 프로세스가\n * 아니라 사용자가 PTY 안에서 직접 띄운 claude TUI 를 종료하는 경로.\n *\n * 모바일은 stdin 을 직접 못 건드리므로(claude 가 점유) 이 종료도 데스크탑이 한다.\n * Ctrl-C 2회를 약간의 간격으로 보내 claude 의 \"한 번 더 누르면 종료\" 프롬프트까지 통과.\n */\n killPtyAgent(machineId: string): void {\n sshLog.info(`[cockpit] killPtyAgent machine=${machineId}`)\n // \\x03 = Ctrl-C (ETX). claude TUI 는 첫 Ctrl-C 에 확인 프롬프트, 둘째에 종료.\n this.sendInput(machineId, '\\x03')\n setTimeout(() => this.sendInput(machineId, '\\x03'), 200)\n }\n\n /**\n * [Cockpit] 머신 cwd 의 claude 세션 목록을 스캔해 `agent.sessions` 로 broadcast.\n *\n * cwd 는 [resolveLocalCwd] (OSC7 추적 → localCwd → process.cwd → home) 로 결정.\n * 로컬 머신 기준 — SSH 원격은 cwd 가 원격 경로라 데스크탑 디스크의 ~/.claude 와\n * 무관(빈 목록). [listAgentSessions] 가 mtime 내림차순 정렬 + 미리보기 추출.\n */\n broadcastAgentSessions(machineId: string): void {\n // 새 요청이 최신 기준 — 직전 머신의 빈-세션 재스캔 스케줄을 먼저 취소(중복 방지).\n this.cancelAgentSessionsRescan(machineId)\n\n const cwd = this.resolveLocalCwd(machineId)\n const disk = listAgentSessions(cwd)\n // [Cockpit 신선도 20260614] 방금 콕핏으로 연 장수명 세션은 `.jsonl` flush 전이라 디스크\n // 스캔에서 빠진다. 데스크탑이 이미 아는 running id(AgentRunner)를 합류시켜 picker 가 \"현재\n // 세션\"을 즉시 보이게 한다. provider 미주입이면 disk 그대로(graceful degrade).\n const runningIds = this.activeSessionProvider?.(machineId) ?? []\n const sessions = mergeRunningSessions(disk, runningIds, Date.now())\n sshLog.info(\n `[cockpit] agent.sessions machine=${machineId} cwd=${cwd} disk=${disk.length} running=${runningIds.length} count=${sessions.length}`\n )\n this.broadcast({ type: 'agent.sessions', machineId, cwd, sessions })\n\n // [Cockpit 신선도] claude 가 방금 떠 `.jsonl` flush 전이면 목록이 0건일 수 있다. running\n // 중인데 비었으면 bounded 재스캔을 예약 — flush 직후 세션이 surface 되면 자동 재방출하고\n // 멈춘다. 순수 0-바이트(첫 턴 전) 영구 케이스는 디스크에 파일이 없어 범위 밖(설계 문서 참조).\n const runningAgent = this.machines.find(m => m.id === machineId)?.runningAgent\n if (sessions.length === 0 && runningAgent === 'claude') {\n const cancel = scheduleSessionRescan({\n delaysMs: AGENT_SESSIONS_RESCAN_DELAYS_MS,\n rescan: () => listAgentSessions(this.resolveLocalCwd(machineId)),\n onSurfaced: surfaced => {\n const surfacedCwd = this.resolveLocalCwd(machineId)\n sshLog.info(\n `[cockpit] agent.sessions rescan machine=${machineId} count=${surfaced.length} — surfaced, broadcasting`\n )\n this.broadcast({\n type: 'agent.sessions',\n machineId,\n cwd: surfacedCwd,\n sessions: surfaced,\n })\n this.agentSessionsRescanCancel.delete(machineId)\n },\n })\n this.agentSessionsRescanCancel.set(machineId, cancel)\n }\n }\n\n /** [Cockpit 신선도] 머신의 빈-세션 재스캔 스케줄을 취소·해제한다(없으면 no-op). */\n private cancelAgentSessionsRescan(machineId: string): void {\n const cancel = this.agentSessionsRescanCancel.get(machineId)\n if (cancel) {\n cancel()\n this.agentSessionsRescanCancel.delete(machineId)\n }\n }\n\n /**\n * [Cockpit] 한 세션의 이전 대화(transcript)를 읽어 `agent.session.transcript` 로 broadcast.\n *\n * cwd 는 [broadcastAgentSessions] 와 동일하게 [resolveLocalCwd] 로 결정 — 세션 목록을\n * 뽑은 그 디렉토리와 일치해야 파일을 찾는다. [readSessionTranscript] 가 user/assistant\n * 텍스트만 최근분(상한)으로 추린다.\n */\n broadcastSessionTranscript(machineId: string, sessionId: string): void {\n const cwd = this.resolveLocalCwd(machineId)\n const messages = readSessionTranscript(cwd, sessionId)\n sshLog.info(\n `[cockpit] agent.session.transcript machine=${machineId} session=${sessionId} count=${messages.length}`\n )\n this.broadcast({ type: 'agent.session.transcript', machineId, sessionId, messages })\n }\n\n cleanup(): void {\n this._destroyed = true\n\n for (const [id] of this.statusTimers) {\n this.clearStatusTimer(id)\n }\n\n // [Cockpit 신선도] 진행 중인 빈-세션 재스캔 타이머 전량 해제.\n for (const [, cancel] of this.agentSessionsRescanCancel) {\n cancel()\n }\n this.agentSessionsRescanCancel.clear()\n\n for (const [id] of this.localSessions) {\n this.pendingDisconnects.add(id)\n }\n\n // spawn-deferring 대기 중인 코루틴이 있으면 즉시 wake — _destroyed 체크 없이도\n // post-await에서 pendingDisconnects로 abort. 메모리 누수 방지.\n for (const [, wake] of this.pendingFirstResize) {\n wake()\n }\n this.pendingFirstResize.clear()\n\n for (const [, proc] of this.localSessions) {\n proc.kill()\n }\n this.localSessions.clear()\n\n for (const [id] of this.sessions) {\n this.disconnect(id)\n }\n\n // v1.5.0 — buffers 디스크 flush. before-quit (sync handler) 안에서 안전.\n // flushSync 는 빈 buffer 면 unlink, 아니면 JSON write. 실패는 swallow.\n try {\n this.persister.flushAllSync(this.scrollbacks)\n } catch {\n // flush 실패 — 종료 흐름 막지 않음. 다음 launch 에서 이전 상태 restore.\n }\n this.scrollbacks.clear()\n this.restartingMachines.clear()\n // PtyLifecycleManager 의 idle timer + Map 모두 해제.\n this.lifecycle.dispose()\n // Phase 1 — AuthorityResolver 의 grace/handover timer 취소 + state Map clear.\n this.authority.destroy()\n\n // SessionManager lifecycle 종료 hook — NoneSessionManager 는 dispose 미구현이라 skip.\n // Promise 는 fire-and-forget — cleanup 흐름을 막지 않는다.\n for (const mgr of this.sessionManagers.values()) {\n void mgr.dispose?.().catch(() => undefined)\n }\n this.sessionManagers.clear()\n }\n\n private parseOsc7(data: string, machineId: string): void {\n const match = data.match(/\\x1b\\]7;file:\\/\\/[^/]*(\\/.+?)(?:\\x07|\\x1b\\\\)/)\n if (match) {\n this.cwdMap.set(machineId, decodeURIComponent(match[1]))\n }\n }\n\n private startStatusTimer(machineId: string): void {\n this.clearStatusTimer(machineId)\n if (!this.gitLimiters.has(machineId)) {\n this.gitLimiters.set(machineId, pLimit(1))\n }\n const limit = this.gitLimiters.get(machineId)!\n // 타이머 자체는 5초 tick (cockpit 탐지의 배너 반응성 유지). git status 는 12 tick\n // 마다(=60초) 만 호출해 [git_result] 로그/네트워크를 줄인다. 첫 tick 에 1회 즉시\n // 호출되도록 count 를 git 주기로 초기화한다.\n let tick = GIT_STATUS_EVERY_N_TICKS\n const timer = setInterval(() => {\n if (++tick > GIT_STATUS_EVERY_N_TICKS) {\n tick = 1\n limit(() => this.executeGitAction(machineId, { kind: 'status' })).catch(\n () => {}\n )\n }\n // [Cockpit] running-agent 탐지 — 매 tick(5초). 변경 시에만 broadcast(내부 dedup).\n void this.detectAndBroadcastRunningAgent(machineId).catch(() => {})\n }, 5000)\n this.statusTimers.set(machineId, timer)\n }\n\n private clearStatusTimer(machineId: string): void {\n const timer = this.statusTimers.get(machineId)\n if (timer) {\n clearInterval(timer)\n this.statusTimers.delete(machineId)\n }\n this.gitLimiters.delete(machineId)\n }\n\n /**\n * 로컬 머신의 git 작업 디렉터리 결정 (폴백 체인).\n * 1) OSC7로 추적된 cwd\n * 2) 머신 등록 시 지정한 localCwd\n * 3) electron 자체 process.cwd()\n * 4) homeDir\n *\n * public: [Cockpit v2 장수명] ws-manager 의 agent.session.open 핸들러가 콕핏 claude 의 spawn\n * cwd 로 쓴다 — 터미널과 같은 cwd 라 같은 `~/.claude` 세션 풀을 공유(Phase C 토대).\n */\n resolveLocalCwd(machineId: string): string {\n const tracked = this.cwdMap.get(machineId)\n if (tracked) return tracked\n const machine = this.machines.find(m => m.id === machineId)\n if (machine?.localCwd) return machine.localCwd\n try {\n const procCwd = process.cwd()\n if (procCwd) return procCwd\n } catch {\n /* ignore */\n }\n return homeDir\n }\n\n /**\n * [Push-Notif Phase 1] Claude 훅이 준 cwd 를 로컬 머신 id 로 역매핑한다.\n *\n * Claude 훅은 데스크탑 PTY 와 무관한 것까지 머신의 **모든** claude 세션에 발화하므로\n * (Phase 0 실측: 동시 4개 세션), 데스크탑이 호스팅하는 로컬 머신의 작업 디렉토리와\n * 일치하는 이벤트만 골라야 한다 — 나머지는 무관 세션으로 drop. cwd 표기가 소스마다\n * 다르므로([resolveLocalCwd] 의 OSC7/localCwd/process.cwd) [normalizeCwd] 로 통일 비교.\n * SSH 원격 머신은 cwd 가 원격 경로라 제외. 일치 없으면 null.\n *\n * @param cwd 훅 stdin 의 `cwd` (절대 경로).\n * @returns 일치하는 로컬 머신 id, 없으면 null.\n */\n findLocalMachineIdByCwd(cwd: string): string | null {\n const target = normalizeCwd(cwd)\n for (const machine of this.machines) {\n if (machine.machineType === 'ssh') continue\n if (normalizeCwd(this.resolveLocalCwd(machine.id)) === target) {\n return machine.id\n }\n }\n return null\n }\n\n /** 후보 디렉터리에서 실제 git 저장소 루트로 정규화. 저장소가 아니면 null. */\n private async normalizeToGitRoot(candidate: string): Promise<string | null> {\n try {\n const git = simpleGit({ baseDir: candidate })\n const top = (await git.revparse(['--show-toplevel'])).trim()\n return top || null\n } catch {\n return null\n }\n }\n\n async executeGitAction(machineId: string, action: GitAction): Promise<void> {\n const machine = this.machines.find(m => m.id === machineId)\n if (!machine) return\n\n if (machine.machineType === 'local') {\n const candidate = this.resolveLocalCwd(machineId)\n const gitRoot = await this.normalizeToGitRoot(candidate)\n const cwd = gitRoot ?? candidate\n return this.executeGitActionLocal(\n machineId,\n action,\n cwd,\n gitRoot !== null\n )\n }\n\n // SSH 경로 — 원격 cwd는 OSC7로만 추적, 없으면 home\n const cwd = this.cwdMap.get(machineId) ?? homeDir\n\n try {\n // validateGitRef/buildGitCommand 를 try 안에 둬, invalid ref 나 미지원 kind 가\n // 조용한 unhandled rejection 대신 git_result{success:false} 로 표면화되게 한다.\n // (검증은 shell 명령을 조립하는 SSH 경로 전용 — relay 경로는 simple-git argv 라\n // shell injection 이 불가능하고, Unicode/CJK 브랜치명을 막으면 안 된다.)\n if (action.kind === 'checkout') validateGitRef(action.branch, 'branch')\n if (action.kind === 'create_branch') validateGitRef(action.name, 'name')\n if (action.kind === 'delete_branch') {\n validateGitRef(action.branch, 'branch')\n // F-E-4 권한 가드 — 기본 브랜치(origin/HEAD)는 force 와 무관하게 거부.\n // git branch -D <default> 는 체크아웃 안 돼 있으면 그냥 지워지므로 (active 와\n // 달리 git 이 안 막음) 서버측 방어선이 필요하다. 모바일 caller-side 가드의\n // 우회/구버전 케이스 대비 defense-in-depth.\n const def = await this.resolveDefaultBranchSsh(machineId, cwd)\n if (def && def === action.branch) {\n gitLog.warn({\n machineId,\n action: 'delete_branch',\n cwd,\n branch: action.branch,\n blocked: 'default_branch',\n })\n this.broadcast({\n type: 'git_result',\n machineId,\n action: action.kind,\n success: false,\n error: `Refusing to delete the default branch '${action.branch}'`,\n errorCode: 'generic',\n })\n return\n }\n }\n const cmd = buildGitCommand(action, cwd)\n const output = await this.execSsh(machineId, cmd)\n if (action.kind === 'status') {\n const data = this.parseGitStatus(output)\n data.cwd = cwd\n gitLog.info({\n machineId,\n action: 'status',\n cwd,\n isGitRepo: data.isGitRepo,\n files: data.files.length,\n })\n this.broadcast({\n type: 'git_result',\n machineId,\n action: action.kind,\n success: true,\n data,\n })\n } else if (action.kind === 'log') {\n const commits = this.parseGitLog(output)\n const data: GitStatusData = {\n branch: '',\n ahead: 0,\n behind: 0,\n branches: [],\n files: [],\n isGitRepo: true,\n changedFiles: 0,\n commits,\n cwd,\n }\n gitLog.info({\n machineId,\n action: 'log',\n cwd,\n commits: commits.length,\n })\n this.broadcast({\n type: 'git_result',\n machineId,\n action: action.kind,\n success: true,\n data,\n })\n } else if (action.kind === 'delete_branch') {\n // execSsh 는 exit code 를 보지 않고 stdout+stderr 를 합쳐 resolve 하므로,\n // git 삭제 실패는 throw 가 아니라 \"error:\"/\"fatal:\" 텍스트로 온다.\n // 텍스트로 성공/실패를 판정해 SSH 경로의 false-success 를 차단한다.\n if (/^(?:error|fatal):/m.test(output)) {\n const msg = output.trim()\n gitLog.info({ machineId, action: 'delete_branch', cwd, error: msg })\n this.broadcast({\n type: 'git_result',\n machineId,\n action: action.kind,\n success: false,\n error: msg,\n errorCode: classifyDeleteError(msg),\n })\n } else {\n gitLog.warn({\n machineId,\n action: 'delete_branch',\n cwd,\n branch: action.branch,\n force: action.force,\n success: true,\n })\n this.broadcast({\n type: 'git_result',\n machineId,\n action: action.kind,\n success: true,\n })\n setTimeout(\n () =>\n this.executeGitAction(machineId, { kind: 'status' }).catch(\n () => {}\n ),\n 500\n )\n }\n } else {\n gitLog.info({\n machineId,\n action: action.kind,\n cwd,\n success: true,\n })\n this.broadcast({\n type: 'git_result',\n machineId,\n action: action.kind,\n success: true,\n })\n setTimeout(\n () =>\n this.executeGitAction(machineId, { kind: 'status' }).catch(\n () => {}\n ),\n 500\n )\n }\n } catch (err) {\n const message = (err as Error).message\n gitLog.info({\n machineId,\n action: action.kind,\n cwd,\n error: message,\n })\n this.broadcast({\n type: 'git_result',\n machineId,\n action: action.kind,\n success: false,\n error: message,\n errorCode:\n action.kind === 'delete_branch'\n ? classifyDeleteError(message)\n : undefined,\n })\n }\n }\n\n private async executeGitActionLocal(\n machineId: string,\n action: GitAction,\n cwd: string,\n isGitRepo: boolean\n ): Promise<void> {\n const git = simpleGit({ baseDir: cwd })\n\n // git 저장소가 아니면 status/log는 빈 응답으로 broadcast (모바일이 cwd 표시 가능)\n if (!isGitRepo) {\n if (action.kind === 'status' || action.kind === 'log') {\n const data: GitStatusData = {\n branch: '',\n ahead: 0,\n behind: 0,\n branches: [],\n files: [],\n isGitRepo: false,\n changedFiles: 0,\n commits: [],\n cwd,\n }\n gitLog.info({\n machineId,\n action: action.kind,\n cwd,\n isGitRepo: false,\n })\n this.broadcast({\n type: 'git_result',\n machineId,\n action: action.kind,\n success: true,\n data,\n })\n return\n }\n // 변경 액션은 git repo 아니면 실패\n gitLog.info({\n machineId,\n action: action.kind,\n cwd,\n error: 'not a git repository',\n })\n this.broadcast({\n type: 'git_result',\n machineId,\n action: action.kind,\n success: false,\n error: `Not a git repository: ${cwd}`,\n })\n return\n }\n\n try {\n if (action.kind === 'status') {\n const status: StatusResult = await git.status()\n const branchSummary: BranchSummary = await git.branch(['-vva'])\n\n const files: GitFileData[] = status.files.map(f => ({\n path: f.path,\n state: mapSimpleGitState(f.index, f.working_dir),\n isStaged: f.index !== ' ' && f.index !== '?',\n }))\n\n const branches: GitBranchData[] = Object.values(\n branchSummary.branches\n ).map(b => ({\n name: b.name.replace('remotes/', ''),\n isRemote: b.name.startsWith('remotes/'),\n isActive: b.current,\n lastCommitHash: b.commit || undefined,\n ahead: b.current ? status.ahead : 0,\n behind: b.current ? status.behind : 0,\n }))\n\n const data: GitStatusData = {\n branch: status.current ?? '',\n ahead: status.ahead,\n behind: status.behind,\n branches,\n files,\n isGitRepo: true,\n changedFiles: files.length,\n cwd,\n }\n\n gitLog.info({\n machineId,\n action: 'status',\n cwd,\n isGitRepo: true,\n branch: data.branch,\n files: files.length,\n })\n this.broadcast({\n type: 'git_result',\n machineId,\n action: 'status',\n success: true,\n data,\n })\n } else if (action.kind === 'log') {\n const limit = Math.max(1, Math.min(action.limit | 0, 200))\n const fmt = '%H%x09%h%x09%an%x09%ar%x09%D%x09%s'\n const output = await git.raw([\n 'log',\n `--pretty=format:${fmt}`,\n '-n',\n String(limit),\n ])\n const commits = this.parseGitLog(output)\n const data: GitStatusData = {\n branch: '',\n ahead: 0,\n behind: 0,\n branches: [],\n files: [],\n isGitRepo: true,\n changedFiles: 0,\n commits,\n cwd,\n }\n gitLog.info({\n machineId,\n action: 'log',\n cwd,\n commits: commits.length,\n })\n this.broadcast({\n type: 'git_result',\n machineId,\n action: 'log',\n success: true,\n data,\n })\n } else {\n switch (action.kind) {\n case 'checkout':\n await git.checkout(action.branch)\n break\n case 'create_branch':\n if (action.checkout) {\n await git.checkoutLocalBranch(action.name)\n } else {\n await git.branch([action.name])\n }\n break\n case 'delete_branch': {\n // F-E-4 권한 가드 — 기본 브랜치(origin/HEAD)는 force 와 무관하게 거부.\n // 모바일 caller-side 차단의 우회/구버전 대비 server 측 defense-in-depth.\n const def = await this.resolveDefaultBranchLocal(git)\n if (def && def === action.branch) {\n gitLog.warn({\n machineId,\n action: 'delete_branch',\n cwd,\n branch: action.branch,\n blocked: 'default_branch',\n })\n throw new Error(\n `Refusing to delete the default branch '${action.branch}'`\n )\n }\n // safe(-d) 는 미머지면 simple-git 이 throw → catch 에서 errorCode 분류 →\n // 모바일이 force CTA 노출. force(-D) 는 미머지도 삭제 (커밋 유실 가능).\n if (action.force) {\n await git.raw(['branch', '-D', action.branch])\n gitLog.warn({\n machineId,\n action: 'delete_branch',\n cwd,\n branch: action.branch,\n force: true,\n })\n } else {\n await git.deleteLocalBranch(action.branch)\n }\n break\n }\n case 'pull':\n await git.pull()\n break\n case 'push':\n await git.push()\n break\n default: {\n // exhaustiveness 가드 — 미지원 kind 가 조용히 success:true 로\n // broadcast 되던 것을 차단 (autoplan F-CEO-2). 새 GitAction kind 는\n // 컴파일 단계(never)에서 case 추가를 강제받는다.\n const _exhaustive: never = action\n throw new Error(\n `Unsupported git action: ${(_exhaustive as GitAction).kind}`\n )\n }\n }\n gitLog.info({\n machineId,\n action: action.kind,\n cwd,\n success: true,\n })\n this.broadcast({\n type: 'git_result',\n machineId,\n action: action.kind,\n success: true,\n })\n setTimeout(\n () =>\n this.executeGitAction(machineId, { kind: 'status' }).catch(\n () => {}\n ),\n 500\n )\n }\n } catch (err) {\n const message = (err as Error).message\n gitLog.info({\n machineId,\n action: action.kind,\n cwd,\n error: message,\n })\n this.broadcast({\n type: 'git_result',\n machineId,\n action: action.kind,\n success: false,\n error: message,\n // 삭제 실패만 errorCode 분류 (모바일 l10n 매핑 + force CTA 판단). 그 외\n // 액션은 기존대로 raw error 만 전달 (스키마 optional).\n errorCode:\n action.kind === 'delete_branch'\n ? classifyDeleteError(message)\n : undefined,\n })\n }\n }\n\n /**\n * 기본 브랜치명(origin/HEAD 의 short ref)을 resolve. 못 찾으면 null.\n *\n * delete_branch 권한 가드용(F-E-4) — null 이면 가드 미적용(git 의 active-branch\n * 보호 + 모바일 caller-side 차단에 의존). simple-git(relay) 경로.\n */\n private async resolveDefaultBranchLocal(\n git: ReturnType<typeof simpleGit>\n ): Promise<string | null> {\n try {\n const ref = await git.raw([\n 'symbolic-ref',\n '--quiet',\n '--short',\n 'refs/remotes/origin/HEAD',\n ])\n return ref.trim().replace(/^origin\\//, '') || null\n } catch {\n return null\n }\n }\n\n /**\n * SSH(shell) 경로용 기본 브랜치 resolver. git 명령이라 cmd/bash 양쪽 동작.\n * 실패(origin/HEAD 미설정 등)면 null → 가드 미적용.\n */\n private async resolveDefaultBranchSsh(\n machineId: string,\n cwd: string\n ): Promise<string | null> {\n try {\n const out = await this.execSsh(\n machineId,\n `cd ${JSON.stringify(cwd)} && git symbolic-ref --quiet --short refs/remotes/origin/HEAD 2>/dev/null`\n )\n return out.trim().replace(/^origin\\//, '') || null\n } catch {\n return null\n }\n }\n\n private parseGitLog(output: string): GitCommitData[] {\n const commits: GitCommitData[] = []\n let dropped = 0\n for (const line of output.split('\\n')) {\n if (!line) continue\n const parts = line.split('\\t')\n if (parts.length < 6) {\n dropped += 1\n continue\n }\n const [hash, shortHash, author, dateRelative, refsRaw, ...subjectParts] =\n parts\n const subject = subjectParts.join('\\t')\n const refs = refsRaw\n ? refsRaw\n .split(',')\n .map(s => s.trim())\n .filter(Boolean)\n : []\n commits.push({\n hash: hash ?? '',\n shortHash: shortHash || (hash ? hash.slice(0, 7) : ''),\n subject: subject ?? '',\n author: author ?? '',\n dateRelative: dateRelative ?? '',\n refs,\n })\n }\n if (dropped > 0) {\n gitLog.warn('parseGitLog dropped malformed lines', {\n dropped,\n parsed: commits.length,\n })\n }\n return commits\n }\n\n private parseGitStatus(output: string): GitStatusData {\n const sections: Record<string, string> = {}\n let current = ''\n for (const line of output.split('\\n')) {\n if (line.startsWith('___') && line.endsWith('___')) {\n current = line.slice(3, -3)\n sections[current] = ''\n } else if (current) {\n sections[current] =\n (sections[current] ? sections[current] + '\\n' : '') + line\n }\n }\n\n const isGitRepo = (sections['GIT_REPO'] ?? '').trim() === 'true'\n if (!isGitRepo) {\n return {\n branch: '',\n ahead: 0,\n behind: 0,\n branches: [],\n files: [],\n isGitRepo: false,\n changedFiles: 0,\n }\n }\n\n const branch = (sections['BRANCH'] ?? '').trim()\n\n const files: GitFileData[] = []\n for (const line of (sections['STATUS'] ?? '').split('\\n')) {\n if (line.length < 3) continue\n const xy = line.slice(0, 2)\n const filePath = line.slice(3).trim()\n const isStaged = xy[0] !== ' ' && xy[0] !== '?'\n let state: GitFileData['state'] = 'modified'\n const char = isStaged ? xy[0] : xy[1]\n if (char === 'A') state = 'added'\n else if (char === 'D') state = 'deleted'\n else if (char === '?') state = 'untracked'\n else if (char === 'R') state = 'renamed'\n else if (char === 'U') state = 'conflicted'\n files.push({ path: filePath, state, isStaged })\n }\n\n const branches: GitBranchData[] = []\n for (const line of (sections['BRANCHES'] ?? '').split('\\n')) {\n if (!line.trim()) continue\n const isActive = line.startsWith('*')\n const isRemote = line.includes('remotes/')\n const parts = line.replace(/^\\*?\\s+/, '').split(/\\s+/)\n const name = parts[0]?.replace('remotes/', '') ?? ''\n const lastCommitHash =\n parts[1] && !parts[1].startsWith('->') ? parts[1] : undefined\n if (name) branches.push({ name, isRemote, isActive, lastCommitHash })\n }\n\n let ahead = 0\n let behind = 0\n const abLine = (sections['AHEAD_BEHIND'] ?? '').trim()\n if (abLine) {\n const nums = abLine.split(/\\s+/)\n ahead = parseInt(nums[0] ?? '0', 10) || 0\n behind = parseInt(nums[1] ?? '0', 10) || 0\n }\n\n return {\n branch,\n ahead,\n behind,\n branches,\n files,\n isGitRepo: true,\n changedFiles: files.length,\n }\n }\n\n private execSsh(machineId: string, cmd: string): Promise<string> {\n return new Promise((resolve, reject) => {\n const session = this.sessions.get(machineId)\n if (!session) {\n reject(new Error('no ssh session'))\n return\n }\n session.client.exec(cmd, (err, stream) => {\n if (err) {\n reject(err)\n return\n }\n let out = ''\n stream.on('data', (d: Buffer) => {\n out += d.toString()\n })\n stream.stderr.on('data', (d: Buffer) => {\n out += d.toString()\n })\n stream.on('close', () => resolve(out))\n })\n })\n }\n\n private updateStatus(\n machineId: string,\n status: ConnectionState,\n error?: string\n ): void {\n if (this._destroyed) return\n const machine = this.machines.find(m => m.id === machineId)\n if (machine) {\n machine.status = status\n if (error) {\n machine.errorMessage = error\n } else if (status === 'connected') {\n machine.errorMessage = undefined\n machine.lastConnected = new Date()\n }\n this.broadcast({ type: 'connection_status', machineId, status, error })\n }\n }\n}\n","import pty from 'node-pty'\r\n\r\nimport type { PtyProcess, PtyCreateOptions } from '@arva/shared/types'\r\n\r\nexport function createNodePty(opts: PtyCreateOptions): PtyProcess {\r\n const shell =\r\n process.platform === 'win32'\r\n ? (opts.shell || process.env.COMSPEC || 'cmd.exe')\r\n : (opts.shell || process.env.SHELL || '/bin/bash')\r\n \r\n const proc = pty.spawn(shell, opts.args ?? [], {\r\n name: 'xterm-256color',\r\n cols: opts.cols,\r\n rows: opts.rows,\r\n cwd: opts.cwd,\r\n env: opts.env as Record<string, string>,\r\n })\r\n\r\n let dataCallback: ((d: string) => void) | null = null\r\n let exitCallback: ((code: number) => void) | null = null\r\n\r\n proc.onData((data) => dataCallback?.(data))\r\n proc.onExit(({ exitCode }) => exitCallback?.(exitCode))\r\n\r\n return {\r\n get pid() {\r\n return proc.pid\r\n },\r\n write: (data) => proc.write(data),\r\n resize: (cols, rows) => proc.resize(cols, rows),\r\n kill: () => proc.kill(),\r\n onData: (cb) => {\r\n dataCallback = cb\r\n },\r\n onExit: (cb) => {\r\n exitCallback = cb\r\n },\r\n }\r\n}\r\n","// PTY scrollback ring buffer.\r\n//\r\n// 본 PR (PTY 영속화) 의 핵심 구성요소. 모든 PTY stdout/stderr 를 머신별로 보관하여\r\n// 모바일 attach 시 session_replay 청크로 재전송한다.\r\n//\r\n// 설계 원칙:\r\n// - byte cap 기준 (UTF-8 byte length). 한계 초과 시 가장 오래된 input chunk 부터 폐기.\r\n// - 청크 단위 폐기라 정확한 byte cap 이 아니라 cap ± (drop 직전 chunk size).\r\n// PTY 출력은 보통 ~KB/chunk 라서 5MB cap 에서는 무시 가능한 오차.\r\n// - WS frame 한계 (1MB 기본) 안에서 송신하기 위해 snapshotChunks() 가 분할.\r\n// - alt-screen mid-state 복원 문제 (vim/less 한가운데 attach) 는 호출자(ws-manager)\r\n// 또는 클라이언트(모바일 xterm)가 \\x1bc full-reset 을 prepend 하는 방식으로 처리.\r\n// 본 클래스는 그 책임을 가지지 않는다 — raw 출력만 보관/재생.\r\n\r\ninterface BufferEntry {\r\n readonly data: string\r\n readonly bytes: number\r\n}\r\n\r\n/**\r\n * 디스크 persist 직렬화 형식. v1.5.0 신규.\r\n *\r\n * `v` 는 schema version — future migration 용. 현재 v: 1 만 지원, 다른 값은 deserialize 시 거부.\r\n * `chunks` 는 append 단위 그대로 보존 (snapshot() 의 concat 과 다름) — restore 시 byte-cap 재적용 가능하도록.\r\n * `savedAt` 은 ISO 8601 — debug / stale-file age 판정용.\r\n */\r\nexport interface ScrollbackSnapshot {\r\n readonly v: 1\r\n readonly chunks: readonly string[]\r\n readonly totalBytes: number\r\n readonly savedAt: string\r\n}\r\n\r\nexport class PtyScrollbackBuffer {\r\n private chunks: BufferEntry[] = []\r\n private totalBytes = 0\r\n\r\n constructor(private readonly maxBytes: number) {\r\n if (maxBytes <= 0)\r\n throw new Error(\r\n `PtyScrollbackBuffer: maxBytes must be > 0, got ${maxBytes}`\r\n )\r\n }\r\n\r\n /** append(data) — PTY 출력 한 청크 저장. cap 초과 시 head 부터 자동 drop. */\r\n append(data: string): void {\r\n if (data.length === 0) return\r\n const bytes = Buffer.byteLength(data, 'utf8')\r\n this.chunks.push({ data, bytes })\r\n this.totalBytes += bytes\r\n while (this.totalBytes > this.maxBytes && this.chunks.length > 0) {\r\n const dropped = this.chunks.shift()!\r\n this.totalBytes -= dropped.bytes\r\n }\r\n }\r\n\r\n /** 현재 보관 중인 byte 수 (UTF-8 기준). */\r\n size(): number {\r\n return this.totalBytes\r\n }\r\n\r\n /** byte 한계 (생성 시 지정). */\r\n capacity(): number {\r\n return this.maxBytes\r\n }\r\n\r\n /** 비었으면 true. */\r\n isEmpty(): boolean {\r\n return this.chunks.length === 0\r\n }\r\n\r\n /**\r\n * 전체 내용을 하나의 string 으로 반환. 큰 buffer 에서는 snapshotChunks() 권장\r\n * (WS frame 한계 회피).\r\n */\r\n snapshot(): string {\r\n if (this.chunks.length === 0) return ''\r\n if (this.chunks.length === 1) return this.chunks[0].data\r\n return this.chunks.map(c => c.data).join('')\r\n }\r\n\r\n /**\r\n * 한 청크 당 maxChunkBytes 이하로 분할한 배열을 반환.\r\n * 빈 buffer → 빈 배열. UTF-8 char 가 input chunk 경계 안에서만 잘림 (PTY 가 이미\r\n * 그 경계로 emit 한 것이라 안전 가정).\r\n *\r\n * 단일 input chunk 가 maxChunkBytes 보다 크면 자르지 않고 그대로 한 청크로 push.\r\n * (PTY 출력은 보통 KB 단위라 거의 일어나지 않음. WS 측에서 이런 경우는 별도 처리 필요.)\r\n */\r\n snapshotChunks(maxChunkBytes: number): string[] {\r\n if (maxChunkBytes <= 0)\r\n throw new Error(`snapshotChunks: maxChunkBytes must be > 0`)\r\n if (this.chunks.length === 0) return []\r\n\r\n const result: string[] = []\r\n let currentData = ''\r\n let currentBytes = 0\r\n\r\n for (const c of this.chunks) {\r\n // 단일 입력 청크가 limit 보다 큰 경우 — 그대로 단독 청크로 emit\r\n if (c.bytes > maxChunkBytes) {\r\n if (currentData.length > 0) {\r\n result.push(currentData)\r\n currentData = ''\r\n currentBytes = 0\r\n }\r\n result.push(c.data)\r\n continue\r\n }\r\n // 추가하면 limit 넘는 경우 → flush 후 새 청크 시작\r\n if (currentBytes + c.bytes > maxChunkBytes && currentData.length > 0) {\r\n result.push(currentData)\r\n currentData = ''\r\n currentBytes = 0\r\n }\r\n currentData += c.data\r\n currentBytes += c.bytes\r\n }\r\n if (currentData.length > 0) result.push(currentData)\r\n return result\r\n }\r\n\r\n /**\r\n * attach 시점 송신 전용 — 최근 [maxTailBytes] 이내만 추출해 [maxChunkBytes]\r\n * 단위로 분할한 배열을 반환한다. 본 인스턴스는 *변경되지 않는다*.\r\n *\r\n * cross-repo with `claude_code_mobile` autoplan 2026-05-22 UC-3 — replay flood\r\n * 의 source 를 desktop 측에서 cap. mobile attach 시 ReplayProgressOverlay\r\n * (PR #48 v1.7.19) 노출 시간 단축. `append()` 의 cap (=`maxBytes`) 는 그대로\r\n * 보존되어 사용자 스크롤백은 desktop 측에 누적된다 — 본 메서드는 *송신 한도*\r\n * 만 제한.\r\n *\r\n * 구현: 임시 buffer 에 모든 chunk 를 append 해 기존 cap 정책 (head drop) 으로\r\n * 자연스럽게 tail 만 유지 → [snapshotChunks] 재사용으로 분할 알고리즘 중복 0.\r\n *\r\n * - `maxTailBytes <= 0` 또는 `maxChunkBytes <= 0` → throw.\r\n * - 빈 buffer → 빈 배열.\r\n * - 단일 chunk 가 [maxTailBytes] 보다 크면 (현실적으로 거의 없음) [snapshotChunks]\r\n * 의 oversized 분기와 동일하게 단독 청크로 emit — 송신 자체는 보장.\r\n */\r\n snapshotChunksForAttach(\r\n maxTailBytes: number,\r\n maxChunkBytes: number\r\n ): string[] {\r\n if (maxTailBytes <= 0)\r\n throw new Error('snapshotChunksForAttach: maxTailBytes must be > 0')\r\n if (maxChunkBytes <= 0)\r\n throw new Error('snapshotChunksForAttach: maxChunkBytes must be > 0')\r\n if (this.chunks.length === 0) return []\r\n\r\n const tmp = new PtyScrollbackBuffer(maxTailBytes)\r\n for (const c of this.chunks) tmp.append(c.data)\r\n return tmp.snapshotChunks(maxChunkBytes)\r\n }\r\n\r\n /** 전체 비움. pty_restart (E4) 시 호출. */\r\n clear(): void {\r\n this.chunks = []\r\n this.totalBytes = 0\r\n }\r\n\r\n /**\r\n * 디스크 persist 직렬화 (v1.5.0). chunk 단위 보존 — snapshot() 의 concat 과 달리\r\n * restore 시 byte-cap 정확 재적용 가능.\r\n *\r\n * 비어 있으면 chunks=[] / totalBytes=0 반환 — caller 가 noop 판단 가능.\r\n */\r\n serialize(): ScrollbackSnapshot {\r\n return {\r\n v: 1,\r\n chunks: this.chunks.map(c => c.data),\r\n totalBytes: this.totalBytes,\r\n savedAt: new Date().toISOString(),\r\n }\r\n }\r\n\r\n /**\r\n * 디스크에서 로드한 snapshot 으로부터 새 instance 복원 (v1.5.0).\r\n *\r\n * `maxBytes` 는 *현재* 인스턴스의 cap — snapshot 이 더 크면 head 부터 자동 drop\r\n * (constructor 의 cap 적용 로직 그대로 사용). caller 가 cap 변경 시 안전 전환.\r\n *\r\n * snapshot.v !== 1 또는 chunks 가 array 아님 / 손상된 entry 등의 경우 throw —\r\n * caller 가 try/catch 후 새 buffer 로 fallback.\r\n */\r\n static deserialize(\r\n maxBytes: number,\r\n snapshot: ScrollbackSnapshot\r\n ): PtyScrollbackBuffer {\r\n if (!snapshot || typeof snapshot !== 'object') {\r\n throw new Error(\r\n 'PtyScrollbackBuffer.deserialize: snapshot is not an object'\r\n )\r\n }\r\n if (snapshot.v !== 1) {\r\n throw new Error(\r\n `PtyScrollbackBuffer.deserialize: unsupported version ${snapshot.v}`\r\n )\r\n }\r\n if (!Array.isArray(snapshot.chunks)) {\r\n throw new Error('PtyScrollbackBuffer.deserialize: chunks is not an array')\r\n }\r\n const buf = new PtyScrollbackBuffer(maxBytes)\r\n for (const data of snapshot.chunks) {\r\n if (typeof data !== 'string') {\r\n throw new Error(\r\n 'PtyScrollbackBuffer.deserialize: chunk is not a string'\r\n )\r\n }\r\n buf.append(data)\r\n }\r\n return buf\r\n }\r\n}\r\n","/**\r\n * ScrollbackPersister — PtyScrollbackBuffer 의 디스크 영속화 (v1.5.0).\r\n *\r\n * 분리 동기: v1.3.4 까지 scrollback 은 in-memory only. 데스크탑 앱 종료 시 모든 backlog 가\r\n * 사라져 모바일 reattach 가 빈 화면. v1.5.0 은 머신별로 JSON 직렬화해 디스크에 저장하고\r\n * relaunch 시 첫 attach 에서 복원해 진짜 session resume 을 제공한다.\r\n *\r\n * 정책:\r\n * - 머신별 1 파일 — `<stateDir>/<sanitized-machineId>.json`. machineId 는 `[^a-zA-Z0-9_-]`\r\n * 문자를 `_` 로 sanitize.\r\n * - 라이브 append 직후 5s debounce 로 비동기 write 예약. 종료 직전 `flushAllSync` 가\r\n * 남은 dirty buffer 를 동기 직렬화 — Electron `before-quit` (sync handler) 안에서 안전.\r\n * - 손상된 JSON / version mismatch / non-existent 는 `restore` 에서 null 반환 (caller 가\r\n * 새 buffer 생성). silent recovery — 사용자 영향은 그 머신 첫 reattach 가 빈 backlog 뿐.\r\n * - `cleanupStale(known)` 는 앱 시작 시 호출 — store 에 없는 machineId 의 파일 unlink.\r\n * orphan 누적 방지 (최대 5MB × N).\r\n * - 사용자 명시 disconnect 시 `remove(machineId)` 호출 — 그 머신 backlog 영구 삭제.\r\n *\r\n * 보안: 평문 JSON. 비밀번호/토큰 echo 가 디스크에 잔존 가능. v1.5.0 CHANGELOG 에서 사용자\r\n * 고지. v1.6+ 에서 OS keychain 기반 암호화 후속 작업.\r\n */\r\n\r\nimport {\r\n existsSync,\r\n mkdirSync,\r\n readdirSync,\r\n readFileSync,\r\n unlinkSync,\r\n writeFileSync,\r\n} from 'node:fs'\r\nimport { join } from 'node:path'\r\n\r\nimport type {\r\n PtyScrollbackBuffer,\r\n ScrollbackSnapshot,\r\n} from './scrollback-buffer'\r\n\r\n/** debounce 간격 — 라이브 append 후 디스크 write 까지의 지연. */\r\nconst FLUSH_DEBOUNCE_MS = 5000\r\n\r\n/**\r\n * 머신별 backlog 의 디스크 영속화 핸들. 1 인스턴스 = 1 stateDir = 1 ssh-manager.\r\n *\r\n * 생성자 의존: stateDir (예: `%LOCALAPPDATA%/ai-rva/scrollback/`). caller (ssh-manager) 가\r\n * platform 별 path 결정. 미존재 시 `mkdirSync(recursive: true)` 로 자동 생성.\r\n */\r\nexport class ScrollbackPersister {\r\n private readonly stateDir: string\r\n /** machineId → 예약된 setTimeout 의 Timer. clear/reschedule 용. */\r\n private readonly pendingFlush = new Map<string, NodeJS.Timeout>()\r\n\r\n constructor(stateDir: string) {\r\n this.stateDir = stateDir\r\n // mkdirSync 는 EEXIST 에 영향받지 않는 recursive 모드. EACCES 등은 caller 가 처리.\r\n try {\r\n mkdirSync(stateDir, { recursive: true, mode: 0o700 })\r\n } catch {\r\n // 디렉토리 생성 실패 → 라이브 운영에 큰 영향 없음 (write 시 다시 시도). 로그만.\r\n console.warn(`[ScrollbackPersister] mkdirSync failed for ${stateDir}`)\r\n }\r\n }\r\n\r\n /**\r\n * machineId 에 대한 디스크 파일 경로. sanitize 는 file system 충돌 방지용.\r\n * 같은 sanitized 결과가 다른 machineId 둘 사이에서 발생할 수 있는 risk —\r\n * 현재 machineId 포맷이 UUID-like 라 충돌 가능성 무시 가능 수준.\r\n */\r\n resolveFilePath(machineId: string): string {\r\n const safe = machineId.replace(/[^a-zA-Z0-9_-]/g, '_')\r\n return join(this.stateDir, `${safe || 'default'}.json`)\r\n }\r\n\r\n /**\r\n * 디스크에서 ScrollbackSnapshot 복원. 파일 없거나 손상이면 null 반환.\r\n *\r\n * sync — caller (ssh-manager.getOrCreateScrollback) 가 hot path 의 동기 호출 안에서\r\n * 즉시 사용 가능해야 한다. 파일 사이즈 5MB cap 이라 sync read 부담 작음.\r\n *\r\n * caller 는 null 일 때 새 buffer 생성, 아니면 `PtyScrollbackBuffer.deserialize(maxBytes, snapshot)` 로 복원.\r\n */\r\n restoreSync(machineId: string): ScrollbackSnapshot | null {\r\n const path = this.resolveFilePath(machineId)\r\n if (!existsSync(path)) return null\r\n try {\r\n const raw = readFileSync(path, 'utf8')\r\n const parsed = JSON.parse(raw) as ScrollbackSnapshot\r\n if (!parsed || typeof parsed !== 'object' || parsed.v !== 1) {\r\n console.warn(\r\n `[ScrollbackPersister] discarding ${path} — bad version/shape`\r\n )\r\n return null\r\n }\r\n return parsed\r\n } catch (err) {\r\n console.warn(\r\n `[ScrollbackPersister] restore failed for ${path}: ${(err as Error).message}`\r\n )\r\n return null\r\n }\r\n }\r\n\r\n /**\r\n * 5s debounce 된 비동기 write 예약. 같은 machineId 에 대해 이미 예약된 게 있으면\r\n * 그것 cancel 하고 다시 예약 — 마지막 호출만 유효 (디스크 throttle).\r\n */\r\n scheduleFlush(machineId: string, buf: PtyScrollbackBuffer): void {\r\n const existing = this.pendingFlush.get(machineId)\r\n if (existing) clearTimeout(existing)\r\n const timer = setTimeout(() => {\r\n this.pendingFlush.delete(machineId)\r\n this.flushSync(machineId, buf)\r\n }, FLUSH_DEBOUNCE_MS)\r\n // setTimeout 결과는 Electron 종료를 막을 수 있어 unref — Electron quit 시 자연\r\n // 폐기. 종료 흐름에서는 어차피 flushAllSync 가 sync 로 처리.\r\n if (typeof timer.unref === 'function') timer.unref()\r\n this.pendingFlush.set(machineId, timer)\r\n }\r\n\r\n /**\r\n * 동기 디스크 write. before-quit / 명시 detach 등에서 호출. 빈 buffer 면 unlink\r\n * (이전 데이터 정리).\r\n *\r\n * write 실패는 swallow + warn — 종료 흐름을 막지 않는다. 사용자 영향: 다음 launch 에서\r\n * 그 머신 backlog 는 이전 상태 유지 (write 못 한 변경분만 손실).\r\n */\r\n flushSync(machineId: string, buf: PtyScrollbackBuffer): void {\r\n // 예약된 비동기 write 가 있으면 cancel — 우리가 sync 로 대체.\r\n const pending = this.pendingFlush.get(machineId)\r\n if (pending) {\r\n clearTimeout(pending)\r\n this.pendingFlush.delete(machineId)\r\n }\r\n const path = this.resolveFilePath(machineId)\r\n if (buf.isEmpty()) {\r\n // 비었으면 stale file 도 제거 — 다음 launch 에서 None 반환되도록.\r\n try {\r\n if (existsSync(path)) unlinkSync(path)\r\n } catch {\r\n // unlink 실패 — 무해.\r\n }\r\n return\r\n }\r\n try {\r\n const snap = buf.serialize()\r\n writeFileSync(path, JSON.stringify(snap), {\r\n encoding: 'utf8',\r\n mode: 0o600,\r\n })\r\n } catch (err) {\r\n console.warn(\r\n `[ScrollbackPersister] flushSync failed for ${path}: ${(err as Error).message}`\r\n )\r\n }\r\n }\r\n\r\n /**\r\n * cleanup 에서 호출. 모든 dirty buffer 를 동기 write — Electron before-quit 의 sync\r\n * handler 안에서 안전. Map<machineId, PtyScrollbackBuffer> 받음.\r\n */\r\n flushAllSync(buffers: Map<string, PtyScrollbackBuffer>): void {\r\n for (const [machineId, buf] of buffers) {\r\n this.flushSync(machineId, buf)\r\n }\r\n }\r\n\r\n /** 머신 명시 삭제 (사용자 disconnect). sync — 종료 흐름과 분리됐지만 sync I/O 가 단순. */\r\n remove(machineId: string): void {\r\n const pending = this.pendingFlush.get(machineId)\r\n if (pending) {\r\n clearTimeout(pending)\r\n this.pendingFlush.delete(machineId)\r\n }\r\n const path = this.resolveFilePath(machineId)\r\n try {\r\n if (existsSync(path)) unlinkSync(path)\r\n } catch (err) {\r\n console.warn(\r\n `[ScrollbackPersister] remove failed for ${path}: ${(err as Error).message}`\r\n )\r\n }\r\n }\r\n\r\n /**\r\n * 앱 launch 시 호출. stateDir 의 모든 *.json 중 known set 에 없는 것 unlink.\r\n * machineId 가 UI 에서 삭제됐는데 남아있는 파일 정리. 반환: 정리된 파일 수.\r\n *\r\n * sanitize 결과를 다시 reverse-mapping 하지 않고 known 의 sanitize 결과와 비교.\r\n * sync — 시작 시 1회 호출이라 부담 적음.\r\n */\r\n cleanupStale(knownMachineIds: Set<string>): number {\r\n if (!existsSync(this.stateDir)) return 0\r\n const knownSafe = new Set<string>()\r\n for (const id of knownMachineIds) {\r\n const safe = id.replace(/[^a-zA-Z0-9_-]/g, '_') || 'default'\r\n knownSafe.add(safe)\r\n }\r\n let removed = 0\r\n let entries: string[]\r\n try {\r\n entries = readdirSync(this.stateDir)\r\n } catch {\r\n return 0\r\n }\r\n for (const name of entries) {\r\n if (!name.endsWith('.json')) continue\r\n const safe = name.slice(0, -'.json'.length)\r\n if (knownSafe.has(safe)) continue\r\n try {\r\n unlinkSync(join(this.stateDir, name))\r\n removed += 1\r\n } catch {\r\n // 무해 — 다음 launch 에서 다시 시도.\r\n }\r\n }\r\n return removed\r\n }\r\n}\r\n","export const ENVIRONMENT = {\r\n IS_DEV: process.env.NODE_ENV === 'development',\r\n}\r\n\r\nexport const PLATFORM = {\r\n IS_MAC: process.platform === 'darwin',\r\n IS_WINDOWS: process.platform === 'win32',\r\n IS_LINUX: process.platform === 'linux',\r\n}\r\n\r\n// ─── Relay Protocol ───────────────────────────────────────────────────────────\r\n// v3: ttyd-style 1-byte opcode prefix + binary frame. v2 (JSON-only text frame) 의\r\n// application JSON 누출을 protocol-level 차단. spec: claude_code_mobile/docs/specs/binary-protocol-v3.md.\r\n// 모바일 측 (claude_code_mobile/lib/services/relay_service.dart) 의 kRelayProtocolVersion\r\n// 과 *반드시 동시* bump — 한쪽만 변경 시 첫 hello 단계에서 silent mismatch + 사용자\r\n// dialog 발화. v3 는 hard switch — v2 호환 0.\r\nexport const PROTOCOL_VERSION = 3\r\n\r\n// 머신당 PTY 출력 보관 한계 default. v1.7.8 에서 5MB → 1MB 로 변경\r\n// (cross-repo with `claude_code_mobile` PR #48 autoplan UC-3 결정 반영).\r\n//\r\n// 사용자 선택: 모바일 측 overlay 노출 시간 단축이 주 목적 — 5MB chunks 보다\r\n// 1MB chunks 가 5배 빨리 끝남. 큰 scrollback 필요 시 머신 편집 UI 에서\r\n// 5MB / 10MB stepper 선택 가능 (`Machine.ringBufferBytes`).\r\n//\r\n// 1MB ≈ 80x24 기준 ~10K-20K 줄, 32 머신 worst case 합 = 32MB scrollback (이전 5배\r\n// 절감). ADR-0001 의 worst-case 메모리 산정 같이 갱신 필요 (별 issue).\r\nexport const SCROLLBACK_BUFFER_BYTES_DEFAULT = 1 * 1024 * 1024\r\n\r\n/**\r\n * @deprecated v1.7.8 — `SCROLLBACK_BUFFER_BYTES_DEFAULT` 사용. 기존 caller\r\n * 호환을 위해 별칭 유지. 다음 PR 에서 모든 호출처 신규 이름으로 교체 후 제거.\r\n */\r\nexport const SCROLLBACK_BUFFER_BYTES = SCROLLBACK_BUFFER_BYTES_DEFAULT\r\n\r\n// PTY 동시 보유 상한. spawn 직전 LRU eviction 트리거. 근거 (ADR-0001):\r\n// Windows conpty ~30MB/PTY → 32 = ~960MB worst case. macOS/Linux ~8MB/PTY → 256MB.\r\n// Windows 보수적 기준으로 32 채택. 추후 platform 별 override 검토.\r\nexport const PTY_COUNT_MAX = 32\r\n\r\n// PTY 가 attach/activity 없이 살아있을 수 있는 최대 시간. 초과 시 idle eviction.\r\n// 7d = 일반 사용자 1주 휴가/주말 cycle 보전. <7d 면 PTO 후 잃을 위험.\r\nexport const PTY_IDLE_TIMEOUT_MS = 7 * 24 * 60 * 60 * 1000\r\n\r\n// last-activity (출력/입력) 가 이 시간 이내면 'running', 아니면 'idle' status broadcast.\r\n// 30s = 사용자가 명령 입력 후 결과 기다리는 typical 시간 < 30s.\r\nexport const PTY_ACTIVITY_THRESHOLD_MS = 30_000\r\n\r\n// pty_status broadcast 의 throttle 간격. 같은 status 가 이 시간 안에 또 나오면 skip.\r\n// 단 status 변경 (running→idle 등) 은 즉시 송신 (bypass throttle).\r\nexport const PTY_STATUS_BROADCAST_THROTTLE_MS = 5_000\r\n\r\n// session_replay 메시지 1개당 최대 payload 바이트.\r\n// WS 기본 frame 한계 (1MB) 보다 충분히 작게 — JSON 인코딩 오버헤드 + UTF-8 escape 고려.\r\nexport const REPLAY_CHUNK_BYTES = 256 * 1024\r\n\r\n// session_attach 응답 송신용 tail cap.\r\n//\r\n// cross-repo with `claude_code_mobile` autoplan 2026-05-22 UC-3 — replay flood\r\n// 의 source 를 desktop 측에서 cap 하여 모바일 ReplayProgressOverlay (PR #48 v1.7.19)\r\n// 노출 시간 단축. 모바일 mid-session reconnect UX (claude_code_mobile v1.7.20+51)\r\n// 와 짝.\r\n//\r\n// 16KB ≈ 80 cols × ~200 lines (rotation 여유). scrollback buffer 자체\r\n// (`SCROLLBACK_BUFFER_BYTES_DEFAULT` = 1MB) 는 변경 없음 — append 그대로 누적\r\n// 되어 모바일 측 ReplayProgressOverlay 가 사라진 뒤에도 이론적으로는 desktop 의\r\n// 전체 backlog 가 PTY 측 ANSI \\x1b[?1049l/h 또는 사용자 명령으로 회복 가능.\r\n//\r\n// 본 cap 은 *송신 한도* 만 제한 — `PtyScrollbackBuffer.snapshotChunksForAttach`\r\n// 가 tail 부터 16KB 만 송신. 사용자가 attach 직후 위로 스크롤하면 그 너머 데이터는\r\n// 모바일 xterm 의 scrollback 에 없다 (추후 pagination 으로 확장 가능).\r\nexport const REPLAY_ATTACH_TAIL_BYTES = 16 * 1024\r\n\r\n// session_attach 후 session_replay 가 안 올 때 모바일이 legacy connect 로 fallback 하는 시간.\r\n// 신모바일 + 구데스크탑 시나리오에서만 의미. 정상 케이스는 ms 단위로 도착.\r\nexport const REPLAY_FALLBACK_TIMEOUT_MS = 5000\r\n\r\n// ─── Auto-Update Polling ──────────────────────────────────────────────────────\r\n\r\n/**\r\n * 주기 업데이트 체크 인터벌 (1시간).\r\n *\r\n * 근거 (autoplan v2 §\"분석 — 적정 체크 주기\"): 익명 GitHub API rate limit 의 40%\r\n * 만 사용, 사용자 인지 지연 평균 30분. 1시간보다 짧으면 GitHub rate limit 위험,\r\n * 길면 새 릴리즈 발견 지연 체감 가능.\r\n *\r\n * dev 모드 (`ENVIRONMENT.IS_DEV === true`) 에서는 autoUpdater 자체가 비활성이라 본\r\n * 상수도 미사용.\r\n */\r\nexport const UPDATE_CHECK_INTERVAL_MS = 60 * 60 * 1000\r\n\r\n/**\r\n * Auto-update install 시 mobile-safe graceful shutdown 의 grace 시간 (5s).\r\n *\r\n * autoplan E1 — 사용자가 \"지금 설치 및 재시작\" 을 confirm 한 직후 모든 mobile client\r\n * 에 `app:update-installing` broadcast 후 본 시간 만큼 대기, 그 후 `wsManager.cleanup()`\r\n * + `autoUpdater.quitAndInstall()`. 모바일이 disconnect 토스트 표시 + scrollback 보존\r\n * 할 시간 마진.\r\n */\r\nexport const UPDATE_INSTALL_GRACE_MS = 5000\r\n\r\n/**\r\n * Auto-update 수동 체크 (`update:check-now` IPC) 의 최대 대기 시간 (10s).\r\n *\r\n * autoplan E6 — `autoUpdater.checkForUpdates()` 가 네트워크 단절 등으로 무한 hang\r\n * 가능. Promise.race timeout 으로 보호하고 caller 에는 `{ ok: false, reason: 'timeout' }`\r\n * 반환. polling 의 schedulePoll 은 별도 try/catch 만 — interval 자체가 1시간이라\r\n * timeout 불요.\r\n */\r\nexport const UPDATE_CHECK_NOW_TIMEOUT_MS = 10_000\r\n\r\n// ─── Log Cleanup ────────────────────────────────────────────────────────────\r\n\r\n/**\r\n * 로그 청소 주기 (24h). `auto-update.ts` 의 schedulePoll 방식을 재사용하되 주기는 하루 1회.\r\n *\r\n * 근거 (autoplan 2026-06-07 결정1): 로그 보존이 14일이라 시간당 granularity 는 startup-only/\r\n * daily 대비 이득이 거의 없다(스캔 대상 파일 셋이 1시간 안엔 거의 안 변함). 스케줄러 '방식'은\r\n * 유지하되 주기만 하루로 둬 타이머 churn 을 줄인다.\r\n */\r\nexport const LOG_CLEANUP_INTERVAL_MS = 24 * 60 * 60 * 1000\r\n\r\n/**\r\n * 로그 파일 보존 한도 (14일). mtime 이 이보다 오래된 `*.log` / `*.old.log` 만 삭제 대상.\r\n *\r\n * 근거: `PTY_IDLE_TIMEOUT_MS`(7일, 1주 휴가 cycle)보다 길게 둬 1주 부재 후 복귀 시에도\r\n * 디버깅용 로그가 남도록 마진을 둔다. `.old` 아카이브에도 동일 age-gate 가 적용되어 갓\r\n * 로테이션된 신선한 백업은 보존된다.\r\n */\r\nexport const LOG_MAX_AGE_MS = 14 * 24 * 60 * 60 * 1000\r\n","/**\r\n * vt100/ANSI emulator wrap — A3 server-truth grid 의 단일 소스.\r\n *\r\n * 책임:\r\n * - PTY stdout chunk → grid cell 변환 (`@xterm/headless` 위임)\r\n * - GRID_SNAPSHOT (0x04) 직렬화 (Buffer)\r\n * - 마지막 broadcast 이후 변경된 cell 만 GRID_DIFF (0x05) 직렬화\r\n * - resize cap 검사 (max 500 cols × 200 rows — plan §13-3 / spec §3-β)\r\n * - seq / base_seq 단조증가 관리 (client 가 gap 감지로 snapshot.req 발사 가능)\r\n *\r\n * Owner: PtyLifecycleManager. PTY 1:1.\r\n *\r\n * single-path 정책 (plan §13-2 / spec §6.5): mobile/grid.diff 클라이언트는 client-side\r\n * Terminal.resize() 직접 호출 안 함. 모든 grid 변경은 본 클래스가 권위 — server-truth.\r\n *\r\n * spike note: dirty range 정밀 추적은 v2 — 현재는 보수적 markFullDirty() 로 정확성 우선.\r\n * GRID_DIFF 가 사실상 GRID_SNAPSHOT 크기와 비슷할 수 있으나 wire 호환은 정확.\r\n *\r\n * spec source of truth: claude_code_mobile/docs/specs/binary-protocol-v3.md §3, §3-α, §3-β.\r\n */\r\nimport headless from '@xterm/headless'\r\nconst { Terminal } = headless;\r\nimport { SerializeAddon } from '@xterm/addon-serialize'\r\nimport { encodeGridSnapshot, encodeGridDiff } from '../protocol/binary-protocol'\r\n\r\nconst MAX_COLS = 500\r\nconst MAX_ROWS = 200\r\n\r\n/**\r\n * Resize 결과 — capped 인 경우 실제 적용된 cols/rows 가 인자와 다름. ws-manager 가\r\n * client 에 `grid_resize_capped` APP_JSON broadcast (실제값 회신) 시 사용.\r\n */\r\nexport interface ResizeResult {\r\n cols: number\r\n rows: number\r\n capped: boolean\r\n}\r\n\r\n/**\r\n * 단일 PTY 의 grid 권위 emulator. PtyLifecycleManager 가 PTY 1:1 로 owner.\r\n */\r\nexport class GridEmulator {\r\n // `Terminal` 은 `headless` default export 에서 destructure 한 값.\r\n // type annotation 에는 InstanceType<typeof Terminal> 로 instance 타입 추출.\r\n private term: InstanceType<typeof Terminal>\r\n private serializer: SerializeAddon\r\n private seq = 0\r\n private lastBroadcastSeq = 0\r\n private dirty = new Set<string>() // \"r,c\" key\r\n\r\n constructor(initialCols: number, initialRows: number) {\r\n const cols = Math.min(Math.max(1, initialCols), MAX_COLS)\r\n const rows = Math.min(Math.max(1, initialRows), MAX_ROWS)\r\n this.term = new Terminal({ cols, rows, allowProposedApi: true })\r\n // `@xterm/addon-serialize` 의 typings 는 `@xterm/xterm` 의 Terminal 을 expect 하지만\r\n // `@xterm/headless` 의 Terminal 도 동일 IBuffer/loadAddon 인터페이스를 구현해 runtime 에서\r\n // 호환 (relay frame-snapshot.service.ts PoC 검증). `tsconfig.skipLibCheck=true` 로 .d.ts\r\n // 안 type mismatch 는 무시되고, `loadAddon(serializer as any)` 으로 호출부 type 충돌만 우회.\r\n this.serializer = new SerializeAddon()\r\n this.term.loadAddon(this.serializer as any)\r\n // dirty tracking: 보수적 — 매 chunk parse 후 전체 cell 을 dirty 표시.\r\n // 정확한 dirty range 추출은 xterm/headless internal API 조사 후 v2 정밀화.\r\n this.term.onWriteParsed(() => this.markFullDirty())\r\n }\r\n\r\n /** 현재 grid 크기. */\r\n get cols(): number {\r\n return this.term.cols\r\n }\r\n\r\n get rows(): number {\r\n return this.term.rows\r\n }\r\n\r\n /**\r\n * PTY stdout chunk append — ANSI parse + grid 갱신.\r\n * write callback 안에서 onWriteParsed 가 자동 trigger 되어 dirty 표시.\r\n *\r\n * [onParsed] xterm/headless 의 write 는 비동기 파싱이라, 파싱 완료(buffer 반영) 시점에\r\n * 콜백된다. 운영 경로(PTY 흐름)는 콜백 불필요하지만, 테스트가 파싱 완료를 동기화하는 데 사용.\r\n */\r\n write(chunk: string | Buffer, onParsed?: () => void): void {\r\n this.term.write(\r\n typeof chunk === 'string' ? chunk : chunk.toString('utf8'),\r\n onParsed,\r\n )\r\n }\r\n\r\n /**\r\n * RESIZE 수신 — cap 검사 + 적용.\r\n *\r\n * cap (`MAX_COLS=500`, `MAX_ROWS=200`) 초과 시 truncate. ws-manager 가 결과의\r\n * `capped=true` 를 보고 client 에 `grid_resize_capped` APP_JSON broadcast 해야 함.\r\n *\r\n * resize 직후 모든 cell 을 dirty 표시 — 다음 diff broadcast 가 사실상 full snapshot\r\n * 크기. spike v2 에서 resize-aware diff (이전 grid 와 비교) 로 정밀화 가능.\r\n */\r\n resize(cols: number, rows: number): ResizeResult {\r\n const c = Math.min(Math.max(1, cols), MAX_COLS)\r\n const r = Math.min(Math.max(1, rows), MAX_ROWS)\r\n const capped = c !== cols || r !== rows\r\n this.term.resize(c, r)\r\n this.markFullDirty()\r\n return { cols: c, rows: r, capped }\r\n }\r\n\r\n /**\r\n * Cold attach (mobile 가 cap.ack 'grid.diff' 받은 직후) / recover (snapshot.req 응답).\r\n * Full snapshot 송신 — seq 증가, dirty clear, lastBroadcastSeq 갱신.\r\n *\r\n * 반환은 GRID_SNAPSHOT (0x04) frame Buffer. ws-manager 가 ws.send() 로 전송.\r\n */\r\n snapshot(): Buffer {\r\n this.seq++\r\n this.lastBroadcastSeq = this.seq\r\n this.dirty.clear()\r\n const cells = this.serializeCells()\r\n const buf = this.term.buffer.active\r\n return encodeGridSnapshot(\r\n this.seq,\r\n this.term.cols,\r\n this.term.rows,\r\n cells,\r\n buf.cursorY,\r\n buf.cursorX,\r\n )\r\n }\r\n\r\n /**\r\n * Incremental — 마지막 broadcast 이후 dirty cell 만 GRID_DIFF (0x05) 직렬화.\r\n * dirty 가 비어있으면 null 반환 (broadcast skip — keep-alive 는 ws-manager 가 별도 정책).\r\n */\r\n diffSinceLast(): Buffer | null {\r\n if (this.dirty.size === 0) return null\r\n this.seq++\r\n const ops = this.serializeDirtyOps()\r\n const baseSeq = this.lastBroadcastSeq\r\n this.lastBroadcastSeq = this.seq\r\n this.dirty.clear()\r\n return encodeGridDiff(this.seq, baseSeq, ops)\r\n }\r\n\r\n /**\r\n * 현재 화면 state 를 ANSI 문자열로 직렬화 (cursor 위치 + SGR 보존).\r\n *\r\n * `addon-serialize` 의 `serialize()` 는 mobile 이 `\\x1b[2J\\x1b[H` reset 후 그대로 write 하면\r\n * 동일 화면을 재현할 수 있는 ANSI 시퀀스를 반환한다. ws-manager 가 session_attach 시 이 결과를\r\n * FRAME_SNAPSHOT (0x03) 으로 감싸 attach 하는 mobile 에 송신 — busy alt-screen TUI(claude 등)가\r\n * raw scrollback tail 만으로는 복원 불가한 화면을 본 emulator(정확한 PTY size)로 정확히 복원한다.\r\n *\r\n * 직렬화 실패 시 빈 문자열 반환 — 호출부(ws-manager)는 빈 문자열이면 frame 송신을 skip 한다.\r\n */\r\n serializeAnsi(): string {\r\n try {\r\n // scrollback:0 — 보이는 화면만 직렬화(review I1). 기본값은 전체 scrollback(headless 1000줄)\r\n // 까지 포함해 frame 이 수백 KB~1MB 로 비대해질 수 있고(relay maxPayload drop 위험), 모바일은\r\n // `\\x1b[2J\\x1b[H` 후 현재 화면만 overlay 하므로 scrollback 은 불필요하다.\r\n return this.serializer.serialize({ scrollback: 0 })\r\n } catch {\r\n return ''\r\n }\r\n }\r\n\r\n /**\r\n * [non-sync diag 20260614] 빈 스냅샷의 원인이 a1(claude 가 그 순간 화면을 실제로 비움) 인지\r\n * a2(`@xterm/addon-serialize` 가 alt-screen buffer 를 못 잡는 버그) 인지를 캡처 없이 ws.log 로\r\n * 가르기 위한 read-only 진단.\r\n *\r\n * `serializeAnsi()` 결과(FRAME_SNAPSHOT bytes)가 작을 때, **동일한 active buffer 를 직접 순회**해\r\n * 실제 내용 유무를 잰다. buffer 에 내용이 있는데(nonBlankLines 큼) 직렬화만 비면 = a2(버그),\r\n * buffer 도 비면 = a1(claude). busy 인데 `bufType==='normal'` 이면 alt-screen 이탈 보조 단서.\r\n * docs/non-sync.md §4 (claude_code_mobile repo). grid 상태/seq/dirty 불변.\r\n */\r\n snapshotDiag(): {\r\n bufType: string\r\n nonBlankLines: number\r\n rows: number\r\n glyphs: number\r\n } {\r\n const buf = this.term.buffer.active\r\n let nonBlankLines = 0\r\n let glyphs = 0\r\n for (let r = 0; r < this.term.rows; r++) {\r\n const line = buf.getLine(r)\r\n if (!line) continue\r\n const trimmed = line.translateToString(true).trim()\r\n if (trimmed.length === 0) continue\r\n nonBlankLines++\r\n for (const ch of trimmed) if (ch !== ' ') glyphs++\r\n }\r\n return { bufType: buf.type, nonBlankLines, rows: this.term.rows, glyphs }\r\n }\r\n\r\n /** xterm/headless 인스턴스 폐기. */\r\n dispose(): void {\r\n this.term.dispose()\r\n this.dirty.clear()\r\n }\r\n\r\n // ── private helpers ──────────────────────────────────────────────────\r\n\r\n /** 모든 cell 을 dirty 표시 — write/resize 후 호출. */\r\n private markFullDirty(): void {\r\n for (let r = 0; r < this.term.rows; r++) {\r\n for (let c = 0; c < this.term.cols; c++) {\r\n this.dirty.add(`${r},${c}`)\r\n }\r\n }\r\n }\r\n\r\n /**\r\n * xterm buffer → cell array Buffer (full grid).\r\n *\r\n * Encoding (spec §3-α palette mode default):\r\n * per cell: [ch UTF-8 1-4 bytes][fg u8 palette][bg u8 palette][attrs u8]\r\n * 평균 6 bytes/cell — 500×200 = ~600KB (WS frame 1MB 안).\r\n *\r\n * RGB mode (cell.getFgColor() 의 high bit 검사 필요) 는 spike v2.\r\n */\r\n private serializeCells(): Buffer {\r\n const buf = this.term.buffer.active\r\n const out: number[] = []\r\n for (let r = 0; r < this.term.rows; r++) {\r\n const line = buf.getLine(r)\r\n if (!line) continue\r\n for (let c = 0; c < this.term.cols; c++) {\r\n const cell = line.getCell(c)\r\n if (!cell) continue\r\n const ch = cell.getChars() || ' '\r\n const chBytes = Buffer.from(ch, 'utf8')\r\n out.push(...chBytes)\r\n out.push(this.normalizeColor(cell.getFgColor(), 7))\r\n out.push(this.normalizeColor(cell.getBgColor(), 0))\r\n out.push(this.encodeAttrs(cell))\r\n }\r\n }\r\n return Buffer.from(out)\r\n }\r\n\r\n /**\r\n * Dirty cell 만 ops Buffer 로 직렬화. opCount (BE u16) prefix 포함.\r\n *\r\n * Per op: [r BE u16][c BE u16][ch UTF-8][fg u8][bg u8][attrs u8].\r\n */\r\n private serializeDirtyOps(): Buffer {\r\n const buf = this.term.buffer.active\r\n const opCount = this.dirty.size\r\n const head = Buffer.alloc(2)\r\n head.writeUInt16BE(opCount & 0xffff, 0)\r\n const opBytes: number[] = []\r\n for (const key of this.dirty) {\r\n const [rs, cs] = key.split(',')\r\n const r = parseInt(rs, 10)\r\n const c = parseInt(cs, 10)\r\n const line = buf.getLine(r)\r\n if (!line) continue\r\n const cell = line.getCell(c)\r\n if (!cell) continue\r\n const ch = cell.getChars() || ' '\r\n const chBytes = Buffer.from(ch, 'utf8')\r\n const rcBuf = Buffer.alloc(4)\r\n rcBuf.writeUInt16BE(r & 0xffff, 0)\r\n rcBuf.writeUInt16BE(c & 0xffff, 2)\r\n opBytes.push(...rcBuf)\r\n opBytes.push(...chBytes)\r\n opBytes.push(this.normalizeColor(cell.getFgColor(), 7))\r\n opBytes.push(this.normalizeColor(cell.getBgColor(), 0))\r\n opBytes.push(this.encodeAttrs(cell))\r\n }\r\n return Buffer.concat([head, Buffer.from(opBytes)])\r\n }\r\n\r\n /**\r\n * Cell color → palette byte. xterm 의 default (-1) 는 fallback (fg=7 white, bg=0 black).\r\n * RGB mode (24-bit color) 는 spike v2 — 현재는 palette mode 만.\r\n */\r\n private normalizeColor(color: number, fallback: number): number {\r\n if (color < 0) return fallback\r\n return color & 0xff\r\n }\r\n\r\n /**\r\n * xterm cell → attrs byte (spec §3-α bit field).\r\n *\r\n * cell 객체는 xterm/headless 내부 `IBufferCell` — `isBold()` / `isItalic()` 등 method 보유.\r\n * 일부 method 가 미구현이면 try/catch 로 0 fallback.\r\n */\r\n private encodeAttrs(cell: import('@xterm/headless').IBufferCell): number {\r\n let a = 0\r\n try {\r\n if (cell.isBold()) a |= 0x01\r\n if (cell.isItalic()) a |= 0x02\r\n if (cell.isUnderline()) a |= 0x04\r\n if (cell.isInverse()) a |= 0x08\r\n } catch {\r\n // headless 의 IBufferCell 일부 method 미구현 — 0 fallback\r\n }\r\n return a\r\n }\r\n}\r\n","/**\n * Copyright (c) 2019 The xterm.js authors. All rights reserved.\n * @license MIT\n */\n\nimport { IColor, IColorRGB } from 'common/Types';\n\nlet $r = 0;\nlet $g = 0;\nlet $b = 0;\nlet $a = 0;\n\nexport const NULL_COLOR: IColor = {\n css: '#00000000',\n rgba: 0\n};\n\n/**\n * Helper functions where the source type is \"channels\" (individual color channels as numbers).\n */\nexport namespace channels {\n export function toCss(r: number, g: number, b: number, a?: number): string {\n if (a !== undefined) {\n return `#${toPaddedHex(r)}${toPaddedHex(g)}${toPaddedHex(b)}${toPaddedHex(a)}`;\n }\n return `#${toPaddedHex(r)}${toPaddedHex(g)}${toPaddedHex(b)}`;\n }\n\n export function toRgba(r: number, g: number, b: number, a: number = 0xFF): number {\n // Note: The aggregated number is RGBA32 (BE), thus needs to be converted to ABGR32\n // on LE systems, before it can be used for direct 32-bit buffer writes.\n // >>> 0 forces an unsigned int\n return (r << 24 | g << 16 | b << 8 | a) >>> 0;\n }\n\n export function toColor(r: number, g: number, b: number, a?: number): IColor {\n return {\n css: channels.toCss(r, g, b, a),\n rgba: channels.toRgba(r, g, b, a)\n };\n }\n}\n\n/**\n * Helper functions where the source type is `IColor`.\n */\nexport namespace color {\n export function blend(bg: IColor, fg: IColor): IColor {\n $a = (fg.rgba & 0xFF) / 255;\n if ($a === 1) {\n return {\n css: fg.css,\n rgba: fg.rgba\n };\n }\n const fgR = (fg.rgba >> 24) & 0xFF;\n const fgG = (fg.rgba >> 16) & 0xFF;\n const fgB = (fg.rgba >> 8) & 0xFF;\n const bgR = (bg.rgba >> 24) & 0xFF;\n const bgG = (bg.rgba >> 16) & 0xFF;\n const bgB = (bg.rgba >> 8) & 0xFF;\n $r = bgR + Math.round((fgR - bgR) * $a);\n $g = bgG + Math.round((fgG - bgG) * $a);\n $b = bgB + Math.round((fgB - bgB) * $a);\n const css = channels.toCss($r, $g, $b);\n const rgba = channels.toRgba($r, $g, $b);\n return { css, rgba };\n }\n\n export function isOpaque(color: IColor): boolean {\n return (color.rgba & 0xFF) === 0xFF;\n }\n\n export function ensureContrastRatio(bg: IColor, fg: IColor, ratio: number): IColor | undefined {\n const result = rgba.ensureContrastRatio(bg.rgba, fg.rgba, ratio);\n if (!result) {\n return undefined;\n }\n return channels.toColor(\n (result >> 24 & 0xFF),\n (result >> 16 & 0xFF),\n (result >> 8 & 0xFF)\n );\n }\n\n export function opaque(color: IColor): IColor {\n const rgbaColor = (color.rgba | 0xFF) >>> 0;\n [$r, $g, $b] = rgba.toChannels(rgbaColor);\n return {\n css: channels.toCss($r, $g, $b),\n rgba: rgbaColor\n };\n }\n\n export function opacity(color: IColor, opacity: number): IColor {\n $a = Math.round(opacity * 0xFF);\n [$r, $g, $b] = rgba.toChannels(color.rgba);\n return {\n css: channels.toCss($r, $g, $b, $a),\n rgba: channels.toRgba($r, $g, $b, $a)\n };\n }\n\n export function multiplyOpacity(color: IColor, factor: number): IColor {\n $a = color.rgba & 0xFF;\n return opacity(color, ($a * factor) / 0xFF);\n }\n\n export function toColorRGB(color: IColor): IColorRGB {\n return [(color.rgba >> 24) & 0xFF, (color.rgba >> 16) & 0xFF, (color.rgba >> 8) & 0xFF];\n }\n}\n\n/**\n * Helper functions where the source type is \"css\" (string: '#rgb', '#rgba', '#rrggbb',\n * '#rrggbbaa').\n */\nexport namespace css {\n // Attempt to set get the shared canvas context\n let $ctx: CanvasRenderingContext2D | undefined;\n let $litmusColor: CanvasGradient | undefined;\n try {\n // This is guaranteed to run in the first window, so document should be correct\n const canvas = document.createElement('canvas');\n canvas.width = 1;\n canvas.height = 1;\n const ctx = canvas.getContext('2d', {\n willReadFrequently: true\n });\n if (ctx) {\n $ctx = ctx;\n $ctx.globalCompositeOperation = 'copy';\n $litmusColor = $ctx.createLinearGradient(0, 0, 1, 1);\n }\n }\n catch {\n // noop\n }\n\n /**\n * Converts a css string to an IColor, this should handle all valid CSS color strings and will\n * throw if it's invalid. The ideal format to use is `#rrggbb[aa]` as it's the fastest to parse.\n *\n * Only `#rgb[a]`, `#rrggbb[aa]`, `rgb()` and `rgba()` formats are supported when run in a Node\n * environment.\n */\n export function toColor(css: string): IColor {\n // Formats: #rgb[a] and #rrggbb[aa]\n if (css.match(/#[\\da-f]{3,8}/i)) {\n switch (css.length) {\n case 4: { // #rgb\n $r = parseInt(css.slice(1, 2).repeat(2), 16);\n $g = parseInt(css.slice(2, 3).repeat(2), 16);\n $b = parseInt(css.slice(3, 4).repeat(2), 16);\n return channels.toColor($r, $g, $b);\n }\n case 5: { // #rgba\n $r = parseInt(css.slice(1, 2).repeat(2), 16);\n $g = parseInt(css.slice(2, 3).repeat(2), 16);\n $b = parseInt(css.slice(3, 4).repeat(2), 16);\n $a = parseInt(css.slice(4, 5).repeat(2), 16);\n return channels.toColor($r, $g, $b, $a);\n }\n case 7: // #rrggbb\n return {\n css,\n rgba: (parseInt(css.slice(1), 16) << 8 | 0xFF) >>> 0\n };\n case 9: // #rrggbbaa\n return {\n css,\n rgba: parseInt(css.slice(1), 16) >>> 0\n };\n }\n }\n\n // Formats: rgb() or rgba()\n const rgbaMatch = css.match(/rgba?\\(\\s*(\\d{1,3})\\s*,\\s*(\\d{1,3})\\s*,\\s*(\\d{1,3})\\s*(,\\s*(0|1|\\d?\\.(\\d+))\\s*)?\\)/);\n if (rgbaMatch) {\n $r = parseInt(rgbaMatch[1]);\n $g = parseInt(rgbaMatch[2]);\n $b = parseInt(rgbaMatch[3]);\n $a = Math.round((rgbaMatch[5] === undefined ? 1 : parseFloat(rgbaMatch[5])) * 0xFF);\n return channels.toColor($r, $g, $b, $a);\n }\n\n // Validate the context is available for canvas-based color parsing\n if (!$ctx || !$litmusColor) {\n throw new Error('css.toColor: Unsupported css format');\n }\n\n // Validate the color using canvas fillStyle\n // See https://html.spec.whatwg.org/multipage/canvas.html#fill-and-stroke-styles\n $ctx.fillStyle = $litmusColor;\n $ctx.fillStyle = css;\n if (typeof $ctx.fillStyle !== 'string') {\n throw new Error('css.toColor: Unsupported css format');\n }\n\n $ctx.fillRect(0, 0, 1, 1);\n [$r, $g, $b, $a] = $ctx.getImageData(0, 0, 1, 1).data;\n\n // Validate the color is non-transparent as color hue gets lost when drawn to the canvas\n if ($a !== 0xFF) {\n throw new Error('css.toColor: Unsupported css format');\n }\n\n // Extract the color from the canvas' fillStyle property which exposes the color value in rgba()\n // format\n // See https://html.spec.whatwg.org/multipage/canvas.html#serialisation-of-a-color\n return {\n rgba: channels.toRgba($r, $g, $b, $a),\n css\n };\n }\n}\n\n/**\n * Helper functions where the source type is \"rgb\" (number: 0xrrggbb).\n */\nexport namespace rgb {\n /**\n * Gets the relative luminance of an RGB color, this is useful in determining the contrast ratio\n * between two colors.\n * @param rgb The color to use.\n * @see https://www.w3.org/TR/WCAG20/#relativeluminancedef\n */\n export function relativeLuminance(rgb: number): number {\n return relativeLuminance2(\n (rgb >> 16) & 0xFF,\n (rgb >> 8 ) & 0xFF,\n (rgb ) & 0xFF);\n }\n\n /**\n * Gets the relative luminance of an RGB color, this is useful in determining the contrast ratio\n * between two colors.\n * @param r The red channel (0x00 to 0xFF).\n * @param g The green channel (0x00 to 0xFF).\n * @param b The blue channel (0x00 to 0xFF).\n * @see https://www.w3.org/TR/WCAG20/#relativeluminancedef\n */\n export function relativeLuminance2(r: number, g: number, b: number): number {\n const rs = r / 255;\n const gs = g / 255;\n const bs = b / 255;\n const rr = rs <= 0.03928 ? rs / 12.92 : Math.pow((rs + 0.055) / 1.055, 2.4);\n const rg = gs <= 0.03928 ? gs / 12.92 : Math.pow((gs + 0.055) / 1.055, 2.4);\n const rb = bs <= 0.03928 ? bs / 12.92 : Math.pow((bs + 0.055) / 1.055, 2.4);\n return rr * 0.2126 + rg * 0.7152 + rb * 0.0722;\n }\n}\n\n/**\n * Helper functions where the source type is \"rgba\" (number: 0xrrggbbaa).\n */\nexport namespace rgba {\n export function blend(bg: number, fg: number): number {\n $a = (fg & 0xFF) / 0xFF;\n if ($a === 1) {\n return fg;\n }\n const fgR = (fg >> 24) & 0xFF;\n const fgG = (fg >> 16) & 0xFF;\n const fgB = (fg >> 8) & 0xFF;\n const bgR = (bg >> 24) & 0xFF;\n const bgG = (bg >> 16) & 0xFF;\n const bgB = (bg >> 8) & 0xFF;\n $r = bgR + Math.round((fgR - bgR) * $a);\n $g = bgG + Math.round((fgG - bgG) * $a);\n $b = bgB + Math.round((fgB - bgB) * $a);\n return channels.toRgba($r, $g, $b);\n }\n\n /**\n * Given a foreground color and a background color, either increase or reduce the luminance of the\n * foreground color until the specified contrast ratio is met. If pure white or black is hit\n * without the contrast ratio being met, go the other direction using the background color as the\n * foreground color and take either the first or second result depending on which has the higher\n * contrast ratio.\n *\n * `undefined` will be returned if the contrast ratio is already met.\n *\n * @param bgRgba The background color in rgba format.\n * @param fgRgba The foreground color in rgba format.\n * @param ratio The contrast ratio to achieve.\n */\n export function ensureContrastRatio(bgRgba: number, fgRgba: number, ratio: number): number | undefined {\n const bgL = rgb.relativeLuminance(bgRgba >> 8);\n const fgL = rgb.relativeLuminance(fgRgba >> 8);\n const cr = contrastRatio(bgL, fgL);\n if (cr < ratio) {\n if (fgL < bgL) {\n const resultA = reduceLuminance(bgRgba, fgRgba, ratio);\n const resultARatio = contrastRatio(bgL, rgb.relativeLuminance(resultA >> 8));\n if (resultARatio < ratio) {\n const resultB = increaseLuminance(bgRgba, fgRgba, ratio);\n const resultBRatio = contrastRatio(bgL, rgb.relativeLuminance(resultB >> 8));\n return resultARatio > resultBRatio ? resultA : resultB;\n }\n return resultA;\n }\n const resultA = increaseLuminance(bgRgba, fgRgba, ratio);\n const resultARatio = contrastRatio(bgL, rgb.relativeLuminance(resultA >> 8));\n if (resultARatio < ratio) {\n const resultB = reduceLuminance(bgRgba, fgRgba, ratio);\n const resultBRatio = contrastRatio(bgL, rgb.relativeLuminance(resultB >> 8));\n return resultARatio > resultBRatio ? resultA : resultB;\n }\n return resultA;\n }\n return undefined;\n }\n\n export function reduceLuminance(bgRgba: number, fgRgba: number, ratio: number): number {\n // This is a naive but fast approach to reducing luminance as converting to\n // HSL and back is expensive\n const bgR = (bgRgba >> 24) & 0xFF;\n const bgG = (bgRgba >> 16) & 0xFF;\n const bgB = (bgRgba >> 8) & 0xFF;\n let fgR = (fgRgba >> 24) & 0xFF;\n let fgG = (fgRgba >> 16) & 0xFF;\n let fgB = (fgRgba >> 8) & 0xFF;\n let cr = contrastRatio(rgb.relativeLuminance2(fgR, fgG, fgB), rgb.relativeLuminance2(bgR, bgG, bgB));\n while (cr < ratio && (fgR > 0 || fgG > 0 || fgB > 0)) {\n // Reduce by 10% until the ratio is hit\n fgR -= Math.max(0, Math.ceil(fgR * 0.1));\n fgG -= Math.max(0, Math.ceil(fgG * 0.1));\n fgB -= Math.max(0, Math.ceil(fgB * 0.1));\n cr = contrastRatio(rgb.relativeLuminance2(fgR, fgG, fgB), rgb.relativeLuminance2(bgR, bgG, bgB));\n }\n return (fgR << 24 | fgG << 16 | fgB << 8 | 0xFF) >>> 0;\n }\n\n export function increaseLuminance(bgRgba: number, fgRgba: number, ratio: number): number {\n // This is a naive but fast approach to increasing luminance as converting to\n // HSL and back is expensive\n const bgR = (bgRgba >> 24) & 0xFF;\n const bgG = (bgRgba >> 16) & 0xFF;\n const bgB = (bgRgba >> 8) & 0xFF;\n let fgR = (fgRgba >> 24) & 0xFF;\n let fgG = (fgRgba >> 16) & 0xFF;\n let fgB = (fgRgba >> 8) & 0xFF;\n let cr = contrastRatio(rgb.relativeLuminance2(fgR, fgG, fgB), rgb.relativeLuminance2(bgR, bgG, bgB));\n while (cr < ratio && (fgR < 0xFF || fgG < 0xFF || fgB < 0xFF)) {\n // Increase by 10% until the ratio is hit\n fgR = Math.min(0xFF, fgR + Math.ceil((255 - fgR) * 0.1));\n fgG = Math.min(0xFF, fgG + Math.ceil((255 - fgG) * 0.1));\n fgB = Math.min(0xFF, fgB + Math.ceil((255 - fgB) * 0.1));\n cr = contrastRatio(rgb.relativeLuminance2(fgR, fgG, fgB), rgb.relativeLuminance2(bgR, bgG, bgB));\n }\n return (fgR << 24 | fgG << 16 | fgB << 8 | 0xFF) >>> 0;\n }\n\n export function toChannels(value: number): [number, number, number, number] {\n return [(value >> 24) & 0xFF, (value >> 16) & 0xFF, (value >> 8) & 0xFF, value & 0xFF];\n }\n}\n\nexport function toPaddedHex(c: number): string {\n const s = c.toString(16);\n return s.length < 2 ? '0' + s : s;\n}\n\n/**\n * Gets the contrast ratio between two relative luminance values.\n * @param l1 The first relative luminance.\n * @param l2 The first relative luminance.\n * @see https://www.w3.org/TR/WCAG20/#contrast-ratiodef\n */\nexport function contrastRatio(l1: number, l2: number): number {\n if (l1 < l2) {\n return (l2 + 0.05) / (l1 + 0.05);\n }\n return (l1 + 0.05) / (l2 + 0.05);\n}\n","/**\n * Copyright (c) 2017 The xterm.js authors. All rights reserved.\n * @license MIT\n */\n\nimport { CharData, IColor, ICoreTerminal, ITerminalOptions } from 'common/Types';\nimport { IBuffer } from 'common/buffer/Types';\nimport { IDisposable, Terminal as ITerminalApi } from '@xterm/xterm';\nimport { channels, css } from 'common/Color';\nimport type { Event } from 'vs/base/common/event';\n\n/**\n * A portion of the public API that are implemented identially internally and simply passed through.\n */\ntype InternalPassthroughApis = Omit<ITerminalApi, 'buffer' | 'parser' | 'unicode' | 'modes' | 'writeln' | 'loadAddon'>;\n\nexport interface ITerminal extends InternalPassthroughApis, ICoreTerminal {\n screenElement: HTMLElement | undefined;\n browser: IBrowser;\n buffer: IBuffer;\n linkifier: ILinkifier2 | undefined;\n options: Required<ITerminalOptions>;\n\n onBlur: Event<void>;\n onFocus: Event<void>;\n onA11yChar: Event<string>;\n onA11yTab: Event<number>;\n onWillOpen: Event<HTMLElement>;\n\n cancel(ev: MouseEvent | WheelEvent | KeyboardEvent | InputEvent, force?: boolean): boolean | void;\n}\n\nexport type CustomKeyEventHandler = (event: KeyboardEvent) => boolean;\nexport type CustomWheelEventHandler = (event: WheelEvent) => boolean;\n\nexport type LineData = CharData[];\n\nexport interface ICompositionHelper {\n readonly isComposing: boolean;\n compositionstart(): void;\n compositionupdate(ev: CompositionEvent): void;\n compositionend(): void;\n updateCompositionElements(dontRecurse?: boolean): void;\n keydown(ev: KeyboardEvent): boolean;\n}\n\nexport interface IBrowser {\n isNode: boolean;\n userAgent: string;\n platform: string;\n isFirefox: boolean;\n isMac: boolean;\n isIpad: boolean;\n isIphone: boolean;\n isWindows: boolean;\n}\n\nexport interface IColorSet {\n foreground: IColor;\n background: IColor;\n cursor: IColor;\n cursorAccent: IColor;\n selectionForeground: IColor | undefined;\n selectionBackgroundTransparent: IColor;\n /** The selection blended on top of background. */\n selectionBackgroundOpaque: IColor;\n selectionInactiveBackgroundTransparent: IColor;\n selectionInactiveBackgroundOpaque: IColor;\n scrollbarSliderBackground: IColor;\n scrollbarSliderHoverBackground: IColor;\n scrollbarSliderActiveBackground: IColor;\n overviewRulerBorder: IColor;\n ansi: IColor[];\n /** Maps original colors to colors that respect minimum contrast ratio. */\n contrastCache: IColorContrastCache;\n /** Maps original colors to colors that respect _half_ of the minimum contrast ratio. */\n halfContrastCache: IColorContrastCache;\n}\n\nexport type ReadonlyColorSet = Readonly<Omit<IColorSet, 'ansi'>> & { ansi: Readonly<Pick<IColorSet, 'ansi'>['ansi']> };\n\nexport interface IColorContrastCache {\n clear(): void;\n setCss(bg: number, fg: number, value: string | null): void;\n getCss(bg: number, fg: number): string | null | undefined;\n setColor(bg: number, fg: number, value: IColor | null): void;\n getColor(bg: number, fg: number): IColor | null | undefined;\n}\n\nexport interface IPartialColorSet {\n foreground: IColor;\n background: IColor;\n cursor?: IColor;\n cursorAccent?: IColor;\n selectionBackground?: IColor;\n ansi: IColor[];\n}\n\nexport interface IViewport extends IDisposable {\n scrollBarWidth: number;\n readonly onRequestScrollLines: Event<{ amount: number, suppressScrollEvent: boolean }>;\n syncScrollArea(immediate?: boolean, force?: boolean): void;\n getLinesScrolled(ev: WheelEvent): number;\n getBufferElements(startLine: number, endLine?: number): { bufferElements: HTMLElement[], cursorElement?: HTMLElement };\n handleWheel(ev: WheelEvent): boolean;\n handleTouchStart(ev: TouchEvent): void;\n handleTouchMove(ev: TouchEvent): boolean;\n scrollLines(disp: number): void; // todo api name?\n reset(): void;\n}\n\nexport interface ILinkifierEvent {\n x1: number;\n y1: number;\n x2: number;\n y2: number;\n cols: number;\n fg: number | undefined;\n}\n\ninterface ILinkState {\n decorations: ILinkDecorations;\n isHovered: boolean;\n}\nexport interface ILinkWithState {\n link: ILink;\n state?: ILinkState;\n}\n\nexport interface ILinkifier2 extends IDisposable {\n onShowLinkUnderline: Event<ILinkifierEvent>;\n onHideLinkUnderline: Event<ILinkifierEvent>;\n readonly currentLink: ILinkWithState | undefined;\n}\n\nexport interface ILink {\n range: IBufferRange;\n text: string;\n decorations?: ILinkDecorations;\n activate(event: MouseEvent, text: string): void;\n hover?(event: MouseEvent, text: string): void;\n leave?(event: MouseEvent, text: string): void;\n dispose?(): void;\n}\n\nexport interface ILinkDecorations {\n pointerCursor: boolean;\n underline: boolean;\n}\n\nexport interface IBufferRange {\n start: IBufferCellPosition;\n end: IBufferCellPosition;\n}\n\nexport interface IBufferCellPosition {\n x: number;\n y: number;\n}\n\nexport type CharacterJoinerHandler = (text: string) => [number, number][];\n\nexport interface ICharacterJoiner {\n id: number;\n handler: CharacterJoinerHandler;\n}\n\nexport interface IRenderDebouncer extends IDisposable {\n refresh(rowStart: number | undefined, rowEnd: number | undefined, rowCount: number): void;\n}\n\nexport interface IRenderDebouncerWithCallback extends IRenderDebouncer {\n addRefreshCallback(callback: FrameRequestCallback): number;\n}\n\nexport interface IBufferElementProvider {\n provideBufferElements(): DocumentFragment | HTMLElement;\n}\n\n// An IIFE to generate DEFAULT_ANSI_COLORS.\nexport const DEFAULT_ANSI_COLORS = Object.freeze((() => {\n const colors = [\n // dark:\n css.toColor('#2e3436'),\n css.toColor('#cc0000'),\n css.toColor('#4e9a06'),\n css.toColor('#c4a000'),\n css.toColor('#3465a4'),\n css.toColor('#75507b'),\n css.toColor('#06989a'),\n css.toColor('#d3d7cf'),\n // bright:\n css.toColor('#555753'),\n css.toColor('#ef2929'),\n css.toColor('#8ae234'),\n css.toColor('#fce94f'),\n css.toColor('#729fcf'),\n css.toColor('#ad7fa8'),\n css.toColor('#34e2e2'),\n css.toColor('#eeeeec')\n ];\n\n // Fill in the remaining 240 ANSI colors.\n // Generate colors (16-231)\n const v = [0x00, 0x5f, 0x87, 0xaf, 0xd7, 0xff];\n for (let i = 0; i < 216; i++) {\n const r = v[(i / 36) % 6 | 0];\n const g = v[(i / 6) % 6 | 0];\n const b = v[i % 6];\n colors.push({\n css: channels.toCss(r, g, b),\n rgba: channels.toRgba(r, g, b)\n });\n }\n\n // Generate greys (232-255)\n for (let i = 0; i < 24; i++) {\n const c = 8 + i * 10;\n colors.push({\n css: channels.toCss(c, c, c),\n rgba: channels.toRgba(c, c, c)\n });\n }\n\n return colors;\n})());\n","/**\n * Copyright (c) 2019 The xterm.js authors. All rights reserved.\n * @license MIT\n *\n * (EXPERIMENTAL) This Addon is still under development\n */\n\nimport type { IBuffer, IBufferCell, IBufferRange, ITerminalAddon, Terminal } from '@xterm/xterm';\nimport type { IHTMLSerializeOptions, SerializeAddon as ISerializeApi, ISerializeOptions, ISerializeRange } from '@xterm/addon-serialize';\nimport { IAttributeData, IColor } from 'common/Types';\nimport { DEFAULT_ANSI_COLORS } from 'browser/Types';\n\nfunction constrain(value: number, low: number, high: number): number {\n return Math.max(low, Math.min(value, high));\n}\n\nfunction escapeHTMLChar(c: string): string {\n switch (c) {\n case '&': return '&';\n case '<': return '<';\n }\n return c;\n}\n\n// TODO: Refine this template class later\nabstract class BaseSerializeHandler {\n constructor(\n protected readonly _buffer: IBuffer\n ) {\n }\n\n public serialize(range: IBufferRange, excludeFinalCursorPosition?: boolean): string {\n // we need two of them to flip between old and new cell\n const cell1 = this._buffer.getNullCell();\n const cell2 = this._buffer.getNullCell();\n let oldCell = cell1;\n\n const startRow = range.start.y;\n const endRow = range.end.y;\n const startColumn = range.start.x;\n const endColumn = range.end.x;\n\n this._beforeSerialize(endRow - startRow, startRow, endRow);\n\n for (let row = startRow; row <= endRow; row++) {\n const line = this._buffer.getLine(row);\n if (line) {\n const startLineColumn = row === range.start.y ? startColumn : 0;\n const endLineColumn = row === range.end.y ? endColumn: line.length;\n for (let col = startLineColumn; col < endLineColumn; col++) {\n const c = line.getCell(col, oldCell === cell1 ? cell2 : cell1);\n if (!c) {\n console.warn(`Can't get cell at row=${row}, col=${col}`);\n continue;\n }\n this._nextCell(c, oldCell, row, col);\n oldCell = c;\n }\n }\n this._rowEnd(row, row === endRow);\n }\n\n this._afterSerialize();\n\n return this._serializeString(excludeFinalCursorPosition);\n }\n\n protected _nextCell(cell: IBufferCell, oldCell: IBufferCell, row: number, col: number): void { }\n protected _rowEnd(row: number, isLastRow: boolean): void { }\n protected _beforeSerialize(rows: number, startRow: number, endRow: number): void { }\n protected _afterSerialize(): void { }\n protected _serializeString(excludeFinalCursorPosition?: boolean): string { return ''; }\n}\n\nfunction equalFg(cell1: IBufferCell | IAttributeData, cell2: IBufferCell): boolean {\n return cell1.getFgColorMode() === cell2.getFgColorMode()\n && cell1.getFgColor() === cell2.getFgColor();\n}\n\nfunction equalBg(cell1: IBufferCell | IAttributeData, cell2: IBufferCell): boolean {\n return cell1.getBgColorMode() === cell2.getBgColorMode()\n && cell1.getBgColor() === cell2.getBgColor();\n}\n\nfunction equalFlags(cell1: IBufferCell | IAttributeData, cell2: IBufferCell): boolean {\n return cell1.isInverse() === cell2.isInverse()\n && cell1.isBold() === cell2.isBold()\n && cell1.isUnderline() === cell2.isUnderline()\n && cell1.isOverline() === cell2.isOverline()\n && cell1.isBlink() === cell2.isBlink()\n && cell1.isInvisible() === cell2.isInvisible()\n && cell1.isItalic() === cell2.isItalic()\n && cell1.isDim() === cell2.isDim()\n && cell1.isStrikethrough() === cell2.isStrikethrough();\n}\n\nclass StringSerializeHandler extends BaseSerializeHandler {\n private _rowIndex: number = 0;\n private _allRows: string[] = new Array<string>();\n private _allRowSeparators: string[] = new Array<string>();\n private _currentRow: string = '';\n private _nullCellCount: number = 0;\n\n // we can see a full colored cell and a null cell that only have background the same style\n // but the information isn't preserved by null cell itself\n // so wee need to record it when required.\n private _cursorStyle: IBufferCell = this._buffer.getNullCell();\n\n // where exact the cursor styles comes from\n // because we can't copy the cell directly\n // so we remember where the content comes from instead\n private _cursorStyleRow: number = 0;\n private _cursorStyleCol: number = 0;\n\n // this is a null cell for reference for checking whether background is empty or not\n private _backgroundCell: IBufferCell = this._buffer.getNullCell();\n\n private _firstRow: number = 0;\n private _lastCursorRow: number = 0;\n private _lastCursorCol: number = 0;\n private _lastContentCursorRow: number = 0;\n private _lastContentCursorCol: number = 0;\n\n constructor(\n buffer: IBuffer,\n private readonly _terminal: Terminal\n ) {\n super(buffer);\n }\n\n protected _beforeSerialize(rows: number, start: number, end: number): void {\n this._allRows = new Array<string>(rows);\n this._lastContentCursorRow = start;\n this._lastCursorRow = start;\n this._firstRow = start;\n }\n\n private _thisRowLastChar: IBufferCell = this._buffer.getNullCell();\n private _thisRowLastSecondChar: IBufferCell = this._buffer.getNullCell();\n private _nextRowFirstChar: IBufferCell = this._buffer.getNullCell();\n protected _rowEnd(row: number, isLastRow: boolean): void {\n // if there is colorful empty cell at line end, whe must pad it back, or the the color block\n // will missing\n if (this._nullCellCount > 0 && !equalBg(this._cursorStyle, this._backgroundCell)) {\n // use clear right to set background.\n this._currentRow += `\\u001b[${this._nullCellCount}X`;\n }\n\n let rowSeparator = '';\n\n // handle row separator\n if (!isLastRow) {\n // Enable BCE\n if (row - this._firstRow >= this._terminal.rows) {\n this._buffer.getLine(this._cursorStyleRow)?.getCell(this._cursorStyleCol, this._backgroundCell);\n }\n\n // Fetch current line\n const currentLine = this._buffer.getLine(row)!;\n // Fetch next line\n const nextLine = this._buffer.getLine(row + 1)!;\n\n if (!nextLine.isWrapped) {\n // just insert the line break\n rowSeparator = '\\r\\n';\n // we sended the enter\n this._lastCursorRow = row + 1;\n this._lastCursorCol = 0;\n } else {\n rowSeparator = '';\n const thisRowLastChar = currentLine.getCell(currentLine.length - 1, this._thisRowLastChar)!;\n const thisRowLastSecondChar = currentLine.getCell(currentLine.length - 2, this._thisRowLastSecondChar)!;\n const nextRowFirstChar = nextLine.getCell(0, this._nextRowFirstChar)!;\n const isNextRowFirstCharDoubleWidth = nextRowFirstChar.getWidth() > 1;\n\n // validate whether this line wrap is ever possible\n // which mean whether cursor can placed at a overflow position (x === row) naturally\n let isValid = false;\n\n if (\n // you must output character to cause overflow, control sequence can't do this\n nextRowFirstChar.getChars() &&\n isNextRowFirstCharDoubleWidth ? this._nullCellCount <= 1 : this._nullCellCount <= 0\n ) {\n if (\n // the last character can't be null,\n // you can't use control sequence to move cursor to (x === row)\n (thisRowLastChar.getChars() || thisRowLastChar.getWidth() === 0) &&\n // change background of the first wrapped cell also affects BCE\n // so we mark it as invalid to simply the process to determine line separator\n equalBg(thisRowLastChar, nextRowFirstChar)\n ) {\n isValid = true;\n }\n\n if (\n // the second to last character can't be null if the next line starts with CJK,\n // you can't use control sequence to move cursor to (x === row)\n isNextRowFirstCharDoubleWidth &&\n (thisRowLastSecondChar.getChars() || thisRowLastSecondChar.getWidth() === 0) &&\n // change background of the first wrapped cell also affects BCE\n // so we mark it as invalid to simply the process to determine line separator\n equalBg(thisRowLastChar, nextRowFirstChar) &&\n equalBg(thisRowLastSecondChar, nextRowFirstChar)\n ) {\n isValid = true;\n }\n }\n\n if (!isValid) {\n // force the wrap with magic\n // insert enough character to force the wrap\n rowSeparator = '-'.repeat(this._nullCellCount + 1);\n // move back and erase next line head\n rowSeparator += '\\u001b[1D\\u001b[1X';\n\n if (this._nullCellCount > 0) {\n // do these because we filled the last several null slot, which we shouldn't\n rowSeparator += '\\u001b[A';\n rowSeparator += `\\u001b[${currentLine.length - this._nullCellCount}C`;\n rowSeparator += `\\u001b[${this._nullCellCount}X`;\n rowSeparator += `\\u001b[${currentLine.length - this._nullCellCount}D`;\n rowSeparator += '\\u001b[B';\n }\n\n // This is content and need the be serialized even it is invisible.\n // without this, wrap will be missing from outputs.\n this._lastContentCursorRow = row + 1;\n this._lastContentCursorCol = 0;\n\n // force commit the cursor position\n this._lastCursorRow = row + 1;\n this._lastCursorCol = 0;\n }\n }\n }\n\n this._allRows[this._rowIndex] = this._currentRow;\n this._allRowSeparators[this._rowIndex++] = rowSeparator;\n this._currentRow = '';\n this._nullCellCount = 0;\n }\n\n private _diffStyle(cell: IBufferCell | IAttributeData, oldCell: IBufferCell): number[] {\n const sgrSeq: number[] = [];\n const fgChanged = !equalFg(cell, oldCell);\n const bgChanged = !equalBg(cell, oldCell);\n const flagsChanged = !equalFlags(cell, oldCell);\n\n if (fgChanged || bgChanged || flagsChanged) {\n if (cell.isAttributeDefault()) {\n if (!oldCell.isAttributeDefault()) {\n sgrSeq.push(0);\n }\n } else {\n if (fgChanged) {\n const color = cell.getFgColor();\n if (cell.isFgRGB()) { sgrSeq.push(38, 2, (color >>> 16) & 0xFF, (color >>> 8) & 0xFF, color & 0xFF); }\n else if (cell.isFgPalette()) {\n if (color >= 16) { sgrSeq.push(38, 5, color); }\n else { sgrSeq.push(color & 8 ? 90 + (color & 7) : 30 + (color & 7)); }\n }\n else { sgrSeq.push(39); }\n }\n if (bgChanged) {\n const color = cell.getBgColor();\n if (cell.isBgRGB()) { sgrSeq.push(48, 2, (color >>> 16) & 0xFF, (color >>> 8) & 0xFF, color & 0xFF); }\n else if (cell.isBgPalette()) {\n if (color >= 16) { sgrSeq.push(48, 5, color); }\n else { sgrSeq.push(color & 8 ? 100 + (color & 7) : 40 + (color & 7)); }\n }\n else { sgrSeq.push(49); }\n }\n if (flagsChanged) {\n if (cell.isInverse() !== oldCell.isInverse()) { sgrSeq.push(cell.isInverse() ? 7 : 27); }\n if (cell.isBold() !== oldCell.isBold()) { sgrSeq.push(cell.isBold() ? 1 : 22); }\n if (cell.isUnderline() !== oldCell.isUnderline()) { sgrSeq.push(cell.isUnderline() ? 4 : 24); }\n if (cell.isOverline() !== oldCell.isOverline()) { sgrSeq.push(cell.isOverline() ? 53 : 55); }\n if (cell.isBlink() !== oldCell.isBlink()) { sgrSeq.push(cell.isBlink() ? 5 : 25); }\n if (cell.isInvisible() !== oldCell.isInvisible()) { sgrSeq.push(cell.isInvisible() ? 8 : 28); }\n if (cell.isItalic() !== oldCell.isItalic()) { sgrSeq.push(cell.isItalic() ? 3 : 23); }\n if (cell.isDim() !== oldCell.isDim()) { sgrSeq.push(cell.isDim() ? 2 : 22); }\n if (cell.isStrikethrough() !== oldCell.isStrikethrough()) { sgrSeq.push(cell.isStrikethrough() ? 9 : 29); }\n }\n }\n }\n\n return sgrSeq;\n }\n\n protected _nextCell(cell: IBufferCell, oldCell: IBufferCell, row: number, col: number): void {\n // a width 0 cell don't need to be count because it is just a placeholder after a CJK character;\n const isPlaceHolderCell = cell.getWidth() === 0;\n\n if (isPlaceHolderCell) {\n return;\n }\n\n // this cell don't have content\n const isEmptyCell = cell.getChars() === '';\n\n const sgrSeq = this._diffStyle(cell, this._cursorStyle);\n\n // the empty cell style is only assumed to be changed when background changed, because\n // foreground is always 0.\n const styleChanged = isEmptyCell ? !equalBg(this._cursorStyle, cell) : sgrSeq.length > 0;\n\n /**\n * handles style change\n */\n if (styleChanged) {\n // before update the style, we need to fill empty cell back\n if (this._nullCellCount > 0) {\n // use clear right to set background.\n if (!equalBg(this._cursorStyle, this._backgroundCell)) {\n this._currentRow += `\\u001b[${this._nullCellCount}X`;\n }\n // use move right to move cursor.\n this._currentRow += `\\u001b[${this._nullCellCount}C`;\n this._nullCellCount = 0;\n }\n\n this._lastContentCursorRow = this._lastCursorRow = row;\n this._lastContentCursorCol = this._lastCursorCol = col;\n\n this._currentRow += `\\u001b[${sgrSeq.join(';')}m`;\n\n // update the last cursor style\n const line = this._buffer.getLine(row);\n if (line !== undefined) {\n line.getCell(col, this._cursorStyle);\n this._cursorStyleRow = row;\n this._cursorStyleCol = col;\n }\n }\n\n /**\n * handles actual content\n */\n if (isEmptyCell) {\n this._nullCellCount += cell.getWidth();\n } else {\n if (this._nullCellCount > 0) {\n // we can just assume we have same style with previous one here\n // because style change is handled by previous stage\n // use move right when background is empty, use clear right when there is background.\n if (equalBg(this._cursorStyle, this._backgroundCell)) {\n this._currentRow += `\\u001b[${this._nullCellCount}C`;\n } else {\n this._currentRow += `\\u001b[${this._nullCellCount}X`;\n this._currentRow += `\\u001b[${this._nullCellCount}C`;\n }\n this._nullCellCount = 0;\n }\n\n this._currentRow += cell.getChars();\n\n // update cursor\n this._lastContentCursorRow = this._lastCursorRow = row;\n this._lastContentCursorCol = this._lastCursorCol = col + cell.getWidth();\n }\n }\n\n protected _serializeString(excludeFinalCursorPosition: boolean): string {\n let rowEnd = this._allRows.length;\n\n // the fixup is only required for data without scrollback\n // because it will always be placed at last line otherwise\n if (this._buffer.length - this._firstRow <= this._terminal.rows) {\n rowEnd = this._lastContentCursorRow + 1 - this._firstRow;\n this._lastCursorCol = this._lastContentCursorCol;\n this._lastCursorRow = this._lastContentCursorRow;\n }\n\n let content = '';\n\n for (let i = 0; i < rowEnd; i++) {\n content += this._allRows[i];\n if (i + 1 < rowEnd) {\n content += this._allRowSeparators[i];\n }\n }\n\n // restore the cursor\n if (!excludeFinalCursorPosition) {\n const realCursorRow = this._buffer.baseY + this._buffer.cursorY;\n const realCursorCol = this._buffer.cursorX;\n\n const cursorMoved = (realCursorRow !== this._lastCursorRow || realCursorCol !== this._lastCursorCol);\n\n const moveRight = (offset: number): void => {\n if (offset > 0) {\n content += `\\u001b[${offset}C`;\n } else if (offset < 0) {\n content += `\\u001b[${-offset}D`;\n }\n };\n const moveDown = (offset: number): void => {\n if (offset > 0) {\n content += `\\u001b[${offset}B`;\n } else if (offset < 0) {\n content += `\\u001b[${-offset}A`;\n }\n };\n\n if (cursorMoved) {\n moveDown(realCursorRow - this._lastCursorRow);\n moveRight(realCursorCol - this._lastCursorCol);\n }\n }\n\n // Restore the cursor's current style, see https://github.com/xtermjs/xterm.js/issues/3677\n // HACK: Internal API access since it's awkward to expose this in the API and serialize will\n // likely be the only consumer\n const curAttrData: IAttributeData = (this._terminal as any)._core._inputHandler._curAttrData;\n const sgrSeq = this._diffStyle(curAttrData, this._cursorStyle);\n if (sgrSeq.length > 0) {\n content += `\\u001b[${sgrSeq.join(';')}m`;\n }\n\n return content;\n }\n}\n\nexport class SerializeAddon implements ITerminalAddon , ISerializeApi {\n private _terminal: Terminal | undefined;\n\n public activate(terminal: Terminal): void {\n this._terminal = terminal;\n }\n\n private _serializeBufferByScrollback(terminal: Terminal, buffer: IBuffer, scrollback?: number): string {\n const maxRows = buffer.length;\n const correctRows = (scrollback === undefined) ? maxRows : constrain(scrollback + terminal.rows, 0, maxRows);\n return this._serializeBufferByRange(terminal, buffer, {\n start: maxRows - correctRows,\n end: maxRows - 1\n }, false);\n }\n\n private _serializeBufferByRange(terminal: Terminal, buffer: IBuffer, range: ISerializeRange, excludeFinalCursorPosition: boolean): string {\n const handler = new StringSerializeHandler(buffer, terminal);\n return handler.serialize({\n start: { x: 0, y: typeof range.start === 'number' ? range.start : range.start.line },\n end: { x: terminal.cols, y: typeof range.end === 'number' ? range.end : range.end.line }\n }, excludeFinalCursorPosition);\n }\n\n private _serializeBufferAsHTML(terminal: Terminal, options: Partial<IHTMLSerializeOptions>): string {\n const buffer = terminal.buffer.active;\n const handler = new HTMLSerializeHandler(buffer, terminal, options);\n const onlySelection = options.onlySelection ?? false;\n const range = options.range;\n if (range) {\n return handler.serialize({\n start: { x: range.startCol, y: typeof range.startLine === 'number' ? range.startLine : range.startLine },\n end: { x: terminal.cols, y: typeof range.endLine === 'number' ? range.endLine : range.endLine }\n });\n }\n if (!onlySelection) {\n const maxRows = buffer.length;\n const scrollback = options.scrollback;\n const correctRows = (scrollback === undefined) ? maxRows : constrain(scrollback + terminal.rows, 0, maxRows);\n return handler.serialize({\n start: { x: 0, y: maxRows - correctRows },\n end: { x: terminal.cols, y: maxRows - 1 }\n });\n }\n\n const selection = this._terminal?.getSelectionPosition();\n if (selection !== undefined) {\n return handler.serialize({\n start: { x: selection.start.x, y: selection.start.y },\n end: { x: selection.end.x, y: selection.end.y }\n });\n }\n\n return '';\n }\n\n private _serializeModes(terminal: Terminal): string {\n let content = '';\n const modes = terminal.modes;\n\n // Default: false\n if (modes.applicationCursorKeysMode) content += '\\x1b[?1h';\n if (modes.applicationKeypadMode) content += '\\x1b[?66h';\n if (modes.bracketedPasteMode) content += '\\x1b[?2004h';\n if (modes.insertMode) content += '\\x1b[4h';\n if (modes.originMode) content += '\\x1b[?6h';\n if (modes.reverseWraparoundMode) content += '\\x1b[?45h';\n if (modes.sendFocusMode) content += '\\x1b[?1004h';\n // synchronizedOutputMode doesn't need to be serialized as it's a temporary mode\n\n // Default: true\n if (modes.wraparoundMode === false) content += '\\x1b[?7l';\n\n // Default: 'none'\n if (modes.mouseTrackingMode !== 'none') {\n switch (modes.mouseTrackingMode) {\n case 'x10': content += '\\x1b[?9h'; break;\n case 'vt200': content += '\\x1b[?1000h'; break;\n case 'drag': content += '\\x1b[?1002h'; break;\n case 'any': content += '\\x1b[?1003h'; break;\n }\n }\n\n return content;\n }\n\n public serialize(options?: ISerializeOptions): string {\n // TODO: Add combinedData support\n if (!this._terminal) {\n throw new Error('Cannot use addon until it has been loaded');\n }\n\n // Normal buffer\n let content = options?.range\n ? this._serializeBufferByRange(this._terminal, this._terminal.buffer.normal, options.range, true)\n : this._serializeBufferByScrollback(this._terminal, this._terminal.buffer.normal, options?.scrollback);\n\n // Alternate buffer\n if (!options?.excludeAltBuffer) {\n if (this._terminal.buffer.active.type === 'alternate') {\n const alternativeScreenContent = this._serializeBufferByScrollback(this._terminal, this._terminal.buffer.alternate, undefined);\n content += `\\u001b[?1049h\\u001b[H${alternativeScreenContent}`;\n }\n }\n\n // Modes\n if (!options?.excludeModes) {\n content += this._serializeModes(this._terminal);\n }\n\n return content;\n }\n\n public serializeAsHTML(options?: Partial<IHTMLSerializeOptions>): string {\n if (!this._terminal) {\n throw new Error('Cannot use addon until it has been loaded');\n }\n\n return this._serializeBufferAsHTML(this._terminal, options || {});\n }\n\n public dispose(): void { }\n}\n\nexport class HTMLSerializeHandler extends BaseSerializeHandler {\n private _currentRow: string = '';\n\n private _htmlContent = '';\n\n private _ansiColors: Readonly<IColor[]>;\n\n constructor(\n buffer: IBuffer,\n private readonly _terminal: Terminal,\n private readonly _options: Partial<IHTMLSerializeOptions>\n ) {\n super(buffer);\n\n // For xterm headless: fallback to ansi colors\n if ((_terminal as any)._core._themeService) {\n this._ansiColors = (_terminal as any)._core._themeService.colors.ansi;\n }\n else {\n this._ansiColors = DEFAULT_ANSI_COLORS;\n }\n }\n\n private _padStart(target: string, targetLength: number, padString: string): string {\n targetLength = targetLength >> 0;\n padString = padString ?? ' ';\n if (target.length > targetLength) {\n return target;\n }\n\n targetLength -= target.length;\n if (targetLength > padString.length) {\n padString += padString.repeat(targetLength / padString.length);\n }\n return padString.slice(0, targetLength) + target;\n }\n\n protected _beforeSerialize(rows: number, start: number, end: number): void {\n this._htmlContent += '<html><body><!--StartFragment--><pre>';\n\n let foreground = '#000000';\n let background = '#ffffff';\n if (this._options.includeGlobalBackground ?? false) {\n foreground = this._terminal.options.theme?.foreground ?? '#ffffff';\n background = this._terminal.options.theme?.background ?? '#000000';\n }\n\n const globalStyleDefinitions = [];\n globalStyleDefinitions.push('color: ' + foreground + ';');\n globalStyleDefinitions.push('background-color: ' + background + ';');\n globalStyleDefinitions.push('font-family: ' + this._terminal.options.fontFamily + ';');\n globalStyleDefinitions.push('font-size: ' + this._terminal.options.fontSize + 'px;');\n this._htmlContent += '<div style=\\'' + globalStyleDefinitions.join(' ') + '\\'>';\n }\n\n protected _afterSerialize(): void {\n this._htmlContent += '</div>';\n this._htmlContent += '</pre><!--EndFragment--></body></html>';\n }\n\n protected _rowEnd(row: number, isLastRow: boolean): void {\n this._htmlContent += '<div><span>' + this._currentRow + '</span></div>';\n this._currentRow = '';\n }\n\n private _getHexColor(cell: IBufferCell, isFg: boolean): string | undefined {\n const color = isFg ? cell.getFgColor() : cell.getBgColor();\n if (isFg ? cell.isFgRGB() : cell.isBgRGB()) {\n const rgb = [\n (color >> 16) & 255,\n (color >> 8) & 255,\n (color ) & 255\n ];\n return '#' + rgb.map(x => this._padStart(x.toString(16), 2, '0')).join('');\n }\n if (isFg ? cell.isFgPalette() : cell.isBgPalette()) {\n return this._ansiColors[color].css;\n }\n return undefined;\n }\n\n private _diffStyle(cell: IBufferCell, oldCell: IBufferCell): string[] | undefined {\n const content: string[] = [];\n\n const fgChanged = !equalFg(cell, oldCell);\n const bgChanged = !equalBg(cell, oldCell);\n const flagsChanged = !equalFlags(cell, oldCell);\n\n if (fgChanged || bgChanged || flagsChanged) {\n const fgHexColor = this._getHexColor(cell, true);\n if (fgHexColor) {\n content.push('color: ' + fgHexColor + ';');\n }\n\n const bgHexColor = this._getHexColor(cell, false);\n if (bgHexColor) {\n content.push('background-color: ' + bgHexColor + ';');\n }\n\n if (cell.isInverse()) { content.push('color: #000000; background-color: #BFBFBF;'); }\n if (cell.isBold()) { content.push('font-weight: bold;'); }\n if (cell.isUnderline() && cell.isOverline()) { content.push('text-decoration: overline underline;'); }\n else if (cell.isUnderline()) { content.push('text-decoration: underline;'); }\n else if (cell.isOverline()) { content.push('text-decoration: overline;'); }\n if (cell.isBlink()) { content.push('text-decoration: blink;'); }\n if (cell.isInvisible()) { content.push('visibility: hidden;'); }\n if (cell.isItalic()) { content.push('font-style: italic;'); }\n if (cell.isDim()) { content.push('opacity: 0.5;'); }\n if (cell.isStrikethrough()) { content.push('text-decoration: line-through;'); }\n\n return content;\n }\n\n return undefined;\n }\n\n protected _nextCell(cell: IBufferCell, oldCell: IBufferCell, row: number, col: number): void {\n // a width 0 cell don't need to be count because it is just a placeholder after a CJK character;\n const isPlaceHolderCell = cell.getWidth() === 0;\n if (isPlaceHolderCell) {\n return;\n }\n\n // this cell don't have content\n const isEmptyCell = cell.getChars() === '';\n\n const styleDefinitions = this._diffStyle(cell, oldCell);\n\n // handles style change\n if (styleDefinitions) {\n this._currentRow += styleDefinitions.length === 0 ?\n '</span><span>' :\n '</span><span style=\\'' + styleDefinitions.join(' ') + '\\'>';\n }\n\n // handles actual content\n if (isEmptyCell) {\n this._currentRow += ' ';\n } else {\n this._currentRow += escapeHTMLChar(cell.getChars());\n }\n }\n\n protected _serializeString(): string {\n return this._htmlContent;\n }\n}\n","/**\r\n * Binary protocol v3 — opcode 상수 + parser / encoder helpers (desktop).\r\n *\r\n * Spec single source of truth: claude_code_mobile/docs/specs/binary-protocol-v3.md.\r\n * relay (nest-nexus/src/relay/binary-protocol.ts) + mobile (claude_code_mobile/\r\n * lib/services/relay_binary_dispatcher.dart) 와 fixture-driven contract test 로\r\n * wire 호환을 영구 검증.\r\n *\r\n * Desktop 의 역할:\r\n * - 송신: PTY stdout → PTY_DATA / 응용 메시지 → APP_JSON. relay 가 그대로 forward.\r\n * - 수신: mobile → desktop binary frame.\r\n * - APP_JSON → 기존 message handler (terminal_input, session_attach 등)\r\n * - RESIZE → pty.resize fire-and-forget (RFC 4254 §6.7)\r\n * - PROTOCOL_ERROR → close + log\r\n */\r\n\r\nimport { PROTOCOL_VERSION } from '@arva/shared/constants'\r\n\r\nexport const OPCODE = {\r\n PTY_DATA: 0x00,\r\n APP_JSON: 0x01,\r\n RESIZE: 0x02,\r\n FRAME_SNAPSHOT: 0x03,\r\n GRID_SNAPSHOT: 0x04,\r\n GRID_DIFF: 0x05,\r\n PROTOCOL_ERROR: 0xff,\r\n} as const\r\n\r\nexport const PROTOCOL_ERROR_REASON = {\r\n UNKNOWN_OPCODE: 0x01,\r\n MALFORMED_PAYLOAD: 0x02,\r\n VERSION_MISMATCH: 0x03,\r\n UNAUTHORIZED: 0x04,\r\n} as const\r\n\r\nexport type ParsedFrame =\r\n | { type: 'pty_data'; text: string }\r\n | { type: 'app_json'; json: Record<string, unknown> }\r\n | { type: 'resize'; cols: number; rows: number }\r\n | { type: 'frame_snapshot'; machineId: string; ansi: string }\r\n | {\r\n type: 'grid_snapshot'\r\n seq: number\r\n cols: number\r\n rows: number\r\n cells: Buffer\r\n cursorR: number\r\n cursorC: number\r\n }\r\n | { type: 'grid_diff'; seq: number; baseSeq: number; ops: Buffer }\r\n | { type: 'protocol_error'; reason: number }\r\n\r\nexport class FrameParseError extends Error {\r\n /** PROTOCOL_ERROR 송신 시 reason byte. */\r\n readonly reason: number\r\n constructor(message: string, reason: number) {\r\n super(message)\r\n this.reason = reason\r\n }\r\n}\r\n\r\n/**\r\n * binary frame 1개를 [ParsedFrame] 으로 변환. malformed / unknown opcode → throw.\r\n */\r\nexport function parseFrame(buffer: Buffer): ParsedFrame {\r\n if (buffer.length === 0) {\r\n throw new FrameParseError(\r\n 'empty frame',\r\n PROTOCOL_ERROR_REASON.MALFORMED_PAYLOAD,\r\n )\r\n }\r\n const opcode = buffer.readUInt8(0)\r\n switch (opcode) {\r\n case OPCODE.PTY_DATA: {\r\n // desktop 측은 PTY_DATA 수신 안 함 (host → mobile 단방향). 단 fixture-driven\r\n // contract test 에서 동일 parse 가능해야 함.\r\n const text = buffer.toString('utf-8', 1)\r\n return { type: 'pty_data', text }\r\n }\r\n case OPCODE.APP_JSON: {\r\n const text = buffer.toString('utf-8', 1)\r\n let parsed: unknown\r\n try {\r\n parsed = JSON.parse(text)\r\n } catch (err) {\r\n throw new FrameParseError(\r\n `APP_JSON parse failed: ${(err as Error).message}`,\r\n PROTOCOL_ERROR_REASON.MALFORMED_PAYLOAD,\r\n )\r\n }\r\n if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {\r\n throw new FrameParseError(\r\n 'APP_JSON not a JSON object',\r\n PROTOCOL_ERROR_REASON.MALFORMED_PAYLOAD,\r\n )\r\n }\r\n return { type: 'app_json', json: parsed as Record<string, unknown> }\r\n }\r\n case OPCODE.RESIZE: {\r\n if (buffer.length < 9) {\r\n throw new FrameParseError(\r\n 'RESIZE payload < 8 bytes',\r\n PROTOCOL_ERROR_REASON.MALFORMED_PAYLOAD,\r\n )\r\n }\r\n const cols = buffer.readUInt32BE(1)\r\n const rows = buffer.readUInt32BE(5)\r\n return { type: 'resize', cols, rows }\r\n }\r\n case OPCODE.FRAME_SNAPSHOT: {\r\n // desktop 측은 FRAME_SNAPSHOT 수신 안 함 (relay → mobile 만). contract 호환.\r\n if (buffer.length < 5) {\r\n throw new FrameParseError(\r\n 'FRAME_SNAPSHOT machineId len header missing',\r\n PROTOCOL_ERROR_REASON.MALFORMED_PAYLOAD,\r\n )\r\n }\r\n const idLen = buffer.readUInt32BE(1)\r\n const idEnd = 5 + idLen\r\n if (idEnd > buffer.length) {\r\n throw new FrameParseError(\r\n 'FRAME_SNAPSHOT machineId len exceeds buffer',\r\n PROTOCOL_ERROR_REASON.MALFORMED_PAYLOAD,\r\n )\r\n }\r\n const machineId = buffer.toString('utf-8', 5, idEnd)\r\n if (idEnd + 4 > buffer.length) {\r\n throw new FrameParseError(\r\n 'FRAME_SNAPSHOT ansi len header missing',\r\n PROTOCOL_ERROR_REASON.MALFORMED_PAYLOAD,\r\n )\r\n }\r\n const ansiLen = buffer.readUInt32BE(idEnd)\r\n const ansiEnd = idEnd + 4 + ansiLen\r\n if (ansiEnd > buffer.length) {\r\n throw new FrameParseError(\r\n 'FRAME_SNAPSHOT ansi len exceeds buffer',\r\n PROTOCOL_ERROR_REASON.MALFORMED_PAYLOAD,\r\n )\r\n }\r\n const ansi = buffer.toString('utf-8', idEnd + 4, ansiEnd)\r\n return { type: 'frame_snapshot', machineId, ansi }\r\n }\r\n case OPCODE.GRID_SNAPSHOT: {\r\n // A3 server-truth full snapshot. desktop 측은 송신만 — 수신 거의 없음 (relay echo X 가정),\r\n // 단 fixture-driven contract test 는 동일 parse 가능해야 contract 보장.\r\n // header: opcode 1 + seq 4 + cols 2 + rows 2 + (가변 cells) + cursorR 2 + cursorC 2 ≥ 11 bytes\r\n if (buffer.length < 11) {\r\n throw new FrameParseError(\r\n 'GRID_SNAPSHOT header < 11 bytes',\r\n PROTOCOL_ERROR_REASON.MALFORMED_PAYLOAD,\r\n )\r\n }\r\n const seq = buffer.readUInt32BE(1)\r\n const cols = buffer.readUInt16BE(5)\r\n const rows = buffer.readUInt16BE(7)\r\n // cells 는 가변 길이 — cursor 4 byte 가 buffer 끝에서 역계산\r\n const cursorR = buffer.readUInt16BE(buffer.length - 4)\r\n const cursorC = buffer.readUInt16BE(buffer.length - 2)\r\n const cells = buffer.subarray(9, buffer.length - 4)\r\n return { type: 'grid_snapshot', seq, cols, rows, cells, cursorR, cursorC }\r\n }\r\n case OPCODE.GRID_DIFF: {\r\n // A3 incremental cell ops. 마찬가지로 desktop 측은 송신만 — 본 case 는 contract 호환 용.\r\n // header: opcode 1 + seq 4 + base_seq 4 + opCount 2 + (가변 ops) ≥ 11 bytes\r\n if (buffer.length < 11) {\r\n throw new FrameParseError(\r\n 'GRID_DIFF header < 11 bytes',\r\n PROTOCOL_ERROR_REASON.MALFORMED_PAYLOAD,\r\n )\r\n }\r\n const seq = buffer.readUInt32BE(1)\r\n const baseSeq = buffer.readUInt32BE(5)\r\n // opCount 는 ops 안에 prefix 로 있음 — dispatcher 가 cell-by-cell 해석. parser 는 raw ops buffer 만 반환.\r\n const ops = buffer.subarray(9)\r\n return { type: 'grid_diff', seq, baseSeq, ops }\r\n }\r\n case OPCODE.PROTOCOL_ERROR: {\r\n if (buffer.length < 2) {\r\n throw new FrameParseError(\r\n 'PROTOCOL_ERROR payload empty',\r\n PROTOCOL_ERROR_REASON.MALFORMED_PAYLOAD,\r\n )\r\n }\r\n return { type: 'protocol_error', reason: buffer.readUInt8(1) }\r\n }\r\n default:\r\n throw new FrameParseError(\r\n `unknown opcode 0x${opcode.toString(16)}`,\r\n PROTOCOL_ERROR_REASON.UNKNOWN_OPCODE,\r\n )\r\n }\r\n}\r\n\r\n/**\r\n * PTY_DATA frame (`[0x00] [PTY raw bytes]`) — desktop 의 PTY stdout 을 mobile 로 송신.\r\n */\r\nexport function encodePtyData(bytes: Buffer): Buffer {\r\n const frame = Buffer.alloc(1 + bytes.length)\r\n frame.writeUInt8(OPCODE.PTY_DATA, 0)\r\n bytes.copy(frame, 1)\r\n return frame\r\n}\r\n\r\n/**\r\n * APP_JSON frame — application 메시지 (machine_list, hello_ack, terminal_resize_ack 등).\r\n */\r\nexport function encodeAppJson(data: object): Buffer {\r\n const body = Buffer.from(JSON.stringify(data), 'utf-8')\r\n const frame = Buffer.alloc(1 + body.length)\r\n frame.writeUInt8(OPCODE.APP_JSON, 0)\r\n body.copy(frame, 1)\r\n return frame\r\n}\r\n\r\n/**\r\n * PROTOCOL_ERROR frame.\r\n */\r\nexport function encodeProtocolError(reason: number): Buffer {\r\n const frame = Buffer.alloc(2)\r\n frame.writeUInt8(OPCODE.PROTOCOL_ERROR, 0)\r\n frame.writeUInt8(reason & 0xff, 1)\r\n return frame\r\n}\r\n\r\n/**\r\n * FRAME_SNAPSHOT (0x03) frame — desktop 이 session_attach 시 자신의 GridEmulator(정확한 PTY\r\n * size)를 ANSI 로 직렬화해 attach 하는 mobile 에 송신. raw scrollback tail 만으로는 복원 불가한\r\n * busy alt-screen TUI(claude 등) 화면을 정확히 재현한다.\r\n *\r\n * Wire format (mobile `relay_binary_dispatcher.dart` `_parseFrameSnapshot` 와 정확히 일치해야 함):\r\n * `[0x03] [machineId_len: BE u32] [machineId: utf8] [ansi_len: BE u32] [ansi: utf8]`.\r\n * relay 는 host→mobile binary frame 을 opaque 하게 forward 하므로 relay 변경 불필요.\r\n *\r\n * @param machineId 대상 머신 식별자 (mobile 이 어느 머신의 frame 인지 라우팅)\r\n * @param ansi GridEmulator.serializeAnsi() 결과 — cursor + SGR 보존 ANSI 시퀀스\r\n */\r\nexport function encodeFrameSnapshot(machineId: string, ansi: string): Buffer {\r\n const idBytes = Buffer.from(machineId, 'utf-8')\r\n const ansiBytes = Buffer.from(ansi, 'utf-8')\r\n const head = Buffer.alloc(5)\r\n head.writeUInt8(OPCODE.FRAME_SNAPSHOT, 0)\r\n head.writeUInt32BE(idBytes.length >>> 0, 1)\r\n const ansiLen = Buffer.alloc(4)\r\n ansiLen.writeUInt32BE(ansiBytes.length >>> 0, 0)\r\n return Buffer.concat([head, idBytes, ansiLen, ansiBytes])\r\n}\r\n\r\n/**\r\n * hello_ack message wrapped in APP_JSON. desktop 이 mobile 의 첫 hello 받으면 응답.\r\n */\r\nexport function encodeHelloAck(): Buffer {\r\n return encodeAppJson({ type: 'hello_ack', serverVersion: PROTOCOL_VERSION })\r\n}\r\n\r\n/**\r\n * GRID_SNAPSHOT (0x04) frame — A3 server-truth full snapshot.\r\n *\r\n * Wire format: `[0x04] [seq: BE u32] [cols: BE u16] [rows: BE u16] [cellArray] [cursorR: BE u16] [cursorC: BE u16]`.\r\n * cellArray 는 GridEmulator.serializeCells() 가 만든 Buffer — 가변 길이 (palette mode 평균 6 bytes/cell).\r\n * 본 함수는 그 Buffer 를 envelope 만 두름.\r\n *\r\n * @param seq 단조증가 sequence — recover 시 client 가 last_seq 로 reference\r\n * @param cols / rows server-side cap (max_cols/rows) 적용 후 실제 grid 크기\r\n * @param cells GridEmulator.serializeCells() 결과 Buffer\r\n * @param cursorR / cursorC 0-based row/col index\r\n */\r\nexport function encodeGridSnapshot(\r\n seq: number,\r\n cols: number,\r\n rows: number,\r\n cells: Buffer,\r\n cursorR: number,\r\n cursorC: number,\r\n): Buffer {\r\n const head = Buffer.alloc(9)\r\n head.writeUInt8(OPCODE.GRID_SNAPSHOT, 0)\r\n head.writeUInt32BE(seq >>> 0, 1)\r\n head.writeUInt16BE(cols & 0xffff, 5)\r\n head.writeUInt16BE(rows & 0xffff, 7)\r\n const tail = Buffer.alloc(4)\r\n tail.writeUInt16BE(cursorR & 0xffff, 0)\r\n tail.writeUInt16BE(cursorC & 0xffff, 2)\r\n return Buffer.concat([head, cells, tail])\r\n}\r\n\r\n/**\r\n * GRID_DIFF (0x05) frame — A3 incremental cell ops.\r\n *\r\n * Wire format: `[0x05] [seq: BE u32] [base_seq: BE u32] [opsBuffer]`.\r\n * opsBuffer 는 GridEmulator.serializeDirtyOps() 가 만든 Buffer — `[opCount: BE u16] [ops...]` 형태.\r\n * 본 함수는 envelope 만 두름.\r\n *\r\n * @param seq 단조증가 sequence — base_seq + 1 이 되어야 정상 (client 가 gap 감지로 snapshot.req 발사)\r\n * @param baseSeq 직전 broadcast 의 seq — client 가 seq 연속성 검증용\r\n * @param opsBuffer GridEmulator.serializeDirtyOps() 결과 (opCount 포함)\r\n */\r\nexport function encodeGridDiff(\r\n seq: number,\r\n baseSeq: number,\r\n opsBuffer: Buffer,\r\n): Buffer {\r\n const head = Buffer.alloc(9)\r\n head.writeUInt8(OPCODE.GRID_DIFF, 0)\r\n head.writeUInt32BE(seq >>> 0, 1)\r\n head.writeUInt32BE(baseSeq >>> 0, 5)\r\n return Buffer.concat([head, opsBuffer])\r\n}\r\n","// PTY lifecycle policy: per-machine metadata, LRU eviction, idle timer,\n// pty_status broadcast (throttled).\n//\n// 책임: ssh-manager 가 PTY spawn/exit/data 이벤트를 알릴 때마다 metadata 갱신,\n// pty_status broadcast, count cap / idle timer 검사. 추출 이유: ssh-manager 가\n// 1117 LOC + 30 메서드로 비대화 — CLAUDE.md 10-메서드 규칙 위반. 분리하면 미래\n// E1 daemon 분리 시 이 클래스가 daemon 측 자연 boundary.\n//\n// Lifecycle hooks 매트릭스 (정확한 갱신 순서):\n//\n// Trigger | ptyMetadata | pty_status broadcast\n// ---------------------|-----------------|---------------------------\n// onConnect | (count cap chk) | n/a (spawn 후 onAttach)\n// | + create entry |\n// onDataActivity | touch (1s deb) | running (throttled)\n// onUserInput | touch (즉시) | running (throttled)\n// onAttach | touchAttach | bypass throttle, recompute\n// onDetach | n/a | recompute (idle/running)\n// onRestartPty | reset | dead → 새 spawn 후 running\n// onDisconnect | delete | dead, reason='natural' FIRST\n// onPtyExit | (delete) | dead, reason='natural'\n// onRemoveMachine | delete | (chains via onDisconnect)\n// tickIdle (60s) | scan | for each: status; if 7d→evict\n//\n// Eviction 시 송신 순서: pty_status('dead', reason='lru'|'idle') FIRST →\n// disconnect() (그 안에서 connection_status('disconnected')). 모바일이 dead\n// 먼저 받아 UI 전환, 그 후 disconnected — flicker 없음.\n\nimport { createLogger } from '../lib/logger'\n\n// Per-component scoped logger — writes to logs/pty.log.\nconst log = createLogger('pty')\nimport type { ServerMessage } from '@arva/shared/types'\nimport {\n PTY_COUNT_MAX,\n PTY_IDLE_TIMEOUT_MS,\n PTY_ACTIVITY_THRESHOLD_MS,\n PTY_STATUS_BROADCAST_THROTTLE_MS,\n} from '@arva/shared/constants'\nimport { GridEmulator, type ResizeResult } from './grid-emulator'\n\n/** 머신별 lifecycle 추적 데이터. */\nexport interface PtyMetadata {\n lastAttach: number\n lastActivity: number\n attachCount: number\n /** 다음 idle tick 검사 예정 시각. activity/attach 마다 now + IDLE_TIMEOUT 으로 갱신.\n * timestamp-driven tick: tick 시 nextIdleCheckAt > now 면 즉시 skip → broadcast storm 0. */\n nextIdleCheckAt: number\n}\n\nexport type PtyStatus = 'idle' | 'running' | 'dead'\nexport type DeadReason = 'idle' | 'lru' | 'natural'\n\n/** ssh-manager 가 lifecycle manager 에게 제공하는 hook. */\nexport interface PtyLifecycleHost {\n /** 현재 활성 PTY 수 (sessions + localSessions + pendingFirstResize). pre-await sync. */\n currentPtyCount(): number\n /** Eviction 대상 머신을 강제 disconnect. evict 결정 후 호출. */\n evictMachine(machineId: string): void\n /** restartPty 진행 중인 머신은 idle tick 에서 evict skip. */\n isRestarting(machineId: string): boolean\n /** Renderer + mobile 에 메시지 broadcast (ssh-manager 의 broadcast 콜백 위임). */\n broadcast(msg: ServerMessage): void\n}\n\n/**\n * PTY lifecycle 정책 + pty_status broadcast 의 단일 진입점.\n *\n * ssh-manager 는 PTY 라이프사이클 이벤트마다 이 클래스의 hook 메서드를 호출하면\n * 됨. Manager 가 metadata, throttling, idle timer, eviction 결정을 모두 담당.\n *\n * 시작/종료: constructor() 가 ptyIdleTimer 시작, dispose() 가 clear.\n */\nexport class PtyLifecycleManager {\n private readonly host: PtyLifecycleHost\n private readonly metadata = new Map<string, PtyMetadata>()\n private readonly lastBroadcastAt = new Map<string, number>()\n private readonly lastStatus = new Map<string, PtyStatus>()\n private ptyIdleTimer: ReturnType<typeof setInterval> | null = null\n /**\n * A3 server-truth grid emulator owner. PTY 1:1.\n * onConnect 시 생성, onPtyExit 시 dispose. capability=grid.diff client 의 단일 권위.\n * spec: claude_code_mobile/docs/specs/binary-protocol-v3.md §3, §6.5.\n */\n private readonly emulators = new Map<string, GridEmulator>()\n\n /** 테스트용 clock injection. 기본 Date.now. */\n private readonly nowFn: () => number\n\n constructor(host: PtyLifecycleHost, opts: { now?: () => number; idleTickMs?: number } = {}) {\n this.host = host\n this.nowFn = opts.now ?? Date.now\n const tickMs = opts.idleTickMs ?? 60_000\n this.ptyIdleTimer = setInterval(() => this.tickIdle(), tickMs)\n }\n\n /** 모든 timer 해제 + Map clear. ssh-manager.cleanup() 에서 호출. */\n dispose(): void {\n if (this.ptyIdleTimer) {\n clearInterval(this.ptyIdleTimer)\n this.ptyIdleTimer = null\n }\n this.metadata.clear()\n this.lastBroadcastAt.clear()\n this.lastStatus.clear()\n for (const em of this.emulators.values()) em.dispose()\n this.emulators.clear()\n }\n\n // ── Lifecycle hooks (ssh-manager 호출) ─────────────────────────────────────\n\n /**\n * connect() / connectLocal() 진입 시 호출. **첫 await 전 동기적으로** count cap 검사.\n * cap 도달이면 LRU 1개 evict 후 새 metadata create. 같은 tick 동시 호출은\n * pendingFirstResize.set 이 첫 await 전에 일어나므로 host.currentPtyCount() 가 보호.\n *\n * 주의: 이 메서드 자체는 동기. evictMachine 호출은 host 가 동기 처리 (disconnect\n * 가 sync mutate). 따라서 cap 검사 → evict → 새 spawn 가 race 없이 진행.\n */\n onConnect(machineId: string, initialCols = 80, initialRows = 24): void {\n const count = this.host.currentPtyCount()\n if (count >= PTY_COUNT_MAX) {\n const victim = this.pickLruVictim(machineId)\n if (victim) {\n log.info(`[pty-lifecycle] count=${count} cap=${PTY_COUNT_MAX} → evict LRU ${victim}`)\n // 송신 순서: dead FIRST → disconnect (host.evictMachine 안에서 connection_status).\n this.broadcastStatus(victim, 'dead', { reason: 'lru', bypassThrottle: true })\n this.host.evictMachine(victim)\n }\n }\n this.cleanupMachineState(machineId)\n const now = this.nowFn()\n this.metadata.set(machineId, {\n lastAttach: now,\n lastActivity: now,\n attachCount: 0,\n nextIdleCheckAt: now + PTY_IDLE_TIMEOUT_MS,\n })\n // A3: emulator 생성 (PTY 1:1). initialCols/Rows 는 SSH spawn 의 hint 와 일치해야\n // 첫 GRID_SNAPSHOT 이 정확. 인자 default (80×24) 는 SSH RFC 4254 default.\n if (!this.emulators.has(machineId)) {\n this.emulators.set(machineId, new GridEmulator(initialCols, initialRows))\n }\n }\n\n /** PTY 출력 도착 시 호출. 1초 debounce — 핫패스 부담 최소화. */\n onDataActivity(machineId: string): void {\n const meta = this.metadata.get(machineId)\n if (!meta) return\n const now = this.nowFn()\n if (now - meta.lastActivity < 1000) return\n this.touchActivity(meta, now)\n this.broadcastStatus(machineId, this.computeStatus(meta))\n }\n\n /** sendInput() 호출 시. debounce 없음 — 사용자 액션은 항상 기록. */\n onUserInput(machineId: string): void {\n const meta = this.metadata.get(machineId)\n if (!meta) return\n this.touchActivity(meta, this.nowFn())\n this.broadcastStatus(machineId, this.computeStatus(meta))\n }\n\n /** session_attach 처리 시. throttle bypass — 첫 화면 정확성. */\n onAttach(machineId: string): void {\n let meta = this.metadata.get(machineId)\n const now = this.nowFn()\n if (!meta) {\n // PTY 가 아직 spawn 안 됐는데 attach 가 먼저 옴. ssh-manager 가\n // 곧 onConnect 호출할 것이므로 임시 entry. running 으로 broadcast.\n meta = {\n lastAttach: now,\n lastActivity: now,\n attachCount: 1,\n nextIdleCheckAt: now + PTY_IDLE_TIMEOUT_MS,\n }\n this.metadata.set(machineId, meta)\n } else {\n meta.lastAttach = now\n meta.attachCount += 1\n meta.nextIdleCheckAt = now + PTY_IDLE_TIMEOUT_MS\n }\n this.broadcastStatus(machineId, this.computeStatus(meta), { bypassThrottle: true })\n }\n\n /**\n * 활동 timestamp 갱신: lastActivity + nextIdleCheckAt 동시 set.\n * onDataActivity / onUserInput 의 공통 패턴. 미래 lifecycle 필드 추가 시\n * 모든 activity 지점에 일관 적용 보장.\n */\n private touchActivity(meta: PtyMetadata, now: number): void {\n meta.lastActivity = now\n meta.nextIdleCheckAt = now + PTY_IDLE_TIMEOUT_MS\n }\n\n /** session_detach 처리 시. lastActivity 기준 idle/running 재계산해서 송신. */\n onDetach(machineId: string): void {\n const meta = this.metadata.get(machineId)\n if (!meta) return\n this.broadcastStatus(machineId, this.computeStatus(meta), { bypassThrottle: true })\n }\n\n /**\n * restartPty() 호출 시. metadata reset (lastAttach/lastActivity=now,\n * attachCount+1). dead → 새 spawn 후 running (ssh-manager 가 onConnect 재호출).\n */\n onRestartPty(machineId: string): void {\n const now = this.nowFn()\n const prev = this.metadata.get(machineId)\n const newMeta: PtyMetadata = {\n lastAttach: now,\n lastActivity: now,\n attachCount: (prev?.attachCount ?? 0) + 1,\n nextIdleCheckAt: now + PTY_IDLE_TIMEOUT_MS,\n }\n this.broadcastStatus(machineId, 'dead', { reason: 'natural', bypassThrottle: true })\n this.metadata.set(machineId, newMeta)\n }\n\n /**\n * 사용자 명시 disconnect() 시. dead broadcast FIRST → host 가 sessions delete.\n * cleanup 은 ssh-manager 의 disconnect() 안에서 cleanupMachineState 호출.\n */\n onDisconnect(machineId: string, reason: DeadReason = 'natural'): void {\n if (this.metadata.has(machineId) || this.lastStatus.has(machineId)) {\n this.broadcastStatus(machineId, 'dead', { reason, bypassThrottle: true })\n }\n this.cleanupMachineState(machineId)\n }\n\n /** PTY 자연 종료 (proc.onExit / stream.on('close')). disconnect 와 동일 처리. */\n onPtyExit(machineId: string): void {\n this.onDisconnect(machineId, 'natural')\n this.disposeEmulator(machineId)\n }\n\n // ── A3 server-truth (grid emulator) hooks ──────────────────────────────\n\n /**\n * PTY stdout chunk 도착 시 호출 — emulator 의 grid 갱신.\n *\n * ssh-manager 의 `proc.onData` 안에서 기존 `onDataActivity(machineId)` 옆에 호출.\n * SRP — emulator append 만 책임 (broadcast 는 [flushDiff] 또는 ws-manager 에서 별도).\n *\n * cap=`pty.data` (legacy) client 만 있는 경우에도 emulator 는 운영 — capability 가 동적\n * 변경 (재연결 등) 시 즉시 GRID_SNAPSHOT 보낼 수 있도록.\n */\n onPtyData(machineId: string, chunk: string | Buffer): void {\n const em = this.emulators.get(machineId)\n if (em) em.write(chunk)\n }\n\n /**\n * mobile client 가 cap.ack 'grid.diff' 받은 직후 호출 — GRID_SNAPSHOT 1회 송신.\n *\n * `send` 는 ws-manager 가 주입한 단일 client 송신 함수 (`ws.send.bind(ws)` 형태).\n * 모든 client broadcast 가 아닌 신규 attach client 에만 — cold attach 는 1:1.\n */\n onAttachGridDiff(machineId: string, send: (frame: Buffer) => void): void {\n const em = this.emulators.get(machineId)\n if (em) send(em.snapshot())\n }\n\n /**\n * 머신의 현재 grid 를 ANSI 문자열로 직렬화 (cursor + SGR 보존). emulator 미존재 시 빈 문자열.\n *\n * ws-manager 가 session_attach 시 호출 → FRAME_SNAPSHOT (0x03) 로 감싸 attach mobile 에 송신.\n * cap 무관(legacy pty.data client 포함 모두 대상) — raw scrollback tail 만으로 복원 불가한 busy\n * alt-screen TUI 화면을 정확한 PTY size 의 server-truth emulator 로 복원하기 위함.\n */\n serializeGridAnsi(machineId: string): string {\n const em = this.emulators.get(machineId)\n return em ? em.serializeAnsi() : ''\n }\n\n /**\n * [non-sync diag 20260614] 머신 grid 의 \"빈 화면\" 진단 — buffer 직접 순회 결과.\n * ws-manager 가 frame_snapshot 로그에 append 해 a1(claude 비움) vs a2(serialize 버그) 를\n * 가른다. emulator 미존재 시 null. docs/non-sync.md §4 (claude_code_mobile repo).\n */\n gridSnapshotDiag(machineId: string): {\n bufType: string\n nonBlankLines: number\n rows: number\n glyphs: number\n } | null {\n const em = this.emulators.get(machineId)\n return em ? em.snapshotDiag() : null\n }\n\n /**\n * RESIZE 수신 시 — emulator resize + cap 검사.\n *\n * 결과의 `capped=true` 이면 ws-manager 가 client 에 `grid_resize_capped` APP_JSON\n * broadcast (실제 적용된 cols/rows 회신, plan §13-11 trim 알림).\n *\n * resize 직후 emulator 의 markFullDirty 가 동작 — 다음 [flushDiff] 가 사실상 full\n * snapshot 크기. spike v2 에서 resize-aware diff 로 정밀화 가능.\n */\n onResize(machineId: string, cols: number, rows: number): ResizeResult | null {\n const em = this.emulators.get(machineId)\n if (!em) return null\n return em.resize(cols, rows)\n }\n\n /**\n * snapshot.req 수신 시 — manual / recover / reattach 모두 동일 처리.\n * GRID_SNAPSHOT 1회 응답. throttle (3/min/session) 은 ws-manager 가 본 호출 전에 검사.\n */\n onSnapshotReq(machineId: string, send: (frame: Buffer) => void): void {\n const em = this.emulators.get(machineId)\n if (em) send(em.snapshot())\n }\n\n /**\n * 마지막 broadcast 이후 dirty cell 의 GRID_DIFF 송신.\n *\n * `broadcast` 는 ws-manager 가 주입 — cap=`grid.diff` attached client 에만 fan-out.\n * dirty 가 비어있으면 broadcast 발사 X (em.diffSinceLast() 가 null 반환).\n *\n * 호출 시점: ws-manager 가 onPtyData 직후 또는 N ms 디바운스 후 (spike 측정 후 결정).\n */\n flushDiff(machineId: string, broadcast: (frame: Buffer) => void): void {\n const em = this.emulators.get(machineId)\n if (!em) return\n const diff = em.diffSinceLast()\n if (diff) broadcast(diff)\n }\n\n /**\n * 머신별 emulator dispose. onPtyExit / onRemoveMachine / cleanup 에서 호출.\n * disposeEmulator 단독 호출 가능 — emulator 만 정리하고 metadata 보존하는 시나리오 대비.\n */\n private disposeEmulator(machineId: string): void {\n const em = this.emulators.get(machineId)\n if (em) {\n em.dispose()\n this.emulators.delete(machineId)\n }\n }\n\n /** removeMachine 시 (disconnect 가 chain 으로 처리하는 경우 외 explicit cleanup). */\n onRemoveMachine(machineId: string): void {\n this.cleanupMachineState(machineId)\n }\n\n // ── Helpers ─────────────────────────────────────────────────────────────────\n\n /**\n * 3 lifecycle Map 에서 해당 머신 ID 제거. disconnect/restartPty/removeMachine/\n * onConnect (re-create 전 정리) 4지점에서 호출. DRY 보장 — 새 lifecycle Map\n * (예: future RSS oom 도입 시 dropTimestamps) 도입 시 한 곳에서 증설.\n */\n private cleanupMachineState(machineId: string): void {\n this.metadata.delete(machineId)\n this.lastBroadcastAt.delete(machineId)\n this.lastStatus.delete(machineId)\n this.disposeEmulator(machineId)\n }\n\n /**\n * [Reclaim guard 20260606] 머신의 마지막 PTY 활동 시각(ms) 또는 undefined.\n *\n * PTY 출력(`onDataActivity`)/입력(`onUserInput`)으로 갱신되는 `lastActivity` 를 그대로\n * 노출 — AuthorityResolver 의 handed_off watchdog 가 \"응답 스트리밍 중(=연속 출력)\" 을\n * 감지해 회수를 보류하는 데 쓴다. 머신 메타가 없으면(eviction/PTY exit/disconnect) undefined.\n */\n getLastActivity(machineId: string): number | undefined {\n return this.metadata.get(machineId)?.lastActivity\n }\n\n /** lastActivity 가 최근 ACTIVITY_THRESHOLD 안이면 'running', 아니면 'idle'. */\n private computeStatus(meta: PtyMetadata): PtyStatus {\n const now = this.nowFn()\n return now - meta.lastActivity < PTY_ACTIVITY_THRESHOLD_MS ? 'running' : 'idle'\n }\n\n /** LRU key = max(lastAttach, lastActivity). 가장 작은 (가장 오래된) 머신 반환.\n * excludeMachineId 는 새로 connect 시도하는 머신 — eviction 후보 X. */\n private pickLruVictim(excludeMachineId: string): string | null {\n let victim: string | null = null\n let oldest = Number.POSITIVE_INFINITY\n for (const [id, meta] of this.metadata) {\n if (id === excludeMachineId) continue\n const lruKey = Math.max(meta.lastAttach, meta.lastActivity)\n if (lruKey < oldest) {\n oldest = lruKey\n victim = id\n }\n }\n return victim\n }\n\n /**\n * pty_status broadcast 의 단일 진입점.\n * - throttle: now - lastBroadcastAt < THROTTLE && status === lastStatus → skip\n * - bypassThrottle: session_attach/eviction 등 즉시 송신해야 할 때\n * - try/catch + electron-log.warn: BrowserWindow closed race 흡수 (silent)\n */\n private broadcastStatus(\n machineId: string,\n status: PtyStatus,\n opts: { reason?: DeadReason; bypassThrottle?: boolean } = {},\n ): void {\n const now = this.nowFn()\n const lastAt = this.lastBroadcastAt.get(machineId) ?? 0\n const lastStat = this.lastStatus.get(machineId)\n const sameStatus = lastStat === status\n if (!opts.bypassThrottle && sameStatus && now - lastAt < PTY_STATUS_BROADCAST_THROTTLE_MS) {\n return\n }\n this.lastBroadcastAt.set(machineId, now)\n this.lastStatus.set(machineId, status)\n try {\n const msg: ServerMessage =\n status === 'dead' && opts.reason\n ? { type: 'pty_status', machineId, status, reason: opts.reason }\n : { type: 'pty_status', machineId, status }\n this.host.broadcast(msg)\n } catch (err) {\n log.warn('[pty-lifecycle] broadcast failed', { machineId, status, err: (err as Error).message })\n }\n }\n\n /**\n * Idle timer tick (60s). Timestamp-driven: nextIdleCheckAt > now 면 즉시 skip\n * → vi.advanceTimersByTime(7d) 시 broadcast storm 0.\n */\n private tickIdle(): void {\n const now = this.nowFn()\n // host evict 호출이 metadata 를 mutate 하므로 entries snapshot.\n const snapshot = [...this.metadata.entries()]\n for (const [id, meta] of snapshot) {\n if (meta.nextIdleCheckAt > now) continue\n // restarting 중이면 skip — 다음 tick 에 재검사.\n if (this.host.isRestarting(id)) continue\n const lruKey = Math.max(meta.lastAttach, meta.lastActivity)\n if (now - lruKey > PTY_IDLE_TIMEOUT_MS) {\n log.info(`[pty-lifecycle] idle evict machine=${id} idle=${(now - lruKey) / 1000}s`)\n this.broadcastStatus(id, 'dead', { reason: 'idle', bypassThrottle: true })\n this.host.evictMachine(id)\n } else {\n // 활동 있어서 idle 아님 — broadcast 만 하고 nextIdleCheckAt 갱신.\n meta.nextIdleCheckAt = now + PTY_IDLE_TIMEOUT_MS\n this.broadcastStatus(id, this.computeStatus(meta))\n }\n }\n }\n\n // ── Test-only inspectors ──────────────────────────────────────────────────\n\n /** @internal 테스트용 — production 호출 금지. */\n metadataForTest(machineId: string): PtyMetadata | undefined {\n return this.metadata.get(machineId)\n }\n\n /** @internal 테스트용. */\n lastStatusForTest(machineId: string): PtyStatus | undefined {\n return this.lastStatus.get(machineId)\n }\n}\n","import type { BroadcastFn, ServerMessage } from '@arva/shared/types'\nimport { createLogger } from '../lib/logger'\n\n// Per-component scoped logger — writes to logs/authority.log.\n// A2 spike dogfood (2026-05-26~): viewer_mode broadcast / takeback / handover\n// 흐름 가시화 위해 신설.\nconst log = createLogger('authority')\n\n/**\n * 권위 leader. 'desktop' 이면 데스크탑 renderer 가 PTY 사이즈 invariant 결정,\n * 'mobile' 이면 특정 모바일 client (`AuthorityState.mobileClientId`) 가 결정.\n */\nexport type AuthorityLeader = 'desktop' | 'mobile'\n\n/**\n * 머신 1개당 권위 상태. 외부에서는 `AuthorityResolver.getState` 로 readonly 조회.\n *\n * **A2 spike (20260523)**: discriminated union 으로 변환 — 'mobile' 변형이 항상-존재하는\n * `attachedAt`/`lastActivityAt` 를 안전히 보유. 이전 단일 interface 형태에서는 `recordAppliedSize`\n * 의 `{...prev, cols, rows}` spread 가 `attachedAt` 를 erase 할 수 있던 type bug 가 있었음.\n *\n * - `'desktop'` 변형 — `lastDesktopSize` 는 grace expire 시 복귀 size 로 사용.\n * - `'mobile'` 변형 — `cols/rows` 는 mobile 권위 viewer 가 마지막 적용 사이즈.\n * `attachedAt` 는 viewer_mode broadcast 의 mobileMeta 기준 시각.\n * `lastActivityAt` 는 mobile input 마다 갱신 (heartbeat 용, 1Hz throttle 로 재broadcast).\n */\nexport type AuthorityState =\n | {\n leader: 'desktop'\n lastDesktopSize?: { cols: number; rows: number }\n }\n | {\n leader: 'mobile'\n mobileClientId: string\n cols?: number\n rows?: number\n attachedAt: number\n lastActivityAt: number\n }\n\n/**\n * AuthorityResolver 의 외부 의존성. 모든 부수효과는 deps 함수 호출로만 발생 →\n * vitest 에서 SSHManager 전체 mock 없이 순수 state machine 으로 검증 가능.\n */\nexport interface AuthorityResolverDeps {\n /** ServerMessage broadcast — main/index.ts 의 broadcast 클로저. */\n broadcast: BroadcastFn\n /**\n * ssh-manager.resize 위임. grace expire 시 desktop-restore 트리거 등 외부 효과.\n * source 가 'desktop-restore' 면 resolver 의 allowsResize 가드를 무조건 통과.\n */\n applyResize: (\n machineId: string,\n cols: number,\n rows: number,\n source: 'desktop-restore'\n ) => void\n /** 마지막 desktop source resize 사이즈 — grace expire 시 복귀 사이즈로 사용. */\n getLastDesktopSize: (\n machineId: string\n ) => { cols: number; rows: number } | undefined\n /**\n * [Reclaim guard 20260606] 머신의 마지막 PTY 활동 시각(ms). production 은\n * `PtyLifecycleManager.getLastActivity` — PTY 출력(`onDataActivity`)/입력으로 갱신.\n *\n * handed_off watchdog 가 \"모바일 무입력\" 만이 아니라 \"PTY 출력도 정지\" 를 함께 봐야\n * 응답 스트리밍 중(=연속 PTY 출력) 세션을 뺏지 않는다. 미주입(undefined) 시 watchdog 는\n * `lastActivityAt` 단일 기준으로 폴백 — 구 동작과 동일(테스트 W1~W5 회귀 0).\n */\n getLastPtyActivity?: (machineId: string) => number | undefined\n /**\n * [Reclaim guard 20260606] true 면 에이전트 턴 진행 중(예: claude_state working/\n * awaiting-approval) — 출력·입력이 잠시 없어도 watchdog 회수를 절대 상한 전까지 보류한다.\n *\n * resolver 는 신호 출처(Claude 등)를 모른다 — opaque busy 만 받는다. 미주입 시 false 폴백.\n */\n isSessionBusy?: (machineId: string) => boolean\n /** 테스트용 — production 은 Date.now 그대로. */\n now?: () => number\n /** 테스트용 — production 은 setTimeout. handle 타입 미고정 (NodeJS.Timeout / number). */\n setTimer?: (fn: () => void, ms: number) => unknown\n /** 테스트용 — production 은 clearTimeout. */\n clearTimer?: (handle: unknown) => void\n /**\n * [A2 spike 20260523] true 면 single-viewer 동작 (handover 모달 skip + viewer_mode broadcast).\n * false 면 Phase 1 동작 (requestHandover + 모달, viewer_mode broadcast 0).\n *\n * Production 기본값: env `VIEWER_MODE_SPIKE !== 'false'` (즉 default on). CEO Fix-D 의\n * rollback 절차 = env flip + agent restart → 본 옵션이 false 로 evaluate → Phase 1 복원.\n *\n * 테스트는 명시적으로 boolean 전달해 두 모드 모두 검증. 미지정 시 env 평가 (Production 일치).\n */\n a2SpikeEnabled?: boolean\n}\n\n/**\n * Phase 1 (20260519) — PTY 권위 상태기계.\n *\n * 머신 1개당 1개의 상태기계 인스턴스를 관리한다. 60s TTL 휴리스틱(mobileAuthorityUntil)\n * 을 대체해 명시적 handover 모달 + grace timer 로 invariant 를 server-side 에서 보장.\n *\n * State: no-mobile / awaiting-handover / mobile-leading / mobile-rejected / grace-pending\n * (실제 저장은 `states` Map 에 leader + mobileClientId 조합으로만 표현 — pending/grace 는\n * 별도 Map 으로 추적해 합성 상태를 식별)\n *\n * 의존성은 모두 deps 로 주입 — 본 클래스는 SSHManager / Electron / setTimeout 직접 참조 X.\n *\n * BroadcastFn 시그니처의 `perRecipient` 콜백을 사용해 `terminal_authority_changed` 의\n * receiver 별 `isAuthority` 를 계산한다 (Phase 3 review back-port A2 finding).\n */\nexport class AuthorityResolver {\n /** GRACE_PERIOD_MS — 마지막 모바일 detach 후 권위 유지 시간. 5s default (확정 #1). */\n private static readonly GRACE_PERIOD_MS = 5_000\n /** HANDOVER_TIMEOUT_MS — handover 모달 사용자 응답 대기 시간. 10s default (확정 #3). */\n private static readonly HANDOVER_TIMEOUT_MS = 10_000\n /** HANDOVER_DEFAULT_ACCEPT — handover timeout 시 default 결정. true=accept (확정 #8). */\n private static readonly HANDOVER_DEFAULT_ACCEPT = true\n /**\n * [A2 spike 20260523] TAKEBACK_COOLDOWN_MS — desktop 의 Take Back 직후 mobile 재attach\n * 거부 시간. 5s. clientId 와 무관하게 per-machineId 적용 (다른 디바이스로 bypass 방지).\n */\n private static readonly TAKEBACK_COOLDOWN_MS = 5_000\n /**\n * [Bug fix 20260604] HANDOFF_WATCHDOG_MS — handed_off 상태에서 모바일 무활동 한계 (60s).\n *\n * 이 시간 동안 모바일 입력(`lastActivityAt` 갱신)이 없으면 모바일이 사라진 것으로 보고\n * desktop 회수. relay 경로는 per-mobile detach 신호가 없어(`relayWs` 는 agent↔relay 단일\n * 소켓 + relay 가 per-mobile disconnect 를 forward 안 함 + 모바일이 주기적 heartbeat 미전송)\n * `onMobileDetach` 가 호출되지 않는다 → 본 watchdog 가 1Hz heartbeat ticker 의 무한 실행을\n * 막는 유일한 desktop-side 안전장치다.\n *\n * Tradeoff: 입력 없이 출력만 읽는 idle 모바일은 60s 후 회수됨 (재입력 시 cooldown-free 라\n * 깔끔히 재attach). idle 포함 즉시 감지는 모바일 heartbeat/session_detach 전송 필요 (follow-up).\n */\n private static readonly HANDOFF_WATCHDOG_MS = 60_000\n /**\n * [Reclaim guard 20260606] HANDOFF_HARD_CEILING_MS — 모바일 무활동 절대 상한 (10min).\n *\n * 왜 필요한가: `getLastPtyActivity` 게이트는 응답 스트리밍을 보호하지만, **끝없이 출력하는\n * PTY**(`tail -f`/dev server/`watch`/hung build)는 `lastPtyActivity` 가 영원히 신선해\n * ptyIdleMs 가 WATCHDOG 를 못 넘긴다 → 모바일이 relay 로 조용히 사라져도 회수 불능\n * (= commit 3ad27a2 stuck-handed_off 재현). 모바일 무활동(`idleMs`)이 본 상한을 넘으면\n * PTY 출력/busy 와 무관하게 회수해 silent-vanish 복구를 **보장**한다 (noisy PTY 한정 60s→10min).\n *\n * 단 상한은 `idleMs`(모바일 keepalive/입력 침묵) 기준 — keepalive 를 계속 보내는 '존재하는\n * 폰'은 의도적으로 유지된다(복구 보장은 keepalive 가 멎은 뒤 기준). 좀비/relay 가 keepalive 를\n * 대신 보내는 경우만 예외이며 이는 relay 측 신뢰 문제로 본 범위 밖(수동 Take Back 이 최종 escape).\n */\n private static readonly HANDOFF_HARD_CEILING_MS = 600_000\n /**\n * [H6 fix 20260607] HANDOFF_BUSY_HARD_CEILING_MS — busy(에이전트 턴 진행) 중일 때 적용하는\n * 모바일 무활동 절대 상한 (30min). 일반 `HANDOFF_HARD_CEILING_MS`(10min)보다 크다.\n *\n * 왜: 라이브 authority.log 판독(가이드 §0.3 \"H6\")에서 `busy=true` + PTY 활발히 출력 중\n * (`ptyIdleMs<1s`)인데 10min 상한으로 모바일이 회수되는(=작업 중 세션 강탈) 사례 3건 확인.\n * 정상 claude 턴은 분 단위라 30min 안에 끝나므로 전부 보호된다. 그래도 **finite** 인 이유:\n * claude 가 Stop 없이 죽으면 'working' 이 잔류해 busy=true 가 영구화될 수 있어\n * (hook-receiver `isAgentBusy` 주석) 무한 면제 시 stuck-handed_off 가 재발 — 30min backstop\n * 으로 복구를 보장한다. 진짜 foreground 시청 중이면 모바일 keepalive 가 idleMs 를 낮게\n * 유지해 어떤 상한에도 안 닿는 게 정상(상한은 폰이 침묵할 때만 의미).\n */\n private static readonly HANDOFF_BUSY_HARD_CEILING_MS = 1_800_000\n\n private states = new Map<string, AuthorityState>()\n private graceTimers = new Map<string, unknown>()\n private pendingDecisions = new Map<\n string,\n {\n timer: unknown\n requesterClientId: string\n requestedCols: number\n requestedRows: number\n }\n >()\n /**\n * [A2 spike] machineId → cooldown 만료 timestamp (ms). `forceDesktopLeader` 가 now+5000\n * 으로 설정. `onMobileAttach` 가 진입 즉시 체크 → reject + viewer_mode='rejected' broadcast.\n * 인-메모리 only (agent restart 시 손실). spike 단계에서는 acceptable (R-8).\n */\n private takebackCooldownUntil = new Map<string, number>()\n\n /**\n * [Bug #1 fix 20260526 — M12 dogfood] machineId → 1Hz heartbeat ticker handle.\n *\n * 문제: `touchMobileActivity` 가 state 의 `lastActivityAt` 만 갱신하고 broadcast 안 함.\n * desktop renderer 의 HandoffPlaceholder 가 표시하는 \"Last activity: Xs ago\" 가 *오래된*\n * lastActivityAt 으로 계속 increment → reset 안 됨.\n *\n * 해결: handed_off 모드 진입 시 본 ticker 시작. 매 1초 mobileMeta 를 state 에서 새로 읽어\n * viewer_mode='handed_off' 재방송 → renderer 가 갱신된 lastActivityAt 으로 \"Xs ago\" 정상 표시.\n * handed_off 이탈 (interactive/attaching/error/...) 시 즉시 stop.\n *\n * 인-메모리 only. `destroy()` 가 모두 정리.\n */\n private _heartbeatTimers = new Map<string, unknown>()\n\n private readonly deps: AuthorityResolverDeps\n private readonly _setTimer: (fn: () => void, ms: number) => unknown\n private readonly _clearTimer: (handle: unknown) => void\n private readonly _a2SpikeEnabled: boolean\n\n /**\n * @param deps broadcast / applyResize / getLastDesktopSize 외부 효과.\n * now / setTimer / clearTimer 는 production 미지정 시 setTimeout/clearTimeout 사용.\n * a2SpikeEnabled 미지정 시 env `VIEWER_MODE_SPIKE !== 'false'` 평가.\n */\n constructor(deps: AuthorityResolverDeps) {\n this.deps = deps\n this._setTimer = deps.setTimer ?? ((fn, ms) => setTimeout(fn, ms))\n this._clearTimer =\n deps.clearTimer ?? (h => clearTimeout(h as ReturnType<typeof setTimeout>))\n this._a2SpikeEnabled =\n deps.a2SpikeEnabled ?? process.env.VIEWER_MODE_SPIKE !== 'false'\n }\n\n /**\n * resize() 호출 직전 invariant 가드. true 면 진행, false 면 ssh-manager 가 drop.\n *\n * - 'desktop-restore' 는 무조건 통과 (resolver 가 자기 expireGrace 에서 트리거하는 경로).\n * - handover 협상 중이면 모든 source drop — 사이즈 깜빡임 차단.\n * - 그 외는 leader 와 source 일치 시에만 통과 (양방향 invariant).\n */\n allowsResize(\n machineId: string,\n source: 'desktop' | 'mobile' | 'desktop-restore'\n ): boolean {\n if (source === 'desktop-restore') return true\n if (this.pendingDecisions.has(machineId)) return false\n const state = this.states.get(machineId) ?? { leader: 'desktop' as const }\n if (state.leader === 'desktop') return source === 'desktop'\n return source === 'mobile'\n }\n\n /**\n * resize() 가 PTY 에 적용된 직후 호출. broadcast 의 cols/rows fallback 으로 사용된다.\n * state 자체는 만들지 않고 (no-state 머신은 노출 안 함), 이미 state 가 있는 머신만 갱신.\n * 첫 resize 시 ssh-manager 가 호출하기 전 state 가 없을 수도 있으니 ensure-create.\n */\n recordAppliedSize(machineId: string, cols: number, rows: number): void {\n const prev = this.states.get(machineId) ?? { leader: 'desktop' as const }\n if (prev.leader === 'desktop') {\n // desktop 변형 — lastDesktopSize 갱신 (grace expire 시 복귀 size).\n this.states.set(machineId, {\n leader: 'desktop',\n lastDesktopSize: { cols, rows },\n })\n } else {\n // mobile 변형 — cols/rows 갱신, mobileClientId/attachedAt/lastActivityAt 보존\n // (이전 단일 interface 의 spread bug 회피, Eng Fix-K).\n this.states.set(machineId, { ...prev, cols, rows })\n }\n }\n\n /**\n * mobile WS attach 시 ws-manager 가 호출.\n *\n * 분기:\n * (1) grace-pending + 같은 clientId 재연결 → 즉시 복귀, broadcast(reconnected).\n * (2) 그 외 (leader=desktop 또는 leader=mobile + 다른 clientId) → 즉시 leader=mobile.\n *\n * 모바일 attach = 항상 모바일 권위. (R-3 정합 — 2026-06-07 desktop-priority 정책 제거,\n * 모바일은 이미 PtyAuthorityMode 를 무시하므로 데스크탑만 따라간 것.)\n */\n onMobileAttach(\n machineId: string,\n clientId: string,\n hintCols?: number,\n hintRows?: number\n ): void {\n // [A2 spike 20260523] (0) Take Back cooldown 체크 — 5s 거부 + viewer_mode='rejected'.\n // 해당 모바일 ws 는 ws-manager 에서 close code 4008 (TAKEBACK_COOLDOWN) 로 종료.\n // spike-off 일 때는 cooldown 자체가 없으므로 skip.\n if (this._a2SpikeEnabled) {\n const cooldownUntil = this.takebackCooldownUntil.get(machineId) ?? 0\n if (this.now() < cooldownUntil) {\n this._broadcastViewerMode(machineId, 'rejected', {\n cooldownUntil,\n reason: 'takeback_cooldown',\n })\n return\n }\n }\n\n const state = this.states.get(machineId) ?? { leader: 'desktop' as const }\n\n // (1) grace-pending + 같은 clientId 재연결\n const grace = this.graceTimers.get(machineId)\n if (\n grace !== undefined &&\n state.leader === 'mobile' &&\n state.mobileClientId === clientId\n ) {\n this._clearTimer(grace)\n this.graceTimers.delete(machineId)\n this.deps.broadcast(\n { type: 'terminal_mobile_unstable', machineId, state: 'reconnected' },\n { skipMobile: true }\n )\n if (this._a2SpikeEnabled) {\n // viewer_mode 재방송 + lastActivityAt 갱신. spike-off 면 Phase 1 의 reconnect\n // broadcast (위) 만 송신 — state 갱신은 ssh-manager 의 다음 RESIZE 에 위임.\n this.states.set(machineId, {\n ...state,\n cols: hintCols ?? state.cols,\n rows: hintRows ?? state.rows,\n lastActivityAt: this.now(),\n })\n this._broadcastViewerMode(machineId, 'handed_off', {\n cols: hintCols ?? state.cols,\n rows: hintRows ?? state.rows,\n })\n }\n return\n }\n\n // (2) 모바일 attach → 즉시 leader=mobile (정책 게이트 없음).\n if (this._a2SpikeEnabled) {\n // [A2 spike] handover 모달 skip — 즉시 leader 전환. viewer_mode='handed_off' broadcast\n // 가 desktop UI 사용자 피드백 역할 (모달 대체). pendingDecisions cleanup 필수\n // (Eng Fix-M): 직전 다른 mobile B 의 orphan timer 가 5s 후 leader 를 강제 전환할\n // 수 있는 cooldown bypass 시나리오 close.\n const stalePending = this.pendingDecisions.get(machineId)\n if (stalePending) {\n this._clearTimer(stalePending.timer)\n this.pendingDecisions.delete(machineId)\n }\n\n // [handoff diag 20260607 — H5] attach 시점에 아직 grace timer 가 무장돼 있으면 경고.\n // 이 분기는 graceTimers 를 지우지 않으므로(Branch(1) 의 same-clientId 경로만 지움),\n // 무장된 grace 는 ~5s 뒤 expireGrace 를 발화해 방금 leader=mobile 로 만든 이 attach 를\n // 강등한다(= H5 orphan grace). 이 줄이 뜬 뒤 곧 'grace_expired reclaim prevLeader=mobile'\n // 이 따라오면 H5 확정. (fix 후엔 이 줄이 떠도 강등이 안 따라와야 정상.)\n if (this.graceTimers.has(machineId)) {\n log.warn(\n `handoff_diag attach_handoff orphan_grace_pending machine=${machineId} ` +\n `newClient=${clientId} prevStateClient=${state.leader === 'mobile' ? state.mobileClientId : 'n/a'} ` +\n `— grace NOT cleared, will reclaim in ~${AuthorityResolver.GRACE_PERIOD_MS}ms`\n )\n }\n\n // [verify 20260607] desktop-priority 제거 후 \"모바일 attach = 항상 leader=mobile\"\n // 를 배포 빌드 authority.log 에서 확인 가능하게 1줄. 제거된 no_handoff(policy=\n // desktop-priority) 자리를 대체하는 positive 신호 — 이 줄이 보이면 정책 게이트 없이\n // handoff 됐다는 뜻. (per-attach 1회라 노이즈 낮음.)\n log.warn(\n `handoff_diag attach_handoff (always mobile-priority) machine=${machineId} ` +\n `client=${clientId} prevLeader=${state.leader}`\n )\n\n const prevCols =\n hintCols ?? (state.leader === 'mobile' ? state.cols : undefined)\n const prevRows =\n hintRows ?? (state.leader === 'mobile' ? state.rows : undefined)\n\n const next: AuthorityState = {\n leader: 'mobile',\n mobileClientId: clientId,\n cols: prevCols,\n rows: prevRows,\n attachedAt: this.now(),\n lastActivityAt: this.now(),\n }\n this.states.set(machineId, next)\n this._broadcastViewerMode(machineId, 'handed_off', {\n cols: prevCols,\n rows: prevRows,\n })\n this.emitAuthorityChanged(\n machineId,\n 'mobile',\n prevCols ?? 80,\n prevRows ?? 24,\n clientId\n )\n } else {\n // [Phase 1 fallback] handover 모달 요청 — CEO Fix-D rollback 시 사용.\n const fallbackCols =\n state.leader === 'mobile' ? state.cols : state.lastDesktopSize?.cols\n const fallbackRows =\n state.leader === 'mobile' ? state.rows : state.lastDesktopSize?.rows\n this.requestHandover(\n machineId,\n clientId,\n hintCols ?? fallbackCols ?? 80,\n hintRows ?? fallbackRows ?? 24\n )\n }\n }\n\n /**\n * mobile WS detach 시 ws-manager 가 호출.\n *\n * leader=mobile + 같은 clientId 면 grace timer 시작 + broadcast(disconnected).\n * 그 외 (leader=desktop, 다른 clientId) 는 noop — 비권위 모바일 detach 는 무관.\n */\n onMobileDetach(machineId: string, clientId: string): void {\n const state = this.states.get(machineId)\n if (\n !state ||\n state.leader !== 'mobile' ||\n state.mobileClientId !== clientId\n ) {\n return\n }\n\n this.deps.broadcast(\n { type: 'terminal_mobile_unstable', machineId, state: 'disconnected' },\n { skipMobile: true }\n )\n // [A2 spike] viewer_mode='detaching' + graceUntil — desktop placeholder 가 1Hz tick 으로\n // \"Phone reconnecting… {n}s\" countdown 표시 (Design Fix-G).\n const graceUntil = this.now() + AuthorityResolver.GRACE_PERIOD_MS\n this._broadcastViewerMode(machineId, 'detaching', { graceUntil })\n const timer = this._setTimer(\n () => this.expireGrace(machineId),\n AuthorityResolver.GRACE_PERIOD_MS\n )\n this.graceTimers.set(machineId, timer)\n }\n\n /**\n * desktop renderer 의 양보 모달 응답 처리. 또는 handover timer 자동 발화 (default accept).\n *\n * 늦은 응답 (이미 timer 가 발화되어 pendingDecisions 가 비어있는 경우) 은 무시 —\n * already-resolved guard. renderer 응답이 10s 후 도착하면 모달이 의미 없으므로.\n */\n onAuthorityDecision(machineId: string, accept: boolean): void {\n const pending = this.pendingDecisions.get(machineId)\n if (!pending) return\n this._clearTimer(pending.timer)\n this.pendingDecisions.delete(machineId)\n\n // [/review fix P1] 권위 결정 시 stale grace timer 정리. 시나리오:\n // M1 권위 → M1 detach (grace 5s 시작) → M2 attach (handover 요청) → user accept.\n // grace timer 가 정리 안 되면 5s 후 expireGrace 가 leader 를 강제 desktop 으로\n // 전이시켜 M2 가 권위 잃는 race. accept/reject 양쪽 모두 정리해야 안전.\n const stale = this.graceTimers.get(machineId)\n if (stale !== undefined) {\n this._clearTimer(stale)\n this.graceTimers.delete(machineId)\n }\n\n if (accept) {\n // [A2 spike] mobile 변형은 attachedAt/lastActivityAt 필수 (discriminated union).\n const now = this.now()\n const next: AuthorityState = {\n leader: 'mobile',\n mobileClientId: pending.requesterClientId,\n cols: pending.requestedCols,\n rows: pending.requestedRows,\n attachedAt: now,\n lastActivityAt: now,\n }\n this.states.set(machineId, next)\n this.emitAuthorityChanged(\n machineId,\n 'mobile',\n next.cols ?? 80,\n next.rows ?? 24,\n next.mobileClientId\n )\n // [A2 spike] viewer_mode='handed_off' broadcast — desktop UI 가 placeholder 로 swap.\n this._broadcastViewerMode(machineId, 'handed_off', {\n cols: next.cols,\n rows: next.rows,\n })\n } else {\n // mobile-rejected — leader=desktop 유지, 모바일은 비권위.\n const cur = this.states.get(machineId) ?? { leader: 'desktop' as const }\n // [A2 spike discriminated union] desktop 변형은 cols/rows 직접 갖지 않음. cur.cols/rows 는\n // 이전이 mobile 이었다면 거기서 borrow, 아니면 lastDesktopSize 에서 borrow.\n const fallbackCols =\n cur.leader === 'mobile' ? cur.cols : cur.lastDesktopSize?.cols\n const fallbackRows =\n cur.leader === 'mobile' ? cur.rows : cur.lastDesktopSize?.rows\n this.states.set(machineId, {\n leader: 'desktop',\n lastDesktopSize:\n fallbackCols !== undefined && fallbackRows !== undefined\n ? { cols: fallbackCols, rows: fallbackRows }\n : undefined,\n })\n this.emitAuthorityChanged(\n machineId,\n 'desktop',\n fallbackCols ?? 80,\n fallbackRows ?? 24,\n undefined\n )\n }\n }\n\n /**\n * mobile 측 takeover 요청 — desktop 에 handover request 재발행.\n *\n * 분기:\n * - leader=desktop → 새 handover 모달.\n * - leader=mobile + 같은 clientId → noop (이미 권위).\n * - leader=mobile + 다른 clientId → 새 handover 모달.\n */\n onMobileTakeoverRequest(machineId: string, clientId: string): void {\n const state = this.states.get(machineId) ?? { leader: 'desktop' as const }\n if (state.leader === 'mobile' && state.mobileClientId === clientId) return\n const fallbackCols =\n state.leader === 'mobile' ? state.cols : state.lastDesktopSize?.cols\n const fallbackRows =\n state.leader === 'mobile' ? state.rows : state.lastDesktopSize?.rows\n this.requestHandover(\n machineId,\n clientId,\n fallbackCols ?? 80,\n fallbackRows ?? 24\n )\n }\n\n /**\n * 머신 권위 상태 조회 — 테스트/디버깅용. 외부 변경 금지 (readonly).\n * state 가 없으면 initial (leader=desktop) 을 반환 — 호출자가 분기할 필요 없음.\n */\n getState(machineId: string): Readonly<AuthorityState> {\n return this.states.get(machineId) ?? { leader: 'desktop' as const }\n }\n\n /**\n * 머신 삭제 시 cleanup — states / graceTimers / pendingDecisions 모두 정리.\n *\n * [Review P1] ssh.removeMachine 호출 시 동반 호출되어야 함. 다른 머신은 영향 없음.\n */\n removeMachine(machineId: string): void {\n const grace = this.graceTimers.get(machineId)\n if (grace !== undefined) {\n this._clearTimer(grace)\n this.graceTimers.delete(machineId)\n }\n const pending = this.pendingDecisions.get(machineId)\n if (pending !== undefined) {\n this._clearTimer(pending.timer)\n this.pendingDecisions.delete(machineId)\n }\n this.states.delete(machineId)\n // [A2 spike] 머신 삭제 시 cooldown 도 정리.\n this.takebackCooldownUntil.delete(machineId)\n }\n\n /**\n * 앱 종료 시 cleanup — 모든 grace / handover timer 취소. states 도 비움.\n * SSHManager.cleanup 에서 호출하면 누수 0.\n */\n destroy(): void {\n for (const handle of this.graceTimers.values()) {\n this._clearTimer(handle)\n }\n this.graceTimers.clear()\n for (const entry of this.pendingDecisions.values()) {\n this._clearTimer(entry.timer)\n }\n this.pendingDecisions.clear()\n this.states.clear()\n // [A2 spike] cooldown Map 도 정리.\n this.takebackCooldownUntil.clear()\n // [Bug #1 fix 20260526] heartbeat ticker Map 도 정리 — handle 누수 방지.\n for (const handle of this._heartbeatTimers.values()) {\n this._clearTimer(handle)\n }\n this._heartbeatTimers.clear()\n }\n\n // ─── A2 spike public methods (20260523) ────────────────────────────────────\n\n /**\n * Desktop Take Back — leader 강제 desktop 전환 + 5s cooldown + viewer_mode='interactive'.\n *\n * 호출 경로: `ws-manager.handleDesktopTakeback` 가 mobile WS 를 close code 4007 으로 종료한\n * 직후 본 메서드를 호출. mobile 측에서는 close code → DisconnectReason.takenByDesktop →\n * `KickedByDesktopBanner` 풀스크린 표시.\n *\n * [Eng Fix-M] grace timer + pendingDecisions 모두 정리. orphan handover 모달 race 가 5s 후\n * cooldown bypass 로 leader 를 다른 mobile 에 줘버리는 시나리오 close.\n */\n forceDesktopLeader(machineId: string): void {\n const grace = this.graceTimers.get(machineId)\n if (grace !== undefined) {\n this._clearTimer(grace)\n this.graceTimers.delete(machineId)\n }\n const pending = this.pendingDecisions.get(machineId)\n if (pending !== undefined) {\n this._clearTimer(pending.timer)\n this.pendingDecisions.delete(machineId)\n }\n // 이전 mobile state 의 cols/rows 를 lastDesktopSize 로 borrow 할 수도 있지만,\n // takeback 은 desktop 사용자가 자기 화면 size 로 새로 시작하므로 ssh-manager 의 다음\n // RESIZE (TerminalView 의 force-fit useEffect, C4) 가 정확한 size 를 결정한다. 여기서는\n // lastDesktopSize 만 보존.\n const prev = this.states.get(machineId)\n const lastDesktopSize =\n prev?.leader === 'desktop' ? prev.lastDesktopSize : undefined\n this.states.set(machineId, {\n leader: 'desktop',\n lastDesktopSize,\n })\n this.takebackCooldownUntil.set(\n machineId,\n this.now() + AuthorityResolver.TAKEBACK_COOLDOWN_MS\n )\n this._broadcastViewerMode(machineId, 'interactive')\n // 권위 변경 broadcast — 기존 invariant 유지.\n this.emitAuthorityChanged(\n machineId,\n 'desktop',\n lastDesktopSize?.cols ?? 80,\n lastDesktopSize?.rows ?? 24,\n undefined\n )\n }\n\n /**\n * Mobile input 마다 lastActivityAt 갱신 — heartbeat 용.\n *\n * [Eng Fix-K] in-place mutation 금지 — `this.states.set(mid, {...})` 으로 새 객체 할당.\n * `Readonly<AuthorityState>` 계약 유지 (`getState()` 의 caller 는 변경 0 가정).\n *\n * 호출처: ssh-manager 의 mobile input 처리 path. 1Hz throttle 로 viewer_mode 재방송은\n * 별도 ticker (C2 의 pty-lifecycle 또는 ws-manager) 가 담당.\n */\n touchMobileActivity(machineId: string, clientId: string): void {\n const s = this.states.get(machineId)\n if (!s || s.leader !== 'mobile' || s.mobileClientId !== clientId) {\n // [Option D 20260606 — adopt-on-input] leader 아닌 모바일의 input/keepalive 는\n // \"복귀 신호\"로 해석한다. 실기기 RCA(authority.log 18:08): 모바일이 백그라운드 5s\n // 초과로 grace 회수당한 뒤(leader=desktop) 복귀했는데 session_attach 재송신을\n // 놓친 경우, 입력은 PTY 에 도달하지만(ws-manager 가 leader 무관하게 forward) 출력\n // 흐름(viewer)이 모바일로 안 돌아와 \"먹통\"으로 보였다. drop 직전 attach 로그가 0\n // 이었던 게 직접 증거. 여기서 onMobileAttach 로 위임해 leader→mobile 을 복구하면\n // handed_off 재방송 + authority_changed 로 출력 흐름이 재개된다. mobile-only 재attach\n // (PR #94)로는 못 막던 구조 — 폰은 grace 회수를 알 수단이 없으므로(viewer_mode 는\n // desktop-internal) 데스크탑이 활동을 보고 흡수하는 게 구조적 정답(Option D).\n //\n // 가드: spike-off / takeback cooldown 은 adopt 하지 않고 기존처럼 drop. cooldown\n // 이후엔 onMobileAttach 의 \"모바일 attach=즉시 leader\" 의미대로 모바일이 다시 leader.\n const inCooldown =\n this.now() < (this.takebackCooldownUntil.get(machineId) ?? 0)\n if (this._a2SpikeEnabled && !inCooldown) {\n // 마지막으로 알려진 viewport 를 hint 로 — desktop reclaim 후 state 는 mobile\n // cols/rows 를 잃으므로 lastDesktopSize 로 폴백(80x24 강제 snap 방지). 실제 PTY\n // resize 는 모바일이 재adopt 직후 보내는 terminal_resize 가 처리(여기선 강제 X).\n const hintCols =\n s === undefined\n ? undefined\n : s.leader === 'mobile'\n ? s.cols\n : s.lastDesktopSize?.cols\n const hintRows =\n s === undefined\n ? undefined\n : s.leader === 'mobile'\n ? s.rows\n : s.lastDesktopSize?.rows\n log.warn(\n `handoff_diag adopt_on_input (mobile-priority re-attach) machine=${machineId} ` +\n `fromClient=${clientId} prevLeader=${s?.leader ?? 'none'} ` +\n `prevClient=${s && s.leader === 'mobile' ? s.mobileClientId : 'n/a'}`\n )\n this.onMobileAttach(machineId, clientId, hintCols, hintRows)\n return\n }\n\n // [handoff diag 20260606] adopt 불가(spike-off / takeback cooldown) 한 비권위 활동만\n // 여기서 drop. (RC1 desync 케이스는 위 adopt 가 흡수.) relay 재연결로 데스크탑이\n // clientId 를 새로 생성한 경우 등 진단용.\n log.warn(\n `handoff_diag activity_dropped (clientId mismatch) machine=${machineId} ` +\n `fromClient=${clientId} leader=${s?.leader ?? 'none'} ` +\n `stateClient=${s && s.leader === 'mobile' ? s.mobileClientId : 'n/a'}`\n )\n return\n }\n // dogfood 디버깅: lastActivityAt 갱신 매번 기록. M12 heartbeat broadcast bug\n // (별도 ticker 미구현) 진단 위해 추가. 1초당 N회 (input 빈도) — log 부담 있음.\n log.info(\n `touchMobileActivity machine=${machineId} client=${clientId} t=${s.lastActivityAt}`\n )\n this.states.set(machineId, { ...s, lastActivityAt: this.now() })\n }\n\n /**\n * Agent 측 PTY 가 mobile attach 중 죽었을 때 호출 — viewer_mode='error' broadcast.\n *\n * [Design Fix-E F1] desktop UI 가 HandoffPlaceholder 의 rose-tone error branch 로 swap →\n * \"Connection lost — phone offline too\". 사용자가 takeback 또는 reconnect 결정 가능.\n *\n * 호출처: `pty-lifecycle.onPtyExit` 가 leader='mobile' 확인 후 본 메서드 호출 (C2).\n */\n handleAgentCrash(machineId: string): void {\n const s = this.states.get(machineId)\n if (!s || s.leader !== 'mobile') return\n this._broadcastViewerMode(machineId, 'error', { reason: 'agent_crash' })\n }\n\n /**\n * `ws-manager.session_attach` 핸들러가 cooldown 거부 분기에서 사용 — close code 4008\n * (TAKEBACK_COOLDOWN) 으로 mobile WS 종료 결정.\n */\n isInTakebackCooldown(machineId: string): boolean {\n return this.now() < (this.takebackCooldownUntil.get(machineId) ?? 0)\n }\n\n /**\n * [Bug #3 fix 20260526] Cooldown 종료 epoch ms. `ws-manager.handleSessionAttach`\n * 의 cooldown reject 분기가 `desktop.kick` frame 에 `cooldownUntil` 채울 때 사용.\n * 모바일 `RejectedBanner` countdown 의 기준값.\n *\n * 반환값 `undefined` = cooldown 없음 (정상 attach 가능 상태).\n */\n getTakebackCooldownUntil(machineId: string): number | undefined {\n return this.takebackCooldownUntil.get(machineId)\n }\n\n /**\n * `ws-manager.session_attach` 가 진입 직후 호출 — viewer_mode='attaching' broadcast.\n * Design D2-δ row 1 의 \"Phone connecting…\" overlay 트리거. resolver.onMobileAttach 호출\n * 전에 발화되어야 핸드오프 직전 race window UX 가 자연스러움.\n */\n broadcastAttaching(machineId: string, cols?: number, rows?: number): void {\n this._broadcastViewerMode(machineId, 'attaching', { cols, rows })\n }\n\n // ─── private ────────────────────────────────────────────────────────────────\n\n /**\n * handover 요청 발행 + pending 등록.\n *\n * [Review Q1] 이전 pending 이 있으면 cleanup + 첫째 클라이언트에 명시 통지 (현재 leader 재발행).\n * 두 모바일이 5ms 차이로 attach 시 첫째 handover 가 둘째에 의해 덮어쓰이는 경우, 첫째\n * 클라이언트는 \"모달 응답 대기 중\" 상태에서 영원히 대기하지 않도록.\n */\n private requestHandover(\n machineId: string,\n clientId: string,\n cols: number,\n rows: number\n ): void {\n const prev = this.pendingDecisions.get(machineId)\n if (prev) {\n this._clearTimer(prev.timer)\n // 첫째 클라이언트에 cancel 시그널 — 현재 leader 의 authority_changed 재발행.\n const cur = this.states.get(machineId) ?? { leader: 'desktop' as const }\n const curCols =\n cur.leader === 'mobile' ? cur.cols : cur.lastDesktopSize?.cols\n const curRows =\n cur.leader === 'mobile' ? cur.rows : cur.lastDesktopSize?.rows\n this.emitAuthorityChanged(\n machineId,\n cur.leader,\n curCols ?? 80,\n curRows ?? 24,\n cur.leader === 'mobile' ? cur.mobileClientId : undefined\n )\n }\n\n const timer = this._setTimer(\n () =>\n this.onAuthorityDecision(\n machineId,\n AuthorityResolver.HANDOVER_DEFAULT_ACCEPT\n ),\n AuthorityResolver.HANDOVER_TIMEOUT_MS\n )\n this.pendingDecisions.set(machineId, {\n timer,\n requesterClientId: clientId,\n requestedCols: cols,\n requestedRows: rows,\n })\n\n this.deps.broadcast(\n {\n type: 'terminal_authority_request',\n machineId,\n requestedCols: cols,\n requestedRows: rows,\n timeoutMs: AuthorityResolver.HANDOVER_TIMEOUT_MS,\n },\n { skipMobile: true }\n )\n }\n\n /**\n * grace timer 만료 — 마지막 권위 모바일이 5s 안에 재연결하지 않은 경우.\n *\n * [Review A1] broadcast 순서: `terminal_mobile_unstable:expired` → state 전이 →\n * `terminal_authority_changed` → applyResize (ssh.resize 가 `terminal_resize_ack`\n * broadcast 함). 권위 변경이 사이즈 변경 *전에* renderer 에 도달해야 자연스러운 UX.\n *\n * [Review F1] applyResize 가 throw 해도 state 는 이미 desktop 으로 전이. try/catch\n * 로 감싸 console.error 만 남기고 진행. 사용자가 다음 ResizeObserver fire 시 정상 복귀.\n *\n * [Bug fix 20260604] handed_off watchdog 의 desktop 회수 경로로도 호출된다 (grace timer\n * 없이 직접). 위 `graceTimers.delete` + leader=desktop 가드가 grace-pending 아닌 호출도\n * 안전하게 처리하므로 공유 reclaim 루틴으로 재사용한다.\n */\n private expireGrace(machineId: string): void {\n this.graceTimers.delete(machineId)\n\n // [/review fix P1 defense-in-depth] grace timer 가 onAuthorityDecision 에서\n // 정리되지 않은 race window 에 발화하더라도, state 가 이미 desktop 으로 전이됐거나\n // 다른 mobile leader 가 권위 잡은 경우라면 noop. 일반 expire (leader=mobile +\n // mobileClientId 살아있는 경우) 만 정상 처리.\n const cur = this.states.get(machineId)\n // [handoff diag 20260607 — H5] grace-timer 회수를 watchdog 회수(handed_off watchdog\n // expired, :993)·Take Back(forceDesktopLeader) 과 분리해 greppable 하게 1줄. 핵심 진단:\n // prevLeader=mobile 이고 guardEarlyReturn=false 면 = \"백그라운드 detach 로 무장된 grace 가,\n // 그 사이 재attach 된 모바일(같은/다른 clientId)을 5s 뒤 강등\" = H5(orphan grace) 발현이다.\n // 원인은 onMobileAttach Branch(2) 가 pendingDecisions 만 지우고 graceTimers 는 안 지우는 것\n // (:309-313 부근). watchdog 와 달리 이 raw grace 경로는 busy-UNgated(#29 는 watchdog 만 gate).\n // 이 줄이 mobile prevLeader 로 자주 뜨면 fix = Branch(2)에서 graceTimers.delete 또는\n // expireGrace 가드에 mobileClientId 비교 추가(현 가드는 leader==='desktop' 만 봄).\n log.warn(\n `grace_expired reclaim machine=${machineId} prevLeader=${cur?.leader ?? 'none'} ` +\n `prevClient=${cur && cur.leader === 'mobile' ? cur.mobileClientId : 'n/a'} ` +\n `guardEarlyReturn=${cur?.leader === 'desktop'}`\n )\n if (cur && cur.leader === 'desktop') {\n return\n }\n\n this.deps.broadcast(\n { type: 'terminal_mobile_unstable', machineId, state: 'expired' },\n { skipMobile: true }\n )\n\n const last = this.deps.getLastDesktopSize(machineId)\n const nextCols = last?.cols ?? 80\n const nextRows = last?.rows ?? 24\n // [A2 spike discriminated union] desktop 변형은 cols/rows 직접 갖지 않음.\n this.states.set(machineId, {\n leader: 'desktop',\n lastDesktopSize: last ? { cols: nextCols, rows: nextRows } : undefined,\n })\n this.emitAuthorityChanged(\n machineId,\n 'desktop',\n nextCols,\n nextRows,\n undefined\n )\n // [A2 spike] viewer_mode='interactive' — desktop UI 가 placeholder unmount + xterm 복귀.\n this._broadcastViewerMode(machineId, 'interactive')\n\n if (last) {\n try {\n this.deps.applyResize(\n machineId,\n last.cols,\n last.rows,\n 'desktop-restore'\n )\n } catch (err) {\n // state 는 이미 desktop 으로 전이됨 — 다음 사용자 액션 시 정상 복귀.\n log.error(\n `applyResize on grace expire failed machine=${machineId}`,\n err\n )\n }\n }\n }\n\n /**\n * `terminal_authority_changed` broadcast — receiver 별 isAuthority 계산.\n *\n * - renderer 수신: leader='desktop' 이면 true.\n * - mobile (clientId 있는) 수신: leader='mobile' && clientId === mobileClientId.\n * - relay (clientId 미상) 수신: 보수적으로 false fallback.\n */\n private emitAuthorityChanged(\n machineId: string,\n leader: AuthorityLeader,\n cols: number,\n rows: number,\n mobileClientId: string | undefined\n ): void {\n const baseMsg: ServerMessage = {\n type: 'terminal_authority_changed',\n machineId,\n leader,\n isAuthority: false, // fan-out 시 receiver 별로 overwrite\n cols,\n rows,\n }\n this.deps.broadcast(baseMsg, {\n perRecipient: recipient => {\n if (recipient.kind === 'renderer') {\n return { isAuthority: leader === 'desktop' }\n }\n // mobile — clientId 가 있고 mobile leader 와 일치하면 true\n if (\n leader === 'mobile' &&\n mobileClientId !== undefined &&\n recipient.clientId === mobileClientId\n ) {\n return { isAuthority: true }\n }\n return { isAuthority: false }\n },\n })\n }\n\n /**\n * [A2 spike 20260523] `desktop.viewer_mode` 메시지 broadcast helper.\n *\n * env `VIEWER_MODE_SPIKE=false` 시 즉시 no-op — agent 재시작 + flag flip 으로 rollback.\n * (CEO Fix-D: sunk cost mitigation 의 핵심 안전장치)\n *\n * desktop renderer 만 수신 — `skipMobile: true`. mobile 측은 본 message type 을 무시.\n * 예외: `'rejected'` mode 는 거부된 모바일 자기 자신에게도 전달되어야 cooldown UI 가 떠야\n * 하지만, 본 spike 에서는 모바일 close code 4008 (TAKEBACK_COOLDOWN) 만으로 동일 시각 효과\n * 달성 (Phase 2 의 RejectedBanner 가 close code 기반). 따라서 항상 skipMobile.\n *\n * `mobileMeta` 는 `state.leader === 'mobile'` 일 때만 채움 — `'handed_off'` placeholder 의\n * heartbeat / \"cols×rows\" 표시 데이터.\n */\n private _broadcastViewerMode(\n machineId: string,\n mode:\n | 'interactive'\n | 'attaching'\n | 'handed_off'\n | 'detaching'\n | 'error'\n | 'rejected',\n meta?: {\n cols?: number\n rows?: number\n graceUntil?: number\n cooldownUntil?: number\n reason?: string\n }\n ): void {\n if (!this._a2SpikeEnabled) return\n // [Bug #1 fix] mode 별 heartbeat ticker 관리. handed_off 진입 시 1Hz ticker 시작,\n // 그 외 모든 mode 전환 시 즉시 stop. ticker 의 callback 이 본 메서드를 재호출하지만\n // `_ensureHeartbeatTicker` 가 .has() 체크로 중복 set 방지 — 재진입 안전.\n if (mode === 'handed_off') {\n this._ensureHeartbeatTicker(machineId)\n } else {\n this._stopHeartbeatTicker(machineId)\n }\n const s = this.states.get(machineId)\n const mobileMeta =\n s?.leader === 'mobile'\n ? {\n cols: s.cols,\n rows: s.rows,\n attachedAt: s.attachedAt,\n lastActivityAt: s.lastActivityAt,\n }\n : undefined\n // dogfood 디버깅: 모든 viewer_mode broadcast 가시화 — A2 spike 의 핵심 이벤트.\n log.info(\n `broadcast viewer_mode machine=${machineId} mode=${mode}` +\n (mobileMeta\n ? ` lastActivityAt=${mobileMeta.lastActivityAt} cols=${mobileMeta.cols} rows=${mobileMeta.rows}`\n : '') +\n (meta?.graceUntil ? ` graceUntil=${meta.graceUntil}` : '') +\n (meta?.cooldownUntil ? ` cooldownUntil=${meta.cooldownUntil}` : '') +\n (meta?.reason ? ` reason=${meta.reason}` : '')\n )\n this.deps.broadcast(\n {\n type: 'desktop.viewer_mode',\n machineId,\n mode,\n mobileMeta,\n graceUntil: meta?.graceUntil,\n cooldownUntil: meta?.cooldownUntil,\n reason: meta?.reason,\n },\n { skipMobile: true }\n )\n }\n\n /**\n * Date.now 대체 — `AuthorityResolverDeps.now` 가 있으면 사용 (테스트 fake 시계).\n */\n private now(): number {\n return this.deps.now?.() ?? Date.now()\n }\n\n /**\n * [Bug #1 fix 20260526] handed_off heartbeat ticker — 매 1초 viewer_mode 재방송.\n *\n * 이미 timer 있으면 no-op (재진입 안전). `_broadcastViewerMode` 의 callback path 가 본\n * 메서드를 다시 호출하지만 `_heartbeatTimers.has()` 체크로 중복 set 차단.\n *\n * tick 동작:\n * 1. state 가 사라졌거나 leader != mobile → ticker stop + return (cleanup).\n * 2. `_broadcastViewerMode('handed_off')` 호출 — state 의 최신 `lastActivityAt` 가 broadcast.\n * 3. 다음 tick scheduling (1s 후).\n *\n * `_setTimer` 사용 — test fake clock 호환.\n */\n private _ensureHeartbeatTicker(machineId: string): void {\n if (this._heartbeatTimers.has(machineId)) return\n const tick = (): void => {\n const s = this.states.get(machineId)\n if (!s || s.leader !== 'mobile') {\n this._heartbeatTimers.delete(machineId)\n return\n }\n // [Bug fix 20260604 + Reclaim guard 20260606] watchdog — relay 경로엔 per-mobile\n // detach 신호가 없어(HANDOFF_WATCHDOG_MS 주석) 모바일이 조용히 사라지면 본 ticker 가\n // 영구 실행된다. 회수 조건을 3중으로 본다:\n // idleMs = 모바일 무입력 시간 (lastActivityAt 기준).\n // ptyIdleMs = PTY 무활동(출력/입력) 시간 (getLastPtyActivity 기준 — 응답 스트리밍\n // /입력 중이면 ~0). 미주입 시 lastActivityAt 폴백 → 구 mobile-input-only(회귀 0).\n // busy = 에이전트 턴 진행 중(claude_state) — 출력이 잠시 없어도 보호.\n // protectedNow(=PTY 최근 출력 OR busy) 인 동안엔 회수하지 않아 \"응답 끝나기 전엔 안 뺏김\".\n // 단 idleMs 가 절대 상한을 넘으면(연속출력 PTY + 폰 silent-vanish) 보호를 깨고 회수해\n // desktop 복구를 보장한다. 회수 시 viewer_mode='interactive' 가 ticker 를 stop +\n // cooldown 미설정이라 재attach kick 도 없다. 재스케줄 없이 return.\n const now = this.now()\n const idleMs = now - s.lastActivityAt\n const ptyIdleMs =\n now - (this.deps.getLastPtyActivity?.(machineId) ?? s.lastActivityAt)\n const busy = this.deps.isSessionBusy?.(machineId) ?? false\n const protectedNow =\n ptyIdleMs <= AuthorityResolver.HANDOFF_WATCHDOG_MS || busy\n // [H6 fix 20260607] busy(claude_state working/awaiting-approval)는 noisy-PTY(ptyIdleMs)\n // 보다 강한 \"진짜 작업 중\" 신호다. 실로그(가이드 §0.3 H6)에서 busy=true + ptyIdleMs<1s\n // 인데 10min HARD_CEILING 으로 회수돼 사용자가 작업 중 세션을 뺏긴 사례 확인. 따라서 busy\n // 면 더 큰 상한(30min)을 적용해 정상 작업 턴을 보호하되, finite 로 유지해(claude 가 Stop\n // 없이 죽어 'working' 잔류 시 busy=true 영구화 → stuck-handed_off 재발 방지) 복구는 보장.\n const hardCeiling = busy\n ? AuthorityResolver.HANDOFF_BUSY_HARD_CEILING_MS\n : AuthorityResolver.HANDOFF_HARD_CEILING_MS\n const reclaim =\n idleMs > AuthorityResolver.HANDOFF_WATCHDOG_MS &&\n (!protectedNow || idleMs > hardCeiling)\n if (reclaim) {\n log.warn(\n `handed_off watchdog expired machine=${machineId} ` +\n `idleMs=${idleMs} ptyIdleMs=${ptyIdleMs} busy=${busy} ` +\n `ceiling=${hardCeiling} — reclaiming desktop`\n )\n this.expireGrace(machineId)\n return\n }\n // 본 broadcast 의 분기가 _ensureHeartbeatTicker 를 재호출하지만 timer handle 이\n // 아직 map 에 남아있어 (아래 set 직전) .has() = true → return — 무한 재귀 차단.\n this._broadcastViewerMode(machineId, 'handed_off')\n // [authority redelivery 20260609] handed_off heartbeat 의 _broadcastViewerMode 는\n // skipMobile:true 라 데스크탑 renderer 전용 — 모바일엔 안 간다. 모바일에 leader=mobile\n // 을 알리는 유일 메시지인 terminal_authority_changed 는 attach 시 1회뿐이라, 재진입\n // churn 중 그 1회가 유실되면 모바일이 leader=null 로 영구 고착된다(복구 채널 부재 —\n // RCA handoff_authority_redelivery). 본 tick 에서 권위도 함께 재방송해 유실을 ~1s 내\n // 자가치유한다. fan-out perRecipient + 모바일 _authority value-equality dedupe 라 중복\n // 수신은 무해(notify 0). s.leader === 'mobile' 은 위 가드에서 보장됨.\n // cols/rows 는 undefined 가능 → onMobileAttach(:371-372) 와 동일 80/24 폴백.\n this.emitAuthorityChanged(\n machineId,\n 'mobile',\n s.cols ?? 80,\n s.rows ?? 24,\n s.mobileClientId\n )\n const next = this._setTimer(tick, 1000)\n this._heartbeatTimers.set(machineId, next)\n }\n const handle = this._setTimer(tick, 1000)\n this._heartbeatTimers.set(machineId, handle)\n }\n\n /**\n * handed_off 이탈 시 (interactive / attaching / error / rejected / detaching 전환) ticker stop.\n * `_broadcastViewerMode` 가 mode != 'handed_off' 인 모든 경로에서 호출.\n */\n private _stopHeartbeatTicker(machineId: string): void {\n const t = this._heartbeatTimers.get(machineId)\n if (t === undefined) return\n this._clearTimer(t)\n this._heartbeatTimers.delete(machineId)\n }\n}\n","import type { GitAction } from '@arva/shared/types'\r\n\r\n/**\r\n * Git ref(브랜치/이름) shell-injection 가드 — 화이트리스트 정규식.\r\n *\r\n * **SSH 경로 전용**이다. SSH 경로는 [buildGitCommand] 가 ref 를 shell 문자열로\r\n * 조립하므로 메타문자(`;` `$` `` ` `` 공백 등) 주입을 차단해야 한다. relay/local\r\n * 경로는 simple-git 이 ref 를 argv 로 넘겨 shell 이 없으므로 본 가드를 적용하지\r\n * 않는다 — 적용하면 Unicode/CJK 브랜치명(예: `기능/로그인`)을 부당하게 막는다.\r\n * (한때 양 경로 적용을 고려했으나 argv 경로엔 보안 이득이 없고 회귀만 유발해 철회.)\r\n * 호출부는 try 안에 둬야 한다 — throw 가 git_result{success:false} 로 표면화되도록.\r\n *\r\n * `-` 는 합법 브랜치명 문자라 허용되므로 leading-dash 옵션 주입(`--force`/`-D`)은\r\n * 본 가드 범위 밖이다 (checkout/create_branch 의 기존 한계, PR-A scope 아님).\r\n *\r\n * ssh-manager 의 instance state 에 의존하지 않는 순수 함수 — 테스트가 replica\r\n * 가 아닌 본 실제 구현을 직접 import 할 수 있도록 모듈 스코프로 추출한다\r\n * (autoplan F-E-8: Electron/ssh2 무거운 의존 로드 회피).\r\n */\r\nexport function validateGitRef(value: string, field: string): void {\r\n if (!/^[a-zA-Z0-9._\\-/]+$/.test(value)) {\r\n throw new Error(`Invalid ${field}: ${value}`)\r\n }\r\n}\r\n\r\n/** git_result.errorCode 의 분류값. mobile/desktop 공유 스키마(types.ts) 와 동일. */\r\nexport type GitDeleteErrorCode =\r\n | 'not_fully_merged'\r\n | 'checked_out'\r\n | 'not_found'\r\n | 'generic'\r\n\r\n/**\r\n * git 브랜치 삭제 실패 메시지(simple-git Error.message 또는 SSH stdout+stderr)를\r\n * errorCode 로 분류한다. 모바일이 raw 영문 대신 본 code 로 l10n 메시지를 고르고\r\n * force-delete CTA 노출 여부를 판단한다 (autoplan F-E-5).\r\n *\r\n * 순수 함수 — instance state 없음. 테스트가 replica 가 아닌 본 실제 구현을 import\r\n * 한다 (F-E-8). git 의 영문 메시지 표현에 의존하므로 locale-independent 한\r\n * git 출력(`error: The branch 'x' is not fully merged.` 등)을 가정한다.\r\n */\r\nexport function classifyDeleteError(message: string): GitDeleteErrorCode {\r\n const m = message.toLowerCase()\r\n if (m.includes('not fully merged')) return 'not_fully_merged'\r\n if (m.includes('checked out') || m.includes('used by worktree')) {\r\n return 'checked_out'\r\n }\r\n if (m.includes('not found') || m.includes('no such branch')) {\r\n return 'not_found'\r\n }\r\n return 'generic'\r\n}\r\n\r\n/**\r\n * SSH 경로용 git shell 명령 조립. 순수 함수 — instance state 없음.\r\n *\r\n * `default` 분기는 exhaustiveness 가드다: [GitAction] 유니온에 새 kind 가\r\n * 추가됐는데 case 가 누락되면 (1) 컴파일 단계에서 `never` 할당 실패로 잡고,\r\n * (2) 런타임에서도 `undefined` 명령을 조용히 실행하는 대신 throw 한다.\r\n * 기존엔 default 가 없어 미지원 kind 가 `undefined` 를 반환 → `execSsh(undefined)`\r\n * → 조용한 실패로 이어질 수 있었다 (autoplan F-CEO-2 / F-E-8).\r\n */\r\nexport function buildGitCommand(action: GitAction, cwd: string): string {\r\n const cd = `cd ${JSON.stringify(cwd)}`\r\n switch (action.kind) {\r\n case 'status':\r\n return (\r\n `${cd} && ` +\r\n `echo ___GIT_REPO___ && git rev-parse --is-inside-work-tree 2>/dev/null && ` +\r\n `echo ___BRANCH___ && git rev-parse --abbrev-ref HEAD 2>/dev/null && ` +\r\n `echo ___STATUS___ && git status --porcelain 2>/dev/null && ` +\r\n `echo ___BRANCHES___ && git branch -vva 2>/dev/null && ` +\r\n `echo ___AHEAD_BEHIND___ && git rev-list --count HEAD...@{upstream} 2>/dev/null || true`\r\n )\r\n case 'checkout':\r\n return `${cd} && git checkout ${JSON.stringify(action.branch)} 2>&1`\r\n case 'create_branch':\r\n if (action.checkout) {\r\n return `${cd} && git checkout -b ${JSON.stringify(action.name)} 2>&1`\r\n }\r\n return `${cd} && git branch ${JSON.stringify(action.name)} 2>&1`\r\n case 'delete_branch':\r\n // force(-D) 는 미머지 브랜치도 삭제 (커밋 유실 가능). safe(-d) 는 미머지면 git 이\r\n // 거부 → stderr \"not fully merged\" → ssh-manager 가 errorCode 로 분류해 모바일이\r\n // force CTA 를 띄운다. defaultBranch/active 거부는 ssh-manager 의 권한 가드 담당.\r\n return `${cd} && git branch ${action.force ? '-D' : '-d'} ${JSON.stringify(action.branch)} 2>&1`\r\n case 'pull':\r\n return `${cd} && git pull 2>&1`\r\n case 'push':\r\n return `${cd} && git push 2>&1`\r\n case 'log': {\r\n // 탭 구분: hash<TAB>shortHash<TAB>author<TAB>dateRelative<TAB>refs<TAB>subject\r\n const limit = Math.max(1, Math.min(action.limit | 0, 200))\r\n const fmt = '%H%x09%h%x09%an%x09%ar%x09%D%x09%s'\r\n return `${cd} && git log --pretty=format:'${fmt}' -n ${limit} 2>/dev/null || true`\r\n }\r\n default: {\r\n const _exhaustive: never = action\r\n throw new Error(\r\n `Unsupported git action: ${(_exhaustive as GitAction).kind}`\r\n )\r\n }\r\n }\r\n}\r\n","import { exec } from 'child_process'\n\nimport type { RunningAgentKind } from '@arva/shared/types'\nimport { createLogger } from '../lib/logger'\n\nconst log = createLogger('agent-detector')\n\n/**\n * [Cockpit] PTY 의 자식 프로세스 트리에서 실행 중인 AI CLI 를 탐지한다.\n *\n * 왜 데스크탑에서만 가능한가: claude TUI 가 PTY stdin 을 점유하면 모바일이 보내는\n * 어떤 명령도 shell 이 아니라 claude 입력칸에 타이핑된다 — 모바일 측 bash 탐지는\n * 구조적으로 불가능하다. 데스크탑은 PTY 를 직접 소유(`proc.pid`)하므로 OS 프로세스\n * 트리를 조회해 신뢰성 있게 판정할 수 있다.\n *\n * 범위: **로컬 PTY 만**. SSH 원격 PTY 는 `pid` 가 원격 호스트의 것이라 본 OS 조회로\n * 닿지 않는다 → 호출자가 local 머신에 대해서만 호출하고, 그 외는 'none' 으로 둔다.\n *\n * @param pid 로컬 PTY 마스터 프로세스 PID (node-pty `proc.pid`).\n * @returns 자식 트리에서 발견된 첫 agent 종류. 없으면 'none'.\n */\nexport async function detectRunningAgent(\n pid: number\n): Promise<RunningAgentKind> {\n if (!Number.isInteger(pid) || pid <= 0) return 'none'\n try {\n const names =\n process.platform === 'win32'\n ? await listDescendantNamesWindows(pid)\n : await listDescendantNamesUnix(pid)\n const detected = classifyAgent(names)\n log.info(\n `[cockpit] detect pid=${pid} descendants=${names.length} -> ${detected}`\n )\n return detected\n } catch (err) {\n log.warn(\n `[cockpit] detect failed pid=${pid}: ${(err as Error).message}`\n )\n return 'none'\n }\n}\n\n/**\n * 프로세스 이름 목록에서 알려진 AI CLI 를 식별한다.\n *\n * 우선순위 claude > codex > gemini (현재는 claude 만 의미). 이름 매칭은 대소문자\n * 무시 + `.exe`(Windows) / 경로 prefix 를 견디도록 substring 검사.\n */\nexport function classifyAgent(processNames: string[]): RunningAgentKind {\n const joined = processNames.join('\\n').toLowerCase()\n // 'claude' 단어 경계 — node 가 'claude' 스크립트를 실행하면 보통 명령줄에 'claude'\n // 가 그대로 노출된다. cmd 까지 보는 Unix 경로(-o args)에서 정확도가 높다.\n if (/\\bclaude\\b/.test(joined) || joined.includes('claude')) return 'claude'\n if (joined.includes('codex')) return 'codex'\n if (joined.includes('gemini')) return 'gemini'\n return 'none'\n}\n\n/**\n * Unix: `ps` 로 전체 프로세스(ppid + cmd)를 받아 pid 의 자손 트리 명령줄을 모은다.\n *\n * `-o args` 까지 보는 이유: claude 는 node 로 실행될 때 `node .../claude` 형태라\n * comm(=node) 만으로는 안 잡힌다. 전체 명령줄을 봐야 'claude' 가 보인다.\n */\nasync function listDescendantNamesUnix(pid: number): Promise<string[]> {\n const out = await execText('ps -eo pid=,ppid=,args=')\n const rows: { pid: number; ppid: number; cmd: string }[] = []\n for (const line of out.split('\\n')) {\n const m = line.match(/^\\s*(\\d+)\\s+(\\d+)\\s+(.*)$/)\n if (!m) continue\n rows.push({ pid: Number(m[1]), ppid: Number(m[2]), cmd: m[3] })\n }\n return collectDescendantCmds(pid, rows)\n}\n\n/**\n * Windows: WMIC 로 (ProcessId, ParentProcessId, CommandLine) 를 받아 자손 트리를 모은다.\n *\n * WMIC 는 deprecated 지만 모든 Win10/11 에 존재. CommandLine 까지 받아 `node ... claude`\n * 패턴을 잡는다. CSV 파싱은 CommandLine 에 콤마가 섞이므로 컬럼 위치 기반으로 처리.\n */\nasync function listDescendantNamesWindows(pid: number): Promise<string[]> {\n const out = await execText(\n 'wmic process get ProcessId,ParentProcessId,CommandLine /format:csv'\n )\n const rows: { pid: number; ppid: number; cmd: string }[] = []\n const lines = out\n .split('\\n')\n .map(l => l.trim())\n .filter(Boolean)\n // CSV 헤더: Node,CommandLine,ParentProcessId,ProcessId\n for (const line of lines) {\n if (line.startsWith('Node,')) continue\n // 뒤에서부터 안전: 마지막 2필드가 PPID,PID (숫자), 그 앞 전부가 CommandLine.\n const parts = line.split(',')\n if (parts.length < 4) continue\n const procId = Number(parts[parts.length - 1])\n const parentId = Number(parts[parts.length - 2])\n const cmd = parts.slice(1, parts.length - 2).join(',')\n if (!Number.isInteger(procId)) continue\n // 진단: root pid 의 직접 자식이 0개일 때(ConPTY 헬퍼로 체인 끊김 의심) 원인 파악용.\n // claude 띄운 상태 재테스트 시 이 로그로 실제 트리 구조를 확인한다.\n if (parentId === pid || procId === pid) {\n log.info(\n `[cockpit] detect-trace pid=${pid} matched row: proc=${procId} parent=${parentId} cmd=\"${cmd.slice(0, 80)}\"`\n )\n }\n rows.push({ pid: procId, ppid: parentId, cmd })\n }\n return collectDescendantCmds(pid, rows)\n}\n\n/**\n * `rootPid` 의 모든 자손 프로세스 명령줄을 BFS 로 수집한다. root 자신은 shell 이라\n * 제외하고 자손만 본다 (shell 의 cmd 에 'claude' 가 우연히 없도록).\n *\n * 순환 ppid (이론상 OS 가 만들 일은 없지만 방어) 는 `seen` set 으로 차단.\n * export 이유: 단위 테스트가 OS 호출 없이 트리 수집 로직만 검증하기 위함.\n */\nexport function collectDescendantCmds(\n rootPid: number,\n rows: { pid: number; ppid: number; cmd: string }[]\n): string[] {\n const childrenByParent = new Map<number, typeof rows>()\n for (const r of rows) {\n const arr = childrenByParent.get(r.ppid) ?? []\n arr.push(r)\n childrenByParent.set(r.ppid, arr)\n }\n const cmds: string[] = []\n const queue = [...(childrenByParent.get(rootPid) ?? [])]\n const seen = new Set<number>()\n for (let i = 0; i < queue.length; i++) {\n const node = queue[i]\n if (seen.has(node.pid)) continue\n seen.add(node.pid)\n cmds.push(node.cmd)\n const kids = childrenByParent.get(node.pid)\n if (kids) queue.push(...kids)\n }\n return cmds\n}\n\n/** `exec` 를 Promise<string>(stdout) 로 감싼다. 1.5s timeout — 폴링이라 빨리 포기. */\nfunction execText(command: string): Promise<string> {\n return new Promise((resolve, reject) => {\n exec(\n command,\n { timeout: 1500, maxBuffer: 4 * 1024 * 1024 },\n (err, stdout) => {\n if (err) reject(err)\n else resolve(stdout)\n }\n )\n })\n}\n","import fs from 'fs'\nimport os from 'os'\nimport path from 'path'\n\nimport { createLogger } from '../lib/logger'\n\nimport type { TranscriptMessage } from '@arva/shared/types'\n\nconst log = createLogger('agent-sessions')\n\n/** transcript 메시지 1건당 텍스트 길이 상한 (초과분은 잘라 `…` 부착) — 거대 붙여넣기 방어. */\nconst TRANSCRIPT_TEXT_CAP = 4000\n\n/**\n * [Cockpit] claude 세션 1건의 메타데이터 — 모바일 \"세션 선택/이어하기\" UI 데이터.\n */\nexport interface AgentSessionMeta {\n /** session_id (= jsonl 파일명, `--resume` 인자). */\n id: string\n /** 마지막 수정 시각 (epoch ms) — 정렬 기준. 가장 큰 값 = 현재/직전 세션. */\n mtime: number\n /** 첫 user 메시지 일부 (목록 제목용, 최대 80자). 없으면 빈 문자열. */\n preview: string\n}\n\n/**\n * [Cockpit] cwd 의 claude 세션 목록을 조회한다.\n *\n * claude 는 세션을 `~/.claude/projects/<cwd-슬러그>/<session-id>.jsonl` 로 저장한다.\n * 슬러그 = cwd 의 `/`,`\\`,`:` 를 모두 `-` 로 치환한 문자열\n * (예: `C:\\Users\\me\\proj` → `C--Users-me-proj`).\n *\n * 반환은 mtime 내림차순 — [0] 이 가장 최근(=현재 터미널에서 돌던 세션일 가능성).\n * 디렉토리 부재/읽기 실패는 빈 배열 (세션 없음과 동일 취급, fail-safe).\n *\n * @param cwd 절대 경로 작업 디렉토리.\n * @param limit 최대 반환 개수 (기본 20). 오래된 세션은 잘라낸다.\n */\nexport function listAgentSessions(cwd: string, limit = 20): AgentSessionMeta[] {\n try {\n const root = claudeProjectsRoot()\n const slug = slugifyCwd(cwd)\n const dir = path.join(root, slug)\n log.info(`[cockpit] listAgentSessions cwd=${cwd} slug=${slug}`)\n if (!fs.existsSync(dir)) {\n log.info(`[cockpit] sessions dir not found: ${dir}`)\n // 디버그: 슬러그 불일치 시 실제 존재하는 디렉토리 후보를 찍어 원인 추적을 돕는다.\n try {\n const siblings = fs.existsSync(root) ? fs.readdirSync(root) : []\n const hint = siblings.filter(d => {\n const a = d.toLowerCase().replace(/-/g, '')\n const b = slug.toLowerCase().replace(/-/g, '')\n return a.includes(b.slice(-12)) || b.includes(a.slice(-12))\n })\n log.info(\n `[cockpit] projects root has ${siblings.length} dirs; near-matches=${JSON.stringify(hint)}`\n )\n } catch {\n // ignore (디버그 로그 실패는 무시)\n }\n return []\n }\n const entries = fs\n .readdirSync(dir)\n .filter(f => f.endsWith('.jsonl'))\n .map(f => {\n const full = path.join(dir, f)\n const stat = fs.statSync(full)\n return {\n id: f.replace(/\\.jsonl$/, ''),\n mtime: stat.mtimeMs,\n full,\n }\n })\n .sort((a, b) => b.mtime - a.mtime)\n .slice(0, limit)\n\n return entries.map(e => ({\n id: e.id,\n mtime: Math.floor(e.mtime),\n preview: firstUserMessage(e.full),\n }))\n } catch (err) {\n log.warn(`[cockpit] listAgentSessions failed cwd=${cwd}: ${(err as Error).message}`)\n return []\n }\n}\n\n/**\n * [Cockpit 신선도] 디스크 세션 목록에, 데스크탑이 아는 running 세션을 합류시킨다.\n *\n * 방금 콕핏으로 연 장수명 세션은 첫 턴까지 `.jsonl` 을 flush 하지 않아 [listAgentSessions]\n * (디스크 스캔)에서 누락된다. 데스크탑은 그 sessionId 를 in-memory 로 이미 알고 있으므로\n * (AgentRunner.sessions Map), 디스크에 **없는** running id 만 `{ id, mtime: now, preview: '' }`\n * 로 합류시켜 picker 가 \"현재 세션\"을 즉시 보이게 한다.\n *\n * 디스크에 **이미 있는** running id 는 디스크 항목(실제 mtime/preview 보유)을 그대로 둔다 —\n * now 로 덮어쓰면 미리보기를 잃는다. 결과는 id 로 dedupe, mtime 내림차순 정렬(running 항목은\n * now 라 보통 최상단). `agent.sessions` 스키마는 불변이라 모바일 무영향 — 목록만 더 채워진다.\n *\n * @param disk [listAgentSessions] 결과(디스크 스캔, 이미 mtime 내림차순).\n * @param runningIds 데스크탑이 보유한 running sessionId 목록(AgentRunner.getActiveSessionIds).\n * @param now running-only 항목에 부여할 mtime(= Date.now(), 최상단 정렬용).\n */\nexport function mergeRunningSessions(\n disk: AgentSessionMeta[],\n runningIds: string[],\n now: number\n): AgentSessionMeta[] {\n const seen = new Set(disk.map(s => s.id))\n const merged = [...disk]\n for (const id of runningIds) {\n if (seen.has(id)) continue // 디스크 항목(실제 mtime/preview)을 우선 — 덮어쓰지 않음\n seen.add(id) // runningIds 내부 중복 방어\n merged.push({ id, mtime: now, preview: '' })\n }\n return merged.sort((a, b) => b.mtime - a.mtime)\n}\n\n/**\n * [Cockpit 신선도] 비어 있는 세션 목록을 flush 타이밍 창 동안 **bounded 재스캔**한다.\n *\n * 방금 시작한 claude 세션은 `.jsonl` 을 디스크에 flush 하기 전이라 [listAgentSessions] 가\n * 0건을 낼 수 있다(`agent.sessions.list` 는 1회성 스냅샷이라 그 뒤 갱신 안 됨). `delaysMs`\n * 시점마다 `rescan()` 을 다시 호출해, **처음으로 비어있지 않은 결과**가 나오면 `onSurfaced`\n * 로 알리고 **남은 재스캔을 취소**한다(조기중단). 모두 비면 조용히 종료한다 — 빈 목록 재방출은\n * 무의미하므로 추가 broadcast 를 하지 않는다.\n *\n * 본체(`broadcastAgentSessions`)가 아니라 여기로 분리한 이유: 타이머 스케줄/취소/조기중단\n * 불변식을 OS·파일시스템·Electron 의존 없이 fake timer 로 단위 검증하기 위함(SSHManager 는\n * 생성자가 디스크를 건드려 직접 인스턴스화가 비싸다).\n *\n * @param opts.delaysMs 재스캔 지연(ms) 목록. 길이 = 최대 재스캔 횟수.\n * @param opts.rescan 재스캔 함수(= `listAgentSessions(cwd)`).\n * @param opts.onSurfaced 비어있지 않은 결과가 처음 나왔을 때 1회 호출(= 재방출).\n * @returns 취소 함수. 새 요청/disconnect 시 호출해 남은 타이머를 모두 해제한다.\n */\nexport function scheduleSessionRescan(opts: {\n delaysMs: number[]\n rescan: () => AgentSessionMeta[]\n onSurfaced: (sessions: AgentSessionMeta[]) => void\n}): () => void {\n const handles: Array<ReturnType<typeof setTimeout>> = []\n const cancel = (): void => {\n for (const h of handles) clearTimeout(h)\n handles.length = 0\n }\n for (const delay of opts.delaysMs) {\n handles.push(\n setTimeout(() => {\n const sessions = opts.rescan()\n if (sessions.length > 0) {\n opts.onSurfaced(sessions)\n cancel() // surface 됨 — 남은 재스캔 불필요.\n }\n }, delay)\n )\n }\n return cancel\n}\n\n/**\n * [Cockpit] 한 세션의 이전 대화(transcript)를 읽어 user/assistant **텍스트만** 추린다.\n *\n * claude `-p --resume <id>` 출력에는 새 턴만 나오고 과거 대화는 재생되지 않으므로,\n * 모바일이 \"세션 이어하기\" 시 화면을 채우려면 디스크의 `.jsonl` 을 직접 읽어야 한다\n * (claude 자신이 읽는 그 파일). tool_use/tool_result/thinking/attachment 등 노이즈는\n * 모두 제거하고 prose 텍스트만 남긴다.\n *\n * 반환은 **시간순(오래된 것 → 최신)**. 텍스트 메시지가 [limit] 을 넘으면 **최근분만**\n * 담는다(앞부분 잘림 — 이어하기 맥락엔 최근이 더 중요). 파일 부재/파싱 실패는 빈 배열.\n *\n * @param cwd 절대 경로 작업 디렉토리 (슬러그 변환 후 파일 경로 구성).\n * @param sessionId 세션 UUID (= jsonl 파일명).\n * @param limit 최대 반환 메시지 수 (기본 40).\n */\nexport function readSessionTranscript(\n cwd: string,\n sessionId: string,\n limit = 40\n): TranscriptMessage[] {\n try {\n const file = path.join(claudeProjectsRoot(), slugifyCwd(cwd), `${sessionId}.jsonl`)\n if (!fs.existsSync(file)) {\n log.info(`[cockpit] transcript file not found: ${file}`)\n return []\n }\n const raw = fs.readFileSync(file, 'utf-8')\n const out: TranscriptMessage[] = []\n for (const line of raw.split(/\\r?\\n/)) {\n const trimmed = line.trim()\n if (!trimmed) continue\n let obj: Record<string, unknown>\n try {\n obj = JSON.parse(trimmed)\n } catch {\n continue\n }\n const role: 'user' | 'assistant' | null =\n obj.type === 'user' ? 'user' : obj.type === 'assistant' ? 'assistant' : null\n if (!role) continue\n const message = obj.message as { content?: unknown } | undefined\n let text = extractContentText(message?.content).trim()\n if (!text) continue // tool_use/tool_result/thinking 만 있는 턴은 스킵\n if (text.length > TRANSCRIPT_TEXT_CAP) {\n text = `${text.slice(0, TRANSCRIPT_TEXT_CAP)}…`\n }\n const tsRaw = obj.timestamp\n const ts = typeof tsRaw === 'string' ? Date.parse(tsRaw) || 0 : 0\n out.push({ role, text, ts })\n }\n const sliced = out.length > limit ? out.slice(out.length - limit) : out\n log.info(\n `[cockpit] readSessionTranscript session=${sessionId} parsed=${out.length} returned=${sliced.length}`\n )\n return sliced\n } catch (err) {\n log.warn(\n `[cockpit] readSessionTranscript failed session=${sessionId}: ${(err as Error).message}`\n )\n return []\n }\n}\n\n/**\n * [Push-Notif Phase 1] transcript 파일을 **절대 경로로 직접** 읽어 tail 컨텍스트를 추출한다.\n *\n * Claude 훅이 `transcript_path` 를 절대 경로로 주므로 슬러그 재구성이 불필요하다 — 이는\n * 단순 단순화가 아니라 **필수**다. `.ccs` 인스턴스/커스텀 `CLAUDE_CONFIG_DIR` 환경에서는\n * projects 루트가 `~/.claude/projects` 가 아니라 `~/.ccs/instances/<i>/projects` 라\n * [slugifyCwd]+[claudeProjectsRoot] 조합(= [readSessionTranscript])이 빈 결과를 낸다\n * (Phase 0 실측). 본 함수는 그 루트 의존을 우회한다.\n *\n * 파일을 끝에서부터 훑어 (1) 마지막 assistant 텍스트, (2) 마지막 `tool_use`(name + 입력\n * 미리보기)를 찾는다 — 후자는 permission_prompt 알림의 \"무엇을 허용?\" 본문이 된다.\n * 파일 부재/파싱 실패는 `{ lastAssistantText: '' }` (fail-safe).\n *\n * @param transcriptPath 훅 stdin 의 `transcript_path` (절대 경로).\n */\nexport function readTranscriptTailByPath(transcriptPath: string): {\n lastAssistantText: string\n lastToolUse?: { name: string; inputPreview: string }\n} {\n try {\n if (!transcriptPath || !fs.existsSync(transcriptPath)) {\n return { lastAssistantText: '' }\n }\n const lines = fs\n .readFileSync(transcriptPath, 'utf-8')\n .split(/\\r?\\n/)\n .filter(l => l.trim())\n let lastAssistantText = ''\n let lastToolUse: { name: string; inputPreview: string } | undefined\n for (let i = lines.length - 1; i >= 0; i--) {\n let obj: Record<string, unknown>\n try {\n obj = JSON.parse(lines[i])\n } catch {\n continue\n }\n if (obj.type !== 'assistant') continue\n const content = (obj.message as { content?: unknown } | undefined)?.content\n if (!lastToolUse && Array.isArray(content)) {\n const tu = content.find(\n c =>\n c &&\n typeof c === 'object' &&\n (c as { type?: unknown }).type === 'tool_use'\n ) as { name?: unknown; input?: unknown } | undefined\n if (tu) {\n lastToolUse = {\n name: typeof tu.name === 'string' ? tu.name : 'tool',\n inputPreview: previewToolInput(tu.input),\n }\n }\n }\n if (!lastAssistantText) {\n const t = extractContentText(content).trim()\n if (t) lastAssistantText = t\n }\n if (lastAssistantText && lastToolUse) break\n }\n return { lastAssistantText, lastToolUse }\n } catch {\n return { lastAssistantText: '' }\n }\n}\n\n/**\n * `tool_use.input` 에서 사람이 읽을 짧은 미리보기를 뽑는다 (최대 200자).\n *\n * Bash 의 `command`, 파일 도구의 `file_path`/`path`, 검색의 `pattern`/`url` 같은 흔한\n * 1차 키를 우선 노출하고, 없으면 JSON 직렬화로 fallback. risk 분류/알림 본문 재료다.\n */\nfunction previewToolInput(input: unknown): string {\n if (!input || typeof input !== 'object') return ''\n const o = input as Record<string, unknown>\n const primary =\n o.command ?? o.file_path ?? o.path ?? o.pattern ?? o.url ?? o.description\n const s = typeof primary === 'string' ? primary : JSON.stringify(input)\n return s.length > 200 ? `${s.slice(0, 200)}…` : s\n}\n\n/**\n * jsonl 메시지의 `content`(문자열 또는 블록 배열)에서 **text 블록만** 이어붙인다.\n *\n * 배열인 경우 `{type:'text', text}` 블록만 취하고 tool_use/tool_result/thinking 은 버린다.\n * 문자열이면 그대로 반환. 그 외(undefined 등)는 빈 문자열.\n */\nfunction extractContentText(content: unknown): string {\n if (typeof content === 'string') return content\n if (!Array.isArray(content)) return ''\n return content\n .map(c => {\n if (c && typeof c === 'object' && (c as { type?: unknown }).type === 'text') {\n const text = (c as { text?: unknown }).text\n return typeof text === 'string' ? text : ''\n }\n return ''\n })\n .filter(Boolean)\n .join('\\n')\n}\n\n/**\n * `~/.claude/projects` 루트 — **크로스 플랫폼**.\n *\n * `os.homedir()` + `path.join` 으로 OS 별 정확히 해석:\n * Windows = `C:\\Users\\<u>\\.claude\\projects`, macOS = `/Users/<u>/.claude/projects`,\n * Linux = `/home/<u>/.claude/projects`. 홈 경로/구분자 하드코딩 금지 (CLAUDE.md 규칙).\n */\nfunction claudeProjectsRoot(): string {\n return path.join(os.homedir(), '.claude', 'projects')\n}\n\n/**\n * cwd → claude 프로젝트 디렉토리 슬러그. **알파벳/숫자가 아닌 모든 문자 → `-`**.\n *\n * claude 본체는 `/`,`\\`,`:` 뿐 아니라 **`_`(언더스코어)·`.`·공백 등 비영숫자 문자를\n * 모두 `-`로 치환**한다. 예: `C:\\...\\claude_code_mobile` →\n * `C--Users-...-claude-code-mobile` (`_` 도 `-`). 처음엔 `[/\\\\:]` 만 치환해서\n * `claude_code_mobile` 의 언더스코어를 놓쳐 디렉토리를 못 찾는 버그가 있었음\n * (2026-06-01 Windows 실기기 로그로 확인 후 `[^a-zA-Z0-9]` 로 수정).\n *\n * **크로스 플랫폼 주의**: macOS 는 `/Users/me/my_proj` → `-Users-me-my-proj` 가 된다\n * (선행 `/` 도 `-`). claude 버전에 따라 규칙이 바뀔 수 있으므로 불일치 시 세션 목록이\n * 빈 배열로 나올 뿐 — 크래시는 없음(fail-safe). listAgentSessions 가 dir 부재 시\n * projects 루트의 near-match 디렉토리를 로그로 남기므로 슬러그 회귀를 즉시 추적 가능.\n */\nexport function slugifyCwd(cwd: string): string {\n return cwd.replace(/[^a-zA-Z0-9]/g, '-')\n}\n\n/**\n * jsonl 의 첫 `type:\"user\"` 메시지 content 를 미리보기 문자열로 추출 (최대 80자).\n *\n * 큰 파일 전체를 읽지 않도록 앞부분만(최대 64KB) 읽어 줄 단위 파싱한다 — 첫 user\n * 메시지는 보통 파일 맨 앞에 있다. 파싱 실패 / user 메시지 부재는 빈 문자열.\n */\nfunction firstUserMessage(filePath: string): string {\n let fd: number | null = null\n try {\n fd = fs.openSync(filePath, 'r')\n const buf = Buffer.alloc(64 * 1024)\n const bytes = fs.readSync(fd, buf, 0, buf.length, 0)\n const head = buf.toString('utf-8', 0, bytes)\n for (const line of head.split('\\n')) {\n const trimmed = line.trim()\n if (!trimmed) continue\n let obj: Record<string, unknown>\n try {\n obj = JSON.parse(trimmed)\n } catch {\n continue\n }\n if (obj.type !== 'user') continue\n const message = obj.message as { content?: unknown } | undefined\n const content = message?.content\n const text =\n typeof content === 'string'\n ? content\n : Array.isArray(content)\n ? content\n .map(c =>\n c && typeof c === 'object' && 'text' in c\n ? String((c as { text: unknown }).text)\n : ''\n )\n .join(' ')\n : ''\n const oneLine = text.replace(/\\s+/g, ' ').trim()\n if (oneLine) return oneLine.slice(0, 80)\n }\n return ''\n } catch {\n return ''\n } finally {\n if (fd !== null) {\n try {\n fs.closeSync(fd)\n } catch {\n // ignore\n }\n }\n }\n}\n","/**\r\n *\r\n * @param {number} ms\r\n * @description Wait for a given number of milliseconds.\r\n * @example\r\n * await waitFor(1000) // Waits for 1 second\r\n */\r\nexport function waitFor(ms: number) {\r\n return new Promise(resolve => setTimeout(resolve, ms))\r\n}\r\n\r\n/**\r\n * 작업 디렉토리 경로를 **비교용**으로 정규화한다.\r\n *\r\n * 같은 디렉토리가 소스마다 다른 표기로 들어온다 — OSC7 `file://` 디코드는 `/C:/Users/...`,\r\n * 머신 `localCwd` 는 사용자 입력 그대로, Claude 훅의 `cwd` 는 `C:\\Users\\...`(백슬래시).\r\n * 직접 `===` 비교는 빗나가므로: 백슬래시→슬래시, 드라이브 앞 선행 슬래시 제거(`/C:/`→`C:/`),\r\n * 말미 슬래시 제거, Windows 는 대소문자 무시(경로가 case-insensitive)로 통일한다.\r\n *\r\n * Push-Notif Phase 1 의 cwd→machineId 역매핑(ssh-manager.findLocalMachineIdByCwd)에서 사용.\r\n */\r\nexport function normalizeCwd(cwd: string): string {\r\n let p = cwd.trim().replace(/\\\\/g, '/')\r\n p = p.replace(/^\\/([a-zA-Z]:)/, '$1') // /C:/... → C:/... (OSC7 file:// 디코드 형태)\r\n p = p.replace(/\\/+$/, '') // 말미 슬래시 제거\r\n if (process.platform === 'win32') p = p.toLowerCase()\r\n return p\r\n}\r\n","/**\r\n * NoneSessionManager — multiplexer 미사용. 현 직접 spawn 동작과 동일.\r\n *\r\n * Phase 0 (docs/plans/0001-terminal-multiplexer-integration.md) 의 안전망. 본 파일 머지\r\n * 후에도 production 동작이 정확히 같아야 함 — ssh-manager.connectLocal 의 기존 cmd.exe /\r\n * zsh 직접 spawn 을 그대로 호출하도록 SpawnSpec 을 만든다.\r\n *\r\n * 본 매니저는 multiplexer wrapping 없음:\r\n * - attachOrCreate: hint 그대로 SpawnSpec 으로 echo. session 영속 X.\r\n * - detach: no-op (어차피 영속화 안 됨)\r\n * - kill: no-op (caller 가 ssh-manager.disconnect 로 PTY 종료)\r\n * - resize: ssh-manager 가 자체적으로 setWindow 호출 — 본 함수도 no-op\r\n * - list: 항상 빈 배열\r\n * - probe: { healthy: true, version: null } — 'none' 은 항상 가용\r\n */\r\n\r\nimport type {\r\n ProbeResult,\r\n ResizeSource,\r\n SessionDescriptor,\r\n SessionManager,\r\n SpawnHint,\r\n SpawnSpec,\r\n} from './types'\r\n\r\nexport class NoneSessionManager implements SessionManager {\r\n readonly kind = 'none' as const\r\n readonly version = null\r\n\r\n // eslint-disable-next-line @typescript-eslint/no-unused-vars -- machineId 는 본 매니저에 의미 없음\r\n async attachOrCreate(_machineId: string, hint: SpawnHint): Promise<SpawnSpec> {\r\n // hint 그대로 SpawnSpec 으로 변환 — multiplexer wrapping 없음.\r\n // ssh-manager 의 connectLocal / connectSSH 가 했던 일을 그대로 재현.\r\n return {\r\n shell: hint.shell,\r\n args: hint.args ?? [],\r\n cwd: hint.cwd,\r\n env: hint.env,\r\n cols: hint.cols,\r\n rows: hint.rows,\r\n }\r\n }\r\n\r\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\r\n async detach(_machineId: string): Promise<void> {\r\n // none 모드 — 영속화 없으므로 detach 도 no-op.\r\n }\r\n\r\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\r\n async kill(_machineId: string): Promise<void> {\r\n // none 모드 — caller (ssh-manager.disconnect) 가 PTY 종료 책임.\r\n }\r\n\r\n async list(): Promise<SessionDescriptor[]> {\r\n return []\r\n }\r\n\r\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\r\n async resize(_machineId: string, _cols: number, _rows: number, _source: ResizeSource): Promise<void> {\r\n // ssh-manager.resize 가 PTY layer 직접 setWindow — 본 매니저는 no-op.\r\n }\r\n\r\n async probe(): Promise<ProbeResult> {\r\n return { healthy: true, version: null }\r\n }\r\n}\r\n","/**\n * Multiplexer factory.\n *\n * 멀티플렉서(psmux/tmux) 동봉·설정은 2026-06-23 제거됨\n * (docs/requests/2026-06/20260623-112930-*, 후속 dead-code sweep 포함). 현재 유일한 구현은\n * {@link NoneSessionManager}(직접 spawn)이며 createSessionManager 는 항상 그것을 반환한다.\n * resolver/psmux/tmux 구현·테스트는 함께 삭제됨 — 되살리려면 git 이력에서 복원.\n */\n\nimport type { MultiplexerSetting } from '@arva/shared/types'\n\nimport { NoneSessionManager } from './none'\nimport type { SessionManager } from './types'\n\n// re-exports — 외부 모듈(ssh-manager)이 본 barrel 만 import 하면 충분.\nexport type {\n MultiplexerKind,\n ProbeResult,\n ResizeSource,\n SessionDescriptor,\n SessionManager,\n SpawnHint,\n SpawnSpec,\n} from './types'\nexport { NoneSessionManager } from './none'\n\n/**\n * createSessionManager 옵션 — 모두 선택적이며 현재 구현(None)은 무시한다(호출부 시그니처 호환용).\n */\nexport interface CreateSessionManagerOptions {\n /** production resources 경로 override. 현재 무시됨. */\n resourcesRootOverride?: string\n /** sessionName prefix 일부. 현재 무시됨. */\n agentInstanceId?: string\n /** multiplexer mouse 옵션. 현재 무시됨. */\n mouse?: boolean\n}\n\n/**\n * SessionManager 를 만든다 — 항상 {@link NoneSessionManager}(직접 spawn).\n *\n * 멀티플렉서 동봉/설정 제거(2026-06-23)로 `setting`·`_opts` 와 무관하게 None 을 반환한다.\n * 과거 저장된 머신의 `multiplexer:'bundled'` 등도 throw 없이 안전하게 None 으로 동작한다.\n * 시그니처는 호출부(ssh-manager) 무변경을 위해 유지.\n */\nexport function createSessionManager(\n setting: MultiplexerSetting | undefined,\n _opts: CreateSessionManagerOptions = {},\n): SessionManager {\n if (setting && setting !== 'none' && setting !== 'auto') {\n console.info(`[multiplexer] setting='${setting}' 무시 — 동봉 제거(2026-06-23)로 항상 None(직접 spawn).`)\n }\n return new NoneSessionManager()\n}\n","import os from 'os'\nimport path from 'path'\nimport fs from 'fs'\nimport { createHmac, randomBytes } from 'crypto'\nimport { WebSocketServer, WebSocket } from 'ws'\nimport type { IncomingMessage } from 'http'\n\nimport type {\n BroadcastFn,\n BroadcastRecipient,\n ClientMessage,\n ServerMessage,\n ConnectionDetails,\n RelayStatus,\n} from '@arva/shared/types'\nimport { type SSHManager, PTY_MIN_ROWS } from './ssh-manager'\nimport { toPairUrl } from './pair-url'\nimport { listAgentSessions, readSessionTranscript } from './agent-sessions'\nimport { AgentRunner } from './agent-runner'\nimport { FileUploadReceiver } from './file-upload-receiver'\nimport { FcmTokenStore } from './fcm-token-store'\nimport { REPLAY_ATTACH_TAIL_BYTES, REPLAY_CHUNK_BYTES } from '@arva/shared/constants'\nimport {\n OPCODE,\n PROTOCOL_ERROR_REASON,\n FrameParseError,\n parseFrame,\n encodeAppJson,\n encodeFrameSnapshot,\n encodeHelloAck,\n encodeProtocolError,\n} from '../protocol/binary-protocol'\nimport type { AgentConfigStore, AgentIdentity } from '../config/agent-config'\nimport { createLogger } from '../lib/logger'\n\n// Per-component scoped logger — writes to logs/ws.log (separate from main.log).\nconst log = createLogger('ws')\n// [handoff diag 20260607 — C] 모바일이 forward 한 인앱 디버그 로그(`mobile.diag_log`)를\n// 별도 파일 logs/mobile-diag.log 에 적재 — authority.log/ws.log 와 같은 폴더에서 모바일+\n// 데스크탑 로그를 시간순으로 함께 확인(끊김 진단 교차검증). ws.log 오염 방지로 파일 분리.\nconst mobileDiagLog = createLogger('mobile-diag')\n\n/**\n * [attach frame-snapshot 20260610] session_attach 시 GridEmulator 직렬화 ANSI 를\n * FRAME_SNAPSHOT (0x03) 으로 송신하는 기능의 kill-switch. 기본 ON.\n *\n * busy alt-screen TUI(claude 등)는 raw scrollback tail 16KB 로는 화면 복원이 불가해 모바일에\n * \"조각\"만 보이던 버그를 고친다. 회귀/문제 시 `VIEWER_ATTACH_FRAME_SNAPSHOT=false` 로 비활성.\n */\nconst attachFrameSnapshotEnabled = (): boolean =>\n process.env.VIEWER_ATTACH_FRAME_SNAPSHOT !== 'false'\n\n/**\n * [resize-resettle frame-snapshot 20260610] RESIZE 수신 후 FRAME_SNAPSHOT 재방출까지의\n * 디바운스(ms). 모바일이 attach 직후 미측정 크기(80x24)로 붙었다가 ~233ms 뒤 실제 viewport\n * 로 RESIZE 하는 race 에서, 그 사이 직렬화된 스냅샷은 잘못된 행수 + claude repaint 전 reflow\n * 잔해라 \"조각\"으로 보인다. RESIZE 가 멎고 claude 가 SIGWINCH repaint 를 끝낸 뒤 한 번 더\n * 직렬화해 올바른 화면을 재방출한다. 연속 RESIZE(0x0→실제)는 마지막 것 기준으로 coalesce.\n */\nconst RESIZE_SNAPSHOT_DEBOUNCE_MS = 250\n\n/**\n * `mobile.diag_log` 의 untrusted 모바일 필드를 로그 파일에 쓰기 전 정화한다.\n *\n * WHY (review 20260607 #1·#2): relay 클라이언트가 보낸 값을 그대로 기록하면 임베드된 개행으로\n * 프리픽스 없는 **가짜 로그 라인을 위조**(인시던트 진단 오도)하거나 ANSI/제어문자로 `tail -f`\n * 를 깨뜨릴 수 있다. 같은 파일의 `terminal_input` 핸들러와 동일한 제어문자 escape 를 적용하고,\n * flood/멀티-MB 단일 메시지 방지로 길이도 cap 한다(cap 후 escape 라 CPU 도 bound).\n *\n * `value: unknown` — relay JSON 은 타입 미보장이라 string 이 아닐 수 있어 방어적으로 coerce.\n */\nconst sanitizeDiagField = (value: unknown, max: number): string =>\n String(value ?? '')\n .slice(0, max)\n // biome-ignore lint/suspicious/noControlCharactersInRegex: 제어문자 정화가 목적 — 의도적 매칭(terminal_input 핸들러와 동일 패턴)\n .replace(/[\\x00-\\x1f\\x7f]/g, c => `\\\\x${c.charCodeAt(0).toString(16).padStart(2, '0')}`)\n// ── Session token persistence ─────────────────────────────────────────────────\n// 앱 재시작 / 네트워크 순단 후에도 모바일이 동일 Relay URL로 재연결할 수 있도록\n// 토큰을 디스크에 영속화한다.\n\n/**\n * 에이전트 데이터 디렉터리 이름.\n *\n * package.json 의 `name` 이 아닌 고정 식별자로 pin 한다 — 모노리포 전환으로 패키지명이\n * `@arva/desktop` 등으로 바뀌어도(또는 헤드리스 앱이 별도 name 을 가져도) 세션 토큰 경로가\n * 흔들리지 않게 한다. 데스크탑·헤드리스가 같은 규약(`~/.ai_remote_vibe_agent`)을 공유한다.\n */\nconst APP_DATA_DIR_NAME = '.ai_remote_vibe_agent'\nconst DATA_DIR = path.join(os.homedir(), APP_DATA_DIR_NAME)\nconst SESSION_TOKEN_FILE = path.join(DATA_DIR, 'session-token.txt')\n\nfunction loadOrCreateSessionToken(): string {\n try {\n const existing = fs.readFileSync(SESSION_TOKEN_FILE, 'utf-8').trim()\n if (existing.length === 36) return existing\n } catch {\n // 파일 없음 → 새로 생성\n }\n const token = crypto.randomUUID()\n try {\n if (!fs.existsSync(DATA_DIR)) fs.mkdirSync(DATA_DIR, { recursive: true })\n fs.writeFileSync(SESSION_TOKEN_FILE, token, { mode: 0o600 })\n } catch (err) {\n console.error('[ws-manager] session-token 저장 실패:', err)\n }\n return token\n}\n\nconst SESSION_TOKEN = loadOrCreateSessionToken()\n\n// 이 데스크탑 프로세스가 cap.ack 로 'first start' 를 이미 알렸는지 — agent_started 대리 발화를\n// 프로세스당 1회로 제한하는 모듈 전역 가드(WsManager 는 프로세스당 단일 인스턴스). 첫 cap.ack(첫\n// 폰 협상)에만 agent_first_start=true 를 실어, 모바일이 agent_started 를 1회 발화하게 한다.\nlet agentFirstStartSignaled = false\n\n/**\n * cap.ack 응답을 한 곳에서 조립한다 — mobile WS path 와 relay path 가 동일 페이로드를 보내도록 DRY.\n *\n * server_ver/max_cols/max_rows(서버-진실 capability) 외에 **익명 페어링 메타**\n * (agent_install_id/version/os/arch)를 동봉한다. 데스크탑엔 analytics 프레임워크가 없어 GA4\n * 직접 전송이 불가하므로, 모바일이 이 메타로 GA4 'agent_paired' 를 대리 발화해 공급측 활성화\n * (= distinct install_id, GitHub 다운로드 vanity 대체)를 계측한다. PII 0: install_id 는 랜덤\n * UUID(머신/유저 식별 아님)이며 relay 토큰 등 시크릿은 절대 동봉하지 않는다.\n *\n * 부수효과: 프로세스 **첫 호출** 시 `agent_first_start: true` 를 동봉하고 모듈 가드를 세운다 —\n * 모바일이 'agent_started'(데스크탑 launch 가 페어링까지 도달, 프로세스당 1회)를 'agent_paired'\n * (페어링 이벤트, 반복 가능)와 구분해 대리 발화하도록. 이후 호출은 플래그를 생략한다.\n */\nfunction buildCapAck(\n selected: 'pty.data' | 'grid.diff',\n installId: string,\n version: string,\n): ServerMessage {\n const firstStart = !agentFirstStartSignaled\n if (firstStart) agentFirstStartSignaled = true\n return {\n type: 'cap.ack',\n selected,\n server_ver: 3,\n max_cols: 500,\n max_rows: 200,\n agent_install_id: installId,\n agent_version: version,\n agent_os: process.platform,\n agent_arch: process.arch,\n // 프로세스 첫 cap.ack 에만 동봉 — 모바일이 agent_started 1회 대리 발화.\n ...(firstStart ? { agent_first_start: true } : {}),\n }\n}\n\nfunction getLocalIP(): string {\n const interfaces = os.networkInterfaces()\n for (const iface of Object.values(interfaces)) {\n if (!iface) continue\n for (const addr of iface) {\n if (addr.family === 'IPv4' && !addr.internal) {\n return addr.address\n }\n }\n }\n return '127.0.0.1'\n}\n\n/**\n * Phase 1 [Review Q2] — mobile WS 메타데이터. `(ws as any).clientId` 패턴 대신\n * WeakMap<WebSocket, MobileClientMeta> 로 저장. ws GC 시 자동 정리.\n *\n * - `clientId`: HMAC auth 성공 직후 부여 (`mob-<ts>-<rand>`). AuthorityResolver 가\n * handover/detach 시 동일 모바일을 식별하는 키.\n * - `attachedMachineId`: 마지막 `session_attach` 의 machineId. ws.on('close') 에서\n * resolver.onMobileDetach 호출 시 머신 식별.\n */\ninterface MobileClientMeta {\n clientId: string\n attachedMachineId?: string\n}\n\nexport class WsManager {\n private ssh: SSHManager\n private broadcast: BroadcastFn\n /**\n * [Cockpit] `agent.run` 별도 프로세스 실행기. stdout/stderr/exit 을 `agent.output`\n * 으로 broadcast — interactive PTY (`terminal_data`) 와 완전 별개 채널.\n */\n private agentRunner: AgentRunner\n\n /**\n * [file-upload 20260614] 모바일→데스크탑 첨부 업로드 수신기. begin/chunk/end 3-메시지를\n * in-memory 누적 후 머신 cwd 에 기록한다. cwd 는 ssh.resolveLocalCwd 로 해석(머신별 OSC7).\n */\n private fileUpload: FileUploadReceiver\n\n /**\n * [Cockpit Phase C3] takeFromTerminal 진입 시 killPtyAgent 후 ~500ms 지연 open 의 타이머\n * (machineId 별). 지연 중 콕핏을 이탈(close)하면 이 타이머를 취소해 \"닫을 주체 없는\" orphan\n * 세션이 떠버리는 것을 막는다.\n */\n private pendingHandoffOpens = new Map<string, ReturnType<typeof setTimeout>>()\n\n private mobileClients = new Set<WebSocket>()\n /**\n * [A2 spike 20260523] machineId → 그 머신에 attach 된 mobile WS lookup.\n *\n * handleDesktopTakeback 에서 close code 4007 (TAKEN_BY_DESKTOP) 전송 시 사용.\n * session_attach 시 set, ws.on('close') 시 delete. mobileClients (Set) 는 보존 — 다른\n * callsite (broadcastToMobile fan-out) 가 의존.\n */\n private mobileClientByMachine = new Map<string, WebSocket>()\n private clientMeta = new WeakMap<WebSocket, MobileClientMeta>()\n /**\n * A3 capability negotiation (binary protocol v3 §6.5).\n *\n * 각 mobile WS 의 선택된 path. cap.req 받기 전 default = 'pty.data' (legacy 호환).\n * cap.req 받으면 cap.ack 응답 + 본 Map 갱신. 한 세션 내 path 전환 금지 (재연결 시 재협상).\n *\n * cap='grid.diff' client 는 PTY_DATA (0x00) 미수신, GRID_SNAPSHOT (0x04) + GRID_DIFF\n * (0x05) 만 수신. cap='pty.data' 는 기존 path 그대로.\n */\n private mobileClientCaps = new Map<WebSocket, 'pty.data' | 'grid.diff'>()\n /**\n * snapshot.req frequency cap (3/min/session). plan §13-9.\n * sliding window — 최근 60s 안 timestamp 배열. 3개 초과 시 throttle.\n * WeakMap 사용 — ws close 시 자동 GC, 명시 cleanup 불요.\n */\n private snapshotReqTimes = new WeakMap<WebSocket, number[]>()\n private wss: WebSocketServer | null = null\n\n private relayWs: WebSocket | null = null\n private relayQrUrl: string | null = null\n private relayRetryCount = 0\n private relayStatus: RelayStatus = 'disconnected'\n private relayUrlIndex = 0\n private destroyed = false\n\n private wsPort: number = 0\n private heartbeatIntervalMs: number = 0\n private pongTimeoutMs: number = 0\n\n // ── 타이머 (cleanup 시 반드시 해제) ──────────────────────────────────────────\n private heartbeatTimer: NodeJS.Timeout | null = null\n private pongTimer: NodeJS.Timeout | null = null\n\n /**\n * 마지막 session_attach 의 machineId. RESIZE opcode 가 machineId 없이 도착하므로\n * (spec 상 wire 에 안 담김), 이 cache 로 routing. 1 mobile = 1 machine attach 가정.\n * ws 별 cache (`ws.attachedMachineId`) 도 유지하지만 race 또는 reconnect 시 누락 발생 →\n * global fallback 으로 보강. F2 (task 13 v3) — production dogfood 의 RESIZE 누락 회귀.\n */\n private lastAttachedMachineId: string | null = null\n\n /**\n * [resize-resettle frame-snapshot 20260610] 머신별 RESIZE→재스냅샷 디바운스 타이머.\n * 새 RESIZE 가 오면 이전 타이머를 교체(coalesce)해 마지막 settle 시점에만 1회 방출.\n * late-fire 는 `sendBinaryToMachine` 가 미attach 시 no-op 이라 무해(별도 cleanup 불필요).\n */\n private readonly resizeSnapshotTimers = new Map<string, NodeJS.Timeout>()\n\n /** [FCM Phase 3] 이 데스크탑에 등록된 폰 FCM 토큰 — device.fcm_register/unregister 로 갱신. */\n private readonly fcmTokenStore: FcmTokenStore\n\n /**\n * @param config 런타임 설정 + FCM 토큰맵 영속 — 데스크탑은 electron-store, 헤드리스는 JSON.\n * 코어가 호스트 저장소를 직접 import 하지 않게 하는 DI 경계(Phase A3).\n * @param identity 호스트 앱 식별(현재 `version` — cap.ack `agent_version`).\n */\n constructor(\n ssh: SSHManager,\n broadcast: BroadcastFn,\n private readonly config: AgentConfigStore,\n private readonly identity: AgentIdentity,\n ) {\n this.ssh = ssh\n this.fcmTokenStore = new FcmTokenStore(config)\n // A3 (Sub-Part 1F-ii-b): GRID_DIFF binary broadcast 콜백 inject.\n // ssh-manager 가 PTY data flush 시점 (16ms 디바운스 후) 마다 emulator dirty cell 을\n // GRID_DIFF (0x05) 로 직렬화 → 본 콜백 호출. cap=grid.diff client filter 는 우리 책임.\n ssh.setBinaryBroadcast((machineId, frame) =>\n this.broadcastGridFrame(machineId, frame)\n )\n this.broadcast = broadcast\n // [Cockpit] agent.run 출력 → agent.output broadcast. mobile 이 requestId 로 필터.\n this.agentRunner = new AgentRunner(\n (machineId, requestId, stream, payload) => {\n // 디버깅 trace — stdout 은 byte 수만(시끄러움 방지), stderr/exit 은 내용 포함.\n if (stream === 'stdout') {\n log.info(\n `[cockpit] agent.output→mobile machine=${machineId} req=${requestId} stdout bytes=${payload.data?.length ?? 0}`\n )\n } else {\n log.info(\n `[cockpit] agent.output→mobile machine=${machineId} req=${requestId} ${stream}` +\n (stream === 'exit' ? ` code=${payload.exitCode}` : ` \"${payload.data?.trim() ?? ''}\"`)\n )\n }\n const out = {\n type: 'agent.output' as const,\n machineId,\n requestId,\n stream,\n data: payload.data,\n exitCode: payload.exitCode,\n }\n // [Cockpit review-fix] exit 는 렌더러 콕핏도 받아 '응답 중'(activeRequestId) 을 해제해야\n // 한다 — turn_complete 가 없는 비정상 종료(exit≠0/spawn-fail) 대비. → broadcast(renderer\n // +mobile). stdout/stderr 는 기존대로 모바일 전용(렌더러는 cockpit.card 로 표시).\n if (stream === 'exit') {\n this.broadcast(out)\n } else {\n this.broadcastToMobile(out)\n }\n },\n // [Cockpit source of truth] 파싱한 카드/세션은 broadcast(renderer + mobile 양쪽 fan-out)\n // 로 — 데스크탑 콕핏이 원본 렌더, 모바일이 미러. raw agent.output(mobile 전용)과 병행.\n {\n onCard: (machineId, requestId, agent, card) => {\n this.broadcast({\n type: 'cockpit.card',\n machineId,\n requestId,\n ts: Date.now(),\n agent,\n card,\n })\n },\n onSessionId: (machineId, requestId, sessionId) => {\n this.broadcast({\n type: 'cockpit.session_started',\n machineId,\n requestId,\n sessionId,\n })\n },\n }\n )\n\n // [Cockpit 신선도 20260614] running 콕핏 세션 id provider 후주입 — broadcastAgentSessions\n // 가 디스크 flush 전 running 장수명 세션을 picker 목록에 합류시킬 때 읽는다(자족적, index.ts 무변경).\n this.ssh.setActiveSessionProvider(mid => this.agentRunner.getActiveSessionIds(mid))\n\n // [file-upload 20260614] 첨부 업로드 수신기 — cwd 는 머신별 resolveLocalCwd 로 해석.\n this.fileUpload = new FileUploadReceiver({\n resolveCwd: (mid) => this.ssh.resolveLocalCwd(mid),\n })\n\n const wsPort = this.config.getWsPort()\n const heartbeatMs = this.config.getHeartbeatIntervalMs()\n const pongMs = this.config.getPongTimeoutMs()\n const authMs = this.config.getAuthTimeoutMs()\n\n log.info(\n `[ws-manager] config: port=${wsPort} heartbeat=${heartbeatMs} pong=${pongMs} auth=${authMs}`\n )\n\n this.wsPort = wsPort\n this.heartbeatIntervalMs = heartbeatMs\n this.pongTimeoutMs = pongMs\n this.startLocalServer(wsPort, authMs)\n this.connectToRelay()\n }\n\n // ── Mobile broadcast (IPC handles renderer separately) ────────────────────\n\n /**\n * mobile WS fan-out — v3 binary protocol APP_JSON opcode wrap.\n *\n * Phase 1 — `perRecipient` 콜백이 주어지면 receiver 별로 메시지 일부를 customize 후\n * encode. `terminal_authority_changed` 의 `isAuthority` 필드처럼 receiver 마다 다른\n * 값이 필요한 경우 사용. relay 경유 모바일은 clientId 가 미상이라 (relayWs 자체는\n * 단일 channel) `clientId: undefined` 로 콜백 호출 — resolver 가 보수적 false fallback.\n */\n broadcastToMobile(\n msg: ServerMessage,\n perRecipient?: (recipient: BroadcastRecipient) => Partial<ServerMessage>\n ): void {\n for (const ws of this.mobileClients) {\n if (ws.readyState !== WebSocket.OPEN) continue\n if (perRecipient) {\n const meta = this.clientMeta.get(ws)\n const customized = {\n ...msg,\n ...perRecipient({ kind: 'mobile', clientId: meta?.clientId }),\n } as ServerMessage\n ws.send(encodeAppJson(customized))\n } else {\n ws.send(encodeAppJson(msg))\n }\n }\n if (this.relayWs?.readyState === WebSocket.OPEN) {\n if (perRecipient) {\n const meta = this.clientMeta.get(this.relayWs)\n const customized = {\n ...msg,\n ...perRecipient({ kind: 'mobile', clientId: meta?.clientId }),\n } as ServerMessage\n this.relayWs.send(encodeAppJson(customized))\n } else {\n this.relayWs.send(encodeAppJson(msg))\n }\n }\n }\n\n // ── [FCM Phase 3] FCM 토큰맵 + relay 발송 ──────────────────────────────────\n\n /** 등록된 폰 FCM 토큰 목록(push-dispatcher 발송용). */\n getFcmTokens(): string[] {\n return this.fcmTokenStore.tokens()\n }\n\n /** 해당 머신에 폰이 attach 중인지(presence 게이트 — true 면 push 생략). */\n isMobilePresent(machineId: string): boolean {\n return this.mobileClientByMachine.has(machineId)\n }\n\n /** relay 로 ServerMessage 송신(fcm.send 등). relay 미연결 시 no-op. */\n sendToRelay(msg: ServerMessage): void {\n if (this.relayWs?.readyState === WebSocket.OPEN) {\n this.relayWs.send(encodeAppJson(msg))\n }\n }\n\n // ── Renderer-only broadcast (relay 상태 등 내부 정보) ────────────────────────\n\n private broadcastToRenderer(msg: ServerMessage): void {\n if (this.destroyed) return\n this.broadcast(msg)\n }\n\n // ── Relay status helpers ──────────────────────────────────────────────────\n\n private setRelayStatus(status: RelayStatus): void {\n this.relayStatus = status\n log.info(\n `[relay] status=${status} retryCount=${this.relayRetryCount} ts=${new Date().toISOString()}`\n )\n // relay_status는 렌더러에만 전달 (Flutter는 broadcastToMobile을 통해 별도 수신)\n this.broadcastToRenderer({\n type: 'relay_status',\n status,\n retryCount: this.relayRetryCount,\n })\n // Flutter 앱에도 relay_status 전달 (상태 표시용)\n this.broadcastToMobile({\n type: 'relay_status',\n status,\n retryCount: this.relayRetryCount,\n })\n }\n\n // 렌더러 마운트 후 초기 상태 동기화용 — get_machines 응답 시 함께 호출\n broadcastCurrentRelayStatus(): void {\n this.broadcastToRenderer({\n type: 'relay_status',\n status: this.relayStatus,\n retryCount: this.relayRetryCount,\n })\n }\n\n // ── QR / relay info ───────────────────────────────────────────────────────\n\n requestQr(): void {\n const info = this.getMobileQrInfo()\n this.broadcast({ type: 'qr_code', ...info })\n }\n\n /**\n * [A2 spike 20260523] desktop renderer 의 Take Back 처리.\n *\n * 호출 경로: main/index.ts 의 `ipcMain.handle('app:send', ...)` → handleMessage 의\n * `case 'desktop.takeback'`. ClientMessage 의 일부로 도착.\n *\n * 동작:\n * 1. mobileClientByMachine 에서 해당 머신의 mobile WS lookup → close code 4007\n * (TAKEN_BY_DESKTOP) 으로 종료. mobile 측 KickedByDesktopBanner 트리거 (C6).\n * 2. authority.forceDesktopLeader(machineId) → leader='desktop' + 5s cooldown 시작 +\n * viewer_mode='interactive' broadcast → desktop UI 의 placeholder unmount.\n * 3. mobile WS 가 5s 안에 재attach 시도하면 viewer_mode='rejected' + close code 4008\n * (TAKEBACK_COOLDOWN) — Eng Fix-M 의 cooldown bypass race close.\n *\n * leader 가 mobile 이 아닌 경우에도 safe — forceDesktopLeader 가 idempotent (이미 desktop\n * 이면 cooldown 만 갱신). mobile WS 가 없는 경우 (이미 closed) 도 try/catch 로 무해 처리.\n */\n handleDesktopTakeback(machineId: string): void {\n log.info(`[ws-manager] desktop.takeback machine=${machineId}`)\n // [Bug #3 fix 20260526] App-level kick frame — relay 경로 mobile 도 도달.\n // 기존 ws.close(4007) 는 relay 경로에서 agent-relay 연결만 끊고 mobile 에는\n // 도달 안 함. 본 frame 은 broadcast 채널로 forward 되어 mobile 이 자체 close +\n // KickedByDesktopBanner 발화 가능. 신버전 mobile 만 본 frame 처리, 구버전은\n // 아래 ws.close(4007) fallback 으로 처리.\n this.broadcast({\n type: 'desktop.kick',\n machineId,\n reason: 'takeback',\n })\n const ws = this.mobileClientByMachine.get(machineId)\n // Local WS 경로 호환 — relayWs 닫지 말 것 (agent 자체 연결 끊김 + auto-reconnect 부작용).\n if (ws && ws !== this.relayWs) {\n try {\n ws.close(4007, 'TAKEN_BY_DESKTOP')\n } catch (err) {\n log.warn(\n `[ws-manager] handleDesktopTakeback close failed machine=${machineId}`,\n err\n )\n }\n }\n this.ssh.authority.forceDesktopLeader(machineId)\n }\n\n /**\n * [Cockpit] `agent.*` 메시지 공유 dispatch.\n *\n * 렌더러(IPC, `index.ts` handleMessage)와 모바일(WS handleMessage)이 같은 `ClientMessage`\n * 를 보내지만 진입점이 분리돼 있어, agent.* 처리를 한 곳으로 모은다 — 한쪽 switch 에만 case\n * 를 두어 누락되는 사고를 막는다(1b 콕핏 무반응 회귀의 근본 수정). agent.* 들은 `source`\n * (모바일 클라이언트)에 무관하므로 양쪽이 안전하게 공유한다. 결과 `cockpit.card`/출력은\n * broadcast(renderer+mobile)로 양쪽에 동일하게 흐른다.\n *\n * [Cockpit v2 장수명] `agent.session.open/send/close` 는 AgentRunner 의 장수명 메서드로\n * 위임 — one-shot `agent.run` 과 병존(back-compat). 모바일은 미전환 시 본 타입을 안 보낸다.\n */\n handleAgentMessage(\n msg: Extract<\n ClientMessage,\n {\n type:\n | 'agent.run'\n | 'agent.kill'\n | 'agent.sessions.list'\n | 'agent.session.transcript.get'\n | 'agent.session.open'\n | 'agent.session.send'\n | 'agent.session.close'\n | 'agent.session.handoffToTerminal'\n }\n >\n ): void {\n switch (msg.type) {\n case 'agent.run':\n log.info(\n `[ws-manager] agent.run machine=${msg.machineId} requestId=${msg.requestId} argv=${JSON.stringify(msg.argv)}`\n )\n this.agentRunner.run(msg.machineId, msg.requestId, msg.argv, msg.cwd)\n break\n case 'agent.kill':\n log.info(\n `[ws-manager] agent.kill machine=${msg.machineId} requestId=${msg.requestId ?? '(pty)'}`\n )\n if (msg.requestId) {\n // 별도 프로세스 종료.\n this.agentRunner.kill(msg.machineId, msg.requestId)\n } else {\n // requestId 없음 = interactive PTY 의 foreground agent 종료 (\"닫고 새로 열기\").\n this.ssh.killPtyAgent(msg.machineId)\n }\n break\n case 'agent.sessions.list':\n // [Cockpit] cwd 의 claude 세션 목록 스캔 → agent.sessions broadcast.\n log.info(`[ws-manager] agent.sessions.list machine=${msg.machineId}`)\n this.ssh.broadcastAgentSessions(msg.machineId)\n break\n case 'agent.session.transcript.get':\n // [Cockpit] 세션 이전 대화 읽기 → agent.session.transcript broadcast.\n log.info(\n `[ws-manager] agent.session.transcript.get machine=${msg.machineId} session=${msg.sessionId}`\n )\n this.ssh.broadcastSessionTranscript(msg.machineId, msg.sessionId)\n break\n case 'agent.session.open': {\n // [Cockpit v2 장수명] 양방향 stream-json 세션 열기. stdout 카드는 cockpit.card 로 흐른다.\n // 콕핏 claude 는 머신의 실제 cwd 에서 띄워야 의미가 있다(터미널과 같은 ~/.claude 세션 풀,\n // Phase C). 렌더러는 live cwd(OSC7)를 모르므로 main 이 resolveLocalCwd 로 해석(미지정 시).\n const cwd = msg.cwd ?? this.ssh.resolveLocalCwd(msg.machineId)\n // 세션 id 결정:\n // - [Phase D] resume + sessionId 가 목록에 있으면 그 세션 resume(picker/영속 복원).\n // - [Phase C] preferLatest(또는 위 id 부재) 면 최신 세션(mtime[0]) resume(터미널 인계).\n // - 둘 다 안 되면 제공된 sessionId 로 새 세션(fresh).\n let openId = msg.sessionId\n let resume = false\n const sessions = listAgentSessions(cwd)\n if (msg.preferLatest || msg.resume) {\n // resume 요청 id 가 목록에 있으면 그것, 아니면 최신, 둘 다 없으면 fresh(아래 openId 유지).\n const picked =\n msg.resume && sessions.some(s => s.id === msg.sessionId)\n ? msg.sessionId\n : sessions[0]?.id\n if (picked) {\n openId = picked\n resume = true\n }\n }\n // [bug 5] fresh(\"새 세션\": preferLatest/resume 둘 다 false)일 때도 **항상** seed 를 보낸다.\n // 이전엔 preferLatest||resume 일 때만 broadcast 해, picker '새 세션'(fresh)이 renderer 의\n // sessionId·카드를 갱신하지 못해 \"무반응\"이었다(COCKPIT_SESSION_OPEN 은 transcript.seed 로만\n // dispatch 됨). fresh 면 빈 messages → 빈 콕핏 + 새 sessionId 확정. 콕핏 전용 메시지 —\n // 모바일 picker 용 agent.session.transcript 와 분리(데스크탑 콕핏 카드 교차오염 방지).\n const messages = resume ? readSessionTranscript(cwd, openId) : []\n this.broadcast({\n type: 'cockpit.transcript.seed',\n machineId: msg.machineId,\n sessionId: openId,\n messages,\n })\n // picker 목록도 같은 스캔으로 함께 broadcast — 렌더러가 별도 agent.sessions.list 를\n // 보내 디렉토리를 두 번 스캔하던 낭비 제거.\n this.broadcast({\n type: 'agent.sessions',\n machineId: msg.machineId,\n cwd,\n sessions,\n })\n log.info(\n `[ws-manager] agent.session.open machine=${msg.machineId} session=${openId} cwd=${cwd} resume=${resume} preferLatest=${msg.preferLatest ?? false} reqResume=${msg.resume ?? false} takeFromTerminal=${msg.takeFromTerminal ?? false}`\n )\n if (msg.takeFromTerminal) {\n // [Phase C3 single-owner] 터미널 claude 를 먼저 Ctrl-C 종료(killPtyAgent: Ctrl-C ×2,\n // 200ms 간격)한 뒤 resume — 같은 세션 동시 write 경합 회피. 카드 시드(위)는 이미 broadcast\n // 됐으니 사용자는 대화를 바로 본다. claude 종료 + jsonl flush 를 기다려 ~500ms 후 open.\n // 지연 중 콕핏 이탈(close)이 오면 타이머를 취소(orphan 세션 방지).\n const mid = msg.machineId\n const prev = this.pendingHandoffOpens.get(mid)\n if (prev) clearTimeout(prev)\n this.pendingHandoffOpens.set(\n mid,\n setTimeout(() => {\n this.pendingHandoffOpens.delete(mid)\n this.agentRunner.openSession(mid, openId, cwd, resume)\n }, 500)\n )\n this.ssh.killPtyAgent(mid)\n } else {\n this.agentRunner.openSession(msg.machineId, openId, cwd, resume)\n }\n break\n }\n case 'agent.session.send':\n // [Cockpit v2 장수명] 열린 세션 stdin 으로 user 메시지 push(프롬프트/approval 응답).\n this.agentRunner.sendToSession(msg.sessionId, msg.text)\n break\n case 'agent.session.close': {\n // [Cockpit v2 장수명] 세션 종료(뷰 전환/탭 닫기/disconnect). sessionId 미지정 시 머신 단위\n // (Phase C: resume 으로 연 실제 id 를 렌더러가 몰라도 닫게).\n log.info(\n `[ws-manager] agent.session.close machine=${msg.machineId} session=${msg.sessionId ?? '(by machine)'}`\n )\n // [Phase C3] 지연 중인 takeFromTerminal open 이 있으면 취소 — 닫은 직후 orphan 세션이\n // 떠버리는 것을 막는다.\n const pending = this.pendingHandoffOpens.get(msg.machineId)\n if (pending) {\n clearTimeout(pending)\n this.pendingHandoffOpens.delete(msg.machineId)\n }\n this.agentRunner.closeSession(msg.sessionId, msg.machineId)\n break\n }\n case 'agent.session.handoffToTerminal': {\n // [Cockpit Phase C/C2] 콕핏 → 터미널 핸드오프. 콕핏 세션을 닫고(single-owner) PTY 에\n // `claude --resume <id>` 주입 → 인터랙티브 터미널이 콕핏 대화를 이어받는다.\n // handleAgentMessage 가 void 라 case 안에서만 async IIFE 로 detect 를 await 한다.\n const mid = msg.machineId\n const sid = msg.sessionId\n void (async () => {\n // [bug 1] main 권위 가드: 주입 직전 실시간 탐지로 최신화. renderer 의 runningAgent 는\n // 5s 폴링이라 stale 일 수 있어 — 그 가드만 믿으면 살아있는 claude 프롬프트에 `claude\n // --resume` 이 텍스트로 박힌다. main 이 최종 차단한다.\n await this.ssh.detectAndBroadcastRunningAgent(mid)\n this.agentRunner.closeSession(undefined, mid) // 콕핏에서 나가므로 콕핏 세션은 닫는다\n if (this.ssh.getRunningAgent(mid) === 'claude') {\n log.warn(\n `[ws-manager] handoff skipped — claude already running in terminal machine=${mid}`\n )\n return\n }\n log.info(\n `[ws-manager] agent.session.handoffToTerminal machine=${mid} session=${sid}`\n )\n // sessionId 는 PTY(셸)로 그대로 주입되므로 charset 검증 필수 — id 는 claude 세션 UUID\n // (`[A-Za-z0-9_-]`)인데, listAgentSessions 가 임의 jsonl 파일명에서 유래할 수 있어 셸\n // 메타문자(`; | $ \\n` 등)가 섞이면 셸 인젝션이 된다. 안전 패턴이 아니면 주입 안 함.\n if (!/^[A-Za-z0-9_-]+$/.test(sid)) {\n log.warn(\n `[ws-manager] handoff rejected — unsafe sessionId charset machine=${mid}`\n )\n return\n }\n // [bug 2] 주입 전 현재 셸 입력 라인 클리어(Ctrl-C) — 셸에 입력 중이던 텍스트(예: dir)\n // 에 명령이 이어붙어 'dirclaude --resume' 이 되는 것 방지. cross-platform: Ctrl-C 는\n // bash/zsh/PowerShell/cmd 모두 현재 라인을 버리고 새 프롬프트를 그린다. 직후엔\n // 프롬프트 재그리기 전이라 ~120ms 뒤 명령을 주입한다.\n this.ssh.sendInput(mid, '\\x03')\n setTimeout(() => {\n this.ssh.sendInput(mid, `claude --resume ${sid}\\n`)\n }, 120)\n })()\n break\n }\n }\n }\n\n private getMobileQrInfo(): {\n url: string\n mode: 'relay' | 'lan'\n details: ConnectionDetails\n } {\n if (this.relayQrUrl) {\n return {\n // 폰 기본 카메라로 찍어도 동작하는 HTTPS 딥링크로 노출 — 미설치 시 스토어,\n // 설치 시 앱 실행. 내부 relayQrUrl 은 원시 ws url(접속 진실원)로 유지.\n url: toPairUrl(this.relayQrUrl),\n mode: 'relay',\n details: {\n relayToken: SESSION_TOKEN.slice(0, 8),\n localPort: this.wsPort,\n },\n }\n }\n // relay 연결 중이거나 실패한 경우 LAN fallback 반환 (절대 null 반환하지 않음)\n const lanIp = getLocalIP()\n return {\n url: `ws://${lanIp}:${this.wsPort}/mobile`,\n mode: 'lan',\n details: { lanIp, localPort: this.wsPort },\n }\n }\n\n // ── Heartbeat (ping/pong) ─────────────────────────────────────────────────\n\n private startHeartbeat(): void {\n this.stopHeartbeat()\n this.heartbeatTimer = setInterval(() => {\n if (this.relayWs?.readyState !== WebSocket.OPEN) return\n this.relayWs.ping()\n this.pongTimer = setTimeout(() => {\n log.info(\n `[relay] heartbeat_miss ts=${new Date().toISOString()} — closing zombie connection`\n )\n this.relayWs?.terminate()\n this.relayWs = null\n }, this.pongTimeoutMs)\n }, this.heartbeatIntervalMs)\n }\n\n private stopHeartbeat(): void {\n if (this.heartbeatTimer) {\n clearInterval(this.heartbeatTimer)\n this.heartbeatTimer = null\n }\n if (this.pongTimer) {\n clearTimeout(this.pongTimer)\n this.pongTimer = null\n }\n }\n\n // ── Message handler (forwarded from mobile/relay) ─────────────────────────\n //\n // source: 메시지가 들어온 ws (LAN 모바일). 없으면 (relay 경유) broadcast.\n // session_attach 의 replay 응답을 해당 클라이언트에게만 보내려면 source 가 필요.\n\n private handleMessage(\n msg: ClientMessage,\n source: WebSocket | null = null\n ): void {\n switch (msg.type) {\n case 'get_machines':\n this.ssh.broadcastMachines()\n // relay_status는 auth 시점에 전달됨 (startLocalServer auth_ok 핸들러) — broadcastCurrentRelayStatus 불필요\n break\n case 'add_machine':\n this.ssh.addMachine(msg.machine)\n this.ssh.broadcastMachines()\n break\n case 'edit_machine':\n this.ssh.editMachine(msg.machine)\n this.ssh.broadcastMachines()\n break\n case 'remove_machine':\n this.ssh.removeMachine(msg.machineId)\n this.ssh.broadcastMachines()\n break\n case 'connect':\n // 구 클라이언트 backward-compat. connect = \"PTY 보장 + 가능하면 active 화\".\n // ssh-manager 가 이미 ensure 패턴으로 변경됨 (existing PTY 면 재사용).\n // session_replay 는 안 보냄 — 구 클라이언트는 메시지 타입 모름.\n // F2 (task 13 v4) — connect 도 lastAttachedMachineId 갱신. mobile 이\n // session_attach 대신 connect 만 송신하는 path 에서도 RESIZE opcode 의\n // machineId fallback 이 동작하도록.\n this.lastAttachedMachineId = msg.machineId\n this.ssh.connect(msg.machineId)\n break\n case 'disconnect':\n // 명시적 disconnect — PTY 와 scrollback 모두 폐기. 사용자 의도 종료.\n this.ssh.disconnect(msg.machineId)\n break\n case 'terminal_input': {\n // F2 (task 13 v3) — terminal_input 진단 log. PTY exit STATUS_CONTROL_C_EXIT\n // (-1073741510) 회귀 추적. 어떤 input 이 매 ~4초 cycle 로 도착해 PTY 가 Ctrl+C\n // 로 죽는지 식별. data 의 byte 시퀀스 + ASCII 표기.\n const data = msg.data ?? ''\n const codes = [...data].map(c => c.charCodeAt(0))\n const hasCtrlC = codes.includes(0x03)\n const ascii = data.replace(\n /[\\x00-\\x1f\\x7f]/g,\n c => `\\\\x${c.charCodeAt(0).toString(16).padStart(2, '0')}`\n )\n if (hasCtrlC) {\n log.warn(\n `[ws-manager] ⚠ terminal_input Ctrl+C machine=${msg.machineId} bytes=${codes.length} ascii=\"${ascii}\"`\n )\n } else if (data.length > 0) {\n log.info(\n `[ws-manager] terminal_input machine=${msg.machineId} bytes=${codes.length} ascii=\"${ascii.slice(0, 40)}\"`\n )\n }\n this.ssh.sendInput(msg.machineId, msg.data)\n // [A2 spike] mobile input 마다 lastActivityAt 갱신 — HandoffPlaceholder 의\n // \"Last activity: Xs ago\" heartbeat 정확도 유지. spike-off 면 resolver 내부에서 no-op.\n if (source) {\n const meta = this.clientMeta.get(source)\n if (meta?.clientId) {\n this.ssh.authority.touchMobileActivity(msg.machineId, meta.clientId)\n }\n }\n break\n }\n case 'device.fcm_register':\n // [FCM Phase 3] 폰 FCM 토큰 등록 — userData 토큰맵에 보관(앱 killed 푸시 대상).\n this.fcmTokenStore.register(msg.token, msg.platform, msg.locale)\n break\n case 'device.fcm_unregister':\n this.fcmTokenStore.unregister(msg.token)\n break\n case 'terminal_resize':\n // Phase 1 — 모바일 우선 권위 (plans/md-woolly-truffle.md). ws-manager 의\n // handleMessage 로 들어온 terminal_resize 는 모두 외부(LAN mobile / relay\n // 경유 mobile) 발신 — 데스크톱 renderer 는 IPC 경로(main/index.ts) 를 거쳐\n // ssh.resize 를 직접 호출하므로 여기로 들어오지 않는다. ssh-manager 가 이\n // mobile-source 메시지로 60s TTL 의 mobileAuthorityUntil 을 갱신하고, 그\n // 동안 데스크톱 source resize 는 server-side 에서 차단되어 ResizeObserver\n // ping-pong 이 봉쇄된다.\n this.ssh.resize(msg.machineId, msg.cols, msg.rows, 'mobile')\n break\n case 'request_qr':\n this.requestQr()\n break\n case 'request_ssh_keys':\n this.ssh.broadcastSshKeys()\n break\n case 'init_commands':\n this.ssh.runInitCommands(msg.machineId)\n break\n case 'active_machine':\n this.broadcast({ type: 'active_machine', machineId: msg.machineId })\n break\n case 'reconnect_relay':\n this.reconnect()\n break\n case 'git_action':\n this.ssh.executeGitAction(msg.machineId, msg.action)\n break\n // ─── PTY persistence (PROTOCOL_VERSION 2+) ─────────────────────────────\n case 'session_attach':\n // F2 — global cache 로 RESIZE opcode 의 machineId fallback 보강\n this.lastAttachedMachineId = msg.machineId\n this.handleSessionAttach(msg.machineId, source, msg.cols, msg.rows)\n break\n case 'session_detach':\n // PTY/scrollback 은 손대지 않음 (영속화 의도). lifecycle metadata 의\n // last-activity 기준 idle/running 재계산 + pty_status 즉시 broadcast.\n log.info(`[ws-manager] session_detach machine=${msg.machineId}`)\n this.ssh.lifecycle.onDetach(msg.machineId)\n // [M1 20260604 desktop-impact] 명시 detach → 권위도 즉시 회수.\n // relay 경로는 단일 agent↔relay 소켓이라 per-mobile ws.on('close') 가 없어\n // (LAN 경로만 1023 close 에서 onMobileDetach 호출) 이 메시지가 유일한 즉시 detach\n // 신호다. 없으면 handed_off 가 60s watchdog 까지 유지돼 desktop↔mobile 동시접근\n // 창이 생긴다. mobile 송신: ai_remote_vibe_mobile RelayService.sendSessionDetach\n // (PR fix/relay-mobile-detach). leader=mobile + clientId 매칭 시에만 grace 시작\n // (onMobileDetach 내부 가드) — 비권위/오매칭 detach 는 noop.\n if (source) {\n const meta = this.clientMeta.get(source)\n if (meta?.clientId) {\n this.ssh.authority.onMobileDetach(msg.machineId, meta.clientId)\n }\n }\n break\n case 'session_keepalive':\n // [Fix 0 / keepalive — desktop-impact] passive-watch(입력 없음) 중 handed_off\n // watchdog(60s)의 무활동 회수 churn 방지. terminal_input(:451)과 동일하게\n // lastActivityAt 만 갱신 — sendInput/onUserInput 은 거치지 않아 PTY/pty_status\n // 무영향. touchMobileActivity 내부가 leader=mobile + clientId 매칭 가드 (비권위\n // /오매칭 keepalive 는 noop). mobile: RelayService.sendSessionKeepalive.\n if (source) {\n const meta = this.clientMeta.get(source)\n if (meta?.clientId) {\n this.ssh.authority.touchMobileActivity(msg.machineId, meta.clientId)\n }\n }\n break\n case 'mobile.diag_log': {\n // [handoff diag 20260607 — C] 모바일이 forward 한 인앱 디버그 로그 1건을\n // logs/mobile-diag.log 에 적재. clientId 로 어느 모바일인지 식별 — authority.log/ws.log\n // 와 timestamp 교차하면 모바일+데스크탑을 한 폴더에서 함께 본다. level 별 라우팅.\n const meta = source ? this.clientMeta.get(source) : undefined\n const client = meta?.clientId ?? 'unknown' // 데스크탑 생성 id — untrusted 아님\n // untrusted 모바일 입력 — 제어문자 escape + 길이 cap (로그 위조/flood 방지, #1·#2)\n const dLevel = sanitizeDiagField(msg.level, 16)\n const dSource = sanitizeDiagField(msg.source, 64)\n const dMessage = sanitizeDiagField(msg.message, 2000)\n const dDetails = msg.details ? ` — ${sanitizeDiagField(msg.details, 2000)}` : ''\n const dTs = sanitizeDiagField(msg.ts, 32)\n const dMachine = msg.machineId ? sanitizeDiagField(msg.machineId, 64) : '(none)'\n const line =\n `[${dLevel}] [${dSource}] ${dMessage}${dDetails}` +\n ` (mobileTs=${dTs} machine=${dMachine} client=${client})`\n // level 별 라우팅 — 모바일 ErrorLogLevel(error/warning/info)만 매핑, 그 외(프로토콜\n // drift/malformed)는 info 로 삼키지 않고 warn 으로 fail-loud (#3 — 에러 누락 방지).\n // 런타임 값은 untrusted relay JSON 이라 타입(union) 밖일 수 있어 string 으로 보고 라우팅.\n const rawLevel: string = msg.level\n if (rawLevel === 'error') mobileDiagLog.error(line)\n else if (rawLevel === 'warning') mobileDiagLog.warn(line)\n else if (rawLevel === 'info') mobileDiagLog.info(line)\n else mobileDiagLog.warn(line)\n break\n }\n case 'file.upload.begin': {\n // [file-upload 20260614] 검증(이름/크기) 후 누적 entry 생성. 거절이면 즉시 done(ok:false).\n log.info(\n `[ws-manager] file.upload.begin machine=${msg.machineId} id=${msg.uploadId} size=${msg.size} chunks=${msg.totalChunks}`\n )\n const rejected = this.fileUpload.begin(\n msg.machineId,\n msg.uploadId,\n msg.name,\n msg.size,\n msg.totalChunks\n )\n if (rejected) {\n log.warn(\n `[ws-manager] file.upload.begin rejected machine=${msg.machineId} id=${msg.uploadId} error=${rejected.error}`\n )\n this.sendToTarget(source, {\n type: 'file.upload.done',\n machineId: msg.machineId,\n uploadId: msg.uploadId,\n ok: false,\n error: rejected.error,\n })\n }\n break\n }\n case 'file.upload.chunk':\n // [file-upload 20260614] 청크 누적 — 알 수 없는 id 는 receiver 가 무시.\n this.fileUpload.chunk(msg.uploadId, msg.index, msg.data)\n break\n case 'file.upload.end': {\n // [file-upload 20260614] 종료 — atomic write 후 done 응답. handleMessage 는 동기라\n // 시그니처를 바꾸지 않고 .then 으로 회신(end 는 fs write 라 Promise).\n const { machineId, uploadId } = msg\n log.info(`[ws-manager] file.upload.end machine=${machineId} id=${uploadId}`)\n this.fileUpload\n .end(uploadId)\n .then(res => {\n log.info(\n `[ws-manager] file.upload.done machine=${machineId} id=${uploadId} ok=${res.ok}` +\n (res.ok ? ` path=${res.path}` : ` error=${res.error}`)\n )\n this.sendToTarget(source, {\n type: 'file.upload.done',\n machineId,\n uploadId,\n ...res,\n })\n })\n .catch(err => {\n // end 자체는 내부 try/catch 지만 .then 콜백(직렬화/송신) throw 시 모바일이\n // 30s 타임아웃까지 매달리는 것을 막는다 — write_failed 로 즉시 회신.\n log.error(`[ws-manager] file.upload.end failed id=${uploadId}: ${err}`)\n this.sendToTarget(source, {\n type: 'file.upload.done',\n machineId,\n uploadId,\n ok: false,\n error: 'write_failed',\n })\n })\n break\n }\n case 'pty_restart':\n // E4 — 사용자 명시 PTY 재시작. ssh-manager 의 200ms debounce lock 으로 더블 클릭 방어.\n this.ssh.restartPty(msg.machineId)\n break\n case 'terminal_authority_takeover_request': {\n // Phase 1 — 모바일이 권위 탈취 요청. resolver 가 desktop renderer 에 모달 재발행.\n // 본 PR 의 desktop renderer UI 는 Phase 2 — 서버는 라우팅만 보장.\n if (source) {\n const meta = this.clientMeta.get(source)\n if (meta?.clientId) {\n this.ssh.authority.onMobileTakeoverRequest(\n msg.machineId,\n meta.clientId\n )\n }\n }\n break\n }\n // ─── [Cockpit] agent.* 는 렌더러(IPC)와 공유하는 handleAgentMessage 로 위임 ──────\n case 'agent.run':\n case 'agent.kill':\n case 'agent.sessions.list':\n case 'agent.session.transcript.get':\n case 'agent.session.open':\n case 'agent.session.send':\n case 'agent.session.close':\n case 'agent.session.handoffToTerminal':\n this.handleAgentMessage(msg)\n break\n }\n }\n\n /**\n * session_attach 처리 — ensure PTY + scrollback 청크 분할 송신.\n * 핵심: 구 모바일이 보내는 connect 와 달리, 신 모바일은 attach 후 session_replay 를\n * 받아 자신의 xterm 에 prepend. attach 시점에 PTY 가 살아있던 자식 프로세스의 출력이\n * 모두 모바일 화면에 복원된다.\n *\n * task 10 C7: cols/rows 가 들어오면 첫 PTY spawn 의 size 로 사용 (default 80x24\n * race 차단). 기존 PTY 가 살아있으면 즉시 ssh.resize(source='mobile') 1회 적용해\n * mobile 의 첫 layout 까지의 race window 동안 grid mismatch 방지.\n */\n private handleSessionAttach(\n machineId: string,\n source: WebSocket | null,\n cols?: number,\n rows?: number\n ): void {\n log.info(\n `[ws-manager] session_attach machine=${machineId} ` +\n `hasSession=${this.ssh.hasSession(machineId)} ` +\n `attachSize=${cols ?? '?'}x${rows ?? '?'}`\n )\n\n // [A2 spike 20260523] cooldown 체크 — Take Back 후 5s 안에 모바일 attach 시도면 거부.\n // close code 4008 (TAKEBACK_COOLDOWN) → mobile DisconnectReason.rejectedCooldown →\n // RejectedBanner 풀스크린 표시 (C6 에서 구현). source=null 인 IPC 경로는 skip.\n if (source && this.ssh.authority.isInTakebackCooldown(machineId)) {\n log.warn(\n `[ws-manager] session_attach REJECTED (cooldown) machine=${machineId}`\n )\n // [Bug #3 fix 20260526] App-level kick frame (cooldown reason).\n // relay 경로 mobile 도 RejectedBanner 발화 가능 — 기존 source.close(4008) 만\n // 으로는 relayWs 닫기 부작용 + relay forward 없어서 mobile 무반응.\n this.broadcast({\n type: 'desktop.kick',\n machineId,\n reason: 'cooldown',\n cooldownUntil: this.ssh.authority.getTakebackCooldownUntil(machineId),\n })\n if (source !== this.relayWs) {\n try {\n source.close(4008, 'TAKEBACK_COOLDOWN')\n } catch {\n // 이미 closed\n }\n }\n // resolver 의 onMobileAttach 가 viewer_mode='rejected' broadcast — desktop 측 UI 갱신.\n // 단 clientId 가 있어야 의미 있으므로 meta lookup 후 호출.\n const meta = this.clientMeta.get(source)\n if (meta?.clientId) {\n this.ssh.authority.onMobileAttach(machineId, meta.clientId, cols, rows)\n }\n return\n }\n\n // [A2 spike] mobileClientByMachine 트래킹 — handleDesktopTakeback 의 WS lookup 용.\n if (source) {\n this.mobileClientByMachine.set(machineId, source)\n }\n\n // [A2 spike] viewer_mode='attaching' broadcast — desktop UI 의 \"Phone connecting…\"\n // overlay 트리거. resolver.onMobileAttach 호출 전에 발화되어야 핸드오프 직전 race\n // window UX 가 자연스러움 (Design Fix-E D2-δ row 1).\n this.ssh.authority.broadcastAttaching(machineId, cols, rows)\n\n // [REQ1 busy-attach 20260608] 기존 살아있는 PTY 에 재attach 하는지 여부 — 아래\n // nudgeFullRepaintOnAttach 게이트로 사용(신규 spawn 은 fresh 출력이라 넛지 불필요).\n const hadSession = this.ssh.hasSession(machineId)\n\n // [재접속 scrollback fix 20260620] mobile resize *직전* 의 PTY 폭 = scrollback tail 이\n // 생성된 폭. 아래 session_replay 에 동봉해 모바일이 그 폭으로 raw replay 를 그린 뒤\n // viewport 폭으로 reflow → 재접속 시 스크롤백이 좁은 폭으로 재-wrap 되어 좌측으로 붕괴하던\n // 버그 회피. 미측정(콜드 spawn — PTY 아직 없음)이면 undefined → omit → 모바일 기존 동작.\n const genSize = this.ssh.getPtySize(machineId)\n const replaySizeFields =\n genSize && genSize.cols > 0 && genSize.rows > 0\n ? { cols: genSize.cols, rows: genSize.rows }\n : {}\n\n // C7 — viewport size 흡수. cols/rows 동봉이 없는 구 모바일 (PROTOCOL_VERSION<3)\n // 은 hint 미사용으로 default fallback (이전 동작).\n if (\n typeof cols === 'number' &&\n typeof rows === 'number' &&\n cols > 0 &&\n rows > 0\n ) {\n if (!this.ssh.hasSession(machineId)) {\n // spawn 직전 hint — ssh-manager 가 spawn cols/rows 로 사용.\n this.ssh.setSpawnSizeHint(machineId, cols, rows)\n } else {\n // 이미 살아있는 PTY — mobile size 로 즉시 resize (mobile-priority).\n // RESIZE opcode 가 곧 따라오지만 첫 frame race 를 막기 위해 attach 시점 1회.\n // ssh.resize 는 sync (void) — 내부 throw 자체 흡수 처리됨.\n this.ssh.resize(machineId, cols, rows, 'mobile')\n }\n }\n\n // PTY 가 없으면 spawn, 있으면 재사용 (ssh.connect 가 ensure 패턴).\n this.ssh.connect(machineId)\n // lifecycle: lastAttach 갱신 + pty_status 즉시 broadcast (throttle bypass).\n // mobile 첫 화면이 정확한 status 받음.\n this.ssh.lifecycle.onAttach(machineId)\n\n // Phase 1 — AuthorityResolver 에 mobile attach 통지 → 즉시 leader=mobile (A2 spike).\n // grace-pending + 같은 clientId 면 즉시 복귀. (desktop-priority 정책은 2026-06-07 제거.)\n if (source) {\n const meta = this.clientMeta.get(source)\n if (meta?.clientId) {\n this.ssh.authority.onMobileAttach(machineId, meta.clientId, cols, rows)\n }\n }\n\n // [REQ1 busy-attach 20260608] 기존 PTY 재attach + 유효 size 면 보장된 full-repaint 넛지.\n // busy alt-screen TUI(claude 등)는 attach 시 raw tail(마지막 상태바 조각)만 복원되는데,\n // 위 line~699 의 attach resize 는 이미 동일 size 면 noop 이거나 desktop-leader reattach 면\n // authority drop 이라, 어느 쪽이든 SIGWINCH 가 안 나 화면이 다시 안 그려질 수 있다.\n // nudgeFullRepaintOnAttach 가 onMobileAttach(leader=mobile) *뒤* 에서 보장된 size 델타로\n // redraw 를 강제한다.\n if (\n hadSession &&\n typeof cols === 'number' &&\n typeof rows === 'number' &&\n cols > 0 &&\n rows > 1\n ) {\n this.nudgeFullRepaintOnAttach(machineId, cols, rows)\n }\n\n // [attach frame-snapshot 20260610] busy alt-screen TUI(claude 등) 화면 정확 복원.\n // raw scrollback tail 만 보내는 아래 session_replay 로는 alt-screen 을 재구성할 수 없어\n // 모바일이 \"조각\"만 보던 버그를 고친다 — desktop 의 정확한-size GridEmulator 를 ANSI 로\n // 직렬화해 FRAME_SNAPSHOT (0x03) binary frame 으로 송신한다.\n //\n // ⚠️ 순서 의존: 모바일은 이 0x03 frame 을 buffer 했다가 session_replay 가 *끝난* 직후\n // (`\\x1b[2J\\x1b[H` reset + ANSI) overlay 한다. 동일 WS 가 순서를 보존하므로 반드시\n // session_replay 송신 *전에* 보낸다.\n if (\n attachFrameSnapshotEnabled() &&\n hadSession &&\n typeof cols === 'number' &&\n typeof rows === 'number' &&\n cols > 0 &&\n rows > 1\n ) {\n const frame = this.buildFrameSnapshot(machineId)\n if (frame) {\n const diag = this.ssh.lifecycle.gridSnapshotDiag(machineId)\n log.info(\n `[ws-manager] attach_frame_snapshot machine=${machineId} bytes=${frame.length}` +\n (diag\n ? ` bufType=${diag.bufType} nonBlankLines=${diag.nonBlankLines}/${diag.rows} glyphs=${diag.glyphs}`\n : '')\n )\n if (source && source.readyState === WebSocket.OPEN) {\n source.send(frame)\n } else {\n // relay 경로 등 source 가 OPEN 이 아니면 이 머신에 attach 된 모바일에 fan-out.\n // legacy pty.data path 이므로 cap filter 없이 attachedMachineId 일치만 검사.\n this.sendBinaryToMachine(machineId, frame)\n }\n }\n }\n\n const buffer = this.ssh.getScrollback(machineId)\n if (!buffer || buffer.isEmpty()) {\n // 빈 replay 라도 한 번 송신 — 클라이언트가 \"attach 완료\" 시그널로 사용 (live data 받기 시작).\n this.sendToTarget(source, {\n type: 'session_replay',\n machineId,\n chunkIndex: 0,\n totalChunks: 1,\n data: '',\n ...replaySizeFields,\n })\n return\n }\n\n // cross-repo with claude_code_mobile autoplan 2026-05-22 UC-3 — attach 시점\n // 송신은 tail cap (`REPLAY_ATTACH_TAIL_BYTES`). scrollback buffer 의 cap\n // (`SCROLLBACK_BUFFER_BYTES_DEFAULT` 또는 머신별 `ringBufferBytes`) 는 그대로 —\n // append 누적 정책 변경 0, mobile attach 시 송신 한도만 제한.\n const chunks = buffer.snapshotChunksForAttach(\n REPLAY_ATTACH_TAIL_BYTES,\n REPLAY_CHUNK_BYTES\n )\n const total = chunks.length\n log.info(\n `[ws-manager] session_replay machine=${machineId} bytes=${buffer.size()} ` +\n `chunks=${total} cap=${buffer.capacity()} tail=${REPLAY_ATTACH_TAIL_BYTES}`\n )\n for (let i = 0; i < total; i++) {\n this.sendToTarget(source, {\n type: 'session_replay',\n machineId,\n chunkIndex: i,\n totalChunks: total,\n data: chunks[i],\n ...replaySizeFields,\n })\n }\n }\n\n /**\n * [resize-resettle frame-snapshot 20260610] GridEmulator 현재 화면을 ANSI 로 직렬화해\n * FRAME_SNAPSHOT (0x03) Buffer 로 인코딩한다. 직렬화 실패/빈 화면이면 null.\n *\n * attach·resize-resettle 두 경로가 공유한다(DRY) — 송신 채널만 호출부가 결정.\n * env 게이트(`attachFrameSnapshotEnabled`)는 호출부 책임(여긴 순수 build).\n */\n private buildFrameSnapshot(machineId: string): Buffer | null {\n const ansi = this.ssh.lifecycle.serializeGridAnsi(machineId)\n if (!ansi || ansi.length === 0) return null\n return encodeFrameSnapshot(machineId, ansi)\n }\n\n /**\n * [resize-resettle frame-snapshot 20260610] RESIZE 수신 후 디바운스해 FRAME_SNAPSHOT 재방출.\n *\n * WHY: 모바일이 attach 직후 미측정 80x24 로 붙었다가 ~233ms 뒤 실제 viewport(예: 77x67)로\n * RESIZE 하는 race 에서, attach 시 직렬화된 스냅샷은 잘못된 행수 + claude SIGWINCH repaint 전\n * reflow 잔해라 \"조각\"으로 보인다. RESIZE 가 멎고(디바운스) claude 가 새 size 로 repaint 를\n * 끝낸 뒤 한 번 더 직렬화해 올바른 화면을 재방출한다. 키보드 토글/회전 리사이즈도 같이 커버.\n *\n * 송신은 `sendBinaryToMachine`(머신 라우팅) — relay/LAN attach 모바일 모두 도달. 모바일은 이\n * standalone 0x03(뒤따르는 session_replay 없음)을 즉시 적용한다(`_applyFrameSnapshotNow`).\n */\n private scheduleResizeFrameSnapshot(machineId: string): void {\n if (!attachFrameSnapshotEnabled()) return\n const existing = this.resizeSnapshotTimers.get(machineId)\n if (existing) clearTimeout(existing)\n this.resizeSnapshotTimers.set(\n machineId,\n setTimeout(() => {\n this.resizeSnapshotTimers.delete(machineId)\n const frame = this.buildFrameSnapshot(machineId)\n if (frame) {\n const diag = this.ssh.lifecycle.gridSnapshotDiag(machineId)\n log.info(\n `[ws-manager] resize_frame_snapshot machine=${machineId} bytes=${frame.length}` +\n (diag\n ? ` bufType=${diag.bufType} nonBlankLines=${diag.nonBlankLines}/${diag.rows} glyphs=${diag.glyphs}`\n : '')\n )\n this.sendBinaryToMachine(machineId, frame)\n }\n }, RESIZE_SNAPSHOT_DEBOUNCE_MS)\n )\n }\n\n /**\n * [REQ1 busy-attach 20260608] busy alt-screen TUI 가 attach 시 화면 전체를 다시 그리도록\n * 강제하는 SIGWINCH 넛지.\n *\n * WHY: `ssh.resize` 는 size 가 직전과 동일하면 noop(setWindow/multiplexer refresh skip,\n * ssh-manager.ts D-1) 이라, attach 시점에 PTY 가 이미 mobile size 면 (line ~699 의 resize 가\n * noop → ) SIGWINCH 가 안 나 화면 repaint 가 안 일어난다. 그 결과 모바일은 raw scrollback\n * tail 의 마지막 부분(busy TUI 의 in-place 상태바 재기록 = \"Actioning…\" 조각)만 복원하고\n * full frame 을 못 받는다 (docs/requests/2026-06/20260608-busy-attach-full-state-restore-...).\n *\n * off-size 로 한 번 흔든 뒤 즉시 정상 size 로 되돌려, 두 resize 모두 실제 델타가 되게 해\n * setWindow(SIGWINCH) + multiplexer refresh-client(alt-screen TUI 강제 redraw, ssh-manager.ts\n * resize 주석)를 발화시킨다 → 앱이 현재 geometry 의 full frame 을 새 viewer 에 흘려보낸다.\n *\n * [adversarial review fix 20260608 — 2건]\n * ① clamp 충돌 회피: `clampRows` 가 PTY_MIN_ROWS(5) 미만을 5 로 floor 하므로 rows<=5 면\n * rows-1 이 같은 5 로 접혀 둘 다 noop(D-1)이 돼 repaint 가 안 난다. 바닥 근처면 off 를\n * 위(PTY_MIN_ROWS+1)로 잡아 clamp 후에도 distinct 하게.\n * ② 동기 실행(setTimeout 제거): 지연 step 은 그 사이 도착한 모바일의 실제 RESIZE 를 stale\n * attach-time size(머신전환 시 80x24 fallback)로 덮어써 영구 misfit 을 만든다(모바일은\n * resize ack 무시). 같은 tick 에 동기로 치면 모바일 RESIZE 가 항상 뒤에 와서 이기고\n * (기존 순서 보존), untracked timer 가 재spawn 세션을 잘못 resize 하는 race 도 없다.\n *\n * source='mobile' — onMobileAttach 직후 leader=mobile 이라 `authority.allowsResize` 통과\n * (이 호출이 onMobileAttach *뒤* 인 것에 정확성이 의존한다 — 그 전엔 desktop-leader 라 drop).\n */\n private nudgeFullRepaintOnAttach(\n machineId: string,\n cols: number,\n rows: number\n ): void {\n const off = rows > PTY_MIN_ROWS ? rows - 1 : PTY_MIN_ROWS + 1\n this.ssh.resize(machineId, cols, off, 'mobile')\n this.ssh.resize(machineId, cols, rows, 'mobile')\n }\n\n /**\n * source ws 가 있으면 그 클라이언트에게만, 없으면 (relay 경유) 전체 broadcast.\n * session_replay 는 큰 데이터라서 broadcast 낭비 — LAN 모바일 attach 시 source 사용.\n * v3 — APP_JSON opcode wrap.\n */\n private sendToTarget(target: WebSocket | null, msg: ServerMessage): void {\n if (target && target.readyState === WebSocket.OPEN) {\n target.send(encodeAppJson(msg))\n } else {\n this.broadcastToMobile(msg)\n }\n }\n\n /**\n * A3 (Sub-Part 1F-ii-b) — GRID_SNAPSHOT (0x04) / GRID_DIFF (0x05) binary frame\n * broadcast helper.\n *\n * Filter 정책 (spec §6.5):\n * - cap=`grid.diff` client 에만 fan-out (cap=`pty.data` 는 PTY_DATA 그대로 받음 = legacy)\n * - 동시에 attachedMachineId 가 일치해야 함 (다른 머신 attach 한 client 무관)\n * - mobile (LAN ws) + relayWs 둘 다 처리\n *\n * 호출 path:\n * - ssh-manager 의 flushData (16ms 디바운스) → lifecycle.flushDiff → 본 함수\n * - bonus: session_attach 시 lifecycle.onAttachGridDiff 도 본 함수 거치지 않고 ws.send 직접\n * (1:1 단일 client 대상 — broadcast 가 아님)\n */\n broadcastGridFrame(machineId: string, frame: Buffer): void {\n for (const ws of this.mobileClients) {\n if (\n this.mobileClientCaps.get(ws) === 'grid.diff' &&\n this.clientMeta.get(ws)?.attachedMachineId === machineId &&\n ws.readyState === WebSocket.OPEN\n ) {\n ws.send(frame)\n }\n }\n if (\n this.relayWs &&\n this.mobileClientCaps.get(this.relayWs) === 'grid.diff' &&\n this.clientMeta.get(this.relayWs)?.attachedMachineId === machineId &&\n this.relayWs.readyState === WebSocket.OPEN\n ) {\n this.relayWs.send(frame)\n }\n }\n\n /**\n * 특정 머신에 attach 된 mobile/relay client 에 binary frame fan-out — **cap filter 없음**.\n *\n * `broadcastGridFrame` 과 달리 cap=`grid.diff` 필터를 적용하지 않는다. session_attach 의\n * FRAME_SNAPSHOT (0x03) 은 legacy pty.data path 의 모바일도 화면 복원에 사용하므로,\n * attachedMachineId 일치 + OPEN 만 검사한다. source 가 OPEN 이 아닐 때(relay 경로 등)의 fallback.\n */\n private sendBinaryToMachine(machineId: string, frame: Buffer): void {\n for (const ws of this.mobileClients) {\n if (\n this.clientMeta.get(ws)?.attachedMachineId === machineId &&\n ws.readyState === WebSocket.OPEN\n ) {\n ws.send(frame)\n }\n }\n if (\n this.relayWs &&\n this.clientMeta.get(this.relayWs)?.attachedMachineId === machineId &&\n this.relayWs.readyState === WebSocket.OPEN\n ) {\n this.relayWs.send(frame)\n }\n }\n\n /**\n * snapshot.req frequency cap (3/min/session) — sliding window.\n *\n * 동일 session (ws) 의 최근 60s 안 timestamp 가 3개 이상이면 throttle (`true` 반환).\n * `snapshotReqTimes` 가 WeakMap 이라 ws GC 시 자동 정리.\n *\n * spec §6.6 + plan §13-9. emergency_hatch_button_pressed_count rate alarm 의 source.\n */\n private snapshotReqThrottle(ws: WebSocket): boolean {\n const now = Date.now()\n const cutoff = now - 60_000\n const times = this.snapshotReqTimes.get(ws) ?? []\n const recent = times.filter(t => t > cutoff)\n if (recent.length >= 3) {\n this.snapshotReqTimes.set(ws, recent)\n return true\n }\n recent.push(now)\n this.snapshotReqTimes.set(ws, recent)\n return false\n }\n\n // ── 수동 재연결 (렌더러 또는 Flutter 앱에서 요청) ──────────────────────────\n\n reconnect(): void {\n if (this.destroyed) return\n log.info('[relay] manual reconnect requested')\n this.stopHeartbeat()\n if (this.relayWs) {\n this.relayWs.terminate()\n this.relayWs = null\n }\n this.relayStatus = 'disconnected'\n this.relayRetryCount = 0\n this.connectToRelay()\n }\n\n // ── Local WebSocket server (mobile clients on LAN) ────────────────────────\n\n private startLocalServer(wsPort: number, authMs: number): void {\n try {\n // WebSocketServer 는 포트 바인딩을 **비동기**로 수행한다 — 포트가 이미 사용 중이면\n // EADDRINUSE 가 동기 throw 가 아니라 wss 인스턴스의 'error' 이벤트로 방출된다. 핸들러가\n // 없으면 Node 가 \"Unhandled 'error' event\" 로 프로세스 전체를 죽여, 헤드리스 TUI/데스크탑\n // 렌더러가 뜨기도 전에 종료된다(= 옵션에서 포트를 바꿔 복구할 길조차 막힘). 따라서\n // try/catch(동기 한정)에 더해 반드시 'error' 리스너를 걸어 비동기 바인딩 실패를 흡수한다.\n const wss = new WebSocketServer({ port: wsPort, host: '0.0.0.0' })\n this.wss = wss\n\n wss.on('error', (err: NodeJS.ErrnoException) => {\n // 로컬 LAN 서버 바인딩 실패는 치명적이지 않다 — 모바일 주 접속 경로인 relay 는 이 서버와\n // 독립적으로 살아있다. 크래시 대신 호스트(헤드리스 verbose 로그 / 데스크탑 console)에\n // 알리고 계속 진행해, 사용자가 옵션에서 WS 포트를 바꾼 뒤 재시작할 수 있게 한다.\n const message =\n err.code === 'EADDRINUSE'\n ? `WS_PORT_IN_USE:${wsPort} (LAN direct unavailable; relay unaffected)`\n : `WS_SERVER_ERROR:${err.code ?? err.message}`\n log.error(`[ws-manager] local server error on port ${wsPort}: ${err.message}`)\n if (this.wss === wss) this.wss = null\n // host 측에만 — 로컬 서버 진단이며 부팅 시점엔 모바일이 없다(relay flap 회피).\n this.broadcast({ type: 'error', message }, { skipMobile: true })\n })\n\n // 실제 바인딩 성공 시점에만 로그. 동기 try-블록 끝에서 찍으면 EADDRINUSE(다음 tick 의\n // 'error' 이벤트)일 때도 \"Listening on\" 이 먼저 찍혀 진단을 오도한다 — 성공 신호인\n // 'listening' 이벤트로 옮겨 성공/실패 로그가 모순되지 않게 한다.\n wss.on('listening', () => {\n log.info(`[ws-manager] Listening on ws://0.0.0.0:${wsPort}`)\n })\n\n wss.on('connection', (ws: WebSocket, req: IncomingMessage) => {\n const isMobile = req.url === '/mobile'\n if (!isMobile) {\n // Non-mobile connections are not supported in Electron mode\n ws.close(1008, 'Renderer uses IPC')\n return\n }\n\n // ── HMAC 챌린지-응답 인증 ────────────────────────────────────────────\n // v3 — 모든 ws.send 가 APP_JSON opcode wrap. 수신은 binary buffer parse.\n let authenticated = false\n const nonce = randomBytes(16).toString('hex')\n ws.send(\n encodeAppJson({\n type: 'auth_challenge',\n nonce,\n } satisfies ServerMessage)\n )\n\n const authTimer = setTimeout(() => {\n if (!authenticated) {\n ws.send(\n encodeAppJson({\n type: 'auth_fail',\n reason: 'timeout',\n } satisfies ServerMessage)\n )\n ws.close()\n }\n }, authMs)\n\n ws.on('message', raw => {\n // v3 binary protocol — text frame 거부. v2 client 가 v3 server 에 붙으면\n // 첫 frame 이 text 이므로 즉시 close.\n if (typeof raw === 'string') {\n ws.send(\n encodeProtocolError(PROTOCOL_ERROR_REASON.MALFORMED_PAYLOAD)\n )\n ws.close(4005, 'binary only (v3)')\n return\n }\n const buffer = Buffer.isBuffer(raw)\n ? raw\n : Array.isArray(raw)\n ? Buffer.concat(raw as unknown as readonly Uint8Array[])\n : Buffer.from(raw as ArrayBuffer)\n\n let parsed: ReturnType<typeof parseFrame>\n try {\n parsed = parseFrame(buffer)\n } catch (err) {\n const reason =\n err instanceof FrameParseError\n ? err.reason\n : PROTOCOL_ERROR_REASON.MALFORMED_PAYLOAD\n ws.send(encodeProtocolError(reason))\n return\n }\n\n // RESIZE opcode (0x02) — 첫 frame 으로 도착 시 auth 안 됨 상태에서 무시.\n if (parsed.type === 'resize') {\n if (!authenticated) return\n const machineId =\n this.clientMeta.get(ws)?.attachedMachineId ??\n this.lastAttachedMachineId ??\n undefined\n if (machineId) {\n this.ssh.resize(machineId, parsed.cols, parsed.rows, 'mobile')\n // [resize-resettle 20260610] settle 후 올바른-size 스냅샷 재방출 (조각 race 차단).\n this.scheduleResizeFrameSnapshot(machineId)\n } else {\n log.warn(\n `[ws-manager] RESIZE opcode 도착했으나 attachedMachineId 없음 — 무시`\n )\n }\n return\n }\n\n // PROTOCOL_ERROR — client 가 server 에 mismatch 통보. 단순 close.\n if (parsed.type === 'protocol_error') {\n log.warn(\n `[ws-manager] client PROTOCOL_ERROR reason=0x${parsed.reason.toString(16)}`\n )\n ws.close()\n return\n }\n\n // 그 외 (PTY_DATA / FRAME_SNAPSHOT) — desktop 측은 송신만, 수신 무시\n if (parsed.type !== 'app_json') return\n\n try {\n const msg = parsed.json as ClientMessage\n\n if (!authenticated) {\n // hello — auth 전 도착 가능. server 가 아직 hello_ack 보낼 자격 없음 (auth 후로 미룸).\n // hello_ack 는 auth_ok 직후 송신.\n if ((msg as any).type === 'hello') {\n ;(ws as any).pendingHello = true\n return\n }\n if (msg.type === 'auth') {\n const expected = createHmac('sha256', SESSION_TOKEN)\n .update(nonce)\n .digest('hex')\n if (msg.token === expected) {\n authenticated = true\n clearTimeout(authTimer)\n ws.send(\n encodeAppJson({ type: 'auth_ok' } satisfies ServerMessage)\n )\n // 인증 성공 + pending hello 가 있었으면 hello_ack 송신\n if ((ws as any).pendingHello) {\n ws.send(encodeHelloAck())\n }\n // 초기 상태 동기화\n this.mobileClients.add(ws)\n // Phase 1 [Review Q2] — clientId 부여. AuthorityResolver 가 handover/\n // detach 시 동일 모바일 식별하는 key. WeakMap 이라 ws GC 시 자동 정리.\n this.clientMeta.set(ws, {\n clientId: `mob-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,\n })\n // A3: cap nego 받기 전 default = legacy 'pty.data'. cap.req 받으면 갱신.\n this.mobileClientCaps.set(ws, 'pty.data')\n ws.send(\n encodeAppJson({\n type: 'machine_list',\n machines: this.ssh.getMachines(),\n })\n )\n ws.send(\n encodeAppJson({\n type: 'relay_status',\n status: this.relayStatus,\n retryCount: this.relayRetryCount,\n })\n )\n } else {\n ws.send(\n encodeAppJson({\n type: 'auth_fail',\n reason: 'invalid_token',\n } satisfies ServerMessage)\n )\n ws.close()\n }\n }\n return\n }\n\n // hello — auth 후 도착 (relay path 등). hello_ack 즉시 응답.\n if ((msg as any).type === 'hello') {\n ws.send(encodeHelloAck())\n return\n }\n\n // session_attach 시 clientMeta.attachedMachineId 갱신 (RESIZE opcode + close handler 의 라우팅 키).\n // [/review fix Q2-complete] (ws as any).attachedMachineId 잔존 제거 — WeakMap 단일화.\n if (\n msg.type === 'session_attach' &&\n typeof (msg as any).machineId === 'string'\n ) {\n const mid = (msg as any).machineId as string\n const existing = this.clientMeta.get(ws)\n if (existing) existing.attachedMachineId = mid\n // A3 (Sub-Part 1F-ii-a bonus): cap=grid.diff client 면 attach 직후 GRID_SNAPSHOT 자동 송신.\n // 사용자가 snapshot.req 명시 발사 안 해도 cold attach 시 grid 즉시 받음.\n if (this.mobileClientCaps.get(ws) === 'grid.diff') {\n log.info(\n `[ws-manager] session_attach + cap=grid.diff → auto GRID_SNAPSHOT machineId=${mid}`\n )\n this.ssh.lifecycle.onAttachGridDiff(mid, frame =>\n ws.send(frame)\n )\n }\n }\n\n // A3: capability negotiation (binary protocol v3 §6.5).\n // Sub-Part 1F-ii-c: 'grid.diff' 활성화. server-side emulator (PtyLifecycleManager 의 emulators)\n // 가 silent 로 grid 누적 중 (Sub-Part 1F-i 완료). GRID_SNAPSHOT/GRID_DIFF wire 송신은 추후\n // 1F-ii-a (snapshot.req hookup) + 1F-ii-b (flushDiff broadcast) 완료 시 활성화.\n // 현 시점: cap.ack 는 'grid.diff' 응답하나 PTY_DATA 도 그대로 broadcast (filter 미적용 = 1F-ii-d).\n // mobile 측 (Part 2E) 도 미활성이라 production mobile 영향 0.\n if ((msg as any).type === 'cap.req') {\n const clientCaps = ((msg as any).client_caps as string[]) ?? []\n const selected: 'pty.data' | 'grid.diff' = clientCaps.includes(\n 'grid.diff'\n )\n ? 'grid.diff'\n : 'pty.data'\n this.mobileClientCaps.set(ws, selected)\n ws.send(\n encodeAppJson(\n buildCapAck(selected, this.config.getInstallId(), this.identity.version)\n )\n )\n return\n }\n\n // A3: snapshot.req — throttle (3/min/session) 검사 후 GRID_SNAPSHOT 응답.\n // Sub-Part 1F-ii-a: ssh.lifecycle.onSnapshotReq hookup. emulator 가 grid 누적 중 (1F-i)\n // 이라 snapshot 즉시 송신 가능. 사용자 새로고침 탭 (manual) / auto-recovery (recover) /\n // cold reattach 모두 동일 처리. 응답 frame 은 GRID_SNAPSHOT (0x04) 1개.\n if ((msg as any).type === 'snapshot.req') {\n if (this.snapshotReqThrottle(ws)) {\n log.warn(\n '[ws-manager] snapshot.req throttled (3/min/session cap)'\n )\n return\n }\n const machineId = this.clientMeta.get(ws)?.attachedMachineId\n if (!machineId) return\n const reason = (msg as any).reason ?? 'manual'\n log.info(\n `[ws-manager] snapshot.req machineId=${machineId} reason=${reason} — sending GRID_SNAPSHOT`\n )\n this.ssh.lifecycle.onSnapshotReq(machineId, frame =>\n ws.send(frame)\n )\n return\n }\n\n this.handleMessage(msg, ws)\n } catch {\n ws.send(\n encodeAppJson({\n type: 'error',\n message: 'Invalid message format',\n })\n )\n }\n })\n\n ws.on('close', () => {\n clearTimeout(authTimer)\n // Phase 1 — close handler 안에서는 ws ref 가 살아있어 WeakMap 안전 lookup.\n // resolver 가 leader=mobile + clientId 매칭 시에만 grace 시작 (false-positive 방지).\n const meta = this.clientMeta.get(ws)\n this.mobileClients.delete(ws)\n this.mobileClientCaps.delete(ws)\n // snapshotReqTimes 는 WeakMap — ws GC 시 자동 정리, 명시 cleanup 불요.\n if (meta?.clientId && meta.attachedMachineId) {\n // [A2 spike] machineId → WS lookup cleanup (handleDesktopTakeback 대상 정리).\n // 다른 ws 가 같은 머신에 이미 attach 했을 가능성은 mobileClientByMachine.get 으로\n // 본인 ws 확인 후만 delete.\n if (this.mobileClientByMachine.get(meta.attachedMachineId) === ws) {\n this.mobileClientByMachine.delete(meta.attachedMachineId)\n }\n this.ssh.authority.onMobileDetach(\n meta.attachedMachineId,\n meta.clientId\n )\n }\n })\n })\n } catch (err) {\n console.error(\n `[ws-manager] Failed to start: port ${this.wsPort} may be in use.`,\n err\n )\n }\n }\n\n // ── Relay client (connects to Fly.io relay as \"host\") ─────────────────────\n\n private connectToRelay(): void {\n // 이미 연결 중이거나 연결된 상태면 중복 연결 방지\n if (\n this.destroyed ||\n this.relayStatus === 'connecting' ||\n this.relayStatus === 'connected'\n )\n return\n\n const relayUrls = this.config.getRelayUrls()\n const rawBase = relayUrls[this.relayUrlIndex % relayUrls.length]\n // trailing slash 정규화: 설정/env 에 `.../relay/` 처럼 끝 슬래시가 있으면\n // 아래 `${base}/host/...` 결합 시 `//host/` 더블 슬래시가 생겨 일부 프록시에서\n // 모바일/호스트 경로가 깨진다. `/relay` `/relay/` 모두 동일하게 정규화한다.\n const relayBase = rawBase.replace(/\\/+$/, '')\n\n try {\n void new URL(\n relayBase\n .replace(/^wss:\\/\\//, 'https://')\n .replace(/^ws:\\/\\//, 'http://')\n )\n } catch {\n console.error(\n '[relay] Invalid RELAY_URL (cannot parse origin):',\n relayBase\n )\n return\n }\n\n this.relayRetryCount++\n this.setRelayStatus('connecting')\n\n const hostUrl = `${relayBase}/host/${SESSION_TOKEN}`\n const mobileUrl = `${relayBase}/mobile/${SESSION_TOKEN}`\n\n log.info(\n `[relay] connecting attempt=${this.relayRetryCount} urlIndex=${this.relayUrlIndex} token=${SESSION_TOKEN.slice(0, 8)}... ts=${new Date().toISOString()}`\n )\n // 진단: env/설정에 들어온 raw base 와 정규화 결과를 함께 남긴다.\n // (더블 슬래시·잘못된 호스트가 모바일 미접속 원인인지 즉시 판별하기 위함)\n // debug 레벨 — 매 재연결마다 찍히므로 프로덕션 로그 노이즈를 줄인다.\n log.debug(`[relay] base raw=\"${rawBase}\" normalized=\"${relayBase}\" host=${relayBase}/host/<token> mobile=${relayBase}/mobile/<token>`)\n\n const agentSecret = this.config.getRelayAgentSecret()\n\n const ws = new WebSocket(hostUrl, {\n headers: { Authorization: `Bearer ${agentSecret}` },\n })\n let hadError = false\n\n ws.on('open', () => {\n this.relayWs = ws\n this.relayQrUrl = mobileUrl\n this.relayRetryCount = 0\n this.relayUrlIndex = 0 // 성공 시 index 초기화\n // [/review fix] relay ws 에도 clientMeta 부여 — terminal_authority_changed 의\n // perRecipient 콜백이 clientId 비교를 위해 사용. 본 PR (Phase 1) 단일 권위 모바일\n // 시나리오 한정: relay 는 multi-mobile fan-out path 라 권위 모바일이 relay 뒤\n // 여러 명 중 하나면 isAuthority 가 모두 동일하게 적용되는 한계 존재. 정확한 per-mobile\n // 분리는 protocol 차원 변경 필요 (relay 가 mobile-id 헤더 forward 등) — follow-up.\n this.clientMeta.set(ws, {\n clientId: `relay-mob-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,\n })\n this.setRelayStatus('connected')\n this.broadcast({\n type: 'qr_code',\n // getMobileQrInfo 와 동일 — 표시 url 은 HTTPS 딥링크, relayQrUrl 은 원시 ws 유지.\n url: toPairUrl(mobileUrl),\n mode: 'relay',\n details: {\n relayToken: SESSION_TOKEN.slice(0, 8),\n localPort: this.wsPort,\n },\n })\n this.startHeartbeat()\n })\n\n ws.on('pong', () => {\n if (this.pongTimer) {\n clearTimeout(this.pongTimer)\n this.pongTimer = null\n }\n })\n\n ws.on('message', raw => {\n // v3 binary protocol — text frame 거부.\n if (typeof raw === 'string') {\n log.warn('[relay] text frame received — v3 violation, ignoring')\n return\n }\n const buffer = Buffer.isBuffer(raw)\n ? raw\n : Array.isArray(raw)\n ? Buffer.concat(raw as unknown as readonly Uint8Array[])\n : Buffer.from(raw as ArrayBuffer)\n\n let parsed: ReturnType<typeof parseFrame>\n try {\n parsed = parseFrame(buffer)\n } catch (err) {\n log.warn(`[relay] frame parse failed: ${(err as Error).message}`)\n return\n }\n\n if (parsed.type === 'resize') {\n const machineId =\n (this.relayWs &&\n this.clientMeta.get(this.relayWs)?.attachedMachineId) ??\n this.lastAttachedMachineId ??\n undefined\n if (machineId) {\n this.ssh.resize(machineId, parsed.cols, parsed.rows, 'mobile')\n // [resize-resettle 20260610] settle 후 올바른-size 스냅샷 재방출 (조각 race 차단).\n this.scheduleResizeFrameSnapshot(machineId)\n } else {\n log.warn(\n '[relay] RESIZE opcode 도착했으나 attachedMachineId 없음 (relay path)'\n )\n }\n return\n }\n\n if (parsed.type !== 'app_json') return\n\n try {\n const msg = parsed.json as ClientMessage\n // hello — relay path 의 첫 frame. desktop 이 hello_ack 응답 후 ssh 흐름.\n if ((msg as any).type === 'hello') {\n // 진단: 이 로그가 찍히면 모바일이 relay 의 /mobile/<token> 까지 도달해\n // host 소켓으로 forward 된 것 — 미접속 원인이 relay 이전(모바일 URL/토큰)인지\n // 이후(데스크탑 처리)인지 가르는 분기점. debug 레벨 — 연결마다 찍히므로 노이즈 절감.\n log.debug('[relay] hello received (mobile reached relay → forwarded to host) — sending hello_ack')\n this.relayWs?.send(encodeHelloAck())\n return\n }\n // session_attach 시 clientMeta.attachedMachineId 갱신 (RESIZE opcode 의 machineId 추적용).\n // [/review fix Q2-complete] (relayWs as any) 패턴 제거 — clientMeta 단일화.\n if (\n msg.type === 'session_attach' &&\n typeof (msg as any).machineId === 'string'\n ) {\n const mid = (msg as any).machineId as string\n if (this.relayWs) {\n const existing = this.clientMeta.get(this.relayWs)\n if (existing) existing.attachedMachineId = mid\n }\n // A3 (Sub-Part 1F-ii-a bonus): mobile path 와 동기화.\n if (\n this.relayWs &&\n this.mobileClientCaps.get(this.relayWs) === 'grid.diff'\n ) {\n log.info(\n `[relay] session_attach + cap=grid.diff → auto GRID_SNAPSHOT machineId=${mid}`\n )\n this.ssh.lifecycle.onAttachGridDiff(mid, frame =>\n this.relayWs?.send(frame)\n )\n }\n }\n // A3 (relay path): capability nego + snapshot.req. mobile path 와 동일 로직.\n // Sub-Part 1F-ii-c: 'grid.diff' 활성화 — mobile path 와 동기화.\n if ((msg as any).type === 'cap.req' && this.relayWs) {\n const clientCaps = ((msg as any).client_caps as string[]) ?? []\n const selected: 'pty.data' | 'grid.diff' = clientCaps.includes(\n 'grid.diff'\n )\n ? 'grid.diff'\n : 'pty.data'\n this.mobileClientCaps.set(this.relayWs, selected)\n this.relayWs.send(\n encodeAppJson(\n buildCapAck(selected, this.config.getInstallId(), this.identity.version)\n )\n )\n return\n }\n if ((msg as any).type === 'snapshot.req' && this.relayWs) {\n if (this.snapshotReqThrottle(this.relayWs)) {\n log.warn('[relay] snapshot.req throttled (3/min/session cap)')\n return\n }\n const machineId = this.clientMeta.get(this.relayWs)\n ?.attachedMachineId as string | undefined\n if (!machineId) return\n const reason = (msg as any).reason ?? 'manual'\n log.info(\n `[relay] snapshot.req machineId=${machineId} reason=${reason} — sending GRID_SNAPSHOT`\n )\n // relayWs.send 는 binary frame 직접 — relay 가 forward 해 mobile 도달.\n this.ssh.lifecycle.onSnapshotReq(machineId, frame =>\n this.relayWs?.send(frame)\n )\n return\n }\n // [Fix 2026-05-26] relay path 도 source 로 relayWs 전달.\n // 누락 시 handleSessionAttach 의 `if (source) { onMobileAttach(...) }` 분기가\n // skip 되어 viewer_mode 가 'attaching' 에서 'handed_off' 로 전이 안 됨 →\n // desktop UI 가 \"폰 연결중\" placeholder 에 영구 stuck.\n // relayWs 는 line ~1009 에서 clientMeta.set 으로 clientId='relay-mob-...' 보유.\n this.handleMessage(msg, this.relayWs)\n } catch {\n log.warn('[relay] Invalid app message from mobile')\n }\n })\n\n ws.on('close', (code, reasonBuf) => {\n const reason = reasonBuf?.toString() || '(none)'\n log.info(\n `[relay] close code=${code} reason=${reason} ts=${new Date().toISOString()}`\n )\n this.relayWs = null\n this.relayQrUrl = null\n this.stopHeartbeat()\n this.setRelayStatus('disconnected')\n\n if (hadError && relayUrls.length > 1) {\n // 연결 오류 발생 시 다음 URL로 즉시 전환\n const nextIndex = (this.relayUrlIndex + 1) % relayUrls.length\n const isFullCycle = nextIndex === 0\n this.relayUrlIndex = nextIndex\n log.info(\n `[relay] failover → urlIndex=${nextIndex} (${relayUrls[nextIndex]}) ts=${new Date().toISOString()}`\n )\n if (!this.destroyed) {\n // 한 바퀴 다 돌았으면 지수 백오프 적용, 아니면 즉시 재시도\n const delay = isFullCycle\n ? Math.min(5_000 * 2 ** Math.min(this.relayRetryCount, 3), 60_000)\n : 0\n setTimeout(() => this.connectToRelay(), delay)\n }\n return\n }\n\n const delay = Math.min(\n 5_000 * 2 ** Math.min(this.relayRetryCount, 3),\n 60_000\n )\n log.info(\n `[relay] disconnected — reconnecting in ${delay / 1000}s (attempt ${this.relayRetryCount + 1}) ts=${new Date().toISOString()}`\n )\n if (!this.destroyed) setTimeout(() => this.connectToRelay(), delay)\n })\n\n ws.on('error', e => {\n hadError = true\n log.info(\n `[relay] error message=\"${e.message}\" ts=${new Date().toISOString()}`\n )\n })\n\n ws.on('unexpected-response', (_req, res) => {\n log.info(\n `[relay] unexpected-response status=${res.statusCode} statusMessage=${res.statusMessage} ts=${new Date().toISOString()}`\n )\n })\n }\n\n // ── Auto-update graceful shutdown (autoplan E1) ───────────────────────────\n\n /**\n * 현재 attach 된 mobile client 의 수 (LAN + relay 합산).\n *\n * 업데이트 install gate 가 사용자 confirmation 필요 여부를 판단하는데 사용 —\n * count > 0 이면 무경고 quitAndInstall 가 mobile attached PTY 를 무경고로 죽임.\n *\n * Relay 경로는 단일 WebSocket 이지만 그 뒤에 다중 모바일이 있을 수 있어 정확한 수는\n * 알 수 없다. 본 카운트는 \"active 한 mobile transport 가 존재하는지\" 의 보수적\n * 신호 — 1 이상이면 confirmation 띄움, 0 이면 silent install.\n */\n getActiveMobileClientCount(): number {\n let n = this.mobileClients.size\n if (this.relayWs?.readyState === WebSocket.OPEN) n += 1\n return n\n }\n\n /**\n * Auto-update install 직전 graceful shutdown (autoplan E1).\n *\n * 1. 모든 mobile client (LAN + relay) 에 `app:update-installing` broadcast — 모바일은\n * \"데스크탑 업데이트 중\" 토스트 표시 후 reconnect retry 준비.\n * 2. `timeoutMs` 동안 대기 — 모바일이 자체 detach + scrollback 보존.\n * 3. 시간 만료 후 caller (main/index.ts before-quit) 가 `cleanup()` 호출 → 모든 WS close.\n *\n * 본 함수는 cleanup 자체를 하지 않는다 — caller 가 quitAndInstall 흐름에서 그대로 진행.\n * 본 함수는 idempotent — 이미 destroyed 면 즉시 resolve.\n */\n async gracefulShutdown(timeoutMs: number): Promise<void> {\n if (this.destroyed) return\n // 빌드타임 글로벌(__APP_VERSION__) 대신 주입된 identity 사용 — 코어 host 비의존(A4).\n const version = this.identity.version\n log.info(\n `[ws-manager] gracefulShutdown begin graceMs=${timeoutMs} ` +\n `mobileClients=${this.mobileClients.size} relay=${this.relayWs?.readyState === WebSocket.OPEN}`\n )\n try {\n this.broadcast({\n type: 'app:update-installing',\n graceMs: timeoutMs,\n version,\n })\n } catch (err) {\n log.warn('[ws-manager] gracefulShutdown broadcast failed', err)\n }\n await new Promise<void>(resolve => setTimeout(resolve, timeoutMs))\n log.info(\n '[ws-manager] gracefulShutdown grace expired — caller should cleanup'\n )\n }\n\n // ── Cleanup ───────────────────────────────────────────────────────────────\n\n cleanup(): void {\n this.destroyed = true\n // [Phase C3] 대기 중인 takeFromTerminal open 타이머 정리(종료 후 openSession 방지).\n for (const timer of this.pendingHandoffOpens.values()) clearTimeout(timer)\n this.pendingHandoffOpens.clear()\n this.agentRunner.cleanup()\n this.stopHeartbeat()\n this.relayWs?.terminate()\n this.relayWs = null\n for (const ws of this.mobileClients) {\n ws.close()\n }\n this.mobileClients.clear()\n // [A2 spike] machineId → WS lookup map 도 정리.\n this.mobileClientByMachine.clear()\n this.wss?.close()\n this.wss = null\n }\n}\n","/**\n * 릴레이/WS 에이전트 설정의 공통 기본값 + URL 해석 로직 (단일 출처).\n *\n * 데스크탑(electron-store + `import.meta.env` 빌드주입)·헤드리스(JSON + `process.env` 런타임)\n * 두 {@link AgentConfigStore} 구현이 **같은 기본값·같은 URL 해석**을 쓰도록 모은다.\n *\n * **순수성 (WHY)**: env 접근 방식이 패키징 환경마다 다르므로(데스크탑=빌드 define, 헤드리스=런타임\n * process.env), 본 모듈은 `process.env`/`import.meta.env` 를 **직접 참조하지 않고** 이미 읽은 값을\n * 인자로 받는다. 그래야 한 함수가 양쪽에서 동일 동작하고, 패키징 환경차·no-electron 가드에 안 걸린다.\n */\n\n/** 릴레이 기본 엔드포인트 — stored 설정도 env 도 없을 때의 최종 fallback. */\nexport const RELAY_URL_DEFAULT = 'wss://juny-api.kr/relay'\n\n/**\n * QR 페어링 딥링크(Universal/App Link)의 base origin — **릴레이 ws 호스트와 의도적으로 분리**한다.\n *\n * 릴레이 ws 는 `wss://juny-api.kr/relay/...`(별도 `apps/relay`)지만, 딥링크는 ① 게시된 앱의\n * associated-domains(iOS)·App Link autoVerify(Android) 와 ② `apple-app-site-association`·\n * `assetlinks.json`·`/pair` 리다이렉트를 **서빙하는 도메인**에 매여야 한다. 그 도메인이\n * `arv.juny-api.kr`(apps/arv) 이므로 여기에 고정한다. 실제 ws url 은 `?u=` 로 링크 안에 실어\n * 옮기므로 두 도메인이 달라도 무방하다(자기 기술적). 게시 앱과 묶인 값이라 사실상 비-가변.\n */\nexport const PAIR_LINK_BASE_DEFAULT = 'https://arv.juny-api.kr'\n\n/** 로컬 WS(모바일 LAN 직결) 서버 기본 포트. */\nexport const WS_PORT_DEFAULT = 3001\n/** relay ping 주기(ms). */\nexport const HEARTBEAT_INTERVAL_MS_DEFAULT = 30_000\n/** relay pong 미수신 타임아웃(ms) — 초과 시 좀비 연결로 보고 terminate. */\nexport const PONG_TIMEOUT_MS_DEFAULT = 10_000\n/** LAN 모바일 auth 핸드셰이크 타임아웃(ms). */\nexport const AUTH_TIMEOUT_MS_DEFAULT = 5_000\n\n/**\n * 릴레이 URL 목록 해석. 우선순위: **stored(명시 설정) > envUrl(콤마=다중 fallback) > 기본값**.\n *\n * @param stored ConfigStore 에 영속된 명시 URL 목록(있으면 최우선).\n * @param envUrl 호출자가 읽은 env 값 — 데스크탑 `import.meta.env.MAIN_VITE_RELAY_URL`,\n * 헤드리스 `process.env.MAIN_VITE_RELAY_URL ?? VIBE_RELAY_URL`. 콤마로 여러 개 가능.\n */\nexport function resolveRelayUrls(\n stored: string[] | undefined,\n envUrl: string | undefined\n): string[] {\n if (stored && stored.length > 0) return stored\n if (envUrl) {\n const urls = envUrl\n .split(',')\n .map((u) => u.trim())\n .filter(Boolean)\n if (urls.length > 0) return urls\n }\n return [RELAY_URL_DEFAULT]\n}\n","import { PAIR_LINK_BASE_DEFAULT } from '@arva/shared/relay-config'\n\n/**\n * relay 모바일 ws URL 을 폰 기본 카메라로도 동작하는 HTTPS 페어링 딥링크로 감싼다.\n *\n * 폰 카메라가 `wss://` QR 을 찍으면 아무 동작도 못 하고, 앱 미설치 시 스토어로 유도할 방법도 없다.\n * Universal Link(iOS)/App Link(Android) 로 가로채이려면 **HTTPS** 여야 하므로 ws URL 을\n * `${pairBase}/pair?u=<원시 ws url>` 로 감싼다 — 앱 설치 시 OS 가 가로채 앱을 실행하고, 미설치 시\n * 브라우저가 `/pair` 페이지를 열어 App Store/Play Store 로 리다이렉트한다.\n *\n * **pairBase 는 릴레이 ws 호스트가 아니다**: ws 는 `wss://juny-api.kr/relay/...` 지만 딥링크는\n * association 파일·`/pair` 를 서빙하고 게시 앱의 associated-domains 와 묶인 도메인\n * ({@link PAIR_LINK_BASE_DEFAULT} = `arv.juny-api.kr`)이어야 한다. 원시 ws url 을 `u` 쿼리에\n * 통째로 실어 ① 모바일이 검증된 기존 파서를 그대로 재사용하고 ② 두 도메인이 달라도 self-describing\n * 하게 한다. LAN(`ws://<lanIp>`)은 공개 도메인이 없어 본 함수를 호출하지 않는다(원시 ws QR 유지).\n *\n * @param wsMobileUrl `wss://<host>/.../mobile/<uuid>` (relay 모드 전용).\n * @param pairBase 딥링크 origin. 기본 {@link PAIR_LINK_BASE_DEFAULT}. 테스트/스테이징용 주입 가능.\n * @returns `${pairBase}/pair?u=<encoded wsMobileUrl>`.\n */\nexport function toPairUrl(\n wsMobileUrl: string,\n pairBase: string = PAIR_LINK_BASE_DEFAULT\n): string {\n const base = pairBase.replace(/\\/+$/, '')\n return `${base}/pair?u=${encodeURIComponent(wsMobileUrl)}`\n}\n","import { spawn, type ChildProcess } from 'child_process'\n\nimport { createLogger } from '../lib/logger'\n\nimport type { CockpitAgentKind, CockpitCard } from '@arva/shared/types'\n\nimport { CockpitStreamParser } from './cockpit-parser'\n\nconst log = createLogger('agent-runner')\n\n/**\n * [Cockpit] `agent.run` 으로 요청된 별도 프로세스 1건의 핸들.\n *\n * interactive PTY 와 무관한 `child_process` — `claude -p ... --output-format\n * stream-json` 같은 비대화형 one-shot 실행에 사용한다. PTY 가 아니므로 TUI / width\n * 협상 / alt-screen 충돌이 없고, stdout 은 줄 단위 stream-json 만 흐른다.\n */\ninterface RunningAgentProcess {\n /** 모바일이 출력을 라우팅하는 키 (`agent.output` 의 requestId). */\n requestId: string\n /** 이 프로세스가 속한 머신. */\n machineId: string\n /** 실제 child process 핸들. */\n proc: ChildProcess\n /**\n * [Cockpit source of truth] 이 프로세스의 stream-json 파서. stdout 을 feed 하여 구조화\n * 카드를 emit 한다. cockpit 콜백 미주입 시(테스트 등) undefined — raw `agent.output` 만 흐른다.\n */\n parser?: CockpitStreamParser\n}\n\n/**\n * [Cockpit v2 장수명] `agent.session.open` 으로 연 양방향 stream-json 세션 1건의 핸들.\n *\n * one-shot [RunningAgentProcess] 와 달리 턴 사이에도 프로세스가 살아있어, stdin 으로 user\n * 메시지를 계속 push 한다(`sendToSession`). `sessionId` 는 cockpit.card 라우팅 키로 쓰며\n * (one-shot 의 requestId 자리), 터미널 측과 공유되는 claude 세션 id(`--session-id`)와 동일.\n */\ninterface CockpitSessionProcess {\n /** cockpit.card 라우팅 키 겸 claude `--session-id`. */\n sessionId: string\n /** 이 세션이 속한 머신. */\n machineId: string\n /** 살아있는 child process (stdin 유지). */\n proc: ChildProcess\n /** stream-json 파서. stdout 을 feed 해 카드 emit. cockpit 콜백 미주입 시 undefined. */\n parser?: CockpitStreamParser\n}\n\n/**\n * [Cockpit source of truth] 파싱한 카드/세션을 broadcast 로 보내는 콜백 묶음.\n *\n * ws-manager 가 inject — `broadcast({ type:'cockpit.card'|'cockpit.session_started', ... })`\n * 로 위임한다(renderer + mobile 양쪽 fan-out). raw `agent.output`(mobile 전용) 과 **병행**.\n */\nexport interface CockpitRunnerEmit {\n onCard: (\n machineId: string,\n requestId: string,\n agent: CockpitAgentKind,\n card: CockpitCard\n ) => void\n onSessionId: (machineId: string, requestId: string, sessionId: string) => void\n}\n\n/**\n * [Cockpit] `agent.run` / `agent.kill` 실행기.\n *\n * SSHManager 와 분리한 이유: interactive PTY(localSessions/sessions) 와 lifecycle 이\n * 완전히 다르고(요청-응답 1회성, PTY 아님), CLAUDE.md 10-메서드 규칙상 ssh-manager 에\n * 더 얹지 않는다. ws-manager 가 `agent.run`/`agent.kill` 핸들러에서 본 클래스에 위임한다.\n *\n * **보안**: argv 는 모바일 어댑터가 stream-json flag 를 붙여 보낸 배열이지만, 데스크탑은\n * 신뢰 경계로서 [validateAgentArgv] 로 1차 검사한다(argv[0] allowlist(claude/codex/gemini)\n * + 원소 개수·총 길이 상한, mobile 측 P3a 가드와 이중 방어). 검사 통과 후 `shell:false` 로\n * 배열 인자 spawn 한다 — 셸을 안 거치므로 prompt 의 공백/따옴표가 인자 경계를 깨지 않는다\n * (인젝션 표면 최소화).\n */\nexport class AgentRunner {\n /** requestId → 실행 중 프로세스. exit/kill 시 제거. */\n private processes = new Map<string, RunningAgentProcess>()\n\n /** [Cockpit v2 장수명] sessionId → 열린 양방향 세션. close/exit 시 제거. */\n private sessions = new Map<string, CockpitSessionProcess>()\n\n /**\n * stdout/stderr/exit 을 모바일로 보내는 콜백. ws-manager 가 생성자에서 inject —\n * `broadcast({ type: 'agent.output', ... })` 로 위임한다.\n */\n private emit: (\n machineId: string,\n requestId: string,\n stream: 'stdout' | 'stderr' | 'exit',\n payload: { data?: string; exitCode?: number }\n ) => void\n\n /**\n * [Cockpit source of truth] 파싱 카드/세션 emit. 미주입 시(테스트 등) stdout 을 파싱하지\n * 않고 raw `agent.output` 만 흐른다 — 기존 동작 보존.\n */\n private cockpit?: CockpitRunnerEmit\n\n constructor(\n emit: (\n machineId: string,\n requestId: string,\n stream: 'stdout' | 'stderr' | 'exit',\n payload: { data?: string; exitCode?: number }\n ) => void,\n cockpit?: CockpitRunnerEmit\n ) {\n this.emit = emit\n this.cockpit = cockpit\n }\n\n /**\n * 별도 프로세스 실행. [validateAgentCommand] 실패 시 즉시 stderr + exit(1) emit 하고\n * spawn 하지 않는다 — 모바일이 에러 카드를 그릴 수 있게.\n *\n * 같은 requestId 가 이미 실행 중이면 무시(중복 송신 방어). 동일 머신에 다른 requestId\n * 의 동시 실행은 허용 — 호출자(모바일)가 1회성으로 쓰는 게 일반적이지만 강제하지 않음.\n */\n run(\n machineId: string,\n requestId: string,\n argv: string[],\n cwd?: string\n ): void {\n if (this.processes.has(requestId)) {\n log.warn(`[agent-runner] duplicate run ignored requestId=${requestId}`)\n return\n }\n\n // stdin='ignore' — `claude -p` 는 prompt 를 argv 로 받으므로 stdin 불필요. 기본 'pipe'\n // 로 두면 claude 가 stdin 입력을 3초 대기(\"no stdin data received in 3s\") 후 진행한다.\n const sp = spawnAgentProcess(argv, cwd, 'ignore')\n if (!sp.ok) {\n log.warn(\n `[agent-runner] ${sp.stage} fail machine=${machineId} reason=${sp.reason}`\n )\n this.emit(machineId, requestId, 'stderr', {\n data:\n sp.stage === 'validate'\n ? `agent.run rejected: ${sp.reason}\\n`\n : `spawn failed: ${sp.reason}\\n`,\n })\n this.emit(machineId, requestId, 'exit', { exitCode: 1 })\n return\n }\n const proc = sp.proc\n\n log.info(\n `[agent-runner] run machine=${machineId} requestId=${requestId} cwd=${cwd ?? '(default)'} argv=${JSON.stringify(argv)}`\n )\n\n const entry: RunningAgentProcess = { requestId, machineId, proc }\n // [Cockpit source of truth] stream-json 파서 부착 — stdout 을 구조화 카드로 변환해\n // cockpit.card broadcast. cockpit 콜백 미주입 시 파서 없음(raw agent.output 만).\n if (this.cockpit) {\n const cockpit = this.cockpit\n entry.parser = new CockpitStreamParser({\n onCard: card => cockpit.onCard(machineId, requestId, 'claude', card),\n onSessionId: sid => cockpit.onSessionId(machineId, requestId, sid),\n onParseFail: (line, err) =>\n log.warn(\n `[cockpit] parse fail req=${requestId}: ${(err as Error).message} line=${line.slice(0, 120)}`\n ),\n })\n }\n this.processes.set(requestId, entry)\n\n proc.stdout?.setEncoding('utf-8')\n proc.stdout?.on('data', (chunk: string) => {\n this.emit(machineId, requestId, 'stdout', { data: chunk })\n // raw 송신과 병행 — 파서가 카드를 emit (cockpit.card broadcast).\n entry.parser?.feed(chunk)\n })\n proc.stderr?.setEncoding('utf-8')\n proc.stderr?.on('data', (chunk: string) => {\n this.emit(machineId, requestId, 'stderr', { data: chunk })\n })\n proc.on('error', (err: Error) => {\n this.emit(machineId, requestId, 'stderr', {\n data: `process error: ${err.message}\\n`,\n })\n // spawn 실패(ENOENT 등)는 'exit' 이 뒤따라오지 않는다 — Node 는 'error' 만 emit.\n // 모바일은 'exit' frame 에서만 busy 를 해제하므로, 여기서 exit 을 보장하지 않으면\n // macOS(직접 spawn) 에서 스피너가 영구 hang 한다. Windows(cmd.exe /c)는 항상\n // 프로세스가 떠 'exit' 이 와서 이 분기를 안 타므로, 같은 오류에 플랫폼 동작이 갈렸다.\n // 드물게 'exit' 가 뒤따를 수 있어 processes 존재 여부로 중복 emit 을 가드한다.\n if (this.processes.has(requestId)) {\n this.processes.delete(requestId)\n this.emit(machineId, requestId, 'exit', { exitCode: 1 })\n }\n })\n proc.on('exit', (code: number | null) => {\n // 'error' 경로가 먼저 종료 처리했으면 중복 emit 방지.\n if (!this.processes.has(requestId)) return\n this.processes.delete(requestId)\n // [Cockpit] buffer 잔여 + 진행 중 assistant text final flush (마지막 카드 누락 방지).\n entry.parser?.close()\n log.info(\n `[agent-runner] exit machine=${machineId} requestId=${requestId} code=${code ?? -1}`\n )\n this.emit(machineId, requestId, 'exit', { exitCode: code ?? -1 })\n })\n }\n\n /**\n * 실행 중 프로세스 종료. requestId 지정 시 그것만, 미지정 시 해당 머신의 모든\n * agent.run 프로세스를 SIGTERM. (interactive PTY agent 종료는 ssh-manager 책임 —\n * 본 클래스는 자기가 spawn 한 것만 안다.)\n */\n kill(machineId: string, requestId?: string): void {\n if (requestId) {\n const entry = this.processes.get(requestId)\n if (entry) {\n log.info(`[agent-runner] kill requestId=${requestId}`)\n entry.proc.kill('SIGTERM')\n }\n return\n }\n for (const entry of this.processes.values()) {\n if (entry.machineId === machineId) {\n log.info(\n `[agent-runner] kill (by machine) requestId=${entry.requestId}`\n )\n entry.proc.kill('SIGTERM')\n }\n }\n }\n\n /**\n * [Cockpit v2 장수명] stream-json 양방향 세션을 연다.\n *\n * one-shot [run] 과 달리 프로세스가 턴 사이에도 살아있어, stdin 으로 user 메시지를 계속\n * push(`sendToSession`)하고 stdout 의 stream-json 은 파서가 카드로 변환한다. cockpit.card\n * 라우팅 키 자리(requestId)에 sessionId 를 그대로 써 기존 broadcast 경로를 재사용한다.\n *\n * `resume` (Phase C): true 면 `--resume <sessionId>` 로 **기존 세션을 이어받는다**(터미널이\n * 만든 세션을 콕핏이 인계 — 같은 cwd=같은 풀). false 면 `--session-id <sessionId>` 로 새 세션을\n * 그 id 로 생성. 둘 다 같은 jsonl 풀이라 추후 터미널이 `claude --resume <id>` 로 도로 이어받을 수 있다.\n *\n * 부수효과: stdout → cockpit.onCard broadcast(renderer+mobile). 종료/실패 시 sessions 에서\n * 제거 + 'exit' emit(렌더러가 세션 종료 인지). raw stdout 은 mobile 로 fan-out 하지 않는다 —\n * 장수명은 현재 데스크탑 전용(back-compat).\n */\n openSession(\n machineId: string,\n sessionId: string,\n cwd?: string,\n resume?: boolean\n ): void {\n if (this.sessions.has(sessionId)) {\n log.warn(\n `[agent-runner] duplicate openSession ignored sessionId=${sessionId}`\n )\n return\n }\n // 장수명 claude: prompt 는 stdin(stream-json user 봉투)으로 받으므로 positional 없음.\n // resume → 기존 세션 이어받기, 아니면 그 id 로 새 세션(둘 다 같은 cwd 풀, 터미널 공유 — Phase C).\n // --verbose 는 -p stream-json 에서 중간 이벤트(stream_event/assistant/result) 방출에 필수 —\n // 없으면 최종 result 만 와 스트리밍이 죽는다. --include-partial-messages 로 text_delta 까지.\n // user_input echo 는 렌더러가 직접 카드로 그린다(파서가 user 텍스트로 카드를 안 만듦) — 그래서\n // --replay-user-messages 는 콕핏에 무의미(향후 파서 확장 시 중복 위험)하므로 넣지 않는다.\n const argv = [\n 'claude',\n '-p',\n '--input-format',\n 'stream-json',\n '--output-format',\n 'stream-json',\n '--verbose',\n '--include-partial-messages',\n resume ? '--resume' : '--session-id',\n sessionId,\n ]\n const sp = spawnAgentProcess(argv, cwd, 'pipe')\n if (!sp.ok) {\n log.warn(\n `[agent-runner] openSession ${sp.stage} fail machine=${machineId} sessionId=${sessionId} reason=${sp.reason}`\n )\n this.emit(machineId, sessionId, 'stderr', {\n data: `agent.session.open failed: ${sp.reason}\\n`,\n })\n this.emit(machineId, sessionId, 'exit', { exitCode: 1 })\n return\n }\n const proc = sp.proc\n\n log.info(\n `[agent-runner] openSession machine=${machineId} sessionId=${sessionId} cwd=${cwd ?? '(default)'} resume=${resume ?? false}`\n )\n\n const entry: CockpitSessionProcess = { sessionId, machineId, proc }\n if (this.cockpit) {\n const cockpit = this.cockpit\n entry.parser = new CockpitStreamParser({\n onCard: card => cockpit.onCard(machineId, sessionId, 'claude', card),\n onSessionId: sid => cockpit.onSessionId(machineId, sessionId, sid),\n onParseFail: (line, err) =>\n log.warn(\n `[cockpit] parse fail session=${sessionId}: ${(err as Error).message} line=${line.slice(0, 120)}`\n ),\n })\n }\n this.sessions.set(sessionId, entry)\n\n proc.stdout?.setEncoding('utf-8')\n proc.stdout?.on('data', (chunk: string) => {\n // 장수명은 raw agent.output 을 fan-out 하지 않고 파서만 feed (데스크탑 전용 카드 경로).\n entry.parser?.feed(chunk)\n })\n proc.stderr?.setEncoding('utf-8')\n proc.stderr?.on('data', (chunk: string) => {\n log.warn(\n `[agent-runner] session stderr sessionId=${sessionId}: ${chunk.trimEnd()}`\n )\n })\n proc.on('error', (err: Error) => {\n // spawn 후 비동기 오류(예: 파이프 끊김). exit 가 안 따라올 수 있어 여기서 정리 보장.\n log.warn(\n `[agent-runner] session error sessionId=${sessionId}: ${err.message}`\n )\n if (this.sessions.has(sessionId)) {\n this.sessions.delete(sessionId)\n this.emit(machineId, sessionId, 'exit', { exitCode: 1 })\n }\n })\n proc.on('exit', (code: number | null) => {\n if (!this.sessions.has(sessionId)) return\n this.sessions.delete(sessionId)\n entry.parser?.close()\n log.info(\n `[agent-runner] session exit machine=${machineId} sessionId=${sessionId} code=${code ?? -1}`\n )\n this.emit(machineId, sessionId, 'exit', { exitCode: code ?? -1 })\n })\n }\n\n /**\n * [Cockpit v2 장수명] 열린 세션 stdin 으로 user 메시지를 push (프롬프트 / approval 응답).\n *\n * stream-json user 봉투로 직렬화해 한 줄(JSON + '\\n')로 write 한다. 세션이 없으면(미오픈/\n * 종료됨) 무시 — 호출자(ws)가 open 을 보장한다. 콕핏 user_input 카드는 렌더러가 직접 그린다\n * (파서가 user 텍스트로 카드를 만들지 않음).\n */\n sendToSession(sessionId: string, text: string): void {\n const entry = this.sessions.get(sessionId)\n if (!entry) {\n log.warn(`[agent-runner] sendToSession: no session sessionId=${sessionId}`)\n return\n }\n const envelope = JSON.stringify({\n type: 'user',\n message: { role: 'user', content: [{ type: 'text', text }] },\n })\n entry.proc.stdin?.write(`${envelope}\\n`)\n }\n\n /**\n * [Cockpit v2 장수명] 세션 종료(SIGTERM). 실제 정리(sessions 제거 + 'exit' emit)는 proc\n * 'exit' 핸들러가 수행 — [kill] 과 동일 패턴(중복 close 안전).\n *\n * `sessionId` 지정 시 그 세션만. 미지정 + `machineId` 지정 시 **그 머신의 콕핏 세션 전부**를\n * 닫는다(Phase C: 콕핏이 resume 으로 연 실제 id 를 렌더러가 몰라도 머신 단위로 닫게 — 단일 세션이라\n * 보통 1개). 둘 다 없으면 no-op.\n */\n closeSession(sessionId?: string, machineId?: string): void {\n if (sessionId) {\n const entry = this.sessions.get(sessionId)\n if (entry) {\n log.info(`[agent-runner] closeSession sessionId=${sessionId}`)\n entry.proc.kill('SIGTERM')\n }\n return\n }\n if (machineId) {\n for (const entry of this.sessions.values()) {\n if (entry.machineId === machineId) {\n log.info(\n `[agent-runner] closeSession (by machine) machine=${machineId} sessionId=${entry.sessionId}`\n )\n entry.proc.kill('SIGTERM')\n }\n }\n }\n }\n\n /**\n * [Cockpit 신선도 20260614] 한 머신에서 데스크탑이 보유한 running 콕핏 세션 id 목록.\n *\n * 방금 [openSession] 한 장수명 세션은 `.jsonl` flush 전이라 디스크 스캔에서 누락된다.\n * ssh-manager 가 이 in-memory 진실을 mergeRunningSessions 로 합류시켜 picker 가 \"현재\n * 세션\"을 즉시 보이게 한다(ws-manager 가 `setActiveSessionProvider` 로 후주입).\n */\n getActiveSessionIds(machineId: string): string[] {\n const ids: string[] = []\n for (const entry of this.sessions.values()) {\n if (entry.machineId === machineId) ids.push(entry.sessionId)\n }\n return ids\n }\n\n /** 앱 종료 시 모든 프로세스·세션 정리. */\n cleanup(): void {\n for (const entry of this.processes.values()) {\n try {\n entry.proc.kill('SIGTERM')\n } catch {\n // already dead\n }\n }\n this.processes.clear()\n for (const entry of this.sessions.values()) {\n try {\n entry.proc.kill('SIGTERM')\n } catch {\n // already dead\n }\n }\n this.sessions.clear()\n }\n}\n\n/**\n * [Cockpit] argv 검증 + OS 분기 + spawn 을 한 곳에 모은 헬퍼.\n *\n * one-shot([AgentRunner.run]) 과 장수명([AgentRunner.openSession]) 이 공유한다 — 둘의 유일한\n * 차이는 stdin 모드뿐이다(one-shot 은 prompt 를 argv 로 받아 'ignore', 장수명은 stdin 으로\n * user 메시지를 push 하므로 'pipe'). 검증/spawn 실패는 throw 하지 않고 `{ ok:false, stage,\n * reason }` 로 돌려, 호출자가 emit 형식(stderr 메시지·종료 코드)을 자기 맥락에 맞게 정한다.\n *\n * - Unix: argv 그대로 spawn(shell:false). PATH 가 claude 를 찾는다.\n * - Windows: claude 는 보통 `claude.cmd`(npm shim) 라 shell:false 직접 spawn 시 ENOENT/EINVAL.\n * 그래서 `cmd.exe /d /s /c claude ...` 로 감싸되 **인자는 배열로** 넘긴다 — Node 가 각 인자를\n * Windows 규칙으로 자동 escape(공백/한글은 \"...\" 로 감쌈)하므로, 모바일이 보낸 작은따옴표가\n * prompt 를 자르던 문제가 사라진다(shell:true 문자열 방식과 달리 따옴표 파싱을 우리가 안 함).\n * - env.PATH 는 [augmentedPath] 로 보강(macOS GUI 앱은 login shell PATH 미상속 → claude ENOENT).\n */\nfunction spawnAgentProcess(\n argv: string[],\n cwd: string | undefined,\n stdin: 'ignore' | 'pipe'\n):\n | { ok: true; proc: ChildProcess }\n | { ok: false; stage: 'validate' | 'spawn'; reason: string } {\n const validation = validateAgentArgv(argv)\n if (!validation.ok) {\n return {\n ok: false,\n stage: 'validate',\n reason: validation.reason ?? 'invalid argv',\n }\n }\n const isWin = process.platform === 'win32'\n const command = isWin ? (process.env.COMSPEC ?? 'cmd.exe') : argv[0]\n const args = isWin ? ['/d', '/s', '/c', ...argv] : argv.slice(1)\n try {\n const proc = spawn(command, args, {\n // shell:false (기본) — 배열 인자가 곧 인자 경계. Node 가 Windows escape 적용.\n cwd,\n env: { ...process.env, TERM: 'dumb', PATH: augmentedPath() },\n stdio: [stdin, 'pipe', 'pipe'],\n windowsHide: true,\n // cmd.exe 인자에 직접 따옴표를 넣지 않고 Node 자동 escape 에 맡김(작은따옴표 버그 방지).\n windowsVerbatimArguments: false,\n })\n return { ok: true, proc }\n } catch (err) {\n return { ok: false, stage: 'spawn', reason: (err as Error).message }\n }\n}\n\n/**\n * [Cockpit] macOS GUI 앱 PATH 보강.\n *\n * macOS 에서 Electron 앱은 login shell(.zshrc 등)의 PATH 를 물려받지 않아\n * Homebrew(`/opt/homebrew/bin`, `/usr/local/bin`), nvm/asdf, 사용자 로컬\n * (`~/.local/bin`) 에 설치된 claude 를 ENOENT 로 못 찾는다. 흔한 설치 경로를\n * 기존 PATH 에 append 한다 (중복은 무해 — 앞쪽 우선이라 시스템 경로 우선순위 유지).\n *\n * Windows/Linux 에서는 기존 PATH 가 보통 충분 — 추가 경로가 없거나 무해.\n */\nfunction augmentedPath(): string {\n const sep = process.platform === 'win32' ? ';' : ':'\n const current = process.env.PATH ?? process.env.Path ?? ''\n if (process.platform !== 'darwin') return current\n const home = process.env.HOME ?? ''\n const extra = [\n '/opt/homebrew/bin',\n '/usr/local/bin',\n '/usr/bin',\n '/bin',\n home ? `${home}/.local/bin` : '',\n home ? `${home}/.npm-global/bin` : '',\n ].filter(Boolean)\n const have = new Set(current.split(sep))\n const missing = extra.filter(p => !have.has(p))\n return missing.length > 0 ? `${current}${sep}${missing.join(sep)}` : current\n}\n\n/**\n * [Cockpit] `agent.run` argv 1차 검증 (데스크탑 신뢰 경계).\n *\n * mobile 측이 P3a 에서 prompt newline 거부 + sessionId 정규식 검사를 이미 하지만,\n * relay 를 거치는 임의 페이로드를 데스크탑이 무조건 신뢰하면 안 된다. argv 방식은\n * 셸을 안 거쳐(shell:false) 인젝션 표면이 작지만, executable(argv[0]) allowlist 로\n * \"cockpit 이 띄울 법한 AI CLI 인가\" 를 보수적으로 검증한다.\n *\n * - argv[0] 가 알려진 agent CLI (claude/codex/gemini) 인가\n * - argv 원소 개수/총 길이 상한 (메모리/오용 방어)\n *\n * 본 검사는 \"사고 방지\" 수준 — 진짜 격리는 후속(전용 user / 컨테이너) 과제.\n */\nexport function validateAgentArgv(argv: string[]): {\n ok: boolean\n reason?: string\n} {\n if (!Array.isArray(argv) || argv.length === 0) {\n return { ok: false, reason: 'empty argv' }\n }\n if (argv.length > 64) {\n return { ok: false, reason: 'too many args' }\n }\n const totalLen = argv.reduce((n, a) => n + (a?.length ?? 0), 0)\n if (totalLen > 16384) {\n return { ok: false, reason: 'argv too long' }\n }\n for (const a of argv) {\n if (typeof a !== 'string') {\n return { ok: false, reason: 'non-string arg' }\n }\n }\n const ALLOWED_AGENTS = new Set(['claude', 'codex', 'gemini'])\n if (!ALLOWED_AGENTS.has(argv[0])) {\n return { ok: false, reason: `unrecognized executable: \"${argv[0]}\"` }\n }\n return { ok: true }\n}\n","/**\r\n * [Cockpit] stream-json 파서의 **무상태** 헬퍼 — 포맷팅/매칭만.\r\n *\r\n * 파서 본체(`cockpit-parser.ts`)에서 분리한 이유: stateful 이벤트 핸들러(세션/턴 상태)와\r\n * 무상태 변환을 갈라 파일당 10-함수 한도(.claude/rules)를 지키고, 헬퍼만 단독 단위 테스트한다.\r\n * 본 파일의 함수들은 모바일 `stream_json_parser.dart` 의 동명 private 헬퍼를 1:1 포팅한 것 —\r\n * 동작이 모바일과 동일해야 같은 fixture(F1-F4)로 양쪽 회귀를 가드할 수 있다.\r\n */\r\n\r\n/**\r\n * tool args(객체) → display 문자열.\r\n *\r\n * 단일 인자면 값만(`Read(\"path\")` → `path`), 다중이면 `k=v` 결합, 빈 객체면 `(no args)`.\r\n */\r\nexport function formatToolArgs(args: Record<string, unknown>): string {\r\n const keys = Object.keys(args)\r\n if (keys.length === 0) return '(no args)'\r\n if (keys.length === 1) return `${args[keys[0]]}`\r\n return keys.map(k => `${k}=${args[k]}`).join(', ')\r\n}\r\n\r\n/**\r\n * tool_result 의 `content`(문자열 또는 블록 배열)를 평탄화한 텍스트.\r\n *\r\n * 배열이면 `{type:'text', text}` 블록은 text 만, 그 외 블록은 JSON 직렬화로 합친다.\r\n * null/그 외는 빈 문자열 또는 `String(...)`.\r\n */\r\nexport function extractToolResultOutput(content: unknown): string {\r\n if (content == null) return ''\r\n if (typeof content === 'string') return content\r\n if (Array.isArray(content)) {\r\n const parts: string[] = []\r\n for (const item of content) {\r\n if (item && typeof item === 'object') {\r\n const t = (item as { type?: unknown }).type\r\n if (t === 'text') {\r\n const text = (item as { text?: unknown }).text\r\n parts.push(typeof text === 'string' ? text : '')\r\n } else {\r\n parts.push(JSON.stringify(item))\r\n }\r\n } else {\r\n parts.push(String(item))\r\n }\r\n }\r\n return parts.join('\\n')\r\n }\r\n return String(content)\r\n}\r\n\r\n/**\r\n * 권한 거부 매칭 — claude-code 표준 문구(\"Claude requested permissions to use X, but you\r\n * haven't granted it yet.\") 검출.\r\n *\r\n * §8-A.0 fallback: explicit `permission_request` 이벤트가 없어, `is_error` tool_result 의 이\r\n * 문구로 `approval` 카드를 합성한다. 다국어/변형은 미지원(spike minimum) — V2.1+ 다국어 매칭.\r\n */\r\nexport function isPermissionDenial(output: string): boolean {\r\n return (\r\n output.includes('requested permissions to use') &&\r\n output.includes(\"haven't granted\")\r\n )\r\n}\r\n\r\n/**\r\n * tool_use_id → tool name. spike minimum: 매핑 미보관 — placeholder(`'tool'`/`'unknown'`).\r\n *\r\n * ToolCall 의 id 와 ToolResult 의 tool_use_id 페어링은 호출자(콕핏 provider) 책임.\r\n */\r\nexport function toolNameFromId(toolUseId: string): string {\r\n return toolUseId.length === 0 ? 'unknown' : 'tool'\r\n}\r\n","import type { CockpitCard } from '@arva/shared/types'\r\n\r\nimport {\r\n extractToolResultOutput,\r\n formatToolArgs,\r\n isPermissionDenial,\r\n toolNameFromId,\r\n} from './cockpit-parser-utils'\r\n\r\n/**\r\n * [Cockpit] 파서 콜백 묶음. emit 봉투(`requestId`/`ts`/`agent`)는 호출자(agent-runner)가 부착.\r\n */\r\nexport interface CockpitParserCallbacks {\r\n /** 카드 1건 emit. */\r\n onCard: (card: CockpitCard) => void\r\n /** `system/init` 의 session_id — `--resume` 영속 / 현재 세션 표시용. */\r\n onSessionId?: (sessionId: string) => void\r\n /** JSON 파싱 실패 — raw line 보존(디버그). 다음 라인 처리 계속(fail-safe, RC2). */\r\n onParseFail?: (rawLine: string, error: unknown) => void\r\n /** 미지원 type/event — skip 로그용 (production 무시 OK). */\r\n onUnknownType?: (type: string) => void\r\n}\r\n\r\n/**\r\n * [Cockpit V2 — desktop source of truth] claude-code `--output-format stream-json` stdout 을\r\n * sealed [CockpitCard] 로 변환하는 **stateful** 파서.\r\n *\r\n * 모바일 `stream_json_parser.dart`(StreamJsonParser)의 **충실한 1:1 포트** — 데스크탑이\r\n * source of truth 가 되며 파싱을 한 곳으로 모은다(모바일 파서는 Phase 2 에서 폐기). 동작/회귀가\r\n * 모바일과 동일해야 하므로(같은 fixture F1-F4 로 가드), 핸들러 분기·dedup 규칙·flush 타이밍을\r\n * 원본 그대로 유지한다. 무상태 포맷 헬퍼는 `cockpit-parser-utils.ts` 로 분리(10-함수 한도).\r\n *\r\n * 핵심 규칙(원본 주석 보존):\r\n * - `--include-partial-messages` 시 같은 텍스트가 (1) stream_event delta→flush, (2)\r\n * `type:\"assistant\"` full message 두 경로로 와 **중복 카드**가 된다. `emittedAssistantTextThisTurn`\r\n * 가드로 full message 의 text item 을 skip(tool_use 는 유지). turn 종료(`result`)에서 리셋.\r\n * - 라이브 스트리밍: delta 마다 `isStreaming:true` emit → 호출자가 직전 streaming 카드를 replace\r\n * (카드 누적 방지). `flushActiveAssistant` 가 `isStreaming:false` 로 최종 확정.\r\n * - `turn_complete` 는 `result` 이벤트에서만 emit(message_stop 아님) — \"응답 완료\" 유일 신호.\r\n * - 권한 거부(§8-A.0): explicit permission 이벤트가 없어, is_error tool_result 의 표준 문구를\r\n * `approval`(options=[], risk=high) 로 합성.\r\n */\r\nexport class CockpitStreamParser {\r\n private readonly cb: CockpitParserCallbacks\r\n /** 부분 라인 버퍼 — chunk 가 newline 중간에 끊겨도 안전. */\r\n private lineBuffer = ''\r\n /** 진행 중 assistant text 누적 — text_delta progressive. content_block_stop 시 final. */\r\n private activeAssistantText: string | null = null\r\n /** 이번 turn 에서 stream_event 경로가 assistant text 를 이미 emit 했는지(중복 차단). */\r\n private emittedAssistantTextThisTurn = false\r\n /** 진행 중 tool_use 의 input JSON 누적 — input_json_delta progressive. */\r\n private activeToolInputJson: string | null = null\r\n private activeToolName: string | null = null\r\n\r\n constructor(cb: CockpitParserCallbacks) {\r\n this.cb = cb\r\n }\r\n\r\n /**\r\n * stdout chunk 수신 — 라인 단위 buffering 후 parse. 미완 라인(newline 미종료)은 buffer 보존,\r\n * 다음 chunk 와 이어 처리.\r\n */\r\n feed(chunk: string): void {\r\n this.lineBuffer += chunk\r\n const lines = this.lineBuffer.split('\\n')\r\n // 마지막 element 가 unterminated line → 다시 buffer 에 보존.\r\n this.lineBuffer = lines.pop() ?? ''\r\n for (const line of lines) {\r\n const trimmed = line.trim()\r\n if (trimmed.length === 0) continue\r\n this.parseLine(trimmed)\r\n }\r\n }\r\n\r\n /** 프로세스 종료 시 — buffer 잔여 처리 + 진행 중 assistant text final flush. close 후 feed 금지. */\r\n close(): void {\r\n const remaining = this.lineBuffer.trim()\r\n this.lineBuffer = ''\r\n if (remaining.length > 0) this.parseLine(remaining)\r\n this.flushActiveAssistant()\r\n }\r\n\r\n /** 단일 라인 parse — top-level type 분기. `system`(작음)은 inline 처리. */\r\n private parseLine(line: string): void {\r\n let raw: Record<string, unknown>\r\n try {\r\n raw = JSON.parse(line) as Record<string, unknown>\r\n } catch (e) {\r\n this.cb.onParseFail?.(line, e)\r\n return\r\n }\r\n const type = typeof raw.type === 'string' ? raw.type : null\r\n if (type === null) {\r\n this.cb.onUnknownType?.('(missing type)')\r\n return\r\n }\r\n try {\r\n switch (type) {\r\n case 'system': {\r\n // system/init 에서 session_id 추출(카드 emit 없음). 그 외 subtype 은 V2.1+ carry-over.\r\n const subtype =\r\n typeof raw.subtype === 'string' ? raw.subtype : '(none)'\r\n if (subtype === 'init') {\r\n const sid =\r\n typeof raw.session_id === 'string' ? raw.session_id : null\r\n if (sid) this.cb.onSessionId?.(sid)\r\n } else {\r\n this.cb.onUnknownType?.(`system/${subtype}`)\r\n }\r\n break\r\n }\r\n case 'stream_event':\r\n this.handleStreamEvent(raw)\r\n break\r\n case 'assistant':\r\n this.handleAssistantFinal(raw)\r\n break\r\n case 'user':\r\n this.handleUserMessage(raw)\r\n break\r\n case 'result':\r\n this.handleResult(raw)\r\n break\r\n default:\r\n this.cb.onUnknownType?.(type)\r\n }\r\n } catch (e) {\r\n this.cb.onParseFail?.(line, e)\r\n }\r\n }\r\n\r\n /** stream_event 분기 — message_stop=flush, content_block_*=[handleContentBlock] 위임. */\r\n private handleStreamEvent(raw: Record<string, unknown>): void {\r\n const event = raw.event\r\n if (!event || typeof event !== 'object') return\r\n const ev = event as Record<string, unknown>\r\n const eventType = typeof ev.type === 'string' ? ev.type : null\r\n switch (eventType) {\r\n case 'message_start':\r\n case 'message_delta':\r\n break // usage metadata — skip (V2.1+)\r\n case 'message_stop':\r\n // assistant turn 종료 — 진행 중 assistant text final emit.\r\n this.flushActiveAssistant()\r\n break\r\n case 'content_block_start':\r\n case 'content_block_delta':\r\n case 'content_block_stop':\r\n this.handleContentBlock(eventType, ev)\r\n break\r\n default:\r\n this.cb.onUnknownType?.(`stream_event/${eventType ?? '(none)'}`)\r\n }\r\n }\r\n\r\n /**\r\n * content_block start/delta/stop 통합 — 진행 중 assistant text / tool_use input 누적 + emit.\r\n *\r\n * (모바일은 3 메서드로 나뉘지만 본 포트는 10-함수 한도로 1 메서드 통합 — 분기 로직은 동일.)\r\n */\r\n private handleContentBlock(\r\n eventType: string,\r\n event: Record<string, unknown>\r\n ): void {\r\n if (eventType === 'content_block_start') {\r\n const block = event.content_block\r\n if (block && typeof block === 'object') {\r\n const blockType = (block as { type?: unknown }).type\r\n if (blockType === 'tool_use') {\r\n // tool_use 시작 — input_json_delta 누적 준비. id 페어링은 호출자 책임.\r\n const name = (block as { name?: unknown }).name\r\n this.activeToolName = typeof name === 'string' ? name : null\r\n this.activeToolInputJson = ''\r\n } else if (blockType === 'text') {\r\n this.activeAssistantText = '' // text block 시작 — 누적 준비.\r\n }\r\n // 'thinking'/'tool_reference' 은 무시.\r\n }\r\n return\r\n }\r\n if (eventType === 'content_block_delta') {\r\n const delta = event.delta\r\n if (!delta || typeof delta !== 'object') return\r\n const d = delta as Record<string, unknown>\r\n const deltaType = typeof d.type === 'string' ? d.type : null\r\n if (deltaType === 'text_delta') {\r\n const text = typeof d.text === 'string' ? d.text : null\r\n if (text !== null && this.activeAssistantText !== null) {\r\n this.activeAssistantText += text\r\n // 라이브 스트리밍 — 매 delta 마다 누적 텍스트를 isStreaming=true 로 emit. 호출자가\r\n // \"직전이 streaming 카드면 replace\" 하므로 카드가 누적되지 않고 한 카드가 실시간 갱신.\r\n this.cb.onCard({\r\n kind: 'assistant',\r\n text: this.activeAssistantText,\r\n isStreaming: true,\r\n })\r\n this.emittedAssistantTextThisTurn = true\r\n }\r\n } else if (deltaType === 'input_json_delta') {\r\n const partial =\r\n typeof d.partial_json === 'string' ? d.partial_json : null\r\n if (partial !== null && this.activeToolInputJson !== null) {\r\n this.activeToolInputJson += partial\r\n }\r\n }\r\n // thinking_delta/signature_delta 무시.\r\n return\r\n }\r\n // content_block_stop — tool_use input 완성 → tool_call emit; text block → flush.\r\n if (this.activeToolName !== null && this.activeToolInputJson !== null) {\r\n let args: Record<string, unknown>\r\n try {\r\n args =\r\n this.activeToolInputJson.length === 0\r\n ? {}\r\n : (JSON.parse(this.activeToolInputJson) as Record<string, unknown>)\r\n } catch {\r\n args = { '(raw)': this.activeToolInputJson }\r\n }\r\n this.cb.onCard({\r\n kind: 'tool_call',\r\n toolName: this.activeToolName,\r\n args: formatToolArgs(args),\r\n })\r\n this.activeToolName = null\r\n this.activeToolInputJson = null\r\n }\r\n // text block 종료 — content_block_stop 에서도 1회 emit 보장(assistant full 이 별도로 안 오는 경우).\r\n this.flushActiveAssistant()\r\n }\r\n\r\n /**\r\n * assistant full message — content[] 의 각 item 처리.\r\n *\r\n * stream_event delta 가 이미 text 를 emit 했으면([emittedAssistantTextThisTurn]) 중복이라\r\n * text item 을 skip. tool_use 는 stream_event 로 안 온 경우가 있어 항상 emit.\r\n */\r\n private handleAssistantFinal(raw: Record<string, unknown>): void {\r\n const message = raw.message\r\n if (!message || typeof message !== 'object') return\r\n const content = (message as { content?: unknown }).content\r\n if (!Array.isArray(content)) return\r\n for (const item of content) {\r\n if (!item || typeof item !== 'object') continue\r\n const itemType = (item as { type?: unknown }).type\r\n if (itemType === 'text') {\r\n if (this.emittedAssistantTextThisTurn) continue // stream_event 가 이미 emit — 중복 skip.\r\n const text = (item as { text?: unknown }).text\r\n if (typeof text === 'string' && text.length > 0) {\r\n this.cb.onCard({ kind: 'assistant', text, isStreaming: false })\r\n }\r\n } else if (itemType === 'tool_use') {\r\n const name = (item as { name?: unknown }).name\r\n const input = (item as { input?: unknown }).input\r\n this.cb.onCard({\r\n kind: 'tool_call',\r\n toolName: typeof name === 'string' ? name : 'unknown',\r\n args:\r\n input && typeof input === 'object'\r\n ? formatToolArgs(input as Record<string, unknown>)\r\n : `${input}`,\r\n })\r\n }\r\n // 'thinking' content 는 무시.\r\n }\r\n }\r\n\r\n /**\r\n * user message — tool_result 가 content 안에 wrap. is_error + 권한 문구 매칭 시\r\n * `approval`(§8-A.0 fallback), 그 외 `tool_result`.\r\n */\r\n private handleUserMessage(raw: Record<string, unknown>): void {\r\n const message = raw.message\r\n if (!message || typeof message !== 'object') return\r\n const content = (message as { content?: unknown }).content\r\n if (!Array.isArray(content)) return\r\n for (const item of content) {\r\n if (!item || typeof item !== 'object') continue\r\n if ((item as { type?: unknown }).type !== 'tool_result') continue\r\n const tid = (item as { tool_use_id?: unknown }).tool_use_id\r\n const toolUseId = typeof tid === 'string' ? tid : ''\r\n const isError = (item as { is_error?: unknown }).is_error === true\r\n const output = extractToolResultOutput(\r\n (item as { content?: unknown }).content\r\n )\r\n\r\n // §8-A.0 fallback — permission 거부 매칭 → approval(자유 응답 없음, risk=high).\r\n if (isError && isPermissionDenial(output)) {\r\n this.cb.onCard({\r\n kind: 'approval',\r\n prompt: output,\r\n options: [],\r\n risk: 'high',\r\n allowsFreeText: false,\r\n })\r\n continue\r\n }\r\n this.cb.onCard({\r\n kind: 'tool_result',\r\n toolName: toolNameFromId(toolUseId),\r\n output,\r\n truncated: false, // V2 spike minimum — truncation 처리 V2.1+\r\n })\r\n }\r\n }\r\n\r\n /** `result` — turn 종료. 진행 중 text 먼저 flush 해 `turn_complete` 가 마지막 카드가 되게. */\r\n private handleResult(raw: Record<string, unknown>): void {\r\n this.flushActiveAssistant()\r\n this.cb.onCard({\r\n kind: 'turn_complete',\r\n subtype: typeof raw.subtype === 'string' ? raw.subtype : undefined,\r\n stopReason:\r\n typeof raw.stop_reason === 'string' ? raw.stop_reason : undefined,\r\n numTurns: typeof raw.num_turns === 'number' ? raw.num_turns : undefined,\r\n totalCostUsd:\r\n typeof raw.total_cost_usd === 'number' ? raw.total_cost_usd : undefined,\r\n isError: raw.is_error === true,\r\n })\r\n // turn 경계 — 다음 turn 의 dedup 플래그 리셋(멀티턴 시 새 turn text 정상 emit).\r\n this.emittedAssistantTextThisTurn = false\r\n }\r\n\r\n /** 진행 중 assistant text 가 있으면 final(isStreaming=false) emit + clear. idempotent. */\r\n private flushActiveAssistant(): void {\r\n const text = this.activeAssistantText\r\n if (text !== null && text.length > 0) {\r\n this.cb.onCard({ kind: 'assistant', text, isStreaming: false })\r\n // stream_event 경로가 이번 turn 의 text 를 emit 했음을 표시 — full message 중복 차단.\r\n this.emittedAssistantTextThisTurn = true\r\n }\r\n this.activeAssistantText = null\r\n }\r\n}\r\n","import fs from 'fs'\r\nimport path from 'path'\r\n\r\n/**\r\n * [file-upload 20260614] 모바일 첨부 업로드의 기본 크기 상한 (25MB).\r\n *\r\n * in-memory 누적(streaming 아님)이라 한 업로드가 메모리를 무한히 점유하지 못하게 한다.\r\n * begin 단계의 선언 size 와 chunk 누적 size 양쪽에 적용 — 모바일이 거짓 size 를 선언해도\r\n * 실제 바이트가 상한을 넘으면 drop(B). 25MB = 모바일 첨부(이미지/로그/작은 코드)에 충분.\r\n */\r\nexport const DEFAULT_MAX_UPLOAD_BYTES = 25 * 1024 * 1024\r\n\r\n/** Windows 에서 파일명에 못 쓰는 문자 — `_` 로 치환한다(cross-platform 안전 파일명). */\r\nconst WINDOWS_ILLEGAL_CHARS = /[<>:\"/\\\\|?*]/g\r\n\r\n/** 파일명에서 제거하는 제어문자 (NUL~US, DEL) — 로그/FS 손상 방지. */\r\n// biome-ignore lint/suspicious/noControlCharactersInRegex: 제어문자 strip 이 목적 — 의도적 매칭\r\nconst CONTROL_CHARS = /[\\x00-\\x1f\\x7f]/g\r\n\r\n/**\r\n * Windows 예약 device 이름(확장자 무관) — 이 이름으로 쓰면 파일이 아니라 콘솔/포트\r\n * device 로 흘러가 예측 불가 동작/행을 일으킨다. `_` prefix 로 무력화한다.\r\n */\r\nconst WINDOWS_RESERVED_NAMES = /^(con|prn|aux|nul|com[1-9]|lpt[1-9])(\\.|$)/i\r\n\r\n/** 동시 진행 업로드 상한 — 미완성(begin 후 end 미수신) 누적으로 인한 메모리 DoS 방어. */\r\nconst MAX_CONCURRENT_UPLOADS = 8\r\n\r\n/** 업로드 idle TTL(ms) — 마지막 활동 후 이 시간이 지나면 begin 시 lazy sweep 으로 회수. */\r\nconst UPLOAD_TTL_MS = 120_000\r\n\r\n/**\r\n * 모바일이 보낸 untrusted 파일명을 데스크탑 FS 에 안전한 단일 segment 로 정화한다.\r\n *\r\n * WHY: 모바일은 cross-platform(POSIX/Windows) 경로를 보낼 수 있고 path traversal\r\n * (`../../etc/passwd`)이나 제어문자/illegal char 를 포함할 수 있다. cwd 밖으로 쓰는 것을\r\n * 막기 위해 **마지막 segment 만** 취하고(POSIX `/` 와 Windows `\\` 양쪽 분리), 제어문자\r\n * strip + Windows illegal char 치환을 적용한다. 빈 문자열 또는 `.`/`..` 은 호출자가\r\n * bad_name 으로 거절하도록 `''` 를 반환한다(파일명으로 부적합).\r\n *\r\n * [name] 모바일이 선언한 파일명(경로 포함 가능). 반환은 정화된 basename 또는 `''`.\r\n */\r\nexport function sanitizeUploadName(name: string): string {\r\n // 양쪽 구분자로 분리 후 마지막 비어있지 않은 segment 채택 (traversal/디렉터리 제거).\r\n const segments = String(name ?? '').split(/[/\\\\]/)\r\n const last = segments[segments.length - 1] ?? ''\r\n let cleaned = last\r\n .replace(CONTROL_CHARS, '')\r\n .replace(WINDOWS_ILLEGAL_CHARS, '_')\r\n .trim()\r\n // 후행 dot/space 제거 — Windows 가 조용히 잘라 확장자 혼동(`x.txt.` → `x.txt`)을 막는다.\r\n cleaned = cleaned.replace(/[. ]+$/, '')\r\n if (cleaned === '' || cleaned === '.' || cleaned === '..') return ''\r\n // 예약 device 이름(CON/NUL/COM1…)은 `_` prefix 로 무력화(device 로 쓰기 방지).\r\n if (WINDOWS_RESERVED_NAMES.test(cleaned)) cleaned = `_${cleaned}`\r\n return cleaned\r\n}\r\n\r\n/**\r\n * `dir/name` 이 비어있으면 그 경로를, 충돌하면 ` (1)`, ` (2)`… 를 확장자 앞에 끼워 빈 경로를 찾는다.\r\n *\r\n * WHY: 같은 이름 파일이 이미 있으면 덮어쓰지 않고 `report (1).pdf` 식으로 회피해 데이터\r\n * 손실을 막는다(브라우저 다운로드 관행). `exists` 를 주입받아 fs 없이 단위 테스트 가능.\r\n * 1000회까지 충돌하면(비현실적) `Date.now()` suffix 로 fall back 해 무한 루프를 방지한다.\r\n *\r\n * [dir] 대상 디렉터리(절대 경로). [name] 이미 정화된 파일명. [exists] 경로 존재 여부 판정.\r\n */\r\nexport function resolveCollisionPath(\r\n dir: string,\r\n name: string,\r\n exists: (p: string) => boolean\r\n): string {\r\n const first = path.join(dir, name)\r\n if (!exists(first)) return first\r\n\r\n const ext = path.extname(name)\r\n const stem = name.slice(0, name.length - ext.length)\r\n for (let i = 1; i <= 1000; i++) {\r\n const candidate = path.join(dir, `${stem} (${i})${ext}`)\r\n if (!exists(candidate)) return candidate\r\n }\r\n // 비현실적 fallback — 타임스탬프 suffix 로 충돌 회피 (정상 코드라 Date.now 허용).\r\n return path.join(dir, `${stem} (${Date.now()})${ext}`)\r\n}\r\n\r\n/** 한 업로드의 in-memory 누적 상태. `end` 까지 디스크에 아무것도 쓰지 않는다. */\r\ninterface UploadEntry {\r\n /** 머신 식별자 — end 시 cwd 해석에 사용. */\r\n machineId: string\r\n /** 정화 전 원본 파일명(end 에서 다시 sanitize). */\r\n name: string\r\n /** begin 이 선언한 size — end 에서 실제 누적과 일치하는지 검증(truncation 탐지). */\r\n size: number\r\n /** 예상 청크 수 — end 에서 모든 index 가 채워졌는지 검증. */\r\n totalChunks: number\r\n /** 지금까지 누적된 바이트 수(maxBytes 가드 + size 일치 검증). */\r\n received: number\r\n /**\r\n * index → raw 청크. **arrival 순서가 아니라 index 로 보관**해 relay/네트워크가 재배열·중복\r\n * 해도 end 에서 0..totalChunks-1 순서로 정확히 재조립한다(silent corruption 방어).\r\n */\r\n chunks: Map<number, Buffer>\r\n /** maxBytes 초과 / 잘못된 index 등으로 손상되면 true — end 가 write_failed 로 거절. */\r\n errored: boolean\r\n /** 마지막 활동 시각(ms) — idle TTL sweep 기준. */\r\n lastTouchMs: number\r\n}\r\n\r\n/**\r\n * 모바일→데스크탑 첨부 파일 업로드를 in-memory 로 누적해 머신 cwd 에 기록하는 수신기.\r\n *\r\n * WHY: ws-manager 의 begin/chunk/end 3-메시지 핸들러에서 fs/ws 의존성을 분리해 단위\r\n * 테스트 가능하게 한다. streaming 이 아니라 25MB 상한 안에서 메모리에 모은 뒤 `end` 에서\r\n * atomic write(`.part` → rename)한다 — 부분 쓰기 파일이 cwd 에 남지 않게 한다. 파일명/크기는\r\n * 모두 untrusted 라 sanitize·cap 한다. cwd 해석은 주입된 `resolveCwd` 에 위임(머신별 OSC7 cwd).\r\n */\r\nexport class FileUploadReceiver {\r\n private readonly resolveCwd: (machineId: string) => string\r\n private readonly maxBytes: number\r\n private readonly uploads = new Map<string, UploadEntry>()\r\n\r\n /**\r\n * [resolveCwd] machineId → 기록 대상 디렉터리(절대 경로) 해석기(ssh.resolveLocalCwd).\r\n * [maxBytes] 업로드 1건의 최대 바이트(기본 25MB). begin size 와 chunk 누적 양쪽에 적용.\r\n */\r\n constructor(opts: {\r\n resolveCwd: (machineId: string) => string\r\n maxBytes?: number\r\n }) {\r\n this.resolveCwd = opts.resolveCwd\r\n this.maxBytes = opts.maxBytes ?? DEFAULT_MAX_UPLOAD_BYTES\r\n }\r\n\r\n /**\r\n * 업로드 시작 — 파일명/크기를 검증하고 누적 entry 를 만든다.\r\n *\r\n * 검증 실패 시 즉시 회신할 에러 객체를 반환한다(호출자가 file.upload.done 으로 전달).\r\n * 성공 시 entry 를 등록하고 `null` 을 반환한다(아직 응답 없음 — end 에서 응답).\r\n * 동일 uploadId 가 재시작되면 이전 entry 를 덮어쓴다(중복 begin 방어).\r\n *\r\n * 반환: 거절 시 `{ ok:false, error }`(bad_name/too_large/too_many), 정상 시 `null`.\r\n */\r\n begin(\r\n machineId: string,\r\n uploadId: string,\r\n name: string,\r\n size: number,\r\n totalChunks: number\r\n ): { ok: false; error: string } | null {\r\n this.sweepExpired()\r\n if (sanitizeUploadName(name) === '') return { ok: false, error: 'bad_name' }\r\n // 음수/소수/NaN size 거부 — `> maxBytes` 만으로는 `-1` 같은 값이 통과한다.\r\n if (!Number.isInteger(size) || size < 0 || size > this.maxBytes) {\r\n return { ok: false, error: 'too_large' }\r\n }\r\n if (!Number.isInteger(totalChunks) || totalChunks < 1) {\r\n return { ok: false, error: 'protocol' }\r\n }\r\n // 동시 업로드 수 상한 — 새 uploadId 일 때만 카운트(같은 uploadId 재시작은 허용).\r\n if (\r\n !this.uploads.has(uploadId) &&\r\n this.uploads.size >= MAX_CONCURRENT_UPLOADS\r\n ) {\r\n return { ok: false, error: 'too_many' }\r\n }\r\n this.uploads.set(uploadId, {\r\n machineId,\r\n name,\r\n size,\r\n totalChunks,\r\n received: 0,\r\n chunks: new Map(),\r\n errored: false,\r\n lastTouchMs: Date.now(),\r\n })\r\n return null\r\n }\r\n\r\n /**\r\n * 청크 1개 수신 — base64 decode 해 누적한다. 알 수 없는 uploadId 는 무시(stale/취소 방어).\r\n *\r\n * 누적 바이트가 maxBytes 를 넘으면 entry 를 errored 로 표시하고 청크를 비운다(메모리 보호) —\r\n * end 가 write_failed 로 거절한다. [index] 는 begin 의 totalChunks 범위로 검증하고 index 위치에\r\n * 보관한다(arrival 순서 무관) — 범위 밖/중복 index 는 entry 를 errored 로 만든다(무결성 가드).\r\n */\r\n chunk(uploadId: string, index: number, dataB64: string): void {\r\n const entry = this.uploads.get(uploadId)\r\n if (!entry || entry.errored) return\r\n // index 범위/중복 검증 — 손상되면 즉시 errored 로 마킹(silent corruption 방어).\r\n if (\r\n !Number.isInteger(index) ||\r\n index < 0 ||\r\n index >= entry.totalChunks ||\r\n entry.chunks.has(index)\r\n ) {\r\n entry.errored = true\r\n entry.chunks.clear()\r\n return\r\n }\r\n const buf = Buffer.from(String(dataB64 ?? ''), 'base64')\r\n if (entry.received + buf.length > this.maxBytes) {\r\n entry.errored = true\r\n entry.chunks.clear()\r\n return\r\n }\r\n entry.chunks.set(index, buf)\r\n entry.received += buf.length\r\n entry.lastTouchMs = Date.now()\r\n }\r\n\r\n /**\r\n * 업로드 종료 — 청크를 concat 해 머신 cwd 에 atomic write 하고 결과를 반환한다.\r\n *\r\n * `<final>.part` 로 먼저 쓴 뒤 final 로 rename 해 부분 파일 노출을 막는다. 충돌 파일명은\r\n * resolveCollisionPath 로 회피한다. entry 는 성공/실패 무관 finally 에서 제거한다(누수 방지).\r\n *\r\n * 반환: 알 수 없는 uploadId 면 `{ ok:false, error:'protocol' }`, 손상이면 write_failed,\r\n * 정상 시 `{ ok:true, path }`(기록된 절대 경로), 기록 실패 시 `{ ok:false, error:'write_failed' }`.\r\n */\r\n async end(\r\n uploadId: string\r\n ): Promise<{ ok: boolean; path?: string; error?: string }> {\r\n const entry = this.uploads.get(uploadId)\r\n if (!entry) return { ok: false, error: 'protocol' }\r\n try {\r\n if (entry.errored) return { ok: false, error: 'write_failed' }\r\n // 무결성 가드: 모든 청크가 도착했고(구멍 없음) 누적 바이트가 선언 size 와 일치해야\r\n // 한다. 불일치면 truncated/재배열 → ok:true 로 깨진 파일을 넘기지 않는다.\r\n if (\r\n entry.chunks.size !== entry.totalChunks ||\r\n entry.received !== entry.size\r\n ) {\r\n return { ok: false, error: 'write_failed' }\r\n }\r\n const name = sanitizeUploadName(entry.name)\r\n if (name === '') return { ok: false, error: 'bad_name' }\r\n\r\n const dir = this.resolveCwd(entry.machineId)\r\n // 세션 cwd 가 stale(리네임/삭제된 옛 경로)이면 부모 디렉토리 부재로 open 이 ENOENT 를\r\n // 던져 write_failed 가 됐다(2026-06-20: 머신 cwd=…\\claude_code_mobile 부재 → 100% 실패).\r\n // 기록 전 대상 폴더를 보장해 stale cwd 여도 업로드가 성공하게 한다(cross-platform).\r\n await fs.promises.mkdir(dir, { recursive: true })\r\n const finalPath = resolveCollisionPath(dir, name, p => fs.existsSync(p))\r\n // .part 는 uploadId 로 고유화 — 같은 이름 동시 업로드의 temp 파일 충돌 방지.\r\n const partPath = `${finalPath}.${uploadId}.part`\r\n try {\r\n // index 순서로 파일 핸들에 직접 스트리밍 — Buffer.concat 의 전체 2차 복사(최대 25MB)를\r\n // 피하고 async write 로 메인 프로세스 이벤트 루프를 양보한다(relay/PTY 펌프 stall 방지).\r\n const fh = await fs.promises.open(partPath, 'w')\r\n try {\r\n for (let i = 0; i < entry.totalChunks; i++) {\r\n await fh.write(entry.chunks.get(i) as Buffer)\r\n }\r\n } finally {\r\n await fh.close()\r\n }\r\n await fs.promises.rename(partPath, finalPath)\r\n } catch {\r\n // 부분 파일 정리 (best-effort) — 실패해도 무시.\r\n try {\r\n await fs.promises.rm(partPath, { force: true })\r\n } catch {\r\n /* noop */\r\n }\r\n return { ok: false, error: 'write_failed' }\r\n }\r\n return { ok: true, path: finalPath }\r\n } finally {\r\n this.uploads.delete(uploadId)\r\n }\r\n }\r\n\r\n /**\r\n * 진행 중인 업로드 1건을 버린다(클라이언트 disconnect 시 best-effort).\r\n * end 전까지 디스크에 아무것도 쓰지 않으므로 fs 부수효과 없음.\r\n */\r\n abort(uploadId: string): void {\r\n this.uploads.delete(uploadId)\r\n }\r\n\r\n /** 진행 중인 모든 업로드를 버린다(전체 shutdown 등). fs 부수효과 없음. */\r\n abortAll(): void {\r\n this.uploads.clear()\r\n }\r\n\r\n /**\r\n * idle TTL 초과 entry 회수 — relay 경로는 per-client `close` 신호가 없어(단일 소켓)\r\n * begin/chunk 만 보내고 end 를 안 보내는 미완성 업로드가 메모리에 영구 잔존할 수 있다.\r\n * begin 진입마다 lazy sweep 해 [UPLOAD_TTL_MS] 무활동 entry 를 제거한다(별도 타이머 없이).\r\n */\r\n private sweepExpired(): void {\r\n const now = Date.now()\r\n for (const [id, entry] of this.uploads) {\r\n if (now - entry.lastTouchMs > UPLOAD_TTL_MS) this.uploads.delete(id)\r\n }\r\n }\r\n}\r\n","import type { AgentConfigStore, FcmTokenEntry } from '../config/agent-config'\r\n\r\n/** FcmTokenStore 가 영속에 필요한 ConfigStore 부분만 추린 의존(테스트 주입 용이). */\r\ntype FcmTokenPersistence = Pick<AgentConfigStore, 'getFcmTokens' | 'setFcmTokens'>\r\n\r\n/**\r\n * 이 데스크탑에 등록된 폰 FCM 토큰을 메모리 + 영속 백엔드(암호화)에 보관한다(FCM Phase 3).\r\n *\r\n * `device.fcm_register`/`device.fcm_unregister` 를 ws-manager 가 받아 위임한다. machineId\r\n * 별 구분 없이 데스크탑(=relay 세션) 단위 목록으로 둔다 — device.fcm_register 에 machineId\r\n * 가 없고(폰-데스크탑 연결 단위 등록), 발송 대상 머신은 claude_state 가 결정해 payload 에\r\n * 싣기 때문이다. 변경 시 즉시 영속해 앱 재시작/killed 후에도 살아남는다.\r\n *\r\n * 영속 백엔드(electron-store/JSON)는 {@link AgentConfigStore} 로 **주입**받는다 — 코어가\r\n * 호스트 전용 저장소(electron-store)를 직접 import 하지 않게 하는 DI 경계(Phase A3).\r\n */\r\nexport class FcmTokenStore {\r\n private entries: FcmTokenEntry[]\r\n\r\n constructor(private readonly config: FcmTokenPersistence) {\r\n this.entries = config.getFcmTokens()\r\n }\r\n\r\n /** 토큰 등록(동일 token 이면 platform/locale 갱신). 빈 token 은 무시. 즉시 영속. */\r\n register(token: string, platform: string, locale: string): void {\r\n if (!token) return\r\n const idx = this.entries.findIndex((e) => e.token === token)\r\n if (idx >= 0) this.entries[idx] = { token, platform, locale }\r\n else this.entries.push({ token, platform, locale })\r\n this.config.setFcmTokens(this.entries)\r\n }\r\n\r\n /** 토큰 해제(로그아웃). 등록돼 있을 때만 제거 후 영속. */\r\n unregister(token: string): void {\r\n const next = this.entries.filter((e) => e.token !== token)\r\n if (next.length === this.entries.length) return\r\n this.entries = next\r\n this.config.setFcmTokens(this.entries)\r\n }\r\n\r\n /** 현재 등록된 토큰 문자열 목록(발송용). */\r\n tokens(): string[] {\r\n return this.entries.map((e) => e.token)\r\n }\r\n}\r\n","import type {\n BroadcastFn,\n BroadcastRecipient,\n ServerMessage,\n} from '@arva/shared/types'\n\nimport type { AgentConfigStore, AgentIdentity } from './config/agent-config'\nimport { configureLogDir } from './lib/logger'\nimport { SSHManager } from './managers/ssh-manager'\nimport { WsManager } from './managers/ws-manager'\n\n/**\n * {@link createAgentCore} 입력. host(데스크탑/헤드리스)가 자기 설정·식별·로그경로·메시지 sink 를\n * 주입한다 — 코어는 호스트 전용 의존(electron-store/렌더러)을 알지 못한다.\n */\nexport interface AgentCoreOptions {\n config: AgentConfigStore\n identity: AgentIdentity\n /** scoped 로그 파일 디렉터리. 헤드리스는 `~/.ai_remote_vibe_agent/logs`. */\n logDir: string\n /**\n * host 측 메시지 sink — 데스크탑은 렌더러 IPC, 헤드리스는 Ink 상태 갱신. mobile fan-out 은\n * 코어가 내부에서 `ws.broadcastToMobile` 로 처리하므로 host 는 자기 화면 갱신만 신경쓰면 된다.\n */\n onHostMessage: (msg: ServerMessage) => void\n}\n\n/**\n * createAgentCore 반환 — 기동된 SSH/Ws 매니저 + 합성 broadcast + cleanup.\n */\nexport interface AgentCore {\n ssh: SSHManager\n ws: WsManager\n /** host + mobile fan-out 합성 broadcast(매니저들이 내부적으로 이미 보유, 외부 트리거용 노출). */\n broadcast: BroadcastFn\n /** 프로세스 종료 훅(SIGINT 등)에서 호출 — 모든 PTY/WS 정리. */\n cleanup(): void\n}\n\n/**\n * 호스트 비의존 에이전트 코어를 와이어링해 **즉시 기동**한다.\n *\n * 부수효과(생성자에서 발생): `SSHManager` 가 `local` 머신을 자동 등록하고, `WsManager` 가\n * 로컬 WS 서버를 띄운 뒤 relay host 에 자동 접속한다. 따라서 호출 직후부터 폰이 페어링 가능하다.\n * 로컬 셸 PTY 는 폰 `session_attach` 시점에 on-demand 로 spawn 되므로 추가 코드가 필요 없다.\n *\n * broadcast 의 host 측은 `onHostMessage` 로, mobile 측은 `ws.broadcastToMobile` 로 분기한다\n * (데스크탑 index.ts 의 broadcast 클로저와 동일한 skipRenderer/skipMobile/perRecipient 규약).\n */\nexport function createAgentCore(opts: AgentCoreOptions): AgentCore {\n // 첫 로그 write 전에 scoped 로그 디렉터리 확정.\n configureLogDir(opts.logDir)\n\n // ws 는 broadcast 클로저가 참조하지만 broadcast 보다 늦게 생성된다 — late-bound(`let`)로 해소\n // (데스크탑 index.ts 와 동일 패턴). 첫 broadcast 는 ssh/ws 기동 이후라 항상 정의돼 있다.\n let ws: WsManager | undefined\n\n const broadcast: BroadcastFn = (msg, bopts) => {\n if (!bopts?.skipRenderer) {\n const recipient: BroadcastRecipient = { kind: 'renderer' }\n const hostMsg = bopts?.perRecipient\n ? ({ ...msg, ...bopts.perRecipient(recipient) } as ServerMessage)\n : msg\n opts.onHostMessage(hostMsg)\n }\n if (!bopts?.skipMobile) ws?.broadcastToMobile(msg, bopts?.perRecipient)\n }\n\n const ssh = new SSHManager(broadcast)\n ws = new WsManager(ssh, broadcast, opts.config, opts.identity)\n\n // 종료 경로가 여럿(Ink exit / SIGTERM / waitUntilExit)이라 cleanup 가 2~3회 호출될 수 있다.\n // 매니저 cleanup 이 현재는 멱등이지만 가드로 다중 호출을 명시적으로 무해화(미래 회귀 방어).\n let cleanedUp = false\n\n return {\n ssh,\n ws,\n broadcast,\n cleanup() {\n if (cleanedUp) return\n cleanedUp = true\n ssh.cleanup()\n ws?.cleanup()\n },\n }\n}\n","import crypto from 'crypto'\nimport http from 'http'\nimport type { IncomingMessage, Server, ServerResponse } from 'http'\n\nimport { createLogger } from '../lib/logger'\n\nimport type {\n BroadcastFn,\n ClaudeSessionState,\n ClaudeStateSummary,\n} from '@arva/shared/types'\nimport { readTranscriptTailByPath } from './agent-sessions'\nimport type { SSHManager } from './ssh-manager'\n\nconst log = createLogger('hook-receiver')\n\n/** 알림 본문 미리보기 최대 길이 (시크릿 마스킹 후 절단). */\nconst PREVIEW_CAP = 280\n\n/**\n * 미리보기 문자열을 만든다 — **시크릿 마스킹 후 절단**.\n *\n * transcript/프롬프트에는 토큰·키가 섞일 수 있어 원문을 알림에 실으면 안 된다(보안).\n * Bearer/`sk-`류 prefix/`key=value` 패턴을 best-effort 로 가린다. 완전한 DLP 가 아니라\n * \"알림 본문에 명백한 시크릿이 노출되지 않게\" 하는 1차 방어선이다.\n */\nfunction toPreview(text: string): string {\n if (!text) return ''\n const masked = text\n .replace(/(Bearer\\s+)[A-Za-z0-9._-]+/gi, '$1***')\n .replace(/\\b(sk|pk|rk|ghp|gho|ghs|xox[baprs])[-_][A-Za-z0-9_-]{6,}\\b/g, '$1-***')\n .replace(\n /((?:api[_-]?key|token|secret|password|passwd|pwd)\\s*[:=]\\s*)\\S+/gi,\n '$1***'\n )\n return masked.length > PREVIEW_CAP ? `${masked.slice(0, PREVIEW_CAP)}…` : masked\n}\n\n/** assistant 텍스트가 코드 블록을 포함하면 'code', 아니면 'text'. */\nfunction classifyKind(text: string): 'text' | 'code' {\n return text.includes('```') ? 'code' : 'text'\n}\n\n/**\n * [A2/A9 20260614] 고위험 파괴적 동작 denylist (소문자 비교).\n *\n * 모바일 `agent_notification_event.isHighRiskApproval`(`../ai_remote_vibe_mobile`)의 패턴을\n * 데스크탑으로 이전한 것 — **권위 single-source 는 데스크탑**, 모바일 측은 advisory(2차)로\n * 격하된다. drift 방지를 위해 모바일 denylist 와 동기 유지할 것. 패턴 추가/변경 시 양쪽 갱신.\n */\nconst RISK_PATTERNS = [\n 'rm -rf',\n 'rm -fr',\n 'sudo ',\n 'push --force',\n 'push -f',\n 'force-with-lease',\n 'drop table',\n 'drop database',\n 'truncate ',\n 'mkfs',\n 'dd if=',\n 'chmod 777',\n ':(){',\n 'reset --hard',\n 'git clean -',\n]\n\n/**\n * [A2/A9 20260614] 권한 승인 텍스트가 **고위험**인지 desktop-side 권위 판정.\n *\n * 입력은 permission_prompt 의 결합 문자열(`\"<도구명>: <input 미리보기>\"` 또는 fallback\n * message) — `tool_use` 의 도구명·구조화 input 이 녹아 있어 모바일의 최종 preview 추정보다\n * 정확하다. [toPreview] 의 시크릿 마스킹 **이전** raw 로 분류해 패턴이 가려지지 않게 한다.\n * 미일치 = 비고위험(false). denylist 는 [RISK_PATTERNS] (모바일과 동기).\n */\nexport function classifyRisk(text: string): boolean {\n const t = text.toLowerCase()\n return RISK_PATTERNS.some(p => t.includes(p))\n}\n\n/**\n * [Push-Notif Phase 1] Claude 훅이 POST 하는 상태 이벤트를 받아 `claude_state` 로 emit 하는\n * **loopback HTTP 수신부**.\n *\n * 왜 별도 HTTP 서버인가: LAN WS 는 `/mobile` 에서 바이너리+HMAC 라 text 프레임을 거부하므로\n * `node` 훅 러너가 도달할 수 없다. 그래서 127.0.0.1 전용 평문 HTTP 를 따로 연다(per-launch\n * 랜덤 토큰 인증, 비-loopback 차단). 받은 이벤트는 `cwd→machineId` 역매핑으로 무관 세션을\n * 걸러낸 뒤 `broadcast()`(renderer + 모든 모바일 + relay) 로 fan-out 한다.\n *\n * 부수효과: `broadcast` 호출(IPC/WS 송신). `currentOpenPrompt` 에 needs_input 의 eventId 를\n * 기록(Phase 2 quick_reply idempotency 가 참조).\n */\nexport class HookReceiver {\n /** 훅 러너 인증용 per-launch 토큰. installClaudeHooks 로 전달돼 config 에 기록된다. */\n readonly token = crypto.randomBytes(24).toString('hex')\n private server: Server | null = null\n /** machineId → 단조 증가 seq (eventId 생성). */\n private seq = new Map<string, number>()\n /** machineId → 현재 열린 prompt 의 eventId (needs_input 시 set, complete 시 clear). */\n private currentOpenPrompt = new Map<string, string>()\n /**\n * [FCM Phase 3] awaiting-approval edge 시 호출 — push-dispatcher 로 연결(index 에서 set).\n * 앱이 killed 된 폰에 승인 푸시를 보내는 트리거. 미설정(훅 비활성/실패)이면 no-op.\n */\n onApprovalPending?: (\n machineId: string,\n eventId: string,\n summary?: ClaudeStateSummary\n ) => void\n /**\n * [Reclaim guard 20260606] machineId → 마지막 claude_state. AuthorityResolver 의 handed_off\n * watchdog 가 `isAgentBusy` 로 조회 — 에이전트 턴 중(working/awaiting-approval)이면 출력이\n * 잠시 없어도 회수를 보류한다(권한 프롬프트 응시 등). `emit` 이 단일 갱신 지점.\n *\n * 주의: 머신 제거/disconnect 시 정리되지 않는다(`seq`/`currentOpenPrompt` 와 동일). 에이전트가\n * Stop 없이 죽으면(Ctrl-C/크래시) 'working' 이 잔류해 busy=true 가 유지될 수 있으나,\n * AuthorityResolver 의 busy 절대 상한 `HANDOFF_BUSY_HARD_CEILING_MS`(30min, H6 fix 20260607)가\n * 회수를 보장하므로 영구 보호로 이어지지 않는다(최악 30min 지연, map 증가는 로컬 머신 수로 bound).\n */\n private lastState = new Map<string, ClaudeSessionState>()\n\n constructor(\n private broadcast: BroadcastFn,\n private ssh: SSHManager\n ) {}\n\n /**\n * 127.0.0.1 의 임의 포트로 수신부를 연다. 반환된 `{port, token}` 을 installClaudeHooks 에\n * 넘겨 훅 러너가 찾아올 수 있게 한다.\n */\n start(): Promise<{ port: number; token: string }> {\n return new Promise((resolve, reject) => {\n const server = http.createServer((req, res) => this.handleRequest(req, res))\n server.on('error', err => {\n log.warn(`hook-receiver listen 실패: ${err.message}`)\n reject(err)\n })\n server.listen(0, '127.0.0.1', () => {\n this.server = server\n const addr = server.address()\n const port = typeof addr === 'object' && addr ? addr.port : 0\n log.info(`hook-receiver 수신 시작 127.0.0.1:${port}`)\n resolve({ port, token: this.token })\n })\n })\n }\n\n /** 수신부 종료 (데스크탑 종료 시). */\n stop(): void {\n try {\n this.server?.close()\n } catch {\n // 무시\n }\n this.server = null\n }\n\n /**\n * 머신의 현재 열린 prompt eventId (Phase 2 quick_reply 의 stale-응답 idempotency 용).\n * 없으면 null.\n */\n currentOpenPromptId(machineId: string): string | null {\n return this.currentOpenPrompt.get(machineId) ?? null\n }\n\n /**\n * [Reclaim guard 20260606] 머신의 에이전트가 턴 진행 중인지 — handed_off watchdog 가\n * 회수 보류 판단에 사용. working/awaiting-approval=true, complete/미수신=false.\n */\n isAgentBusy(machineId: string): boolean {\n const s = this.lastState.get(machineId)\n return s === 'working' || s === 'awaiting-approval'\n }\n\n private handleRequest(req: IncomingMessage, res: ServerResponse): void {\n if (req.method !== 'POST' || req.url !== '/claude-state') {\n res.writeHead(404).end()\n return\n }\n // 토큰 검증 (constant-time). 길이 불일치/부재는 즉시 거부.\n const raw = req.headers['x-hook-token']\n const got = Array.isArray(raw) ? raw[0] : raw\n if (\n typeof got !== 'string' ||\n got.length !== this.token.length ||\n !crypto.timingSafeEqual(Buffer.from(got), Buffer.from(this.token))\n ) {\n res.writeHead(401).end()\n return\n }\n let body = ''\n req.on('data', chunk => {\n body += chunk\n if (body.length > 1_000_000) req.destroy() // 1MB 가드\n })\n req.on('end', () => {\n res.writeHead(204).end()\n try {\n const parsed = JSON.parse(body) as {\n event?: unknown\n payload?: unknown\n }\n if (\n typeof parsed.event === 'string' &&\n parsed.payload &&\n typeof parsed.payload === 'object'\n ) {\n this.processEvent(\n parsed.event,\n parsed.payload as Record<string, unknown>\n )\n }\n } catch {\n // malformed body 무시\n }\n })\n req.on('error', () => {\n try {\n res.writeHead(400).end()\n } catch {\n // 무시\n }\n })\n }\n\n /**\n * 훅 이벤트 1건을 `claude_state` 로 변환·emit. cwd 가 로컬 머신과 매칭 안 되면 drop.\n *\n * - Stop → `complete` (요약은 payload 의 `last_assistant_message` 인라인 사용, transcript\n * read 0). `stop_hook_active===true` 는 재귀 가드로 무시.\n * - Notification `permission_prompt` → `awaiting-approval`(kind permission, 본문은\n * `transcript_path` tail 의 pending tool_use). `elicitation_*` → `awaiting-approval`(question).\n * `idle_prompt` 등은 Phase 1 에서 무시(Stop 과 중복 — Phase 2 정교화).\n * - UserPromptSubmit → `working` (요약 없음, 진행 표시).\n */\n private processEvent(event: string, payload: Record<string, unknown>): void {\n const cwd = typeof payload.cwd === 'string' ? payload.cwd : ''\n if (!cwd) return\n const machineId = this.ssh.findLocalMachineIdByCwd(cwd)\n if (!machineId) return // 무관 세션 — drop\n\n if (event === 'Stop') {\n if (payload.stop_hook_active === true) return\n const text =\n typeof payload.last_assistant_message === 'string'\n ? payload.last_assistant_message\n : ''\n const eventId = this.nextEventId(machineId)\n this.currentOpenPrompt.delete(machineId)\n this.emit(\n machineId,\n eventId,\n 'complete',\n text ? { kind: classifyKind(text), preview: toPreview(text) } : undefined\n )\n return\n }\n\n if (event === 'Notification') {\n const nt =\n typeof payload.notification_type === 'string'\n ? payload.notification_type\n : ''\n const message = typeof payload.message === 'string' ? payload.message : ''\n if (nt === 'permission_prompt') {\n const tail = readTranscriptTailByPath(\n typeof payload.transcript_path === 'string'\n ? payload.transcript_path\n : ''\n )\n const raw = tail.lastToolUse\n ? `${tail.lastToolUse.name}: ${tail.lastToolUse.inputPreview}`\n : message\n const eventId = this.nextEventId(machineId)\n this.currentOpenPrompt.set(machineId, eventId)\n // [A2/A9] risk 는 마스킹 전 raw 로 권위 분류 — preview 는 마스킹 후(보안), 분류는 정확도 우선.\n this.emit(machineId, eventId, 'awaiting-approval', {\n kind: 'permission',\n preview: toPreview(raw),\n isHighRisk: classifyRisk(raw),\n })\n return\n }\n if (nt.startsWith('elicitation')) {\n const eventId = this.nextEventId(machineId)\n this.currentOpenPrompt.set(machineId, eventId)\n this.emit(machineId, eventId, 'awaiting-approval', {\n kind: 'question',\n preview: toPreview(message),\n })\n }\n return\n }\n\n if (event === 'UserPromptSubmit') {\n this.emit(machineId, this.nextEventId(machineId), 'working')\n }\n }\n\n private nextEventId(machineId: string): string {\n const n = (this.seq.get(machineId) ?? 0) + 1\n this.seq.set(machineId, n)\n return `${machineId}#${n}`\n }\n\n /** claude_state 를 로깅 + fan-out — 4개 분기의 broadcast 를 한 곳으로 모아 관측성 확보. */\n private emit(\n machineId: string,\n eventId: string,\n state: ClaudeSessionState,\n summary?: ClaudeStateSummary\n ): void {\n log.info(\n `claude_state machine=${machineId} event=${eventId} state=${state}${summary ? ` kind=${summary.kind}` : ''}`\n )\n // [Reclaim guard 20260606] busy 판단 source 갱신 — broadcast 와 동일 funnel 에서.\n this.lastState.set(machineId, state)\n this.broadcast({ type: 'claude_state', machineId, eventId, state, summary })\n // [FCM Phase 3] 앱 killed 폰에 승인 푸시 트리거(push-dispatcher 위임). presence·토큰\n // 게이트는 dispatcher 내부에서 처리한다.\n if (state === 'awaiting-approval') {\n this.onApprovalPending?.(machineId, eventId, summary)\n }\n }\n}\n","import fs from 'fs'\nimport os from 'os'\nimport path from 'path'\n\nimport { createLogger } from '../lib/logger'\n\nconst log = createLogger('claude-hooks')\n\n/**\n * [Push-Notif Phase 1] Claude 훅이 발화 시 실행하는 **번들 러너**의 소스.\n *\n * 데스크탑이 런타임에 `<userData>/claude-state-hook.js` 로 기록한다(asar/버전 경로 churn\n * 회피 — settings.json 은 이 안정 경로만 참조). 이 스크립트는:\n * 1. 같은 폴더의 `claude-hook.json` 에서 `{port, token}` 을 읽고(없으면 즉시 exit 0 —\n * 데스크탑이 꺼져 있으면 네트워크 시도조차 안 함),\n * 2. stdin 의 훅 JSON 을 받아,\n * 3. 데스크탑 loopback 수신부(`127.0.0.1:<port>/claude-state`)로 `x-hook-token` 과 함께 POST.\n * 모든 실패는 swallow + exit 0 — **Claude TUI 를 절대 막거나 깨뜨리지 않는다**.\n *\n * 순수 Node stdlib(http/fs/path)만 사용 — jq/curl/bash 비의존(크로스 플랫폼).\n */\nconst HOOK_SCRIPT = `// AUTO-GENERATED by ai_remote_vibe_agent (Push-Notif Phase 1). Safe to delete.\nconst fs = require('fs')\nconst http = require('http')\nconst path = require('path')\nconst event = process.argv[2] || 'unknown'\nlet cfg\ntry {\n cfg = JSON.parse(fs.readFileSync(path.join(__dirname, 'claude-hook.json'), 'utf8'))\n} catch (_) {\n process.exit(0)\n}\nlet buf = ''\nprocess.stdin.setEncoding('utf8')\nprocess.stdin.on('data', c => { buf += c })\nprocess.stdin.on('error', () => process.exit(0))\nprocess.stdin.on('end', () => {\n let payload = {}\n try { payload = JSON.parse(buf) } catch (_) {}\n const body = JSON.stringify({ event, payload })\n const req = http.request(\n {\n host: '127.0.0.1',\n port: cfg.port,\n path: '/claude-state',\n method: 'POST',\n timeout: 2000,\n headers: {\n 'content-type': 'application/json',\n 'x-hook-token': cfg.token,\n 'content-length': Buffer.byteLength(body),\n },\n },\n res => { res.resume(); res.on('end', () => process.exit(0)) }\n )\n req.on('error', () => process.exit(0))\n req.on('timeout', () => { req.destroy(); process.exit(0) })\n req.write(body)\n req.end()\n})\n`\n\n/** 설치 대상 훅 이벤트. Stop=complete, Notification=needs_input, UserPromptSubmit=working. */\nconst HOOK_EVENTS = ['Stop', 'Notification', 'UserPromptSubmit'] as const\n\ninterface HookCommand {\n type: 'command'\n command: string\n timeout?: number\n}\ninterface HookEntry {\n matcher?: string\n hooks: HookCommand[]\n}\n\n/** `~/.claude/settings.json` 경로 (크로스 플랫폼, 홈 하드코딩 금지). */\nfunction settingsPath(): string {\n return path.join(os.homedir(), '.claude', 'settings.json')\n}\n\n/** 번들 훅 러너의 안정 경로 (`<userData>/claude-state-hook.js`). */\nfunction hookScriptPath(userDataDir: string): string {\n return path.join(userDataDir, 'claude-state-hook.js')\n}\n\n/** 런타임 설정(port/token) 경로 (`<userData>/claude-hook.json`). */\nfunction hookConfigPath(userDataDir: string): string {\n return path.join(userDataDir, 'claude-hook.json')\n}\n\n/**\n * 우리가 주입할 hooks 블록을 만든다. command 는 백슬래시 대신 forward-slash 경로를\n * 써서(Node 가 수용) Git Bash/PowerShell 양쪽의 따옴표·이스케이프 차이를 회피한다.\n */\nfunction buildHooksBlock(scriptPath: string): Record<string, HookEntry[]> {\n const sp = scriptPath.replace(/\\\\/g, '/')\n const cmd = (event: string): HookCommand => ({\n type: 'command',\n command: `node \"${sp}\" ${event}`,\n timeout: 10,\n })\n return {\n Stop: [{ hooks: [cmd('Stop')] }],\n Notification: [\n {\n // 권한/질문/유휴만 — auth_success 등 무관 타입은 받지 않는다.\n matcher:\n 'permission_prompt|idle_prompt|elicitation_dialog|elicitation_complete|elicitation_response',\n hooks: [cmd('Notification')],\n },\n ],\n UserPromptSubmit: [{ hooks: [cmd('UserPromptSubmit')] }],\n }\n}\n\n/** 어떤 hooks 엔트리가 **우리 것**인지(= 우리 스크립트 파일명을 command 에 포함) 판별. */\nfunction isOurHookEntry(entry: unknown, scriptFileName: string): boolean {\n const e = entry as HookEntry | undefined\n return Array.isArray(e?.hooks)\n ? e.hooks.some(\n h => typeof h?.command === 'string' && h.command.includes(scriptFileName)\n )\n : false\n}\n\n/**\n * [Push-Notif Phase 1] Claude 훅을 설치한다 — **idempotent + additive**.\n *\n * 부수효과: (1) `<userData>/claude-state-hook.js` 기록, (2) `<userData>/claude-hook.json`\n * 에 `{port, token}` 기록, (3) `~/.claude/settings.json` 의 `hooks` 에 우리 엔트리 merge.\n * 사용자/타 도구의 기존 훅은 보존하고, **우리 이전 엔트리는 제거 후 새로 추가**(중복 방지 +\n * 경로/포트 갱신). settings.json 파싱 실패 시 **clobber 하지 않고 중단**(false 반환).\n *\n * @returns 성공 여부. 실패해도 throw 하지 않음 — 알림 기능은 best-effort.\n */\nexport function installClaudeHooks(\n userDataDir: string,\n port: number,\n token: string\n): boolean {\n try {\n const scriptPath = hookScriptPath(userDataDir)\n fs.writeFileSync(scriptPath, HOOK_SCRIPT, 'utf-8')\n fs.writeFileSync(\n hookConfigPath(userDataDir),\n JSON.stringify({ port, token }),\n 'utf-8'\n )\n\n const settingsFile = settingsPath()\n let settings: Record<string, unknown> = {}\n if (fs.existsSync(settingsFile)) {\n try {\n settings = JSON.parse(fs.readFileSync(settingsFile, 'utf-8'))\n } catch (err) {\n // 손상/비표준 settings.json 을 덮어쓰면 사용자 설정이 날아간다 — 중단.\n log.warn(\n `settings.json 파싱 실패 → 훅 설치 중단: ${(err as Error).message}`\n )\n return false\n }\n } else {\n fs.mkdirSync(path.dirname(settingsFile), { recursive: true })\n }\n\n const scriptFile = path.basename(scriptPath)\n const fresh = buildHooksBlock(scriptPath)\n const hooks =\n settings.hooks && typeof settings.hooks === 'object'\n ? (settings.hooks as Record<string, unknown[]>)\n : {}\n for (const event of HOOK_EVENTS) {\n const existing = Array.isArray(hooks[event]) ? hooks[event] : []\n const others = existing.filter(e => !isOurHookEntry(e, scriptFile))\n hooks[event] = [...others, ...fresh[event]]\n }\n settings.hooks = hooks\n fs.writeFileSync(\n settingsFile,\n `${JSON.stringify(settings, null, 2)}\\n`,\n 'utf-8'\n )\n log.info(`Claude 훅 설치 완료 (port=${port}) → ${settingsFile}`)\n return true\n } catch (err) {\n log.warn(`훅 설치 실패: ${(err as Error).message}`)\n return false\n }\n}\n\n/**\n * 런타임 설정(`claude-hook.json`)만 삭제한다 — 데스크탑 종료 시 호출.\n *\n * settings.json 의 훅 엔트리는 그대로 두되(다음 실행 시 재사용), 설정 파일을 지우면\n * 훅 러너가 \"설정 없음 → 즉시 exit 0\"로 **네트워크 시도조차 안 한다**(stale 포트로 엉뚱한\n * 프로세스를 POST 하는 일 방지).\n */\nexport function removeRuntimeConfig(userDataDir: string): void {\n try {\n fs.rmSync(hookConfigPath(userDataDir), { force: true })\n } catch {\n // 무시 — 종료 흐름 막지 않음.\n }\n}\n\n/**\n * Claude 훅을 **완전 제거**한다 — settings.json 의 우리 엔트리 + 스크립트/설정 파일.\n *\n * 사용자/타 도구의 다른 훅은 보존. (Phase 1 에서는 자동 호출 안 함 — 향후 설정 토글/\n * 언인스톨러용 export.)\n */\nexport function uninstallClaudeHooks(userDataDir: string): void {\n try {\n const scriptFile = path.basename(hookScriptPath(userDataDir))\n const settingsFile = settingsPath()\n if (fs.existsSync(settingsFile)) {\n const settings = JSON.parse(fs.readFileSync(settingsFile, 'utf-8')) as Record<\n string,\n unknown\n >\n const hooks = settings.hooks as Record<string, unknown[]> | undefined\n if (hooks) {\n for (const event of HOOK_EVENTS) {\n if (!Array.isArray(hooks[event])) continue\n hooks[event] = hooks[event].filter(e => !isOurHookEntry(e, scriptFile))\n if (hooks[event].length === 0) delete hooks[event]\n }\n if (Object.keys(hooks).length === 0) delete settings.hooks\n fs.writeFileSync(\n settingsFile,\n `${JSON.stringify(settings, null, 2)}\\n`,\n 'utf-8'\n )\n }\n }\n fs.rmSync(hookScriptPath(userDataDir), { force: true })\n fs.rmSync(hookConfigPath(userDataDir), { force: true })\n log.info('Claude 훅 제거 완료')\n } catch (err) {\n log.warn(`훅 제거 실패: ${(err as Error).message}`)\n }\n}\n","import { randomUUID } from 'node:crypto'\nimport os from 'node:os'\nimport path from 'node:path'\n\nimport type { AgentConfigStore, FcmTokenEntry } from '@arva/core'\nimport {\n AUTH_TIMEOUT_MS_DEFAULT,\n HEARTBEAT_INTERVAL_MS_DEFAULT,\n PONG_TIMEOUT_MS_DEFAULT,\n WS_PORT_DEFAULT,\n resolveRelayUrls,\n} from '@arva/shared/relay-config'\n\nimport { type HeadlessStoreShape, readStore, writeStore } from './json-file'\n\nconst STORE_FILE = path.join(os.homedir(), '.ai_remote_vibe_agent', 'headless-store.json')\n\n/**\n * 헤드리스용 {@link AgentConfigStore} 구현 — JSON 파일 백엔드.\n *\n * 데스크탑 electron-store(safeStorage) 대신 `~/.ai_remote_vibe_agent/headless-store.json` 평문을\n * 쓴다(헤드리스엔 OS 암호화 API 가 없음). install_id 는 최초 1회 생성·영속(GA4 distinct 키).\n * relay 시크릿/URL 은 파일에 두지 않고 **런타임 env** 로 받되, 없으면 빌드 시 박은 기본값\n * (`__REINS_DEFAULT_RELAY_*__`, tsup `define`)으로 fallback 한다 — `npm i -g @junyoung-kim/reins` 사용자가 별도\n * 설정 없이 클라우드 relay 에 곧장 붙도록(데스크탑이 같은 값을 바이너리에 임베드하는 것과 동일).\n * 우선순위: **셸 env > `.env` > 빌드 기본값**. relay URL 은 추가로 stored(TUI 편집)가 최우선.\n */\nexport class HeadlessConfigStore implements AgentConfigStore {\n private data: HeadlessStoreShape\n\n constructor(private readonly file: string = STORE_FILE) {\n this.data = readStore(file)\n if (!this.data.install_id) {\n this.data.install_id = randomUUID()\n writeStore(file, this.data)\n }\n }\n\n getRelayUrls(): string[] {\n // stored(JSON) > env(MAIN_VITE_RELAY_URL/VIBE_RELAY_URL) > 빌드 기본값. 해석 로직은 데스크탑과\n // 공유(@arva/shared/relay-config) — env 접근만 헤드리스(런타임 process.env)가 책임진다.\n // 빌드 기본값(__REINS_DEFAULT_RELAY_URL__)은 env 둘 다 없을 때만 쓰여, npm 설치 직후에도 relay 가 잡힌다.\n return resolveRelayUrls(\n this.data.relay_urls,\n process.env.MAIN_VITE_RELAY_URL ?? process.env.VIBE_RELAY_URL ?? __REINS_DEFAULT_RELAY_URL__\n )\n }\n\n getWsPort(): number {\n return this.data.ws_port ?? WS_PORT_DEFAULT\n }\n\n getHeartbeatIntervalMs(): number {\n return this.data.heartbeat_interval_ms ?? HEARTBEAT_INTERVAL_MS_DEFAULT\n }\n\n getPongTimeoutMs(): number {\n return this.data.pong_timeout_ms ?? PONG_TIMEOUT_MS_DEFAULT\n }\n\n getAuthTimeoutMs(): number {\n return this.data.auth_timeout_ms ?? AUTH_TIMEOUT_MS_DEFAULT\n }\n\n /**\n * relay URL 목록을 영속화한다(index 0=primary). TUI 옵션 화면에서 편집.\n *\n * **적용 시점**: `getRelayUrls()` 는 코어가 relay 연결마다 fresh read 하므로,\n * 저장 후 `core.ws.reconnect()` 로 즉시 반영된다(재시작 불필요). 검증(ws/wss 스킴)은\n * 호출 측(UI)이 {@link ../config/validate} 로 수행 — 이 setter 는 영속만 책임진다.\n */\n setRelayUrls(urls: string[]): void {\n this.data.relay_urls = urls\n writeStore(this.file, this.data)\n }\n\n /**\n * 로컬 WS 서버 포트를 영속화한다.\n *\n * **적용 시점**: 코어가 부팅 시 1회 read 후 서버를 bind 하므로 **재시작 필요**(라이브 리바인드 없음).\n */\n setWsPort(port: number): void {\n this.data.ws_port = port\n writeStore(this.file, this.data)\n }\n\n /** heartbeat 간격(ms) 영속화. 코어가 부팅 시 캐시 → **재시작 후 반영**. */\n setHeartbeatIntervalMs(ms: number): void {\n this.data.heartbeat_interval_ms = ms\n writeStore(this.file, this.data)\n }\n\n /** pong 타임아웃(ms) 영속화. **재시작 후 반영**. */\n setPongTimeoutMs(ms: number): void {\n this.data.pong_timeout_ms = ms\n writeStore(this.file, this.data)\n }\n\n /** auth 타임아웃(ms) 영속화. **재시작 후 반영**. */\n setAuthTimeoutMs(ms: number): void {\n this.data.auth_timeout_ms = ms\n writeStore(this.file, this.data)\n }\n\n /**\n * 옵션 화면 일괄 저장 — 제공된 키만 병합하고 `writeStore` 를 **1회만** 호출한다.\n * 필드별 setter 를 연달아 부르면 같은 파일을 N회 쓰므로(한 저장 = 5 fsync), 옵션 화면은 이걸 쓴다.\n * 적용 시맨틱(relay=reconnect 즉시, ws/timeout=재시작)은 개별 setter 의 TSDoc 참조.\n */\n setOptions(\n patch: Partial<\n Pick<\n HeadlessStoreShape,\n 'relay_urls' | 'ws_port' | 'heartbeat_interval_ms' | 'pong_timeout_ms' | 'auth_timeout_ms'\n >\n >\n ): void {\n Object.assign(this.data, patch)\n writeStore(this.file, this.data)\n }\n\n getInstallId(): string {\n // 생성자에서 보장되므로 항상 존재.\n return this.data.install_id as string\n }\n\n getFcmTokens(): FcmTokenEntry[] {\n return this.data.fcm_tokens ?? []\n }\n\n setFcmTokens(entries: FcmTokenEntry[]): void {\n this.data.fcm_tokens = entries\n writeStore(this.file, this.data)\n }\n\n getRelayAgentSecret(): string {\n // 셸 env > `.env` > 빌드 기본값(__REINS_DEFAULT_RELAY_SECRET__, tsup define). 비로깅.\n // 데스크탑 바이너리가 같은 시크릿을 임베드하므로 npm 배포본에 박아도 새 노출은 아니다(공유 부트스트랩 시크릿).\n return (\n process.env.MAIN_VITE_RELAY_AGENT_SECRET ??\n process.env.VIBE_RELAY_AGENT_SECRET ??\n __REINS_DEFAULT_RELAY_SECRET__\n )\n }\n}\n","import fs from 'node:fs'\nimport path from 'node:path'\n\nimport type { FcmTokenEntry } from '@arva/core'\n\n/** 헤드리스 설정 파일 스키마(`~/.ai_remote_vibe_agent/headless-store.json`). */\nexport interface HeadlessStoreShape {\n relay_urls?: string[]\n ws_port?: number\n /** relay ping 간격(ms). 미설정 시 {@link HEARTBEAT_INTERVAL_MS_DEFAULT}. 코어가 부팅 시 1회 read → 변경 적용엔 재시작 필요. */\n heartbeat_interval_ms?: number\n /** pong 대기 타임아웃(ms). 미설정 시 기본값. 재시작 시 반영. */\n pong_timeout_ms?: number\n /** LAN 인증 핸드셰이크 타임아웃(ms). 미설정 시 기본값. 재시작 시 반영. */\n auth_timeout_ms?: number\n install_id?: string\n fcm_tokens?: FcmTokenEntry[]\n}\n\n/**\n * 설정 파일을 읽는다. 없거나 손상 시 빈 객체 — 데스크탑 electron-store 와 달리 평문 JSON\n * (safeStorage 부재). FCM 토큰은 PII 라 향후 OS 키체인 연동 여지를 남긴다(현재 MVP 는 평문).\n */\nexport function readStore(file: string): HeadlessStoreShape {\n try {\n return JSON.parse(fs.readFileSync(file, 'utf8')) as HeadlessStoreShape\n } catch {\n return {}\n }\n}\n\n/**\n * 설정 파일을 best-effort 로 저장한다(디렉터리 보장 후 write).\n *\n * FCM 토큰 등 PII 를 평문으로 담으므로 멀티유저 호스트에서 동거 사용자 노출을 줄이려 파일/디렉터리\n * 권한을 0600/0700 으로 제한한다(POSIX). Windows 는 mode 가 무시되어 무해.\n */\nexport function writeStore(file: string, data: HeadlessStoreShape): void {\n fs.mkdirSync(path.dirname(file), { recursive: true, mode: 0o700 })\n fs.writeFileSync(file, JSON.stringify(data, null, 2), { encoding: 'utf8', mode: 0o600 })\n}\n","import fs from 'node:fs'\r\nimport os from 'node:os'\r\nimport path from 'node:path'\r\n\r\n/**\r\n * reins 가 실제 소비하는 env 키 — `.env` 에선 이 키들만 `process.env` 로 주입한다.\r\n *\r\n * **WHY(보안)**: `.env` 는 cwd 상위에서 자동 발견되므로(신뢰 못 하는 ancestor 가능), 거기 적힌\r\n * 임의 `KEY=VALUE` 를 전부 주입하면 `NODE_OPTIONS`/`PATH` 등으로 자식 프로세스 코드 실행을 유발할\r\n * 수 있다. reins 가 쓰는 relay 키만 통과시켜 그 표면을 없앤다. (셸 env 는 이 필터를 거치지 않음.)\r\n */\r\nexport const ALLOWED_ENV_KEYS: ReadonlySet<string> = new Set([\r\n 'MAIN_VITE_RELAY_AGENT_SECRET',\r\n 'VIBE_RELAY_AGENT_SECRET',\r\n 'MAIN_VITE_RELAY_URL',\r\n 'VIBE_RELAY_URL',\r\n])\r\n\r\n/**\r\n * cwd 에서 위로 거슬러 올라가며 가장 가까운 `.env` 를 찾는다(없으면 null).\r\n *\r\n * **WHY**: `pnpm --filter @junyoung-kim/reins start` 의 cwd 는 `apps/headless` 라, 모노리포 루트의 `.env`\r\n * 를 찾으려면 상위 탐색이 필요하다(사용자가 패키지 폴더에 `.env` 를 복제하지 않도록). `npx @junyoung-kim/reins`\r\n * 도 프로젝트 하위 어디서 실행하든 그 프로젝트 `.env` 를 찾는다. 로드한 경로는 부팅 로그에 찍혀\r\n * 어떤 `.env` 가 쓰였는지 투명하다.\r\n *\r\n * @param start 탐색 시작 디렉터리(보통 `process.cwd()`).\r\n */\r\nexport function findUpwardEnvFile(start: string): string | null {\r\n let dir = path.resolve(start)\r\n for (;;) {\r\n const candidate = path.join(dir, '.env')\r\n try {\r\n fs.accessSync(candidate)\r\n return candidate\r\n } catch {\r\n // 이 레벨엔 없음 — 상위로\r\n }\r\n const parent = path.dirname(dir)\r\n if (parent === dir) return null // 파일시스템 루트 도달\r\n dir = parent\r\n }\r\n}\r\n\r\n/**\r\n * `.env` 자동 탐색 경로(앞쪽=가까운 것이 우선). cwd 상위에서 가장 가까운 `.env`(모노리포 루트 포함)\r\n * + 고정 위치 `~/.ai_remote_vibe_agent/.env`(어디서 실행하든 동일).\r\n */\r\nfunction defaultEnvFiles(): string[] {\r\n const files: string[] = []\r\n const nearest = findUpwardEnvFile(process.cwd())\r\n if (nearest) files.push(nearest)\r\n files.push(path.join(os.homedir(), '.ai_remote_vibe_agent', '.env'))\r\n return files\r\n}\r\n\r\n/**\r\n * `.env` 파일들을 `process.env` 로 로드한다(의존성 없는 최소 파서).\r\n *\r\n * reins 도 빌드 시 relay 기본값을 박지만(tsup define), 그건 어디까지나 **최종 fallback** 이라 사용자가\r\n * 자기 시크릿/URL 로 오버라이드하려면 env 가 필요하다. 매 실행마다 셸 export 를 치지 않도록 `.env` 를\r\n * 읽되, **이미 설정된 실제 env 는 절대 덮어쓰지 않는다**\r\n * (우선순위: 셸 env > 먼저 로드한 파일 > 나중 파일). 시크릿 값은 로깅하지 않으며, 검증 없이 그대로 주입.\r\n * `allowed` 에 든 키만 주입한다({@link ALLOWED_ENV_KEYS}) — ancestor `.env` 의 임의 env 주입 차단.\r\n *\r\n * @param files 테스트용 경로 주입(생략 시 {@link defaultEnvFiles}).\r\n * @param allowed 주입 허용 키 집합(생략 시 {@link ALLOWED_ENV_KEYS}).\r\n * @returns 실제로 읽어 적용한 파일 경로 목록(부팅 로그용 — 값은 포함하지 않음).\r\n */\r\nexport function loadDotEnv(\r\n files: string[] = defaultEnvFiles(),\r\n allowed: ReadonlySet<string> = ALLOWED_ENV_KEYS\r\n): string[] {\r\n const loaded: string[] = []\r\n for (const file of files) {\r\n let text: string\r\n try {\r\n text = fs.readFileSync(file, 'utf8')\r\n } catch {\r\n continue // 파일 없음 — 무시\r\n }\r\n for (const raw of text.split(/\\r?\\n/)) {\r\n const line = raw.trim().replace(/^export\\s+/, '')\r\n if (!line || line.startsWith('#')) continue\r\n const eq = line.indexOf('=')\r\n if (eq <= 0) continue\r\n const key = line.slice(0, eq).trim()\r\n if (!allowed.has(key)) continue // reins 가 소비하는 키만 주입(임의 env 주입 차단)\r\n if (key in process.env) continue // 셸 env / 먼저 로드한 파일이 우선 — 덮어쓰지 않음\r\n let val = line.slice(eq + 1).trim()\r\n // length>=2 가드: 값이 따옴표 1글자(`\"`)뿐일 때 slice(1,-1)가 빈 문자열로 만드는 것 방지.\r\n if (\r\n val.length >= 2 &&\r\n ((val.startsWith('\"') && val.endsWith('\"')) ||\r\n (val.startsWith(\"'\") && val.endsWith(\"'\")))\r\n ) {\r\n val = val.slice(1, -1)\r\n }\r\n process.env[key] = val\r\n }\r\n loaded.push(file)\r\n }\r\n return loaded\r\n}\r\n","{\n \"name\": \"@junyoung-kim/reins\",\n \"version\": \"0.1.0\",\n \"type\": \"module\",\n \"description\": \"폰에서 AI 코딩 에이전트의 고삐를 쥐다 — 헤드리스 TUI (npx @junyoung-kim/reins)\",\n \"bin\": {\n \"reins\": \"dist/cli.mjs\"\n },\n \"files\": [\n \"dist\"\n ],\n \"publishConfig\": {\n \"access\": \"public\"\n },\n \"engines\": {\n \"node\": \">=18\"\n },\n \"repository\": {\n \"type\": \"git\",\n \"url\": \"git+https://gitlab.com/juny-glre/ai_remote_vibe_agent.git\",\n \"directory\": \"apps/headless\"\n },\n \"scripts\": {\n \"build\": \"tsup\",\n \"start\": \"node ./dist/cli.mjs\",\n \"dev\": \"tsx src/cli.tsx\",\n \"typecheck\": \"tsc --noEmit\",\n \"test\": \"vitest run\",\n \"prepack\": \"tsup\"\n },\n \"dependencies\": {\n \"@xterm/headless\": \"^6.0.0\",\n \"electron-log\": \"^5.4.3\",\n \"ink\": \"^5.0.1\",\n \"ink-select-input\": \"^6.2.0\",\n \"ink-text-input\": \"^6.0.0\",\n \"node-pty\": \"^1.1.0\",\n \"p-limit\": \"^7.3.0\",\n \"qrcode-terminal\": \"^0.12.0\",\n \"react\": \"^18.3.1\",\n \"simple-git\": \"^3.36.0\",\n \"ssh2\": \"^1.17.0\",\n \"ws\": \"^8.20.0\"\n },\n \"devDependencies\": {\n \"@arva/core\": \"workspace:*\",\n \"@arva/shared\": \"workspace:*\",\n \"@types/node\": \"^24.10.1\",\n \"@types/react\": \"^18.3.12\",\n \"ink-testing-library\": \"^4.0.0\",\n \"tsup\": \"^8.3.5\",\n \"tsx\": \"^4.21.0\",\n \"typescript\": \"^5.9.3\",\n \"vite-tsconfig-paths\": \"^5.1.4\",\n \"vitest\": \"^4.1.4\"\n }\n}\n","import type { AgentIdentity } from '@arva/core'\n\nimport pkg from '../package.json'\n\n/**\n * 헤드리스 앱 식별 — cap.ack `agent_version` 에 실린다. 데스크탑(@arva/desktop)과 별개 버전이라\n * 별도로 주입한다(코어는 `~/package.json` 을 직접 읽지 않는다, A3).\n */\nexport const identity: AgentIdentity = { version: pkg.version }\n","import fs from 'node:fs'\nimport os from 'node:os'\nimport path from 'node:path'\n\nimport type { HeadlessConfigStore } from '../config/json-store'\nimport { computePairUrl } from '../pairing'\nimport { hasSystemctlUser, isServiceSupported, resolveExec } from './platform'\nimport {\n installService,\n lingerCommand,\n restartService,\n serviceStatus,\n uninstallService,\n} from './systemd'\n\nexport { resolveExec } from './platform'\nexport {\n isServiceActive,\n installService,\n restartService,\n serviceStatus,\n startService,\n stopService,\n uninstallService,\n lingerCommand,\n} from './systemd'\nexport type { ServiceStatus, InstallResult } from './systemd'\n\n/**\n * `reins service <action>` 진입점 — 코어/TUI 없이 systemd 등록만 수행하고 종료코드를 반환한다.\n *\n * 비-linux / systemctl 미존재는 깨지지 않고 안내 후 1 을 반환한다(릴리즈 규칙: 플랫폼 차이는\n * 명시적으로). 출력은 `console.log`(이 경로는 TUI 렌더 전이라 stdout 오염 우려 없음).\n *\n * @returns process exit code.\n */\nexport function runServiceCommand(\n action: string | undefined,\n config: HeadlessConfigStore\n): number {\n if (!isServiceSupported()) {\n console.log('reins service: 자동 시작 등록은 현재 Linux(systemd)만 지원합니다.')\n console.log(`(현재 플랫폼: ${process.platform} — macOS/Windows 는 후속 작업)`)\n return 1\n }\n if (!hasSystemctlUser()) {\n console.log('reins service: `systemctl --user` 를 찾을 수 없습니다 (systemd user 인스턴스 필요).')\n return 1\n }\n switch (action) {\n case 'install':\n return doInstall()\n case 'uninstall':\n return doUninstall()\n case 'status':\n return doStatus(config)\n default:\n console.log('usage: reins service <install | uninstall | status>')\n return 1\n }\n}\n\n/** 유닛 작성 + enable --now + linger/시크릿 안내. */\nfunction doInstall(): number {\n const exec = resolveExec()\n if (exec.isNpxCache) {\n console.log('⚠ npx 캐시 경로로 실행 중입니다 — 휘발성이라 서비스로 부적합합니다.')\n console.log(' 전역 설치 후 다시 시도하세요:')\n console.log(' npm i -g @junyoung-kim/reins')\n console.log(' reins service install')\n return 1\n }\n\n const res = installService(exec)\n for (const m of res.messages) console.log(m)\n if (!res.ok) return 1\n\n // CLI 경로는 포그라운드 인스턴스가 없으므로 즉시 시작한다. start 가 아니라 restart 를 쓰는 이유:\n // 재설치(버전 업)로 ExecStart 가 바뀌어도 이미 active 인 구버전이 새 유닛으로 교체되도록\n // (단순 start 는 active 면 no-op 라 구버전이 계속 떠 있는다).\n if (restartService()) console.log('service started')\n else console.log('warning: service start failed (systemctl --user restart reins 로 재시도)')\n\n console.log('')\n if (res.linger) {\n console.log('✅ 자동 시작 등록 완료 — 재부팅·SSH 로그아웃 후에도 유지됩니다.')\n } else {\n console.log('⚠ SSH 를 끊어도 유지되려면 아래를 1회 실행하세요 (root 권한 필요):')\n console.log(` ${res.lingerCommand}`)\n console.log(' (linger 없이는 로그아웃 시 user 서비스가 함께 종료됩니다.)')\n }\n\n if (!serviceHasSecretOverride()) {\n console.log('')\n console.log('ℹ relay 시크릿: 내장 기본값으로 접속합니다 (별도 설정 불필요).')\n console.log(' 자신의 시크릿으로 바꾸려면 ~/.ai_remote_vibe_agent/.env 에 추가하세요:')\n console.log(' 예) echo \"MAIN_VITE_RELAY_AGENT_SECRET=<your-secret>\" >> ~/.ai_remote_vibe_agent/.env')\n console.log(' 추가 후: systemctl --user restart reins')\n }\n return 0\n}\n\n/** disable --now + 유닛 삭제. */\nfunction doUninstall(): number {\n const res = uninstallService()\n for (const m of res.messages) console.log(m)\n console.log('자동 시작 등록을 해제했습니다.')\n return 0\n}\n\n/** 상태 출력 + (가능하면) 페어링 URL 재계산 노출. */\nfunction doStatus(config: HeadlessConfigStore): number {\n const s = serviceStatus()\n console.log(`platform : ${process.platform} (supported: ${s.supported})`)\n console.log(`installed : ${s.installed}`)\n console.log(`active : ${s.active}`)\n console.log(`enabled : ${s.enabled}`)\n console.log(`linger : ${s.linger} (user: ${s.user})`)\n\n const pairUrl = computePairUrl(config.getRelayUrls())\n if (pairUrl) console.log(`pair url : ${pairUrl}`)\n\n if (s.installed && !s.linger) {\n console.log('')\n console.log(`⚠ SSH 로그아웃 후 유지하려면: ${lingerCommand(s.user)}`)\n }\n return 0\n}\n\n/**\n * 서비스(systemd)가 **사용자 지정** relay 시크릿 오버라이드를 갖는지 점검.\n *\n * 시크릿이 비어도 빌드 기본값(`__REINS_DEFAULT_RELAY_SECRET__`)으로 접속되므로 더 이상 실패 경고가\n * 아니다. 다만 서비스는 cwd=`/` 라 cwd 상위 탐색 `.env` 가 안 잡히고 **고정 경로\n * `~/.ai_remote_vibe_agent/.env`** 만 로드하므로(load-env 의 `defaultEnvFiles`), 이 파일에 오버라이드가\n * 없으면 \"기본값으로 동작 + 바꾸는 법\" 안내를 띄우는 용도로만 쓴다.\n */\nfunction serviceHasSecretOverride(): boolean {\n const envFile = path.join(os.homedir(), '.ai_remote_vibe_agent', '.env')\n try {\n const text = fs.readFileSync(envFile, 'utf-8')\n // `=\\s*\\S`: 키만 있고 값이 빈(`KEY=`) 경우를 \"있음\"으로 오판하지 않도록 비어있지 않은 값 요구.\n return /^(export\\s+)?(MAIN_VITE_RELAY_AGENT_SECRET|VIBE_RELAY_AGENT_SECRET)\\s*=\\s*\\S/m.test(text)\n } catch {\n return false\n }\n}\n","import fs from 'node:fs'\nimport os from 'node:os'\nimport path from 'node:path'\n\nimport { toPairUrl } from '@arva/core'\n\n/**\n * 영속 세션 토큰 파일 — 코어(`ws-manager`)가 `loadOrCreateSessionToken` 으로 생성·소유한다.\n * 데스크탑·헤드리스가 공유하는 고정 경로(`~/.ai_remote_vibe_agent/session-token.txt`).\n */\nconst SESSION_TOKEN_FILE = path.join(os.homedir(), '.ai_remote_vibe_agent', 'session-token.txt')\n\n/**\n * 영속 세션 토큰을 **읽기만** 한다(없으면 null — 생성하지 않음).\n *\n * 관리 화면은 라이브 코어 없이 동작하므로 코어가 만들어 둔 토큰을 그대로 읽는다. 새로 만들면\n * 서비스가 쓰는 토큰과 달라져 QR 이 어긋나므로, 토큰이 없으면(=서비스 미기동 이력) null 을 반환한다.\n * UUID(36자) 검증으로 깨진 파일을 거른다.\n */\nexport function readSessionToken(): string | null {\n try {\n const t = fs.readFileSync(SESSION_TOKEN_FILE, 'utf-8').trim()\n return t.length === 36 ? t : null\n } catch {\n return null\n }\n}\n\n/**\n * 페어링 URL 을 조립하는 **순수** 부분(단위테스트 대상) — IO 없는 변환만.\n *\n * 코어 `connectToRelay` 와 동일한 변환을 재현한다: primary relay base 의 trailing slash 정규화 →\n * `${base}/mobile/${token}` → {@link toPairUrl}. token/relayUrl 이 없으면 null.\n *\n * @param relayUrls index 0 = primary.\n * @param token 영속 세션 토큰(없으면 null).\n */\nexport function buildPairUrl(relayUrls: string[], token: string | null): string | null {\n const rawBase = relayUrls[0]\n if (!token || !rawBase) return null\n const relayBase = rawBase.replace(/\\/+$/, '')\n return toPairUrl(`${relayBase}/mobile/${token}`)\n}\n\n/**\n * 라이브 코어 없이 페어링 URL(HTTPS 딥링크)을 **재계산**한다 — 관리/상태 화면용.\n *\n * 토큰은 디스크 영속이라 서비스가 실제 서빙 중인 QR 과 **동일**하다. 토큰 미존재 또는 relay URL\n * 없음 시 null(호출부가 \"준비 안 됨\" 표시).\n *\n * **한계(다중 relay)**: 항상 primary(index 0)를 가정한다. 코어가 fallback relay 로 failover 한\n * 상태면 이 URL 은 죽은 primary 를 가리킬 수 있다 — 실제 활성 relay 는 서비스 프로세스만 안다.\n * 호출부는 relay 가 2개 이상이면 \"primary 가정\" 라벨로 이 가정을 사용자에게 노출한다.\n *\n * @param relayUrls `config.getRelayUrls()` (index 0 = primary).\n */\nexport function computePairUrl(relayUrls: string[]): string | null {\n return buildPairUrl(relayUrls, readSessionToken())\n}\n","import { spawnSync } from 'node:child_process'\nimport fs from 'node:fs'\nimport os from 'node:os'\nimport path from 'node:path'\n\n/**\n * 자동 시작 서비스 등록을 지원하는 플랫폼인지.\n *\n * 현재 **리눅스(systemd) 우선만** 지원한다. macOS(launchd)/Windows(Task Scheduler)는 후속 작업이라,\n * 비-linux 에서는 깨지지 않고 \"미지원\" 안내만 하도록 호출부가 이 값을 먼저 확인한다.\n */\nexport function isServiceSupported(): boolean {\n return process.platform === 'linux'\n}\n\n/** systemctl 가용성 캐시 — 프로세스 수명 동안 불변이라 1회만 spawn 한다(관리 화면 폴링의 중복 제거). */\nlet systemctlUserAvailable: boolean | undefined\n\n/**\n * `systemctl --user` 가용 여부. systemd 미설치(컨테이너·일부 배포판)면 false 라,\n * install 이 깨지지 않고 안내 후 종료할 수 있다. 바이너리 미존재(ENOENT)도 false 로 흡수한다.\n *\n * 결과를 모듈 스코프에 캐시한다 — `serviceStatus()` 가드가 매 호출(관리 화면 폴링 포함)마다\n * `systemctl --version` 을 spawn 하던 것을 첫 1회로 줄인다.\n */\nexport function hasSystemctlUser(): boolean {\n if (systemctlUserAvailable === undefined) {\n const r = spawnSync('systemctl', ['--user', '--version'], { stdio: 'ignore' })\n systemctlUserAvailable = !r.error && r.status === 0\n }\n return systemctlUserAvailable\n}\n\n/** linger 안내(`loginctl enable-linger <user>`)에 쓸 현재 사용자명. */\nexport function currentUser(): string {\n return os.userInfo().username\n}\n\n/** {@link resolveExec} 결과. */\nexport interface ExecResolution {\n /** node 절대경로. */\n nodePath: string\n /** cli 스크립트 절대경로(심링크 해소). */\n scriptPath: string\n /** `npx`(npm 캐시) 경로로 실행 중인지 — 서비스 ExecStart 로는 부적합. */\n isNpxCache: boolean\n}\n\n/**\n * 유닛 `ExecStart` 에 박을 실행 경로를 resolve 한다.\n *\n * `process.execPath`(node) + `process.argv[1]`(cli) 절대경로를 쓴다. 전역 설치 시 bin 심링크를\n * 가리킬 수 있어 `realpath` 로 실제 파일을 고정한다. `npx @junyoung-kim/reins`(npm 캐시 `_npx`)로 실행하면 그\n * 경로가 임시·휘발성이라 서비스로 부적합 → `isNpxCache=true` 로 호출부가 경고하게 한다.\n */\nexport function resolveExec(): ExecResolution {\n const nodePath = process.execPath\n const rawScript = process.argv[1] ?? ''\n let scriptPath = rawScript\n try {\n scriptPath = fs.realpathSync(path.resolve(rawScript))\n } catch {\n scriptPath = path.resolve(rawScript)\n }\n // npm 의 npx 실행은 `.../_npx/<hash>/node_modules/...` 캐시에 풀린다(OS 무관 `_npx` 세그먼트).\n const isNpxCache = scriptPath.split(path.sep).includes('_npx')\n return { nodePath, scriptPath, isNpxCache }\n}\n","import { spawnSync } from 'node:child_process'\nimport fs from 'node:fs'\n\nimport {\n currentUser,\n type ExecResolution,\n hasSystemctlUser,\n isServiceSupported,\n} from './platform'\nimport { renderUnit, UNIT_FILE_NAME, userUnitDir, userUnitPath } from './unit-template'\n\n/** systemd user 서비스의 종합 상태(관리 화면·`status` 서브커맨드 공유). */\nexport interface ServiceStatus {\n /** linux + `systemctl --user` 둘 다 가용한지. */\n supported: boolean\n /** 유닛 파일이 존재하는지. */\n installed: boolean\n active: boolean\n enabled: boolean\n /** linger 활성 — false 면 SSH 로그아웃 시 서비스가 함께 종료된다. */\n linger: boolean\n user: string\n}\n\n/** {@link installService} 결과 — 호출부가 linger 안내·npx 경고를 출력하는 데 쓴다. */\nexport interface InstallResult {\n ok: boolean\n unitPath: string\n linger: boolean\n /** linger OFF 일 때 사용자가 1회 복붙할 root 명령. */\n lingerCommand: string\n messages: string[]\n}\n\n/**\n * `systemctl --user <args>` 를 **배열 인자·`shell:false`** 로 실행한다(셸 escape 불필요).\n * 바이너리 미존재(ENOENT)·비정상 종료를 `ok=false` 로 흡수해 호출부가 깨지지 않게 한다.\n */\nfunction systemctlUser(args: string[]): { ok: boolean; stdout: string; stderr: string } {\n const r = spawnSync('systemctl', ['--user', ...args], { encoding: 'utf-8' })\n return {\n ok: !r.error && r.status === 0,\n stdout: (r.stdout ?? '').trim(),\n stderr: (r.stderr ?? '').trim(),\n }\n}\n\n/**\n * 서비스가 가동 중인지 — `cli.tsx` 단일 인스턴스 가드의 진입 판정.\n *\n * `is-active` 는 비활성 시 비정상 종료코드를 내므로 종료코드가 아닌 **stdout==='active'** 로 본다.\n * 비-linux·systemctl 미존재는 false(=가드 비활성, 풀 TUI 진행).\n */\nexport function isServiceActive(): boolean {\n if (process.platform !== 'linux') return false\n return systemctlUser(['is-active', UNIT_FILE_NAME]).stdout === 'active'\n}\n\n/** 해당 사용자 linger 활성 여부(`loginctl show-user`). loginctl 미존재 시 false. */\nfunction lingerEnabled(user: string): boolean {\n const r = spawnSync('loginctl', ['show-user', user, '--property=Linger'], {\n encoding: 'utf-8',\n })\n if (r.error) return false\n return (r.stdout ?? '').includes('Linger=yes')\n}\n\n/** SSH 로그아웃 후 유지를 위해 사용자가 1회 실행할 root 명령(순수 문자열). */\nexport function lingerCommand(user: string): string {\n return `sudo loginctl enable-linger ${user}`\n}\n\n/** 서비스 종합 상태 조회(관리 화면·status 공유). 비지원 플랫폼은 supported=false 로 단락. */\nexport function serviceStatus(): ServiceStatus {\n const user = currentUser()\n const installed = fs.existsSync(userUnitPath())\n if (!(isServiceSupported() && hasSystemctlUser())) {\n return { supported: false, installed, active: false, enabled: false, linger: false, user }\n }\n return {\n supported: true,\n installed,\n active: isServiceActive(),\n enabled: systemctlUser(['is-enabled', UNIT_FILE_NAME]).stdout === 'enabled',\n linger: lingerEnabled(user),\n user,\n }\n}\n\n/**\n * user 유닛을 작성하고 부팅 자동 시작으로 `enable` 한다(**시작은 하지 않음** — {@link startService} 분리).\n *\n * 시작을 분리하는 이유: 포그라운드 TUI 에서 등록할 때 즉시 `--now` 로 띄우면 포그라운드 코어와\n * 서비스 코어가 같은 포트·같은 install_id 로 충돌한다(두 번째 인스턴스 footgun). 호출부가\n * 포그라운드 정리(`core.cleanup()`) 뒤에 {@link startService} 를 부르도록 시점을 분리한다.\n * CLI `reins service install` 은 포그라운드가 없으므로 install 직후 바로 start 한다.\n *\n * relay 시크릿은 유닛에 박지 않는다(앱이 `~/.ai_remote_vibe_agent/.env` 자동 로드).\n * linger 는 root 권한이 필요해 여기서 켜지 않고, 현재 상태만 담아 호출부가 안내하게 한다.\n */\nexport function installService(exec: ExecResolution): InstallResult {\n const user = currentUser()\n const unitPath = userUnitPath()\n const messages: string[] = []\n\n fs.mkdirSync(userUnitDir(), { recursive: true })\n fs.writeFileSync(\n unitPath,\n renderUnit({ nodePath: exec.nodePath, scriptPath: exec.scriptPath }),\n { mode: 0o644 }\n )\n messages.push(`unit written: ${unitPath}`)\n\n const reload = systemctlUser(['daemon-reload'])\n if (!reload.ok) {\n // reload 실패 시 enable 이 (디스크의 새 유닛이 아닌) stale 정의를 잡을 수 있어, \"성공\"으로\n // 보고하면 안 된다 — fatal 처리해 호출부가 재시도하게 한다.\n messages.push(`error: daemon-reload failed: ${reload.stderr}`)\n return {\n ok: false,\n unitPath,\n linger: lingerEnabled(user),\n lingerCommand: lingerCommand(user),\n messages,\n }\n }\n\n const enable = systemctlUser(['enable', UNIT_FILE_NAME])\n if (enable.ok) messages.push('service enabled (boot)')\n else messages.push(`error: enable failed: ${enable.stderr}`)\n\n return {\n ok: enable.ok,\n unitPath,\n linger: lingerEnabled(user),\n lingerCommand: lingerCommand(user),\n messages,\n }\n}\n\n/** 서비스 시작(`start`). {@link installService} 로 enable 한 뒤 또는 핸드오프 종료 시 호출. */\nexport function startService(): boolean {\n return systemctlUser(['start', UNIT_FILE_NAME]).ok\n}\n\n/** `disable --now` + 유닛 삭제 + reload. 이미 없는 상태에도 안전(멱등). */\nexport function uninstallService(): { ok: boolean; messages: string[] } {\n const messages: string[] = []\n const disable = systemctlUser(['disable', '--now', UNIT_FILE_NAME])\n if (!disable.ok && disable.stderr) messages.push(`warning: disable: ${disable.stderr}`)\n try {\n fs.rmSync(userUnitPath())\n messages.push('unit removed')\n } catch {\n // 이미 없음 — 무시\n }\n systemctlUser(['daemon-reload'])\n return { ok: true, messages }\n}\n\n/** 서비스 재시작(관리 화면 `r`). */\nexport function restartService(): boolean {\n return systemctlUser(['restart', UNIT_FILE_NAME]).ok\n}\n\n/** 서비스 중지(관리 화면 `x`). */\nexport function stopService(): boolean {\n return systemctlUser(['stop', UNIT_FILE_NAME]).ok\n}\n","import os from 'node:os'\nimport path from 'node:path'\n\n/**\n * systemd **user** 서비스 식별자. install 한 유닛명(`reins.service`)으로\n * `systemctl --user`·`cli.tsx` 단일 인스턴스 감지가 모두 이 상수를 공유한다.\n */\nexport const SERVICE_NAME = 'reins'\n\n/** 유닛 파일명(`reins.service`). */\nexport const UNIT_FILE_NAME = `${SERVICE_NAME}.service`\n\n/** {@link renderUnit} 입력 — install 시점에 resolve 한 실행 경로(둘 다 절대경로). */\nexport interface UnitParams {\n /** node 절대경로(`process.execPath`). */\n nodePath: string\n /** 번들된 cli 절대경로(`dist/cli.mjs`의 realpath). */\n scriptPath: string\n}\n\n/**\n * systemd user 유닛 파일 텍스트를 생성한다(부수효과 없는 순수 함수 — 단위테스트 대상).\n *\n * - `Restart=always`/`RestartSec=3`: 크래시·OOM 후 자동 복구(클라우드 상시 구동 목적).\n * - `After/Wants=network-online.target`: relay 접속이 네트워크 준비 후 시작되도록.\n * - relay 시크릿은 **유닛에 박지 않는다** — 앱이 `~/.ai_remote_vibe_agent/.env` 를 자동 로드하고,\n * user 서비스라 `HOME` 이 설정돼 `os.homedir()` 가 정상 해석된다.\n *\n * ExecStart 경로에 공백이 있으면 systemd 가 토큰을 잘못 분리하므로 각 경로를 `\"`로 감싼다.\n */\nexport function renderUnit(p: UnitParams): string {\n const exec = `${quoteIfNeeded(p.nodePath)} ${quoteIfNeeded(p.scriptPath)}`\n return `${[\n '[Unit]',\n 'Description=reins — AI remote vibe agent (headless)',\n 'After=network-online.target',\n 'Wants=network-online.target',\n '',\n '[Service]',\n 'Type=simple',\n `ExecStart=${exec}`,\n 'Restart=always',\n 'RestartSec=3',\n '',\n '[Install]',\n 'WantedBy=default.target',\n ].join('\\n')}\\n`\n}\n\n/** 공백 포함 경로만 systemd 더블쿼트로 감싼다(공백 없으면 원본 — 일반 케이스 가독성). */\nfunction quoteIfNeeded(p: string): string {\n return /\\s/.test(p) ? `\"${p}\"` : p\n}\n\n/** user 유닛 디렉터리(`~/.config/systemd/user`). */\nexport function userUnitDir(): string {\n return path.join(os.homedir(), '.config', 'systemd', 'user')\n}\n\n/** user 유닛 파일 절대경로. */\nexport function userUnitPath(): string {\n return path.join(userUnitDir(), UNIT_FILE_NAME)\n}\n","import { EventEmitter } from 'node:events'\n\nimport type { ConnectionDetails, Machine, ServerMessage } from '@arva/shared/types'\n\n/**\n * PTY 운영 상태 — 코어 `pty_status.status` 에 바인딩(폰 attach 전 미관측은 'idle' 로 표기).\n *\n * 손수 적은 리터럴 유니온이 아니라 프로토콜에서 추출 — shared 가 상태를 추가/변경하면 여기서도\n * 타입이 즉시 깨져 드리프트를 막는다.\n */\nexport type PtyState = Extract<ServerMessage, { type: 'pty_status' }>['status']\n\n/** 세션 카드용 머신 요약(이름 표시 + PTY 상태 오버레이 키). 프로토콜 `Machine` 에서 필드명 드리프트-락. */\nexport type MachineSummary = Pick<Machine, 'id' | 'name'>\n\n/** TUI 가 구독하는 헤드리스 UI 상태(불변 갱신 — Ink 가 참조 비교로 리렌더). */\nexport interface HeadlessUiState {\n relayStatus: string\n /** relay 재시도 횟수(0=정상). 연결 실패가 반복되는지 기본 화면 상태줄에 노출. */\n relayRetry: number\n /**\n * 모바일 페어링 정보. `url` 은 폰이 연결할 페어링 URL(relay 모드=토큰 포함 HTTPS 딥링크,\n * LAN 모드=`ws://<ip>:<port>/mobile`) — Info/Main 화면이 텍스트로 노출한다. `details` 는\n * relayToken(8자)·localPort·lanIp 등 부가 진단 정보(`qr_code` 메시지 그대로 보존).\n */\n qr: { url: string; mode: string; details: ConnectionDetails } | null\n /** 코어가 보유한 머신 목록(`machine_list` broadcast 로 갱신 — 폰이 머신 추가/삭제하면 라이브 반영). */\n machines: MachineSummary[]\n /** machineId → PTY 상태 오버레이. 머신 목록(이름)은 위 machines, 여기선 라이브 상태만 덮는다. */\n sessions: Record<string, PtyState>\n /** 상세 로그(verbose 전용·스크롤). 기본 화면엔 미표시 — control-plane 이벤트만 누적. */\n logs: string[]\n}\n\n/** verbose 로그 보관 한도. 스크롤백을 위해 기본 화면 표시량보다 넉넉히 잡는다. */\nconst MAX_LOGS = 500\n\n/**\n * 코어 broadcast(host 측)를 받아 TUI 상태로 환원하는 store. `apply(msg)` 가 상태를 불변 갱신하고\n * `'change'` 를 emit 하면 Ink 컴포넌트(useStore)가 리렌더한다. 데스크탑의 렌더러 IPC 수신부에\n * 대응하는 헤드리스판 sink.\n *\n * **데이터-플레인 제외**: `terminal_data`/`terminal_resize_ack`/`session_replay`/`ping` 은\n * 폰 사용 중 매 청크마다 쏟아져 로그를 뒤덮고 불필요한 리렌더를 유발한다. 실제 화면은 폰에 미러링되므로\n * 호스트 TUI 에선 무시한다(no log·no emit) — control-plane(relay/pty/conn/error/machine_list)만 추적해\n * verbose 로그의 신호 대 잡음을 유지한다.\n */\nexport class HeadlessStore extends EventEmitter {\n private state: HeadlessUiState = {\n relayStatus: 'connecting',\n relayRetry: 0,\n qr: null,\n machines: [],\n sessions: {},\n logs: [],\n }\n\n getState(): HeadlessUiState {\n return this.state\n }\n\n /** 코어 ServerMessage 1건을 상태에 반영. 구조화 상태(relay/qr/session)는 화면에, 그 외는 로그에. */\n apply(msg: ServerMessage): void {\n switch (msg.type) {\n case 'relay_status':\n this.state = { ...this.state, relayStatus: msg.status, relayRetry: msg.retryCount }\n this.pushLog(`[relay] ${msg.status}${msg.retryCount ? ` (retry ${msg.retryCount})` : ''}`)\n return\n case 'qr_code':\n this.state = { ...this.state, qr: { url: msg.url, mode: msg.mode, details: msg.details } }\n this.pushLog(`[qr] ${msg.mode}`)\n return\n case 'pty_status':\n this.state = {\n ...this.state,\n sessions: { ...this.state.sessions, [msg.machineId]: msg.status },\n }\n this.pushLog(`[pty] ${msg.machineId} → ${msg.status}${msg.pid ? ` pid=${msg.pid}` : ''}`)\n return\n case 'machine_list':\n // 폰이 머신을 추가/삭제하면 코어가 broadcastMachines() → 여기로 흐른다. Sessions 카드를 라이브로.\n this.state = {\n ...this.state,\n machines: msg.machines.map(m => ({ id: m.id, name: m.name })),\n }\n this.pushLog(`[machines] ${msg.machines.length}`)\n return\n case 'connection_status':\n this.pushLog(`[conn] ${msg.machineId} → ${msg.status}`)\n return\n case 'error':\n this.pushLog(`[error] ${msg.message}`)\n return\n // 데이터-플레인(고빈도) — 무시. emit 도 하지 않아 리렌더 churn 을 막는다.\n case 'terminal_data':\n case 'terminal_resize_ack':\n case 'session_replay':\n case 'ping':\n return\n default:\n this.pushLog(`· ${msg.type}`)\n }\n }\n\n /** 사람이 읽는 로그 라인을 직접 추가(cli 부팅 메시지 등). verbose 로그에만 보인다. */\n log(line: string): void {\n this.pushLog(line)\n }\n\n private pushLog(line: string): void {\n const ts = new Date().toISOString().slice(11, 19)\n const logs = [...this.state.logs, `${ts} ${line}`].slice(-MAX_LOGS)\n this.state = { ...this.state, logs }\n this.emit('change')\n }\n}\n","import { Box, Text, useApp } from 'ink'\nimport React, { useEffect, useState } from 'react'\n\nimport type { AgentCore } from '@arva/core'\n\nimport type { HeadlessConfigStore } from '../config/json-store'\nimport { identity } from '../identity'\nimport type { HeadlessStore, HeadlessUiState } from '../state'\nimport { InfoScreen } from './screens/InfoScreen'\nimport { MachinesScreen } from './screens/MachinesScreen'\nimport { MainScreen } from './screens/MainScreen'\nimport { OptionsScreen } from './screens/OptionsScreen'\n\n/** 라우터 화면 식별자. machineForm 은 MachinesScreen 내부 sub-mode 로 처리(여기선 top-level 4개). */\nexport type Screen = 'main' | 'machines' | 'options' | 'info'\n\n/** store 의 'change' 를 구독해 최신 상태를 반환하는 훅(참조 갱신으로 Ink 리렌더 유도). */\nfunction useStore(store: HeadlessStore): HeadlessUiState {\n const [state, setState] = useState(store.getState())\n useEffect(() => {\n const onChange = () => setState({ ...store.getState() })\n store.on('change', onChange)\n return () => {\n store.off('change', onChange)\n }\n }, [store])\n return state\n}\n\n/** relay 상태별 도트 + 색. */\nconst STATUS: Record<string, { dot: string; color: string }> = {\n connected: { dot: '●', color: 'green' },\n connecting: { dot: '◌', color: 'yellow' },\n disconnected: { dot: '○', color: 'red' },\n error: { dot: '✗', color: 'red' },\n}\n\n/**\n * 헤드리스 메인 셸/라우터. 항상 보이는 헤더(reins·relay 상태·relay 타깃) + 활성 화면(main/machines/options).\n *\n * **키 소유권(중요)**: 각 화면이 자신의 `useInput` 을 `isActive`(=현재 화면) 로 게이팅한다. Ink 는\n * 핸들러를 stop-propagate 하지 않으므로, 게이팅 없이는 폼 타이핑 중 다른 화면 단축키가 동시 발동한다.\n * App 자체는 키를 잡지 않고 라우팅만 한다.\n *\n * relay 타깃 헤더는 `config.getRelayUrls()` 를 매 렌더 라이브로 읽어, 옵션 화면에서 URL 편집 후에도\n * (재시작 없이) 즉시 갱신된다.\n */\nexport function App({\n store,\n core,\n config,\n}: {\n store: HeadlessStore\n core: AgentCore\n config: HeadlessConfigStore\n}) {\n const state = useStore(store)\n const { exit } = useApp()\n const [screen, setScreen] = useState<Screen>('main')\n\n // relay 가 붙으면 relay QR 재요청(부팅 직후엔 LAN fallback QR 만 가능). 화면 무관 글로벌 lifecycle.\n useEffect(() => {\n if (state.relayStatus === 'connected') core.ws.requestQr()\n }, [state.relayStatus, core])\n\n const s = STATUS[state.relayStatus] ?? { dot: '·', color: 'gray' }\n const retry = state.relayRetry ? ` (retry ${state.relayRetry})` : ''\n const relayTarget = config.getRelayUrls().join(', ')\n\n return (\n <Box flexDirection=\"column\" borderStyle=\"round\" paddingX={1}>\n <Box justifyContent=\"space-between\">\n <Text bold>reins</Text>\n <Text color=\"gray\">v{identity.version}</Text>\n </Box>\n\n <Box>\n <Text color=\"gray\">Relay </Text>\n <Text color={s.color}>\n {s.dot} {state.relayStatus}\n {retry}\n </Text>\n {state.qr ? <Text color=\"gray\"> ({state.qr.mode})</Text> : null}\n </Box>\n <Text color=\"gray\">{relayTarget}</Text>\n\n {screen === 'main' ? (\n <MainScreen\n state={state}\n core={core}\n active\n onNavigate={setScreen}\n onExit={exit}\n />\n ) : null}\n {screen === 'machines' ? (\n <MachinesScreen\n machines={core.ssh.getMachines()}\n core={core}\n active\n onBack={() => setScreen('main')}\n />\n ) : null}\n {screen === 'options' ? (\n <OptionsScreen config={config} core={core} active onBack={() => setScreen('main')} />\n ) : null}\n {screen === 'info' ? (\n <InfoScreen\n state={state}\n config={config}\n core={core}\n active\n onBack={() => setScreen('main')}\n />\n ) : null}\n </Box>\n )\n}\n","import os from 'node:os'\nimport path from 'node:path'\n\nimport { Box, Text, useInput } from 'ink'\nimport React, { useEffect, useState } from 'react'\n\nimport type { AgentCore } from '@arva/core'\n\nimport type { HeadlessConfigStore } from '../../config/json-store'\nimport { identity } from '../../identity'\nimport type { HeadlessUiState } from '../../state'\nimport { DIVIDER } from '../constants'\nimport { prettyUrl } from '../pretty-url'\n\n/** relay 상태별 색(헤더와 동일 관용 — connected=초록 / connecting=노랑 / 그 외=빨강). */\nconst STATUS_COLOR: Record<string, string> = {\n connected: 'green',\n connecting: 'yellow',\n disconnected: 'red',\n error: 'red',\n}\n\n/**\n * 비-internal IPv4 주소 목록. WS 포트와 합쳐 LAN 페어링 엔드포인트를 추정하는 데 쓴다.\n * `os.networkInterfaces()` 는 전 플랫폼 동일 API라 분기 없이 안전하다.\n */\nfunction localAddresses(): string[] {\n const out: string[] = []\n for (const iface of Object.values(os.networkInterfaces())) {\n if (!iface) continue\n for (const addr of iface) {\n if (addr.family === 'IPv4' && !addr.internal) out.push(addr.address)\n }\n }\n return out\n}\n\n/** 라벨(고정폭) + 값 한 줄. 값이 길면 Ink 가 wrap 한다(pair url 등). */\nfunction Row({ label, value, color }: { label: string; value: string; color?: string }) {\n return (\n <Box>\n <Text color=\"gray\">{` ${label.padEnd(9)} `}</Text>\n <Text color={color}>{value}</Text>\n </Box>\n )\n}\n\n/**\n * 정보(Info) 화면 — 읽기전용 진단 집약. 페어링 URL(토큰 url)·relay·로컬 서버·에이전트·타임아웃·\n * 시스템 정보를 한곳에 모은다. 편집은 OptionsScreen 담당(여긴 조회만).\n *\n * **보안**: relay 시크릿은 **값을 표시하지 않고** 설정 여부(✓/✗)만 노출한다(시크릿 비로깅 규약과 일관).\n * **키 소유권**: `useInput` 은 active(=현재 화면이 info)일 때만 Esc(뒤로)를 처리한다.\n * clients 수는 ws 내부 카운트가 신뢰 소스라 1s 폴링(MainScreen 과 동일 패턴).\n *\n * @param active 현재 라우터 화면이 info 인지(키 입력 게이팅).\n */\nexport function InfoScreen({\n state,\n config,\n core,\n active,\n onBack,\n}: {\n state: HeadlessUiState\n config: HeadlessConfigStore\n core: AgentCore\n active: boolean\n onBack: () => void\n}) {\n const [clients, setClients] = useState(0)\n\n useEffect(() => {\n const t = setInterval(() => {\n setClients(core.ws.getActiveMobileClientCount())\n }, 1000)\n return () => clearInterval(t)\n }, [core])\n\n useInput(\n (_input, key) => {\n if (key.escape) onBack()\n },\n { isActive: active }\n )\n\n const statusColor = STATUS_COLOR[state.relayStatus] ?? 'gray'\n const retry = state.relayRetry ? ` (retry ${state.relayRetry})` : ''\n const [primary, ...fallbacks] = config.getRelayUrls()\n const secretSet = config.getRelayAgentSecret().length > 0\n const lan = localAddresses()\n const dataDir = path.join(os.homedir(), '.ai_remote_vibe_agent')\n\n return (\n <Box flexDirection=\"column\">\n <Text color=\"gray\">{DIVIDER}</Text>\n <Text>Info</Text>\n\n <Text color=\"gray\">{DIVIDER}</Text>\n <Text color=\"gray\">CONNECTION</Text>\n {state.qr ? (\n <>\n <Row label=\"pair url\" value={prettyUrl(state.qr.url)} color=\"cyan\" />\n <Row\n label=\"mode\"\n value={`${state.qr.mode}${state.qr.details.relayToken ? ` token ${state.qr.details.relayToken}` : ''}`}\n />\n </>\n ) : (\n <Row label=\"pair url\" value=\"Preparing… (waiting for relay)\" color=\"yellow\" />\n )}\n\n <Text color=\"gray\">{DIVIDER}</Text>\n <Text color=\"gray\">RELAY</Text>\n <Row label=\"status\" value={`${state.relayStatus}${retry}`} color={statusColor} />\n <Row label=\"target\" value={primary ?? '-'} />\n {fallbacks.map(url => (\n <Row key={url} label=\"fallback\" value={url} />\n ))}\n <Row\n label=\"secret\"\n value={secretSet ? '✓ set' : '✗ missing (relay auth unavailable)'}\n color={secretSet ? 'green' : 'red'}\n />\n\n <Text color=\"gray\">{DIVIDER}</Text>\n <Text color=\"gray\">LOCAL SERVER</Text>\n <Row label=\"ws port\" value={String(config.getWsPort())} />\n <Row label=\"lan\" value={lan.length > 0 ? lan.join(', ') : '(none)'} />\n <Row label=\"clients\" value={String(clients)} color={clients > 0 ? 'green' : 'gray'} />\n\n <Text color=\"gray\">{DIVIDER}</Text>\n <Text color=\"gray\">AGENT</Text>\n <Row label=\"version\" value={identity.version} />\n <Row label=\"install\" value={`${config.getInstallId().slice(0, 8)}…`} />\n <Row label=\"fcm\" value={String(config.getFcmTokens().length)} />\n <Row label=\"machines\" value={String(core.ssh.getMachines().length)} />\n\n <Text color=\"gray\">{DIVIDER}</Text>\n <Text color=\"gray\">TIMEOUTS</Text>\n <Row\n label=\"hb/po/au\"\n value={`${config.getHeartbeatIntervalMs()} · ${config.getPongTimeoutMs()} · ${config.getAuthTimeoutMs()} ms`}\n />\n\n <Text color=\"gray\">{DIVIDER}</Text>\n <Text color=\"gray\">SYSTEM</Text>\n <Row label=\"os\" value={`${os.platform()} ${os.arch()} · ${os.hostname()}`} />\n <Row label=\"node\" value={process.version} />\n <Row label=\"data\" value={dataDir} />\n <Row label=\"logs\" value={path.join(dataDir, 'logs')} />\n\n <Text color=\"gray\">{DIVIDER}</Text>\n <Text color=\"cyan\">Esc back</Text>\n </Box>\n )\n}\n","/** 화면 구분선(가로줄 40폭). 모든 헤드리스 TUI 화면이 공유하는 단일 정의. */\nexport const DIVIDER = '─'.repeat(40)\n","/**\n * 페어링 URL 을 사람이 읽기 쉬운 형태로 디코드한다 — **화면 표시 전용**.\n *\n * relay 모드 URL 은 `…/pair?u=<encodeURIComponent(ws)>` 구조라 `wss%3A%2F%2F…` 처럼 보여\n * 원격 SSH·작은 터미널에서 폰으로 직접 입력·공유하기 어렵다. percent-encoding 을 풀어\n * `…/pair?u=wss://relay…/mobile/<id>` 로 노출한다.\n *\n * **원본을 바꾸지 않는다**: QR 인코딩·실제 페어링에는 원시 URL(`state.qr.url`)을 써야 한다.\n * 표시값만 디코드하며, 디코드된 딥링크도 모바일 URL 파서(`searchParams.get('u')`)가 동일 ws 로\n * 환원하므로(ws 경로에 `&`/`#` 없음) 사람이 보고 직접 입력해도 동작한다.\n *\n * 깨진 `%` 시퀀스 등으로 decode 가 throw 하면 원본을 그대로 반환한다(표시 깨짐 방지).\n *\n * @param url `state.qr.url` (relay 딥링크 또는 LAN `ws://…` — LAN 은 `%` 없어 no-op).\n */\nexport function prettyUrl(url: string): string {\n try {\n return decodeURIComponent(url)\n } catch {\n return url\n }\n}\n","import { Box, Text, useInput } from 'ink'\nimport React, { useState } from 'react'\n\nimport type { AgentCore } from '@arva/core'\nimport type { ConnectionState, Machine } from '@arva/shared/types'\n\nimport { ConfirmDialog } from '../components/ConfirmDialog'\nimport { DIVIDER } from '../constants'\nimport { MachineFormScreen } from './MachineFormScreen'\n\n/** 한 화면에 보일 머신 행 수(초과 시 위/아래 \"N more\"). */\nconst ROWS = 8\n\n/** 연결 상태별 도트 + 색. 데스크탑 MachineItem 과 동일 시맨틱(connected=green …). */\nconst STATUS_DOT: Record<ConnectionState, { dot: string; color: string }> = {\n connected: { dot: '●', color: 'green' },\n disconnected: { dot: '○', color: 'gray' },\n connecting: { dot: '◌', color: 'yellow' },\n error: { dot: '✗', color: 'red' },\n}\n\n/** 머신 연결 정보 한 줄: local 은 \"local\", ssh 는 user@host:port. */\nfunction connInfo(m: Machine): string {\n const isLocal = m.machineType === 'local' || m.host === '127.0.0.1' || m.host === 'localhost'\n return isLocal ? 'local' : `${m.username}@${m.host}:${m.port}`\n}\n\n/** 머신 한 행(선택 표시 ▶ + 이름 + 연결정보 + 상태 도트). */\nfunction MachineRow({ m, selected }: { m: Machine; selected: boolean }) {\n const s = STATUS_DOT[m.status] ?? { dot: '·', color: 'gray' }\n return (\n <Box>\n <Text color=\"cyan\">{selected ? '▶ ' : ' '}</Text>\n <Box width={18}>\n <Text>{m.name}</Text>\n </Box>\n <Box width={24}>\n <Text color=\"gray\">{connInfo(m)}</Text>\n </Box>\n <Text color={s.color}>\n {s.dot} {m.status}\n </Text>\n </Box>\n )\n}\n\n/**\n * 머신 관리 화면 — 목록/선택(↑↓) + 추가(a)/수정(e)/삭제(d). 삭제는 ConfirmDialog 로 확인하고\n * `removeMachine` 후 **반드시 `broadcastMachines()`** 를 호출한다(코어 mutation 은 broadcast 를\n * 자동으로 하지 않음 — ws-manager 도 동일하게 짝지어 호출). broadcast 가 store 를 갱신해 목록이 라이브로 반영된다.\n *\n * **키 소유권**: list 모드일 때만 list `useInput` 이 활성. form/confirm sub-mode 에선 비활성으로 두어\n * 자식(폼 필드 / ConfirmDialog)이 키를 단독 소비한다(이중 처리 방지).\n *\n * @param machines App 이 `core.ssh.getMachines()` 로 매 렌더 전달하는 전체 Machine 목록(수정에 전 필드 필요).\n */\nexport function MachinesScreen({\n machines,\n core,\n active,\n onBack,\n}: {\n machines: Machine[]\n core: AgentCore\n active: boolean\n onBack: () => void\n}) {\n const [selected, setSelected] = useState(0)\n const [mode, setMode] = useState<'list' | 'form' | 'confirm'>('list')\n const [editing, setEditing] = useState<Machine | null>(null)\n\n const count = machines.length\n // 삭제/외부 갱신으로 목록이 줄면 selected 가 범위를 벗어날 수 있어 읽을 때 클램프.\n const sel = Math.min(selected, Math.max(0, count - 1))\n\n useInput(\n (input, key) => {\n if (key.escape) {\n onBack()\n } else if (key.upArrow) {\n setSelected(Math.max(0, sel - 1))\n } else if (key.downArrow) {\n setSelected(Math.min(count - 1, sel + 1))\n } else if (input === 'a') {\n setEditing(null)\n setMode('form')\n } else if (input === 'e') {\n if (machines[sel]) {\n setEditing(machines[sel])\n setMode('form')\n }\n } else if (input === 'd') {\n if (machines[sel]) setMode('confirm')\n }\n },\n { isActive: active && mode === 'list' }\n )\n\n // confirm 모드 Esc 취소(ConfirmDialog 의 SelectInput 은 ↑↓/Enter 만 소비 → Esc 는 여기서).\n useInput(\n (_input, key) => {\n if (key.escape) setMode('list')\n },\n { isActive: active && mode === 'confirm' }\n )\n\n if (mode === 'form') {\n return (\n <MachineFormScreen\n machine={editing}\n core={core}\n active={active}\n onDone={() => setMode('list')}\n onCancel={() => setMode('list')}\n />\n )\n }\n\n // selected 가 보이도록 중앙 정렬 슬라이스(긴 목록 윈도잉).\n const start = Math.max(0, Math.min(sel - Math.floor(ROWS / 2), Math.max(0, count - ROWS)))\n const visible = machines.slice(start, start + ROWS)\n const hiddenAbove = start\n const hiddenBelow = Math.max(0, count - (start + ROWS))\n\n const target = machines[sel]\n const hasSession = target ? core.ssh.hasSession(target.id) : false\n // local 머신은 코어가 부팅 시 자동 재시드 → 삭제는 세션 한정. 사용자에게 알린다.\n const deleteWarning = target\n ? target.machineType === 'local'\n ? 'local machine — auto-restored on restart'\n : hasSession\n ? 'active session — will disconnect on delete'\n : undefined\n : undefined\n\n return (\n <Box flexDirection=\"column\">\n <Text color=\"gray\">{DIVIDER}</Text>\n <Text>Machines ({count})</Text>\n {hiddenAbove > 0 ? <Text color=\"gray\">↑ {hiddenAbove} more</Text> : null}\n {visible.map((m, i) => (\n <MachineRow key={m.id} m={m} selected={start + i === sel} />\n ))}\n {hiddenBelow > 0 ? <Text color=\"gray\">↓ {hiddenBelow} more</Text> : null}\n <Text color=\"gray\">{DIVIDER}</Text>\n {mode === 'confirm' && target ? (\n <ConfirmDialog\n message={`Delete \"${target.name}\"?`}\n confirmLabel=\"Delete\"\n warning={deleteWarning}\n isActive={active && mode === 'confirm'}\n onConfirm={() => {\n // 코어 mutation 은 useInput(SelectInput) 핸들러 안 — throw 시 TUI 가 죽지 않게 가드.\n // 머신이 이미 사라졌다면 무시(broadcast/재조회로 목록이 정리됨).\n try {\n core.ssh.removeMachine(target.id)\n core.ssh.broadcastMachines()\n } catch {\n // 이미 제거됨 — no-op\n }\n // count 는 삭제 전 길이 → count-2 = 삭제 후 마지막 유효 인덱스(읽기 클램프와 함께 안전).\n setSelected(Math.max(0, Math.min(sel, count - 2)))\n setMode('list')\n }}\n onCancel={() => setMode('list')}\n />\n ) : (\n <Text color=\"cyan\">↑↓ select · a add · e edit · d delete · Esc back</Text>\n )}\n </Box>\n )\n}\n","import { Box, Text } from 'ink'\nimport SelectInput from 'ink-select-input'\nimport React from 'react'\n\n/**\n * 파괴적 작업 확인 다이얼로그(머신 삭제 등). Cancel 이 첫 항목(안전 기본값)으로 하이라이트된다.\n *\n * **focus 모델**: `isActive` 일 때만 ↑↓/Enter 를 소비한다. 호출 화면은 다이얼로그가 떠 있는 동안\n * 자신의 리스트 `useInput` 을 비활성(`isActive:false`)으로 두어 키 이중 처리(목록 이동 + 다이얼로그\n * 선택 동시 발생)를 막는다.\n *\n * @param warning 선택적 경고 줄(예: \"활성 세션이 있습니다\") — 노란색으로 본문 아래 표기.\n */\nexport function ConfirmDialog({\n message,\n confirmLabel,\n warning,\n isActive,\n onConfirm,\n onCancel,\n}: {\n message: string\n confirmLabel: string\n warning?: string\n isActive: boolean\n onConfirm: () => void\n onCancel: () => void\n}) {\n return (\n <Box flexDirection=\"column\" borderStyle=\"round\" borderColor=\"red\" paddingX={1}>\n <Text>{message}</Text>\n {warning ? <Text color=\"yellow\">⚠ {warning}</Text> : null}\n <Box marginTop={1}>\n <SelectInput\n isFocused={isActive}\n items={[\n { key: 'cancel', label: 'Cancel', value: 'cancel' },\n { key: 'confirm', label: confirmLabel, value: 'confirm' },\n ]}\n onSelect={item => (item.value === 'confirm' ? onConfirm() : onCancel())}\n />\n </Box>\n </Box>\n )\n}\n","import path from 'node:path'\n\nimport { Box, Text } from 'ink'\nimport React, { useEffect, useMemo, useState } from 'react'\n\nimport type { AgentCore } from '@arva/core'\nimport type { Machine } from '@arva/shared/types'\n\nimport { validateMachineForm } from '../../config/validate'\nimport { Field } from '../components/Field'\nimport { SelectField } from '../components/SelectField'\nimport { DIVIDER } from '../constants'\nimport { useFieldFocus } from '../useFieldFocus'\nimport { type MachineFormState, buildMachinePayload, machineToForm } from './machine-payload'\n\n/**\n * 머신 추가/수정 폼. add(machine=null)는 type 선택부터, edit 는 type 고정(데스크탑과 동일 — 생성 후 타입 불변).\n *\n * **키 소유권 / focus**: 폼 레벨 `useInput`(active) 은 Tab(다음 필드)·Esc(취소)만 처리한다. 타이핑/Enter/↑↓는\n * 포커스된 필드(TextInput/SelectInput)가 단독 소비 → 한 시점에 한 필드만 키를 받는다. 비포커스 SelectField 는\n * 드롭다운을 마운트하지 않아 잔여 `useInput` 이 없다. Tab=필드 이동, ↑↓=포커스된 select 내부 이동으로 분리.\n *\n * **저장**: 검증 통과 시 add→`addMachine`, edit→`editMachine`(비파괴 머지 payload) 후 **반드시\n * `broadcastMachines()`**(코어 mutation 은 broadcast 안 함). 그 broadcast 가 목록을 라이브 갱신한다.\n *\n * @param machine null=추가, 값=수정(전체 Machine — 고급 옵션 보존 베이스).\n */\nexport function MachineFormScreen({\n machine: base,\n core,\n active,\n onDone,\n onCancel,\n}: {\n machine: Machine | null\n core: AgentCore\n active: boolean\n onDone: () => void\n onCancel: () => void\n}) {\n const [form, setForm] = useState<MachineFormState>(() => machineToForm(base))\n const [errors, setErrors] = useState<string[]>([])\n const keys = useMemo(() => core.ssh.listAvailableKeys(), [core])\n\n const set = (patch: Partial<MachineFormState>) => setForm(f => ({ ...f, ...patch }))\n\n // key 인증인데 선택 키가 없고 사용 가능한 키가 있으면 첫 키를 기본 선택(표시값=실제 선택값 일치).\n useEffect(() => {\n if (\n form.machineType === 'ssh' &&\n form.authMethod === 'key' &&\n !form.privateKeyPath &&\n keys.length > 0\n ) {\n set({ privateKeyPath: keys[0] })\n }\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [form.machineType, form.authMethod, keys])\n\n // 활성 필드 순서(타입/인증에 따라 동적). type 은 add 에서만(edit 은 타입 불변).\n const order: string[] = []\n if (!base) order.push('type')\n order.push('name')\n if (form.machineType === 'ssh') {\n order.push('host', 'port', 'username', 'auth', form.authMethod === 'key' ? 'key' : 'password')\n } else {\n order.push('localShell', 'localCwd')\n }\n\n const submit = () => {\n const errs = validateMachineForm({\n machineType: form.machineType,\n name: form.name,\n host: form.host,\n port: Number.parseInt(form.port, 10),\n username: form.username,\n authMethod: form.authMethod,\n privateKeyPath: form.privateKeyPath,\n password: form.password,\n isAdd: !base,\n })\n if (errs.length > 0) {\n setErrors(errs)\n return\n }\n // 코어 mutation 은 useInput 핸들러 안에서 호출된다. editMachine 은 머신이 폰에서 삭제되면\n // 'not found Machine' 을 throw 하는데, Ink 는 핸들러 throw 를 잡지 않아 렌더 루프가 무너지고\n // 헤드리스 에이전트(+모든 폰 세션)가 죽는다. → try/catch 로 인라인 에러 전환.\n try {\n const payload = buildMachinePayload(base, form)\n if (base) core.ssh.editMachine({ ...payload, id: base.id })\n else core.ssh.addMachine(payload)\n core.ssh.broadcastMachines()\n onDone()\n } catch {\n setErrors(['save'])\n }\n }\n\n const { focusedKey, advance } = useFieldFocus(order, {\n isActive: active,\n onSubmit: submit,\n onEscape: onCancel,\n })\n\n const invalid = (k: string) => errors.includes(k)\n\n // 인증 필드: key(목록 선택) / key 없음(password 전환 안내) / password 입력. return JSX 평탄화를 위해 분리.\n const authField =\n form.authMethod === 'key' ? (\n keys.length > 0 ? (\n <SelectField\n label=\"Key\"\n value={form.privateKeyPath || keys[0]}\n isFocused={focusedKey === 'key'}\n options={keys.map(k => ({ label: path.basename(k), value: k }))}\n onSelect={v => {\n set({ privateKeyPath: v })\n advance('key')\n }}\n />\n ) : (\n <Box>\n <Box width={12}>\n <Text color={focusedKey === 'key' ? 'cyan' : 'gray'}>Key</Text>\n </Box>\n <Text color=\"yellow\">no SSH keys — switch Auth to password</Text>\n </Box>\n )\n ) : (\n <Field\n label=\"Password\"\n value={form.password}\n onChange={v => set({ password: v })}\n focus={focusedKey === 'password'}\n onSubmit={() => advance('password')}\n mask=\"*\"\n placeholder={base ? (base.passwordLost ? '(lost — re-enter)' : '(unchanged)') : ''}\n invalid={invalid('password')}\n />\n )\n\n return (\n <Box flexDirection=\"column\">\n <Text>{base ? `Edit ${base.name}` : 'Add machine'}</Text>\n <Text color=\"gray\">{DIVIDER}</Text>\n\n {order.includes('type') ? (\n <SelectField\n label=\"Type\"\n value={form.machineType}\n isFocused={focusedKey === 'type'}\n options={[\n { label: 'ssh', value: 'ssh' },\n { label: 'local', value: 'local' },\n ]}\n onSelect={v => {\n set({ machineType: v })\n advance('type')\n }}\n />\n ) : null}\n\n <Field\n label=\"Name\"\n value={form.name}\n onChange={v => set({ name: v })}\n focus={focusedKey === 'name'}\n onSubmit={() => advance('name')}\n invalid={invalid('name')}\n />\n\n {form.machineType === 'ssh' ? (\n <>\n <Field\n label=\"Host\"\n value={form.host}\n onChange={v => set({ host: v })}\n focus={focusedKey === 'host'}\n onSubmit={() => advance('host')}\n invalid={invalid('host')}\n />\n <Field\n label=\"Port\"\n value={form.port}\n onChange={v => set({ port: v.replace(/[^0-9]/g, '') })}\n focus={focusedKey === 'port'}\n onSubmit={() => advance('port')}\n invalid={invalid('port')}\n />\n <Field\n label=\"Username\"\n value={form.username}\n onChange={v => set({ username: v })}\n focus={focusedKey === 'username'}\n onSubmit={() => advance('username')}\n invalid={invalid('username')}\n />\n <SelectField\n label=\"Auth\"\n value={form.authMethod}\n isFocused={focusedKey === 'auth'}\n options={[\n { label: 'key', value: 'key' },\n { label: 'password', value: 'password' },\n ]}\n onSelect={v => {\n set({ authMethod: v })\n advance('auth')\n }}\n />\n {authField}\n </>\n ) : (\n <>\n <Field\n label=\"Shell\"\n value={form.localShell}\n onChange={v => set({ localShell: v })}\n focus={focusedKey === 'localShell'}\n onSubmit={() => advance('localShell')}\n placeholder=\"(auto)\"\n />\n <Field\n label=\"Cwd\"\n value={form.localCwd}\n onChange={v => set({ localCwd: v })}\n focus={focusedKey === 'localCwd'}\n onSubmit={() => advance('localCwd')}\n placeholder=\"(home)\"\n />\n </>\n )}\n\n <Text color=\"gray\">{DIVIDER}</Text>\n {errors.length > 0 ? (\n <Text color=\"red\">\n {errors.includes('save')\n ? 'Save failed — machine may have been changed/deleted'\n : `Required/format error: ${errors.join(', ')}`}\n </Text>\n ) : null}\n <Text color=\"cyan\">Tab move · Enter next/save · Esc cancel</Text>\n </Box>\n )\n}\n","/**\n * 옵션 입력 검증(순수 함수). TUI 옵션 화면이 저장 전에 호출하고, 단위 테스트가 동일 함수를 검증한다.\n *\n * setter({@link ./json-store})는 영속만 책임지므로, 잘못된 값이 파일에 들어가지 않도록 입력 경계(UI)에서\n * 이 함수들로 막는다. 사용자 노출 메시지는 호출 측이 만든다(여기선 boolean/사유 코드만).\n */\n\n/** relay URL 스킴 검증 — `ws://` 또는 `wss://` 만 허용(코어가 WebSocket 으로 접속). */\nexport function isValidRelayUrl(url: string): boolean {\n const u = url.trim()\n if (!(u.startsWith('ws://') || u.startsWith('wss://'))) return false\n try {\n // URL 파서로 host 존재까지 확인(ws:// 처럼 host 누락 거부).\n return new URL(u).host.length > 0\n } catch {\n return false\n }\n}\n\n/** WS 포트 검증 — 1..65535 정수. */\nexport function isValidPort(n: number): boolean {\n return Number.isInteger(n) && n >= 1 && n <= 65535\n}\n\n/** 타임아웃(ms) 검증 — 양의 정수. */\nexport function isValidTimeoutMs(n: number): boolean {\n return Number.isInteger(n) && n > 0\n}\n\n/**\n * 머신 폼 필수/숫자/인증 검증. SSH 머신은 host/username 필수, port 는 1..65535.\n * local 머신은 host/port/username 검증 생략(코어가 127.0.0.1 로 채움).\n *\n * 인증: key 인증이면 privateKeyPath 필수(SSH 키 0개면 선택 불가 → password 로 전환 유도).\n * password 인증은 **추가(isAdd)** 시에만 비밀번호 필수 — 편집은 빈칸=기존 유지라 검증 제외.\n *\n * @returns 유효하면 빈 배열, 아니면 위반 필드 키 목록(UI 가 i18n 없이 인라인 표기).\n */\nexport function validateMachineForm(input: {\n machineType: 'local' | 'ssh'\n name: string\n host: string\n port: number\n username: string\n authMethod?: 'key' | 'password'\n privateKeyPath?: string\n password?: string\n isAdd?: boolean\n}): string[] {\n const errs: string[] = []\n if (!input.name.trim()) errs.push('name')\n if (input.machineType === 'ssh') {\n if (!input.host.trim()) errs.push('host')\n if (!input.username.trim()) errs.push('username')\n if (!isValidPort(input.port)) errs.push('port')\n if (input.authMethod === 'key' && !(input.privateKeyPath ?? '').trim()) errs.push('key')\n if (input.authMethod === 'password' && input.isAdd && !(input.password ?? '').trim()) {\n errs.push('password')\n }\n }\n return errs\n}\n","import { Box, Text } from 'ink'\nimport TextInput from 'ink-text-input'\nimport React from 'react'\n\n/**\n * 라벨 + 텍스트 입력 한 줄. 머신/옵션 폼의 기본 입력 단위.\n *\n * **focus 모델**: `focus` 가 true 인 필드만 키 입력을 소비한다(`ink-text-input` 의 `focus` prop).\n * 폼은 한 시점에 한 필드만 `focus=true` 로 둔다 — 이렇게 하지 않으면 Ink 가 핸들러를 stop-propagate\n * 하지 않아 여러 입력에 같은 키가 동시에 들어간다(폼에서 'q' 타이핑 시 앱 종료되는 버그의 원인).\n * 필드 간 이동(Tab)은 폼 레벨 `useInput` 이 담당하고, 타이핑/Enter 만 여기서 처리한다.\n */\nexport function Field({\n label,\n value,\n onChange,\n onSubmit,\n focus,\n placeholder,\n mask,\n invalid,\n}: {\n label: string\n value: string\n onChange: (v: string) => void\n /** Enter 시 호출 — 폼이 다음 필드로 advance 하는 데 사용. */\n onSubmit?: () => void\n focus: boolean\n placeholder?: string\n /** 비밀번호 입력처럼 마스킹할 문자(예: '*'). */\n mask?: string\n /** 검증 실패 표시(빨간 ✗). 메시지는 폼이 별도 표기. */\n invalid?: boolean\n}) {\n return (\n <Box>\n <Box width={12}>\n <Text color={focus ? 'cyan' : 'gray'}>{label}</Text>\n </Box>\n <TextInput\n value={value}\n onChange={onChange}\n onSubmit={onSubmit}\n focus={focus}\n placeholder={placeholder}\n mask={mask}\n />\n {invalid ? <Text color=\"red\"> ✗</Text> : null}\n </Box>\n )\n}\n","import { Box, Text } from 'ink'\nimport SelectInput from 'ink-select-input'\nimport React from 'react'\n\n/** SelectField 한 항목 — 표시 라벨과 값. 값 타입은 호출 측이 지정. */\nexport interface SelectOption<V extends string> {\n label: string\n value: V\n}\n\n/**\n * 라벨 + 세로 선택 목록(머신 타입/인증/SSH 키 등 열거형 선택).\n *\n * **focus 모델**: `isFocused` 일 때만 ↑↓ 로 항목 이동 + Enter 선택을 소비한다. 필드 간 이동은\n * 폼 레벨 Tab 이 담당하므로 ↑↓ 충돌이 없다(Tab=필드 이동, ↑↓=포커스된 select 내부 이동).\n * 현재 값은 indicator(▶)가 아니라 라벨 옆 `= value` 로도 보여 비포커스 상태에서 선택값을 식별한다.\n *\n * @param onSelect 항목 확정 시 호출(폼이 상태 갱신 + 다음 필드 advance 에 사용).\n */\nexport function SelectField<V extends string>({\n label,\n options,\n value,\n isFocused,\n onSelect,\n}: {\n label: string\n options: Array<SelectOption<V>>\n value: V\n isFocused: boolean\n onSelect: (value: V) => void\n}) {\n const initialIndex = Math.max(\n 0,\n options.findIndex(o => o.value === value)\n )\n const current = options.find(o => o.value === value)\n return (\n <Box flexDirection=\"column\">\n <Box>\n <Box width={12}>\n <Text color={isFocused ? 'cyan' : 'gray'}>{label}</Text>\n </Box>\n <Text>{current?.label ?? value}</Text>\n </Box>\n {isFocused ? (\n <Box marginLeft={12}>\n <SelectInput\n items={options.map(o => ({ key: o.value, label: o.label, value: o.value }))}\n isFocused={isFocused}\n initialIndex={initialIndex}\n onSelect={item => onSelect(item.value)}\n />\n </Box>\n ) : null}\n </Box>\n )\n}\n","import { useInput } from 'ink'\nimport { useState } from 'react'\n\n/**\n * Tab/Enter 기반 폼 필드 포커스 상태머신. MachineForm·Options 두 폼이 공유한다(동일 로직 중복 제거).\n *\n * `order` 는 매 렌더 동적일 수 있어(머신 폼은 type/auth 에 따라 필드 집합이 바뀜) 인자로 받고,\n * `focusedKey` 는 읽을 때 clamp 한다 — 필드가 줄어 stale focused 인덱스가 범위를 벗어나도 안전.\n * Tab=다음 필드(wrap), Esc=onEscape, 마지막 필드에서 `advance`=onSubmit.\n *\n * **키 소유권**: `useInput` 은 `isActive` 일 때만 동작한다. Ink 는 핸들러를 stop-propagate 하지 않아\n * 게이팅이 없으면 비활성 화면의 Tab/Esc 가 동시 발동한다.\n *\n * @returns `focusedKey`(현재 포커스 필드 키)와 `advance(key)`(Enter/select 확정 → 다음 필드 또는 제출).\n */\nexport function useFieldFocus<K extends string>(\n order: readonly K[],\n opts: { isActive: boolean; onSubmit: () => void; onEscape: () => void }\n): { focusedKey: K; advance: (key: K) => void } {\n const [focused, setFocused] = useState(0)\n const focusedKey = order[Math.min(focused, order.length - 1)]\n\n const advance = (key: K): void => {\n const i = order.indexOf(key)\n if (i >= order.length - 1) opts.onSubmit()\n else setFocused(i + 1)\n }\n\n useInput(\n (_input, key) => {\n if (key.escape) opts.onEscape()\n else if (key.tab) setFocused(f => (Math.min(f, order.length - 1) + 1) % order.length)\n },\n { isActive: opts.isActive }\n )\n\n return { focusedKey, advance }\n}\n","import os from 'node:os'\n\nimport type { Machine } from '@arva/shared/types'\n\n/** 머신 폼이 제어하는 입력 상태(전부 문자열 — TextInput 기반, port 는 제출 시 parse). */\nexport interface MachineFormState {\n machineType: 'local' | 'ssh'\n name: string\n host: string\n port: string\n username: string\n authMethod: 'key' | 'password'\n privateKeyPath: string\n /** 빈 문자열 = 미입력. 편집 시 미입력은 기존 비밀번호 유지(빈값으로 덮어쓰지 않음). */\n password: string\n localShell: string\n localCwd: string\n}\n\n/** base 의 id/status 를 떼낸 나머지(고급 옵션 보존용 — multiplexer/ringBufferBytes/spawn* 등). */\nfunction carryOver(base: Machine | null): Partial<Machine> {\n if (!base) return {}\n const { id: _id, status: _status, ...rest } = base\n return rest\n}\n\n/** Machine → 폼 초기 상태. add(null)는 기본값, edit 는 기존 값(password 는 항상 빈칸으로 시작). */\nexport function machineToForm(m: Machine | null): MachineFormState {\n const isLocal = m?.machineType === 'local'\n return {\n machineType: m?.machineType ?? 'ssh',\n name: m?.name ?? '',\n host: m && !isLocal ? m.host : '',\n port: m ? String(m.port) : '22',\n username: m && !isLocal ? m.username : '',\n authMethod: m?.authMethod ?? 'key',\n privateKeyPath: m?.privateKeyPath ?? '',\n password: '',\n localShell: m?.localShell ?? '',\n localCwd: m?.localCwd ?? '',\n }\n}\n\n/**\n * 폼 상태 → addMachine/editMachine payload(Omit<Machine,'id'|'status'>).\n *\n * **비파괴 머지(리뷰 F3)**: edit 시 base 의 전 필드를 먼저 펼치고 폼이 제어하는 필드만 덮어쓴다 →\n * 폼에 없는 고급 옵션(multiplexer/ringBufferBytes/spawn*)이 보존된다. password 는 사용자가 새로\n * 입력하지 않으면 base 값을 유지(빈 문자열로 덮어써 `passwordLost` 가 되는 사고 방지). privateKeyPath\n * 도 마찬가지로 미선택 시 base 값을 유지한다.\n */\nexport function buildMachinePayload(\n base: Machine | null,\n f: MachineFormState\n): Omit<Machine, 'id' | 'status'> {\n const carried = carryOver(base)\n if (f.machineType === 'local') {\n return {\n ...carried,\n machineType: 'local',\n name: f.name.trim(),\n host: base?.host ?? '127.0.0.1',\n port: base?.port ?? 22,\n username: base?.username ?? os.userInfo().username,\n localShell: f.localShell.trim() || base?.localShell,\n localCwd: f.localCwd.trim() || base?.localCwd,\n }\n }\n return {\n ...carried,\n machineType: 'ssh',\n name: f.name.trim(),\n host: f.host.trim(),\n port: Number.parseInt(f.port, 10),\n username: f.username.trim(),\n authMethod: f.authMethod,\n privateKeyPath: f.authMethod === 'key' ? f.privateKeyPath || undefined : base?.privateKeyPath,\n password: f.authMethod === 'password' ? f.password || base?.password : base?.password,\n }\n}\n","import { Box, Text, useInput } from 'ink'\nimport React, { useEffect, useMemo, useState } from 'react'\n\nimport type { AgentCore } from '@arva/core'\n\nimport {\n installService,\n resolveExec,\n serviceStatus,\n startService,\n uninstallService,\n} from '../../service'\nimport type { HeadlessUiState, PtyState } from '../../state'\nimport { DIVIDER } from '../constants'\nimport { prettyUrl } from '../pretty-url'\nimport { renderQr } from '../qr'\nimport { scrollWindow } from '../scroll-window'\n\n/** verbose 로그 뷰에 한 번에 보일 줄 수(스크롤 윈도). */\nconst VERBOSE_ROWS = 16\n\n/** PTY 상태별 색(idle=회색, running=초록, dead/oom=빨강). */\nconst PTY_COLOR: Record<PtyState, string> = {\n idle: 'gray',\n running: 'green',\n dead: 'red',\n oom: 'red',\n}\n\n/**\n * 기본 화면 본문 — 세션(머신/PTY) · 클라이언트 수 · 페어링 URL(+ 토글형 QR).\n * 로그 스팸 없이 \"지금 무슨 상태인지\"만 보여준다. 상세는 verbose(`v`)로.\n *\n * 페어링 URL 은 **항상 텍스트로** 노출한다 — 카메라로 못 찍는 환경(원격 SSH·작은 터미널)에서 폰에\n * 직접 입력/공유할 수 있게. relay 모드면 토큰 포함 딥링크, LAN 모드면 `ws://…/mobile`.\n * QR ASCII 는 화면을 크게 차지하므로 **기본 숨김** — `c` 토글(`qrVisible`)로 펼친다.\n */\nfunction DefaultBody({\n sessions,\n clients,\n qrText,\n qrUrl,\n qrMode,\n qrVisible,\n}: {\n sessions: Array<{ id: string; name: string; pty: PtyState }>\n clients: number\n qrText: string | null\n qrUrl: string | null\n qrMode: string | null\n qrVisible: boolean\n}) {\n return (\n <Box flexDirection=\"column\">\n <Text color=\"gray\">{DIVIDER}</Text>\n\n {qrUrl ? (\n <Box flexDirection=\"column\">\n <Text color=\"gray\">Scan to pair:</Text>\n {qrVisible && qrText ? <Text>{qrText}</Text> : null}\n <Box>\n <Text color=\"gray\">URL </Text>\n <Text color=\"cyan\">{qrUrl}</Text>\n {qrMode ? <Text color=\"gray\"> ({qrMode})</Text> : null}\n </Box>\n {!qrVisible ? <Text color=\"gray\">(press c to show QR)</Text> : null}\n </Box>\n ) : (\n <Text color=\"yellow\">Preparing QR… (waiting for relay)</Text>\n )}\n\n <Text color=\"gray\">{DIVIDER}</Text>\n\n <Text color=\"gray\">Sessions</Text>\n {sessions.map(sess => (\n <Box key={sess.id}>\n <Text color=\"gray\">{' '}</Text>\n <Text>{sess.name}</Text>\n <Text color=\"gray\"> · </Text>\n <Text color={PTY_COLOR[sess.pty]}>{sess.pty}</Text>\n </Box>\n ))}\n <Box>\n <Text color=\"gray\">Clients </Text>\n <Text color={clients > 0 ? 'green' : 'gray'}>{clients}</Text>\n </Box>\n </Box>\n )\n}\n\n/**\n * verbose 로그 뷰 — control-plane 이벤트를 시간순으로, `↑/↓` 스크롤.\n */\nfunction VerboseLog({ logs, scroll }: { logs: string[]; scroll: number }) {\n const { start, end, hiddenAbove, hiddenBelow } = scrollWindow(logs.length, scroll, VERBOSE_ROWS)\n const view = logs.slice(start, end)\n return (\n <Box flexDirection=\"column\">\n <Text color=\"gray\">{hiddenAbove > 0 ? `↑ ${hiddenAbove} more` : DIVIDER}</Text>\n {view.length === 0 ? (\n <Text color=\"gray\">…</Text>\n ) : (\n view.map((line, i) => (\n <Text key={`${start + i}-${line}`} color=\"gray\">\n {line}\n </Text>\n ))\n )}\n {hiddenBelow > 0 ? <Text color=\"gray\">↓ {hiddenBelow} more</Text> : null}\n </Box>\n )\n}\n\n/**\n * 메인(모니터) 화면 — 세션·클라이언트·URL(+`c` 토글 QR) + verbose 로그 토글. `m` 머신 / `o` 옵션 / `i` 정보 전환.\n *\n * **키 소유권**: `useInput` 은 `isActive`(=현재 화면이 main) 일 때만 동작한다. `q`(종료)가 main 에서만\n * 살아 있어, 머신/옵션 폼에서 'q' 타이핑이 앱을 종료시키는 사고를 구조적으로 막는다(다른 화면은 Esc→main 후 q).\n *\n * @param active 현재 라우터 화면이 main 인지(키 입력 게이팅).\n * @param onNavigate 화면 전환 콜백(App 의 screen state 변경).\n * @param onExit 종료(App 의 Ink exit 호출).\n */\nexport function MainScreen({\n state,\n core,\n active,\n onNavigate,\n onExit,\n}: {\n state: HeadlessUiState\n core: AgentCore\n active: boolean\n onNavigate: (screen: 'machines' | 'options' | 'info') => void\n onExit: () => void\n}) {\n const [verbose, setVerbose] = useState(false)\n const [scroll, setScroll] = useState(0)\n const [clients, setClients] = useState(0)\n // QR ASCII 는 화면을 크게 차지 → 기본 숨김(URL 텍스트만). `c`(QR code)로 토글 — 소문자 q=종료와 헷갈리던 Shift+Q 를 대체.\n const [qrVisible, setQrVisible] = useState(false)\n // 자동 시작(systemd): `a` 는 부팅 enable 만 하고, 실제 백그라운드 전환은 'q' 종료 시 핸드오프\n // (포그라운드 코어 cleanup → 서비스 start)로 처리한다. 즉시 --now 로 띄우면 같은 포트·install_id 로\n // 두 번째 인스턴스가 충돌하기 때문(footgun). 서비스 가동 중 재접속은 cli 가 ManagerApp 으로 분기.\n const [handoffOnQuit, setHandoffOnQuit] = useState(false)\n const [autostartNotice, setAutostartNotice] = useState<string | null>(null)\n\n // 폰 접속 수는 ws 내부 카운트가 신뢰 소스 — 1s 폴링(동일값이면 setState skip).\n useEffect(() => {\n const t = setInterval(() => {\n setClients(core.ws.getActiveMobileClientCount())\n }, 1000)\n return () => clearInterval(t)\n }, [core])\n\n // 자동 시작 토글 — 미등록이면 등록(부팅 enable, start 는 q 핸드오프로 미룸), 등록 상태면 해제.\n const toggleAutostart = (): void => {\n const st = serviceStatus()\n if (!st.supported) {\n setAutostartNotice('자동 시작은 Linux(systemd) 전용입니다')\n return\n }\n if (st.enabled) {\n uninstallService()\n setHandoffOnQuit(false)\n setAutostartNotice('자동 시작 꺼짐')\n return\n }\n const exec = resolveExec()\n if (exec.isNpxCache) {\n setAutostartNotice('npx 실행은 서비스로 부적합 — npm i -g @junyoung-kim/reins 후 사용하세요')\n return\n }\n const res = installService(exec)\n if (!res.ok) {\n setAutostartNotice('자동 시작 등록 실패 (systemctl --user 확인)')\n return\n }\n setHandoffOnQuit(true)\n setAutostartNotice(\n res.linger\n ? \"✅ 자동 시작 켜짐 — 'q' 종료 시 백그라운드로 전환 (로그아웃·재부팅도 유지)\"\n : `✅ 등록됨 — SSH 종료 후 유지하려면 1회: ${res.lingerCommand} ('q' 종료 시 백그라운드 전환)`\n )\n }\n\n useInput(\n (input, key) => {\n if (input === 'q') {\n // 자동 시작을 켠 세션이면 핸드오프: 포그라운드 코어를 먼저 정리(포트/relay 해제)한 뒤\n // 서비스를 start 한다 — 빈 포트에 서비스가 깨끗이 바인딩되도록(중복 인스턴스 방지).\n if (handoffOnQuit) {\n // 잔여 레이스(수용, /review 결정): cleanup 의 wss.close() 는 포트 해제를 *시작*만 하므로,\n // 바로 아래 startService() 가 아직 안 풀린 로컬 WS 포트를 바인딩 시도할 수 있다. 단 이는\n // **LAN-direct 로컬 서버**에만 영향 — 목표 시나리오(클라우드)에서 모바일 재연결은 relay\n // 경로라 레이스와 무관하다(서비스의 relay 접속은 포트 바인딩과 별개로 성공). LAN 직결까지\n // 보장하려면 코어의 bind 재시도가 정석이며 별도 작업으로 둔다.\n core.cleanup()\n // start 실패 시 포그라운드는 이미 정리돼 아무것도 안 떠 있는 상태가 된다 → stderr 로\n // 알려 사용자가 수동 복구하게 한다(Ink stdout 버퍼와 분리돼 종료 후에도 보임).\n if (!startService()) {\n process.stderr.write(\n 'reins: service start failed — run `systemctl --user start reins` manually\\n'\n )\n }\n }\n onExit()\n } else if (input === 'r') core.ws.reconnect()\n else if (input === 'n') core.ws.requestQr()\n else if (input === 'm') onNavigate('machines')\n else if (input === 'o') onNavigate('options')\n else if (input === 'i') onNavigate('info')\n else if (input === 'a') toggleAutostart()\n else if (input === 'c') setQrVisible(v => !v)\n else if (input === 'v') {\n setVerbose(v => !v)\n setScroll(0)\n } else if (verbose && key.upArrow) {\n const { maxScroll } = scrollWindow(state.logs.length, scroll, VERBOSE_ROWS)\n setScroll(s => Math.min(s + 1, maxScroll))\n } else if (verbose && key.downArrow) {\n setScroll(s => Math.max(s - 1, 0))\n }\n },\n { isActive: active }\n )\n\n const sessions = state.machines.map(m => ({\n id: m.id,\n name: m.name,\n pty: state.sessions[m.id] ?? 'idle',\n }))\n\n const qrText = useMemo(() => (state.qr ? renderQr(state.qr.url) : null), [state.qr?.url])\n\n return (\n <Box flexDirection=\"column\">\n {verbose ? (\n <VerboseLog logs={state.logs} scroll={scroll} />\n ) : (\n <DefaultBody\n sessions={sessions}\n clients={clients}\n qrText={qrText}\n qrUrl={state.qr ? prettyUrl(state.qr.url) : null}\n qrMode={state.qr?.mode ?? null}\n qrVisible={qrVisible}\n />\n )}\n <Text color=\"gray\">{DIVIDER}</Text>\n {autostartNotice ? <Text color=\"yellow\">{autostartNotice}</Text> : null}\n <Text color=\"cyan\">\n q quit · r reconnect · n new QR · m machines · o options · i info · a auto-start · c{' '}\n {qrVisible ? 'hide' : 'show'} qr · v {verbose ? 'hide log' : 'verbose'}\n {verbose ? ' · ↑↓ scroll' : ''}\n </Text>\n </Box>\n )\n}\n","import qrcode from 'qrcode-terminal'\n\n/**\n * url 을 ASCII QR 문자열로 변환한다. `qrcode-terminal` 은 콜백을 **동기** 호출하므로 즉시 캡처 가능.\n * 메인 화면(라이브 코어)과 관리 화면(코어 없이 재계산) 양쪽이 공유한다.\n */\nexport function renderQr(url: string): string {\n let out = ''\n qrcode.generate(url, { small: true }, s => {\n out = s\n })\n return out.replace(/\\n$/, '')\n}\n","/** {@link scrollWindow} 결과 — 슬라이스 경계 + 가려진 줄 수 + 스크롤 상한. */\nexport interface ScrollWindow {\n /** 표시 시작 인덱스(slice start). */\n start: number\n /** 표시 끝 인덱스(slice end, exclusive). */\n end: number\n /** 윈도 위로 가려진 줄 수(\"↑ N more\"). */\n hiddenAbove: number\n /** 윈도 아래로 가려진 줄 수(\"↓ N more\"). */\n hiddenBelow: number\n /** 허용되는 최대 scroll(바닥에서 더 못 올라가는 한계). */\n maxScroll: number\n}\n\n/**\n * verbose 로그 스크롤 윈도를 계산하는 순수 함수. `scroll` 은 **바닥(최신)에서 위로 떨어진 줄 수**다.\n *\n * VerboseLog 렌더와 키 입력 클램프가 같은 수식을 두 번 쓰지 않도록 한 곳으로 모은다(드리프트 방지).\n * `scroll` 은 내부에서 `[0, maxScroll]` 로 클램프하므로 호출자가 stale 값을 넘겨도 슬라이스가 범위를\n * 벗어나지 않는다.\n *\n * @param total 전체 로그 줄 수.\n * @param scroll 바닥에서 위로 떨어진 줄 수(클램프 전 raw 값 허용).\n * @param rows 한 화면에 보일 줄 수.\n */\nexport function scrollWindow(total: number, scroll: number, rows: number): ScrollWindow {\n const maxScroll = Math.max(0, total - rows)\n const clamped = Math.min(Math.max(0, scroll), maxScroll)\n const end = total - clamped\n const start = Math.max(0, end - rows)\n return { start, end, hiddenAbove: start, hiddenBelow: total - end, maxScroll }\n}\n","import { Box, Text } from 'ink'\nimport React, { useState } from 'react'\n\nimport type { AgentCore } from '@arva/core'\n\nimport type { HeadlessConfigStore } from '../../config/json-store'\nimport { isValidPort, isValidRelayUrl, isValidTimeoutMs } from '../../config/validate'\nimport { Field } from '../components/Field'\nimport { DIVIDER } from '../constants'\nimport { useFieldFocus } from '../useFieldFocus'\n\n/** 옵션 폼 필드 순서(마지막 필드 Enter = 저장). */\nconst ORDER = ['relayUrls', 'wsPort', 'heartbeat', 'pong', 'auth'] as const\n\n/** config 값을 폼 문자열 상태로 로드. relay URL 은 쉼표 구분(첫 항목=primary). */\nfunction loadForm(config: HeadlessConfigStore) {\n return {\n relayUrls: config.getRelayUrls().join(', '),\n wsPort: String(config.getWsPort()),\n heartbeat: String(config.getHeartbeatIntervalMs()),\n pong: String(config.getPongTimeoutMs()),\n auth: String(config.getAuthTimeoutMs()),\n }\n}\n\n/**\n * 옵션 편집 화면 — relay URL(쉼표 구분, 첫=primary)·WS 포트·타임아웃(heartbeat/pong/auth) 편집 +\n * install_id / FCM 토큰 수 / relay 타깃 읽기전용 표시.\n *\n * **적용 시점**: relay URL 은 저장 후 변경됐으면 `core.ws.reconnect()` 로 즉시 반영(코어가 연결마다\n * getRelayUrls 재read). WS 포트·타임아웃은 코어가 부팅 시 1회 read 하므로 **재시작 후 반영**(저장은 즉시) —\n * 화면에 안내. relay URL 만 쉼표 편집으로 단순화(per-item add/remove 대신 — TUI 단순성, 순서=우선순위 유지).\n *\n * **키 소유권**: 폼 레벨 `useInput`(active) 은 Tab(다음)·Esc(뒤로)만. 타이핑/Enter 는 포커스된 필드가 소비.\n */\nexport function OptionsScreen({\n config,\n core,\n active,\n onBack,\n}: {\n config: HeadlessConfigStore\n core: AgentCore\n active: boolean\n onBack: () => void\n}) {\n const [form, setForm] = useState(() => loadForm(config))\n const [errors, setErrors] = useState<string[]>([])\n const [saved, setSaved] = useState(false)\n\n const set = (patch: Partial<typeof form>) => {\n setForm(f => ({ ...f, ...patch }))\n setSaved(false)\n }\n\n const save = () => {\n const urls = form.relayUrls\n .split(',')\n .map(s => s.trim())\n .filter(Boolean)\n const port = Number.parseInt(form.wsPort, 10)\n const heartbeat = Number.parseInt(form.heartbeat, 10)\n const pong = Number.parseInt(form.pong, 10)\n const auth = Number.parseInt(form.auth, 10)\n\n const errs: string[] = []\n if (urls.length === 0 || !urls.every(isValidRelayUrl)) errs.push('relayUrls')\n if (!isValidPort(port)) errs.push('wsPort')\n if (!isValidTimeoutMs(heartbeat)) errs.push('heartbeat')\n if (!isValidTimeoutMs(pong)) errs.push('pong')\n if (!isValidTimeoutMs(auth)) errs.push('auth')\n if (errs.length > 0) {\n setErrors(errs)\n return\n }\n\n const relayChanged = JSON.stringify(urls) !== JSON.stringify(config.getRelayUrls())\n // 코어 호출(reconnect)이 useInput 핸들러 안 — throw 시 TUI 가 죽지 않게 가드.\n try {\n // 한 번의 writeStore 로 일괄 저장(필드별 setter 5회 = 같은 파일 5회 쓰기 방지). relay_urls 는\n // 변경됐을 때만 포함 — 안 그러면 env/기본값으로 동적 해석되던 값을 store 에 고착시킨다.\n config.setOptions({\n ...(relayChanged ? { relay_urls: urls } : {}),\n ws_port: port,\n heartbeat_interval_ms: heartbeat,\n pong_timeout_ms: pong,\n auth_timeout_ms: auth,\n })\n // relay URL 만 라이브 반영(reconnect). 포트/타임아웃은 재시작 필요 → reconnect 호출하지 않음.\n if (relayChanged) core.ws.reconnect()\n } catch {\n setErrors(['save'])\n return\n }\n setErrors([])\n setSaved(true)\n }\n\n const { focusedKey, advance } = useFieldFocus(ORDER, {\n isActive: active,\n onSubmit: save,\n onEscape: onBack,\n })\n\n const invalid = (k: string) => errors.includes(k)\n\n return (\n <Box flexDirection=\"column\">\n <Text color=\"gray\">{DIVIDER}</Text>\n <Text>Options</Text>\n\n <Field\n label=\"Relay\"\n value={form.relayUrls}\n onChange={v => set({ relayUrls: v })}\n focus={focusedKey === 'relayUrls'}\n onSubmit={() => advance('relayUrls')}\n placeholder=\"wss://… , wss://…(fallback)\"\n invalid={invalid('relayUrls')}\n />\n <Field\n label=\"WS Port\"\n value={form.wsPort}\n onChange={v => set({ wsPort: v.replace(/[^0-9]/g, '') })}\n focus={focusedKey === 'wsPort'}\n onSubmit={() => advance('wsPort')}\n invalid={invalid('wsPort')}\n />\n <Field\n label=\"Heartbeat\"\n value={form.heartbeat}\n onChange={v => set({ heartbeat: v.replace(/[^0-9]/g, '') })}\n focus={focusedKey === 'heartbeat'}\n onSubmit={() => advance('heartbeat')}\n invalid={invalid('heartbeat')}\n />\n <Field\n label=\"Pong\"\n value={form.pong}\n onChange={v => set({ pong: v.replace(/[^0-9]/g, '') })}\n focus={focusedKey === 'pong'}\n onSubmit={() => advance('pong')}\n invalid={invalid('pong')}\n />\n <Field\n label=\"Auth\"\n value={form.auth}\n onChange={v => set({ auth: v.replace(/[^0-9]/g, '') })}\n focus={focusedKey === 'auth'}\n onSubmit={() => advance('auth')}\n invalid={invalid('auth')}\n />\n <Text color=\"gray\">⚠ WS Port·timeouts apply after restart (relay reconnects immediately).</Text>\n\n <Text color=\"gray\">{DIVIDER}</Text>\n <Text color=\"gray\">install_id {config.getInstallId().slice(0, 8)}…</Text>\n <Text color=\"gray\">FCM tokens {config.getFcmTokens().length}</Text>\n\n <Text color=\"gray\">{DIVIDER}</Text>\n {errors.length > 0 ? (\n <Text color=\"red\">\n {errors.includes('save') ? 'Save failed — please retry' : `Format error: ${errors.join(', ')}`}\n </Text>\n ) : null}\n {saved ? <Text color=\"green\">Saved (port/timeouts apply after restart)</Text> : null}\n <Text color=\"cyan\">Tab move · Enter next/save · Esc back</Text>\n </Box>\n )\n}\n","import { Box, Text, useApp, useInput } from 'ink'\nimport React, { useEffect, useMemo, useState } from 'react'\n\nimport type { HeadlessConfigStore } from '../config/json-store'\nimport { identity } from '../identity'\nimport { computePairUrl } from '../pairing'\nimport {\n restartService,\n type ServiceStatus,\n serviceStatus,\n stopService,\n uninstallService,\n} from '../service'\nimport { DIVIDER } from './constants'\nimport { tailLog } from './log-tail'\nimport { prettyUrl } from './pretty-url'\nimport { renderQr } from './qr'\n\n/** 관리 화면 로그 tail 줄 수. */\nconst LOG_ROWS = 8\n\n/**\n * 서비스 가동 중 SSH 재접속 시의 **관리/상태 화면**(코어 미기동).\n *\n * `cli.tsx` 가 단일 인스턴스 가드(`isServiceActive`)로 이 화면을 띄운다 — 두 번째 코어를 만들지 않아\n * 같은 install_id 이중 relay 접속(footgun)을 막는다. 라이브 세션/clients 는 서비스 프로세스 메모리에\n * 있어 보여주지 못하지만, **페어링 QR 은 영속 토큰+config 로 재계산**(서비스가 서빙 중인 것과 동일)하고,\n * 상태(systemctl)·`ws.log` tail 로 사실상의 동작을 확인할 수 있다. 액션은 재시작/중지/자동시작 해제.\n *\n * 상태/로그는 신뢰 소스(systemctl·파일)를 2s 폴링한다.\n */\nexport function ManagerApp({ config }: { config: HeadlessConfigStore }) {\n const { exit } = useApp()\n const [status, setStatus] = useState<ServiceStatus>(() => serviceStatus())\n const [logs, setLogs] = useState<string[]>(() => tailLog('ws', LOG_ROWS))\n const [qrVisible, setQrVisible] = useState(false)\n const [notice, setNotice] = useState<string | null>(null)\n\n // 폴링 비용: serviceStatus() 가 tick 당 systemctl is-active/is-enabled + loginctl 을 spawn 한다\n // (systemctl 가용성은 hasSystemctlUser 캐시로 1회만). 관리 화면은 상태 변화가 드물어 3s 로 둔다.\n useEffect(() => {\n const t = setInterval(() => {\n setStatus(serviceStatus())\n setLogs(tailLog('ws', LOG_ROWS))\n }, 3000)\n return () => clearInterval(t)\n }, [])\n\n // relay primary 가 바뀌지 않는 한 안정 — config 는 stable 참조.\n const pairUrl = useMemo(() => computePairUrl(config.getRelayUrls()), [config])\n const qrText = useMemo(() => (pairUrl ? renderQr(pairUrl) : null), [pairUrl])\n // 다중 relay면 computePairUrl 은 primary 가정 — failover 중이면 어긋날 수 있음을 노출(pairing.ts 한계).\n const multiRelay = useMemo(() => config.getRelayUrls().length > 1, [config])\n\n useInput((input) => {\n if (input === 'q') exit()\n else if (input === 'c') setQrVisible(v => !v)\n else if (input === 'r') {\n setNotice(restartService() ? 'restart requested' : 'restart failed')\n setStatus(serviceStatus())\n } else if (input === 'x') {\n setNotice(stopService() ? 'stopped — q 로 이 창을 닫으세요' : 'stop failed')\n setStatus(serviceStatus())\n } else if (input === 'u') {\n uninstallService()\n setNotice('자동 시작 해제됨 — q 로 닫기')\n setStatus(serviceStatus())\n }\n })\n\n return (\n <Box flexDirection=\"column\" borderStyle=\"round\" paddingX={1}>\n <Box justifyContent=\"space-between\">\n <Text bold>reins — running as service</Text>\n <Text color=\"gray\">v{identity.version}</Text>\n </Box>\n\n <Box>\n <Text color=\"gray\">Service </Text>\n <Text color={status.active ? 'green' : 'red'}>\n {status.active ? '● active' : '○ inactive'}\n </Text>\n <Text color=\"gray\">{status.enabled ? ' (enabled)' : ' (disabled)'}</Text>\n </Box>\n <Box>\n <Text color=\"gray\">Linger </Text>\n {status.linger ? (\n <Text color=\"green\">on → survives SSH logout & reboot</Text>\n ) : (\n <Text color=\"yellow\">off → run: sudo loginctl enable-linger {status.user}</Text>\n )}\n </Box>\n\n <Text color=\"gray\">{DIVIDER}</Text>\n\n {pairUrl ? (\n <Box flexDirection=\"column\">\n <Text color=\"gray\">Scan to pair:</Text>\n {qrVisible && qrText ? <Text>{qrText}</Text> : null}\n <Box>\n <Text color=\"gray\">URL </Text>\n <Text color=\"cyan\">{prettyUrl(pairUrl)}</Text>\n </Box>\n {multiRelay ? (\n <Text color=\"yellow\">(assumes primary relay — may differ if failed over)</Text>\n ) : null}\n {!qrVisible ? <Text color=\"gray\">(press c to show QR)</Text> : null}\n </Box>\n ) : (\n <Text color=\"yellow\">Pair URL unavailable (session-token / relay URL 확인)</Text>\n )}\n\n <Text color=\"gray\">{DIVIDER}</Text>\n <Text color=\"gray\">Logs (ws.log tail)</Text>\n {logs.length === 0 ? (\n <Text color=\"gray\">…</Text>\n ) : (\n logs.map((line, i) => (\n <Text key={`${i}-${line}`} color=\"gray\">\n {line}\n </Text>\n ))\n )}\n\n <Text color=\"gray\">{DIVIDER}</Text>\n {notice ? <Text color=\"green\">{notice}</Text> : null}\n <Text color=\"cyan\">\n r restart · x stop · u disable · c {qrVisible ? 'hide' : 'show'} qr · q quit\n </Text>\n </Box>\n )\n}\n","import fs from 'node:fs'\nimport os from 'node:os'\nimport path from 'node:path'\n\n/** 헤드리스 scoped 로그 디렉터리(코어 logger 기본값과 동일 규약). */\nconst LOG_DIR = path.join(os.homedir(), '.ai_remote_vibe_agent', 'logs')\n\n/**\n * 컴포넌트 로그 파일(`{component}.log`)의 마지막 N줄을 읽는다(없으면 빈 배열).\n *\n * 관리 화면이 라이브 코어 없이 \"지금 무슨 일이 있는지\"를 보여주는 용도. 파일은 5MB rotation 이라\n * 전체를 읽지 않고 **끝 16KB** 만 읽어(중간 줄이 잘릴 수 있으니 첫 줄은 버려도 무방) 폴링 비용을 막는다.\n */\nexport function tailLog(component: string, lines: number): string[] {\n const file = path.join(LOG_DIR, `${component}.log`)\n try {\n const size = fs.statSync(file).size\n const readBytes = Math.min(size, 16 * 1024)\n const fd = fs.openSync(file, 'r')\n try {\n const buf = Buffer.alloc(readBytes)\n fs.readSync(fd, buf, 0, readBytes, size - readBytes)\n return buf\n .toString('utf-8')\n .split(/\\r?\\n/)\n .filter(Boolean)\n .slice(-lines)\n } finally {\n fs.closeSync(fd)\n }\n } catch {\n return []\n }\n}\n"],"mappings":";;;AAAA,OAAOA,UAAQ;AACf,OAAOC,YAAU;;;ACIjB,OAAO,SAAS;AAChB,OAAO,UAAU;AACjB,OAAO,QAAQ;AASf,IAAI,eAAe,KAAK,KAAK,GAAG,QAAQ,GAAG,yBAAyB,MAAM;AAQnE,SAAS,gBAAgB,KAAmB;AACjD,iBAAe;AACjB;AAOA,IAAM,UAAgD,CAAC;AAGvD,IAAI,iBAAiB;AAYd,SAAS,kBAAkB,SAAwB;AACxD,mBAAiB;AACjB,aAAW,UAAU,SAAS;AAC5B,WAAO,WAAW,QAAQ,QAAQ,UAAU,UAAU;AAAA,EACxD;AACF;AA4BO,SAAS,aAAa,WAAmB;AAC9C,QAAM,SAAS,IAAI,OAAO,EAAE,OAAO,UAAU,CAAC;AAI9C,SAAO,WAAW,KAAK,WAAW,GAAG,SAAS;AAC9C,SAAO,WAAW,KAAK,gBAAgB,MACrC,KAAK,KAAK,cAAc,GAAG,SAAS,MAAM;AAC5C,SAAO,WAAW,KAAK,SACrB;AACF,SAAO,WAAW,KAAK,UAAU,IAAI,OAAO;AAG5C,SAAO,WAAW,QAAQ,SACxB;AAIF,UAAQ,KAAK,MAAM;AACnB,MAAI,CAAC,eAAgB,QAAO,WAAW,QAAQ,QAAQ;AAEvD,SAAO;AACT;;;ACvGA,SAAS,cAAc;AAEvB,OAAOC,SAAQ;AACf,OAAOC,SAAQ;AACf,OAAOC,WAAU;AACjB,OAAOC,aAAY;AACnB,SAAS,qBAAqB;AAE9B,OAAO,eAAe;AAEtB,OAAO,YAAY;;;ACVnB,OAAO,SAAS;AAIT,SAAS,cAAc,MAAoC;AAChE,QAAM,QACJ,QAAQ,aAAa,UAChB,KAAK,SAAS,QAAQ,IAAI,WAAW,YACrC,KAAK,SAAS,QAAQ,IAAI,SAAS;AAE1C,QAAM,OAAO,IAAI,MAAM,OAAO,KAAK,QAAQ,CAAC,GAAG;AAAA,IAC7C,MAAM;AAAA,IACN,MAAM,KAAK;AAAA,IACX,MAAM,KAAK;AAAA,IACX,KAAK,KAAK;AAAA,IACV,KAAK,KAAK;AAAA,EACZ,CAAC;AAED,MAAI,eAA6C;AACjD,MAAI,eAAgD;AAEpD,OAAK,OAAO,CAAC,SAAS,eAAe,IAAI,CAAC;AAC1C,OAAK,OAAO,CAAC,EAAE,SAAS,MAAM,eAAe,QAAQ,CAAC;AAEtD,SAAO;AAAA,IACL,IAAI,MAAM;AACR,aAAO,KAAK;AAAA,IACd;AAAA,IACA,OAAO,CAAC,SAAS,KAAK,MAAM,IAAI;AAAA,IAChC,QAAQ,CAAC,MAAM,SAAS,KAAK,OAAO,MAAM,IAAI;AAAA,IAC9C,MAAM,MAAM,KAAK,KAAK;AAAA,IACtB,QAAQ,CAAC,OAAO;AACd,qBAAe;AAAA,IACjB;AAAA,IACA,QAAQ,CAAC,OAAO;AACd,qBAAe;AAAA,IACjB;AAAA,EACF;AACF;;;ACLO,IAAM,sBAAN,MAAM,qBAAoB;AAAA,EAI/B,YAA6B,UAAkB;AAAlB;AAC3B,QAAI,YAAY;AACd,YAAM,IAAI;AAAA,QACR,kDAAkD,QAAQ;AAAA,MAC5D;AAAA,EACJ;AAAA,EAL6B;AAAA,EAHrB,SAAwB,CAAC;AAAA,EACzB,aAAa;AAAA;AAAA,EAUrB,OAAO,MAAoB;AACzB,QAAI,KAAK,WAAW,EAAG;AACvB,UAAM,QAAQ,OAAO,WAAW,MAAM,MAAM;AAC5C,SAAK,OAAO,KAAK,EAAE,MAAM,MAAM,CAAC;AAChC,SAAK,cAAc;AACnB,WAAO,KAAK,aAAa,KAAK,YAAY,KAAK,OAAO,SAAS,GAAG;AAChE,YAAM,UAAU,KAAK,OAAO,MAAM;AAClC,WAAK,cAAc,QAAQ;AAAA,IAC7B;AAAA,EACF;AAAA;AAAA,EAGA,OAAe;AACb,WAAO,KAAK;AAAA,EACd;AAAA;AAAA,EAGA,WAAmB;AACjB,WAAO,KAAK;AAAA,EACd;AAAA;AAAA,EAGA,UAAmB;AACjB,WAAO,KAAK,OAAO,WAAW;AAAA,EAChC;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,WAAmB;AACjB,QAAI,KAAK,OAAO,WAAW,EAAG,QAAO;AACrC,QAAI,KAAK,OAAO,WAAW,EAAG,QAAO,KAAK,OAAO,CAAC,EAAE;AACpD,WAAO,KAAK,OAAO,IAAI,OAAK,EAAE,IAAI,EAAE,KAAK,EAAE;AAAA,EAC7C;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAUA,eAAe,eAAiC;AAC9C,QAAI,iBAAiB;AACnB,YAAM,IAAI,MAAM,2CAA2C;AAC7D,QAAI,KAAK,OAAO,WAAW,EAAG,QAAO,CAAC;AAEtC,UAAM,SAAmB,CAAC;AAC1B,QAAI,cAAc;AAClB,QAAI,eAAe;AAEnB,eAAW,KAAK,KAAK,QAAQ;AAE3B,UAAI,EAAE,QAAQ,eAAe;AAC3B,YAAI,YAAY,SAAS,GAAG;AAC1B,iBAAO,KAAK,WAAW;AACvB,wBAAc;AACd,yBAAe;AAAA,QACjB;AACA,eAAO,KAAK,EAAE,IAAI;AAClB;AAAA,MACF;AAEA,UAAI,eAAe,EAAE,QAAQ,iBAAiB,YAAY,SAAS,GAAG;AACpE,eAAO,KAAK,WAAW;AACvB,sBAAc;AACd,uBAAe;AAAA,MACjB;AACA,qBAAe,EAAE;AACjB,sBAAgB,EAAE;AAAA,IACpB;AACA,QAAI,YAAY,SAAS,EAAG,QAAO,KAAK,WAAW;AACnD,WAAO;AAAA,EACT;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAoBA,wBACE,cACA,eACU;AACV,QAAI,gBAAgB;AAClB,YAAM,IAAI,MAAM,mDAAmD;AACrE,QAAI,iBAAiB;AACnB,YAAM,IAAI,MAAM,oDAAoD;AACtE,QAAI,KAAK,OAAO,WAAW,EAAG,QAAO,CAAC;AAEtC,UAAM,MAAM,IAAI,qBAAoB,YAAY;AAChD,eAAW,KAAK,KAAK,OAAQ,KAAI,OAAO,EAAE,IAAI;AAC9C,WAAO,IAAI,eAAe,aAAa;AAAA,EACzC;AAAA;AAAA,EAGA,QAAc;AACZ,SAAK,SAAS,CAAC;AACf,SAAK,aAAa;AAAA,EACpB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQA,YAAgC;AAC9B,WAAO;AAAA,MACL,GAAG;AAAA,MACH,QAAQ,KAAK,OAAO,IAAI,OAAK,EAAE,IAAI;AAAA,MACnC,YAAY,KAAK;AAAA,MACjB,UAAS,oBAAI,KAAK,GAAE,YAAY;AAAA,IAClC;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAWA,OAAO,YACL,UACA,UACqB;AACrB,QAAI,CAAC,YAAY,OAAO,aAAa,UAAU;AAC7C,YAAM,IAAI;AAAA,QACR;AAAA,MACF;AAAA,IACF;AACA,QAAI,SAAS,MAAM,GAAG;AACpB,YAAM,IAAI;AAAA,QACR,wDAAwD,SAAS,CAAC;AAAA,MACpE;AAAA,IACF;AACA,QAAI,CAAC,MAAM,QAAQ,SAAS,MAAM,GAAG;AACnC,YAAM,IAAI,MAAM,yDAAyD;AAAA,IAC3E;AACA,UAAM,MAAM,IAAI,qBAAoB,QAAQ;AAC5C,eAAW,QAAQ,SAAS,QAAQ;AAClC,UAAI,OAAO,SAAS,UAAU;AAC5B,cAAM,IAAI;AAAA,UACR;AAAA,QACF;AAAA,MACF;AACA,UAAI,OAAO,IAAI;AAAA,IACjB;AACA,WAAO;AAAA,EACT;AACF;;;AC/LA;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,SAAS,YAAY;AAQrB,IAAM,oBAAoB;AAQnB,IAAM,sBAAN,MAA0B;AAAA,EACd;AAAA;AAAA,EAEA,eAAe,oBAAI,IAA4B;AAAA,EAEhE,YAAY,UAAkB;AAC5B,SAAK,WAAW;AAEhB,QAAI;AACF,gBAAU,UAAU,EAAE,WAAW,MAAM,MAAM,IAAM,CAAC;AAAA,IACtD,QAAQ;AAEN,cAAQ,KAAK,8CAA8C,QAAQ,EAAE;AAAA,IACvE;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,gBAAgB,WAA2B;AACzC,UAAM,OAAO,UAAU,QAAQ,mBAAmB,GAAG;AACrD,WAAO,KAAK,KAAK,UAAU,GAAG,QAAQ,SAAS,OAAO;AAAA,EACxD;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAUA,YAAY,WAA8C;AACxD,UAAMC,SAAO,KAAK,gBAAgB,SAAS;AAC3C,QAAI,CAAC,WAAWA,MAAI,EAAG,QAAO;AAC9B,QAAI;AACF,YAAM,MAAM,aAAaA,QAAM,MAAM;AACrC,YAAM,SAAS,KAAK,MAAM,GAAG;AAC7B,UAAI,CAAC,UAAU,OAAO,WAAW,YAAY,OAAO,MAAM,GAAG;AAC3D,gBAAQ;AAAA,UACN,oCAAoCA,MAAI;AAAA,QAC1C;AACA,eAAO;AAAA,MACT;AACA,aAAO;AAAA,IACT,SAAS,KAAK;AACZ,cAAQ;AAAA,QACN,4CAA4CA,MAAI,KAAM,IAAc,OAAO;AAAA,MAC7E;AACA,aAAO;AAAA,IACT;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,cAAc,WAAmB,KAAgC;AAC/D,UAAM,WAAW,KAAK,aAAa,IAAI,SAAS;AAChD,QAAI,SAAU,cAAa,QAAQ;AACnC,UAAM,QAAQ,WAAW,MAAM;AAC7B,WAAK,aAAa,OAAO,SAAS;AAClC,WAAK,UAAU,WAAW,GAAG;AAAA,IAC/B,GAAG,iBAAiB;AAGpB,QAAI,OAAO,MAAM,UAAU,WAAY,OAAM,MAAM;AACnD,SAAK,aAAa,IAAI,WAAW,KAAK;AAAA,EACxC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EASA,UAAU,WAAmB,KAAgC;AAE3D,UAAM,UAAU,KAAK,aAAa,IAAI,SAAS;AAC/C,QAAI,SAAS;AACX,mBAAa,OAAO;AACpB,WAAK,aAAa,OAAO,SAAS;AAAA,IACpC;AACA,UAAMA,SAAO,KAAK,gBAAgB,SAAS;AAC3C,QAAI,IAAI,QAAQ,GAAG;AAEjB,UAAI;AACF,YAAI,WAAWA,MAAI,EAAG,YAAWA,MAAI;AAAA,MACvC,QAAQ;AAAA,MAER;AACA;AAAA,IACF;AACA,QAAI;AACF,YAAM,OAAO,IAAI,UAAU;AAC3B,oBAAcA,QAAM,KAAK,UAAU,IAAI,GAAG;AAAA,QACxC,UAAU;AAAA,QACV,MAAM;AAAA,MACR,CAAC;AAAA,IACH,SAAS,KAAK;AACZ,cAAQ;AAAA,QACN,8CAA8CA,MAAI,KAAM,IAAc,OAAO;AAAA,MAC/E;AAAA,IACF;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,aAAa,SAAiD;AAC5D,eAAW,CAAC,WAAW,GAAG,KAAK,SAAS;AACtC,WAAK,UAAU,WAAW,GAAG;AAAA,IAC/B;AAAA,EACF;AAAA;AAAA,EAGA,OAAO,WAAyB;AAC9B,UAAM,UAAU,KAAK,aAAa,IAAI,SAAS;AAC/C,QAAI,SAAS;AACX,mBAAa,OAAO;AACpB,WAAK,aAAa,OAAO,SAAS;AAAA,IACpC;AACA,UAAMA,SAAO,KAAK,gBAAgB,SAAS;AAC3C,QAAI;AACF,UAAI,WAAWA,MAAI,EAAG,YAAWA,MAAI;AAAA,IACvC,SAAS,KAAK;AACZ,cAAQ;AAAA,QACN,2CAA2CA,MAAI,KAAM,IAAc,OAAO;AAAA,MAC5E;AAAA,IACF;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EASA,aAAa,iBAAsC;AACjD,QAAI,CAAC,WAAW,KAAK,QAAQ,EAAG,QAAO;AACvC,UAAM,YAAY,oBAAI,IAAY;AAClC,eAAW,MAAM,iBAAiB;AAChC,YAAM,OAAO,GAAG,QAAQ,mBAAmB,GAAG,KAAK;AACnD,gBAAU,IAAI,IAAI;AAAA,IACpB;AACA,QAAI,UAAU;AACd,QAAI;AACJ,QAAI;AACF,gBAAU,YAAY,KAAK,QAAQ;AAAA,IACrC,QAAQ;AACN,aAAO;AAAA,IACT;AACA,eAAW,QAAQ,SAAS;AAC1B,UAAI,CAAC,KAAK,SAAS,OAAO,EAAG;AAC7B,YAAM,OAAO,KAAK,MAAM,GAAG,CAAC,QAAQ,MAAM;AAC1C,UAAI,UAAU,IAAI,IAAI,EAAG;AACzB,UAAI;AACF,mBAAW,KAAK,KAAK,UAAU,IAAI,CAAC;AACpC,mBAAW;AAAA,MACb,QAAQ;AAAA,MAER;AAAA,IACF;AACA,WAAO;AAAA,EACT;AACF;;;ACxNO,IAAM,cAAc;AAAA,EACzB,QAAQ,QAAQ,IAAI,aAAa;AACnC;AAEO,IAAM,WAAW;AAAA,EACtB,QAAQ,QAAQ,aAAa;AAAA,EAC7B,YAAY,QAAQ,aAAa;AAAA,EACjC,UAAU,QAAQ,aAAa;AACjC;AAQO,IAAM,mBAAmB;AAWzB,IAAM,kCAAkC,IAAI,OAAO;AAWnD,IAAM,gBAAgB;AAItB,IAAM,sBAAsB,IAAI,KAAK,KAAK,KAAK;AAI/C,IAAM,4BAA4B;AAIlC,IAAM,mCAAmC;AAIzC,IAAM,qBAAqB,MAAM;AAiBjC,IAAM,2BAA2B,KAAK;AAkBtC,IAAM,2BAA2B,KAAK,KAAK;AA+B3C,IAAM,0BAA0B,KAAK,KAAK,KAAK;AAS/C,IAAM,iBAAiB,KAAK,KAAK,KAAK,KAAK;;;AC7GlD,OAAO,cAAc;;;ACbrB,IAAIC,IAAK;AAAT,IACIC,IAAK;AADT,IAEIC,IAAK;AAFT,IAGIC,IAAK;AAUF,IAAUC;CAAAA,OAAV;AACE,WAASC,EAAMC,GAAWC,GAAWC,GAAWC,GAAoB;AACzE,WAAIA,MAAM,SACD,IAAIC,EAAYJ,CAAC,CAAC,GAAGI,EAAYH,CAAC,CAAC,GAAGG,EAAYF,CAAC,CAAC,GAAGE,EAAYD,CAAC,CAAC,KAEvE,IAAIC,EAAYJ,CAAC,CAAC,GAAGI,EAAYH,CAAC,CAAC,GAAGG,EAAYF,CAAC,CAAC;EAC7D;AALOJ,IAAS,QAAAC;AAOT,WAASM,EAAOL,GAAWC,GAAWC,GAAWC,IAAY,KAAc;AAIhF,YAAQH,KAAK,KAAKC,KAAK,KAAKC,KAAK,IAAIC,OAAO;EAC9C;AALOL,IAAS,SAAAO;AAOT,WAASC,EAAQN,GAAWC,GAAWC,GAAWC,GAAoB;AAC3E,WAAO,EACL,KAAKL,EAAS,MAAME,GAAGC,GAAGC,GAAGC,CAAC,GAC9B,MAAML,EAAS,OAAOE,GAAGC,GAAGC,GAAGC,CAAC,EAClC;EACF;AALOL,IAAS,UAAAQ;AAAAA,GAfDR,MAAA,CAAA,CAAA;AA0BV,IAAUS;CAAAA,OAAV;AACE,WAASC,EAAMC,GAAYC,GAAoB;AAEpD,QADAb,KAAMa,EAAG,OAAO,OAAQ,KACpBb,MAAO,EACT,QAAO,EACL,KAAKa,EAAG,KACR,MAAMA,EAAG,KACX;AAEF,QAAMC,IAAOD,EAAG,QAAQ,KAAM,KACxBE,IAAOF,EAAG,QAAQ,KAAM,KACxBG,IAAOH,EAAG,QAAQ,IAAK,KACvBI,IAAOL,EAAG,QAAQ,KAAM,KACxBM,IAAON,EAAG,QAAQ,KAAM,KACxBO,IAAOP,EAAG,QAAQ,IAAK;AAC7Bf,QAAKoB,IAAM,KAAK,OAAOH,IAAMG,KAAOjB,CAAE,GACtCF,IAAKoB,IAAM,KAAK,OAAOH,IAAMG,KAAOlB,CAAE,GACtCD,IAAKoB,IAAM,KAAK,OAAOH,IAAMG,KAAOnB,CAAE;AACtC,QAAMoB,IAAMnB,EAAS,MAAMJ,GAAIC,GAAIC,CAAE,GAC/BsB,IAAOpB,EAAS,OAAOJ,GAAIC,GAAIC,CAAE;AACvC,WAAO,EAAE,KAAAqB,GAAK,MAAAC,EAAK;EACrB;AApBOX,IAAS,QAAAC;AAsBT,WAASW,EAASZ,GAAwB;AAC/C,YAAQA,EAAM,OAAO,SAAU;EACjC;AAFOA,IAAS,WAAAY;AAIT,WAASC,EAAoBX,GAAYC,GAAYW,GAAmC;AAC7F,QAAMC,IAASJ,EAAK,oBAAoBT,EAAG,MAAMC,EAAG,MAAMW,CAAK;AAC/D,QAAKC,EAGL,QAAOxB,EAAS,QACbwB,KAAU,KAAK,KACfA,KAAU,KAAK,KACfA,KAAU,IAAK,GAClB;EACF;AAVOf,IAAS,sBAAAa;AAYT,WAASG,EAAOhB,GAAuB;AAC5C,QAAMiB,KAAajB,EAAM,OAAO,SAAU;AAC1C,WAAA,CAACb,GAAIC,GAAIC,CAAE,IAAIsB,EAAK,WAAWM,CAAS,GACjC,EACL,KAAK1B,EAAS,MAAMJ,GAAIC,GAAIC,CAAE,GAC9B,MAAM4B,EACR;EACF;AAPOjB,IAAS,SAAAgB;AAST,WAASE,EAAQlB,GAAekB,GAAyB;AAC9D,WAAA5B,IAAK,KAAK,MAAM4B,IAAU,GAAI,GAC9B,CAAC/B,GAAIC,GAAIC,CAAE,IAAIsB,EAAK,WAAWX,EAAM,IAAI,GAClC,EACL,KAAKT,EAAS,MAAMJ,GAAIC,GAAIC,GAAIC,CAAE,GAClC,MAAMC,EAAS,OAAOJ,GAAIC,GAAIC,GAAIC,CAAE,EACtC;EACF;AAPOU,IAAS,UAAAkB;AAST,WAASC,EAAgBnB,GAAeoB,GAAwB;AACrE,WAAA9B,IAAKU,EAAM,OAAO,KACXkB,EAAQlB,GAAQV,IAAK8B,IAAU,GAAI;EAC5C;AAHOpB,IAAS,kBAAAmB;AAKT,WAASE,EAAWrB,GAA0B;AACnD,WAAO,CAAEA,EAAM,QAAQ,KAAM,KAAOA,EAAM,QAAQ,KAAM,KAAOA,EAAM,QAAQ,IAAK,GAAI;EACxF;AAFOA,IAAS,aAAAqB;AAAAA,GA9DDrB,MAAA,CAAA,CAAA;AAuEV,IAAUU;CAAAA,OAAV;AAEL,MAAIY,GACAC;AACJ,MAAI;AAEF,QAAMC,IAAS,SAAS,cAAc,QAAQ;AAC9CA,MAAO,QAAQ,GACfA,EAAO,SAAS;AAChB,QAAMC,IAAMD,EAAO,WAAW,MAAM,EAClC,oBAAoB,KACtB,CAAC;AACGC,UACFH,IAAOG,GACPH,EAAK,2BAA2B,QAChCC,IAAeD,EAAK,qBAAqB,GAAG,GAAG,GAAG,CAAC;EAEvD,QACM;EAEN;AASO,WAASvB,EAAQW,GAAqB;AAE3C,QAAIA,EAAI,MAAM,gBAAgB,EAC5B,SAAQA,EAAI,QAAQ;MAClB,KAAK;AACH,eAAAvB,IAAK,SAASuB,EAAI,MAAM,GAAG,CAAC,EAAE,OAAO,CAAC,GAAG,EAAE,GAC3CtB,IAAK,SAASsB,EAAI,MAAM,GAAG,CAAC,EAAE,OAAO,CAAC,GAAG,EAAE,GAC3CrB,IAAK,SAASqB,EAAI,MAAM,GAAG,CAAC,EAAE,OAAO,CAAC,GAAG,EAAE,GACpCnB,EAAS,QAAQJ,GAAIC,GAAIC,CAAE;MAEpC,KAAK;AACH,eAAAF,IAAK,SAASuB,EAAI,MAAM,GAAG,CAAC,EAAE,OAAO,CAAC,GAAG,EAAE,GAC3CtB,IAAK,SAASsB,EAAI,MAAM,GAAG,CAAC,EAAE,OAAO,CAAC,GAAG,EAAE,GAC3CrB,IAAK,SAASqB,EAAI,MAAM,GAAG,CAAC,EAAE,OAAO,CAAC,GAAG,EAAE,GAC3CpB,IAAK,SAASoB,EAAI,MAAM,GAAG,CAAC,EAAE,OAAO,CAAC,GAAG,EAAE,GACpCnB,EAAS,QAAQJ,GAAIC,GAAIC,GAAIC,CAAE;MAExC,KAAK;AACH,eAAO,EACL,KAAAoB,GACA,OAAO,SAASA,EAAI,MAAM,CAAC,GAAG,EAAE,KAAK,IAAI,SAAU,EACrD;MACF,KAAK;AACH,eAAO,EACL,KAAAA,GACA,MAAM,SAASA,EAAI,MAAM,CAAC,GAAG,EAAE,MAAM,EACvC;IACJ;AAIF,QAAMgB,IAAYhB,EAAI,MAAM,oFAAoF;AAChH,QAAIgB,EACF,QAAAvC,IAAK,SAASuC,EAAU,CAAC,CAAC,GAC1BtC,IAAK,SAASsC,EAAU,CAAC,CAAC,GAC1BrC,IAAK,SAASqC,EAAU,CAAC,CAAC,GAC1BpC,IAAK,KAAK,OAAOoC,EAAU,CAAC,MAAM,SAAY,IAAI,WAAWA,EAAU,CAAC,CAAC,KAAK,GAAI,GAC3EnC,EAAS,QAAQJ,GAAIC,GAAIC,GAAIC,CAAE;AAIxC,QAAI,CAACgC,KAAQ,CAACC,EACZ,OAAM,IAAI,MAAM,qCAAqC;AAOvD,QAFAD,EAAK,YAAYC,GACjBD,EAAK,YAAYZ,GACb,OAAOY,EAAK,aAAc,SAC5B,OAAM,IAAI,MAAM,qCAAqC;AAOvD,QAJAA,EAAK,SAAS,GAAG,GAAG,GAAG,CAAC,GACxB,CAACnC,GAAIC,GAAIC,GAAIC,CAAE,IAAIgC,EAAK,aAAa,GAAG,GAAG,GAAG,CAAC,EAAE,MAG7ChC,MAAO,IACT,OAAM,IAAI,MAAM,qCAAqC;AAMvD,WAAO,EACL,MAAMC,EAAS,OAAOJ,GAAIC,GAAIC,GAAIC,CAAE,GACpC,KAAAoB,EACF;EACF;AApEOA,IAAS,UAAAX;AAAAA,GA7BDW,MAAA,CAAA,CAAA;AAuGV,IAAUiB;CAAAA,OAAV;AAOE,WAASC,EAAkBD,GAAqB;AACrD,WAAOE,EACJF,KAAO,KAAM,KACbA,KAAO,IAAM,KACbA,IAAa,GAAI;EACtB;AALOA,IAAS,oBAAAC;AAeT,WAASC,EAAmB,GAAWnC,GAAWC,GAAmB;AAC1E,QAAMmC,IAAK,IAAI,KACTC,IAAKrC,IAAI,KACTsC,IAAKrC,IAAI,KACTsC,IAAKH,KAAM,UAAUA,IAAK,QAAQ,KAAK,KAAKA,IAAK,SAAS,OAAO,GAAG,GACpEI,IAAKH,KAAM,UAAUA,IAAK,QAAQ,KAAK,KAAKA,IAAK,SAAS,OAAO,GAAG,GACpEI,IAAKH,KAAM,UAAUA,IAAK,QAAQ,KAAK,KAAKA,IAAK,SAAS,OAAO,GAAG;AAC1E,WAAOC,IAAK,SAASC,IAAK,SAASC,IAAK;EAC1C;AAROR,IAAS,qBAAAE;AAAAA,GAtBDF,MAAA,CAAA,CAAA;AAoCV,IAAUhB;CAAAA,OAAV;AACE,WAASV,EAAMC,GAAYC,GAAoB;AAEpD,QADAb,KAAMa,IAAK,OAAQ,KACfb,MAAO,EACT,QAAOa;AAET,QAAMC,IAAOD,KAAM,KAAM,KACnBE,IAAOF,KAAM,KAAM,KACnBG,IAAOH,KAAM,IAAK,KAClBI,IAAOL,KAAM,KAAM,KACnBM,IAAON,KAAM,KAAM,KACnBO,IAAOP,KAAM,IAAK;AACxB,WAAAf,IAAKoB,IAAM,KAAK,OAAOH,IAAMG,KAAOjB,CAAE,GACtCF,IAAKoB,IAAM,KAAK,OAAOH,IAAMG,KAAOlB,CAAE,GACtCD,IAAKoB,IAAM,KAAK,OAAOH,IAAMG,KAAOnB,CAAE,GAC/BC,EAAS,OAAOJ,GAAIC,GAAIC,CAAE;EACnC;AAfOsB,IAAS,QAAAV;AA8BT,WAASY,EAAoBuB,GAAgBC,GAAgBvB,GAAmC;AACrG,QAAMwB,IAAMX,EAAI,kBAAkBS,KAAU,CAAC,GACvCG,IAAMZ,EAAI,kBAAkBU,KAAU,CAAC;AAE7C,QADWG,EAAcF,GAAKC,CAAG,IACxBzB,GAAO;AACd,UAAIyB,IAAMD,GAAK;AACb,YAAMG,IAAUC,EAAgBN,GAAQC,GAAQvB,CAAK,GAC/C6B,IAAeH,EAAcF,GAAKX,EAAI,kBAAkBc,KAAW,CAAC,CAAC;AAC3E,YAAIE,IAAe7B,GAAO;AACxB,cAAM8B,IAAUC,EAAkBT,GAAQC,GAAQvB,CAAK,GACjDgC,IAAeN,EAAcF,GAAKX,EAAI,kBAAkBiB,KAAW,CAAC,CAAC;AAC3E,iBAAOD,IAAeG,IAAeL,IAAUG;QACjD;AACA,eAAOH;MACT;AACA,UAAMA,IAAUI,EAAkBT,GAAQC,GAAQvB,CAAK,GACjD6B,IAAeH,EAAcF,GAAKX,EAAI,kBAAkBc,KAAW,CAAC,CAAC;AAC3E,UAAIE,IAAe7B,GAAO;AACxB,YAAM8B,IAAUF,EAAgBN,GAAQC,GAAQvB,CAAK,GAC/CgC,IAAeN,EAAcF,GAAKX,EAAI,kBAAkBiB,KAAW,CAAC,CAAC;AAC3E,eAAOD,IAAeG,IAAeL,IAAUG;MACjD;AACA,aAAOH;IACT;EAEF;AAzBO9B,IAAS,sBAAAE;AA2BT,WAAS6B,EAAgBN,GAAgBC,GAAgBvB,GAAuB;AAGrF,QAAMP,IAAO6B,KAAU,KAAM,KACvB5B,IAAO4B,KAAU,KAAM,KACvB3B,IAAO2B,KAAW,IAAK,KACzBhC,IAAOiC,KAAU,KAAM,KACvBhC,IAAOgC,KAAU,KAAM,KACvB/B,IAAO+B,KAAW,IAAK,KACvBU,IAAKP,EAAcb,EAAI,mBAAmBvB,GAAKC,GAAKC,CAAG,GAAGqB,EAAI,mBAAmBpB,GAAKC,GAAKC,CAAG,CAAC;AACnG,WAAOsC,IAAKjC,MAAUV,IAAM,KAAKC,IAAM,KAAKC,IAAM,KAEhDF,MAAO,KAAK,IAAI,GAAG,KAAK,KAAKA,IAAM,GAAG,CAAC,GACvCC,KAAO,KAAK,IAAI,GAAG,KAAK,KAAKA,IAAM,GAAG,CAAC,GACvCC,KAAO,KAAK,IAAI,GAAG,KAAK,KAAKA,IAAM,GAAG,CAAC,GACvCyC,IAAKP,EAAcb,EAAI,mBAAmBvB,GAAKC,GAAKC,CAAG,GAAGqB,EAAI,mBAAmBpB,GAAKC,GAAKC,CAAG,CAAC;AAEjG,YAAQL,KAAO,KAAKC,KAAO,KAAKC,KAAO,IAAI,SAAU;EACvD;AAlBOK,IAAS,kBAAA+B;AAoBT,WAASG,EAAkBT,GAAgBC,GAAgBvB,GAAuB;AAGvF,QAAMP,IAAO6B,KAAU,KAAM,KACvB5B,IAAO4B,KAAU,KAAM,KACvB3B,IAAO2B,KAAW,IAAK,KACzBhC,IAAOiC,KAAU,KAAM,KACvBhC,IAAOgC,KAAU,KAAM,KACvB/B,IAAO+B,KAAW,IAAK,KACvBU,IAAKP,EAAcb,EAAI,mBAAmBvB,GAAKC,GAAKC,CAAG,GAAGqB,EAAI,mBAAmBpB,GAAKC,GAAKC,CAAG,CAAC;AACnG,WAAOsC,IAAKjC,MAAUV,IAAM,OAAQC,IAAM,OAAQC,IAAM,OAEtDF,KAAM,KAAK,IAAI,KAAMA,IAAM,KAAK,MAAM,MAAMA,KAAO,GAAG,CAAC,GACvDC,IAAM,KAAK,IAAI,KAAMA,IAAM,KAAK,MAAM,MAAMA,KAAO,GAAG,CAAC,GACvDC,IAAM,KAAK,IAAI,KAAMA,IAAM,KAAK,MAAM,MAAMA,KAAO,GAAG,CAAC,GACvDyC,IAAKP,EAAcb,EAAI,mBAAmBvB,GAAKC,GAAKC,CAAG,GAAGqB,EAAI,mBAAmBpB,GAAKC,GAAKC,CAAG,CAAC;AAEjG,YAAQL,KAAO,KAAKC,KAAO,KAAKC,KAAO,IAAI,SAAU;EACvD;AAlBOK,IAAS,oBAAAkC;AAoBT,WAASG,EAAWC,GAAiD;AAC1E,WAAO,CAAEA,KAAS,KAAM,KAAOA,KAAS,KAAM,KAAOA,KAAS,IAAK,KAAMA,IAAQ,GAAI;EACvF;AAFOtC,IAAS,aAAAqC;AAAAA,GAlGDrC,MAAA,CAAA,CAAA;AAuGV,SAASd,EAAYqD,GAAmB;AAC7C,MAAMC,IAAID,EAAE,SAAS,EAAE;AACvB,SAAOC,EAAE,SAAS,IAAI,MAAMA,IAAIA;AAClC;AAQO,SAASX,EAAcY,GAAYC,GAAoB;AAC5D,SAAID,IAAKC,KACCA,IAAK,SAASD,IAAK,SAErBA,IAAK,SAASC,IAAK;AAC7B;ACnMO,IAAMC,IAAsB,OAAO,QAAQ,MAAM;AACtD,MAAMC,IAAS,CAEb7C,EAAI,QAAQ,SAAS,GACrBA,EAAI,QAAQ,SAAS,GACrBA,EAAI,QAAQ,SAAS,GACrBA,EAAI,QAAQ,SAAS,GACrBA,EAAI,QAAQ,SAAS,GACrBA,EAAI,QAAQ,SAAS,GACrBA,EAAI,QAAQ,SAAS,GACrBA,EAAI,QAAQ,SAAS,GAErBA,EAAI,QAAQ,SAAS,GACrBA,EAAI,QAAQ,SAAS,GACrBA,EAAI,QAAQ,SAAS,GACrBA,EAAI,QAAQ,SAAS,GACrBA,EAAI,QAAQ,SAAS,GACrBA,EAAI,QAAQ,SAAS,GACrBA,EAAI,QAAQ,SAAS,GACrBA,EAAI,QAAQ,SAAS,CACvB,GAIM8C,IAAI,CAAC,GAAM,IAAM,KAAM,KAAM,KAAM,GAAI;AAC7C,WAASC,IAAI,GAAGA,IAAI,KAAKA,KAAK;AAC5B,QAAM,IAAID,EAAGC,IAAI,KAAM,IAAI,CAAC,GACtB/D,IAAI8D,EAAGC,IAAI,IAAK,IAAI,CAAC,GACrB9D,IAAI6D,EAAEC,IAAI,CAAC;AACjBF,MAAO,KAAK,EACV,KAAKhE,EAAS,MAAM,GAAGG,GAAGC,CAAC,GAC3B,MAAMJ,EAAS,OAAO,GAAGG,GAAGC,CAAC,EAC/B,CAAC;EACH;AAGA,WAAS8D,IAAI,GAAGA,IAAI,IAAIA,KAAK;AAC3B,QAAMP,IAAI,IAAIO,IAAI;AAClBF,MAAO,KAAK,EACV,KAAKhE,EAAS,MAAM2D,GAAGA,GAAGA,CAAC,GAC3B,MAAM3D,EAAS,OAAO2D,GAAGA,GAAGA,CAAC,EAC/B,CAAC;EACH;AAEA,SAAOK;AACT,GAAG,CAAC;ACrNJ,SAASG,EAAUT,GAAeU,GAAaC,GAAsB;AACnE,SAAO,KAAK,IAAID,GAAK,KAAK,IAAIV,GAAOW,CAAI,CAAC;AAC5C;AAEA,SAASC,EAAeX,GAAmB;AACzC,UAAQA,GAAG;IACT,KAAK;AAAK,aAAO;IACjB,KAAK;AAAK,aAAO;EACnB;AACA,SAAOA;AACT;AAGA,IAAeY,IAAf,MAAoC;EAClC,YACqBC,GACnB;AADmB,SAAA,UAAAA;EAErB;EAEO,UAAUC,GAAqBC,GAA8C;AAElF,QAAMC,IAAQ,KAAK,QAAQ,YAAY,GACjCC,IAAQ,KAAK,QAAQ,YAAY,GACnCC,IAAUF,GAERG,IAAWL,EAAM,MAAM,GACvBM,IAASN,EAAM,IAAI,GACnBO,IAAcP,EAAM,MAAM,GAC1BQ,IAAYR,EAAM,IAAI;AAE5B,SAAK,iBAAiBM,IAASD,GAAUA,GAAUC,CAAM;AAEzD,aAASG,IAAMJ,GAAUI,KAAOH,GAAQG,KAAO;AAC7C,UAAMC,IAAO,KAAK,QAAQ,QAAQD,CAAG;AACrC,UAAIC,GAAM;AACR,YAAMC,IAAkBF,MAAQT,EAAM,MAAM,IAAIO,IAAc,GACxDK,IAAgBH,MAAQT,EAAM,IAAI,IAAIQ,IAAWE,EAAK;AAC5D,iBAASG,IAAMF,GAAiBE,IAAMD,GAAeC,KAAO;AAC1D,cAAM3B,IAAIwB,EAAK,QAAQG,GAAKT,MAAYF,IAAQC,IAAQD,CAAK;AAC7D,cAAI,CAAChB,GAAG;AACN,oBAAQ,KAAK,yBAAyBuB,CAAG,SAASI,CAAG,EAAE;AACvD;UACF;AACA,eAAK,UAAU3B,GAAGkB,GAASK,GAAKI,CAAG,GACnCT,IAAUlB;QACZ;MACF;AACA,WAAK,QAAQuB,GAAKA,MAAQH,CAAM;IAClC;AAEA,WAAA,KAAK,gBAAgB,GAEd,KAAK,iBAAiBL,CAA0B;EACzD;EAEU,UAAUa,GAAmBV,GAAsBK,GAAaI,GAAmB;EAAE;EACrF,QAAQJ,GAAaM,GAA0B;EAAE;EACjD,iBAAiBC,GAAcX,GAAkBC,GAAsB;EAAE;EACzE,kBAAwB;EAAE;EAC1B,iBAAiBL,GAA8C;AAAE,WAAO;EAAI;AACxF;AAEA,SAASgB,EAAQf,GAAqCC,GAA6B;AACjF,SAAOD,EAAM,eAAe,MAAMC,EAAM,eAAe,KAClDD,EAAM,WAAW,MAAMC,EAAM,WAAW;AAC/C;AAEA,SAASe,EAAQhB,GAAqCC,GAA6B;AACjF,SAAOD,EAAM,eAAe,MAAMC,EAAM,eAAe,KAClDD,EAAM,WAAW,MAAMC,EAAM,WAAW;AAC/C;AAEA,SAASgB,EAAWjB,GAAqCC,GAA6B;AACpF,SAAOD,EAAM,UAAU,MAAMC,EAAM,UAAU,KACxCD,EAAM,OAAO,MAAMC,EAAM,OAAO,KAChCD,EAAM,YAAY,MAAMC,EAAM,YAAY,KAC1CD,EAAM,WAAW,MAAMC,EAAM,WAAW,KACxCD,EAAM,QAAQ,MAAMC,EAAM,QAAQ,KAClCD,EAAM,YAAY,MAAMC,EAAM,YAAY,KAC1CD,EAAM,SAAS,MAAMC,EAAM,SAAS,KACpCD,EAAM,MAAM,MAAMC,EAAM,MAAM,KAC9BD,EAAM,gBAAgB,MAAMC,EAAM,gBAAgB;AACzD;AAEA,IAAMiB,IAAN,cAAqCtB,EAAqB;EA2BxD,YACEuB,GACiBC,GACjB;AACA,UAAMD,CAAM;AAFK,SAAA,YAAAC;AA5BnB,SAAQ,YAAoB;AAC5B,SAAQ,WAAqB,IAAI;AACjC,SAAQ,oBAA8B,IAAI;AAC1C,SAAQ,cAAsB;AAC9B,SAAQ,iBAAyB;AAKjC,SAAQ,eAA4B,KAAK,QAAQ,YAAY;AAK7D,SAAQ,kBAA0B;AAClC,SAAQ,kBAA0B;AAGlC,SAAQ,kBAA+B,KAAK,QAAQ,YAAY;AAEhE,SAAQ,YAAoB;AAC5B,SAAQ,iBAAyB;AACjC,SAAQ,iBAAyB;AACjC,SAAQ,wBAAgC;AACxC,SAAQ,wBAAgC;AAgBxC,SAAQ,mBAAgC,KAAK,QAAQ,YAAY;AACjE,SAAQ,yBAAsC,KAAK,QAAQ,YAAY;AACvE,SAAQ,oBAAiC,KAAK,QAAQ,YAAY;EAXlE;EAEU,iBAAiBN,GAAcO,GAAeC,GAAmB;AACzE,SAAK,WAAW,IAAI,MAAcR,CAAI,GACtC,KAAK,wBAAwBO,GAC7B,KAAK,iBAAiBA,GACtB,KAAK,YAAYA;EACnB;EAKU,QAAQd,GAAaM,GAA0B;AAGnD,SAAK,iBAAiB,KAAK,CAACG,EAAQ,KAAK,cAAc,KAAK,eAAe,MAE7E,KAAK,eAAe,QAAU,KAAK,cAAc;AAGnD,QAAIO,IAAe;AAGnB,QAAI,CAACV,GAAW;AAEVN,UAAM,KAAK,aAAa,KAAK,UAAU,QACzC,KAAK,QAAQ,QAAQ,KAAK,eAAe,GAAG,QAAQ,KAAK,iBAAiB,KAAK,eAAe;AAIhG,UAAMiB,IAAc,KAAK,QAAQ,QAAQjB,CAAG,GAEtCkB,IAAW,KAAK,QAAQ,QAAQlB,IAAM,CAAC;AAE7C,UAAI,CAACkB,EAAS,UAEZF,KAAe;GAEf,KAAK,iBAAiBhB,IAAM,GAC5B,KAAK,iBAAiB;WACjB;AACLgB,YAAe;AACf,YAAMG,IAAkBF,EAAY,QAAQA,EAAY,SAAS,GAAG,KAAK,gBAAgB,GACnFG,IAAwBH,EAAY,QAAQA,EAAY,SAAS,GAAG,KAAK,sBAAsB,GAC/FI,IAAmBH,EAAS,QAAQ,GAAG,KAAK,iBAAiB,GAC7DI,IAAgCD,EAAiB,SAAS,IAAI,GAIhEE,IAAU;AAAA,SAIZF,EAAiB,SAAS,KACxBC,IAAgC,KAAK,kBAAkB,IAAI,KAAK,kBAAkB,QAKjFH,EAAgB,SAAS,KAAKA,EAAgB,SAAS,MAAM,MAG9DV,EAAQU,GAAiBE,CAAgB,MAEzCE,IAAU,OAMVD,MACCF,EAAsB,SAAS,KAAKA,EAAsB,SAAS,MAAM,MAG1EX,EAAQU,GAAiBE,CAAgB,KACzCZ,EAAQW,GAAuBC,CAAgB,MAE/CE,IAAU,QAITA,MAGHP,IAAe,IAAI,OAAO,KAAK,iBAAiB,CAAC,GAEjDA,KAAgB,kBAEZ,KAAK,iBAAiB,MAExBA,KAAgB,UAChBA,KAAgB,QAAUC,EAAY,SAAS,KAAK,cAAc,KAClED,KAAgB,QAAU,KAAK,cAAc,KAC7CA,KAAgB,QAAUC,EAAY,SAAS,KAAK,cAAc,KAClED,KAAgB,WAKlB,KAAK,wBAAwBhB,IAAM,GACnC,KAAK,wBAAwB,GAG7B,KAAK,iBAAiBA,IAAM,GAC5B,KAAK,iBAAiB;MAE1B;IACF;AAEA,SAAK,SAAS,KAAK,SAAS,IAAI,KAAK,aACrC,KAAK,kBAAkB,KAAK,WAAW,IAAIgB,GAC3C,KAAK,cAAc,IACnB,KAAK,iBAAiB;EACxB;EAEQ,WAAWX,GAAoCV,GAAgC;AACrF,QAAM6B,IAAmB,CAAC,GACpBC,IAAY,CAACjB,EAAQH,GAAMV,CAAO,GAClC+B,IAAY,CAACjB,EAAQJ,GAAMV,CAAO,GAClCgC,IAAe,CAACjB,EAAWL,GAAMV,CAAO;AAE9C,QAAI8B,KAAaC,KAAaC,EAC5B,KAAItB,EAAK,mBAAmB,EACrBV,GAAQ,mBAAmB,KAC9B6B,EAAO,KAAK,CAAC;SAEV;AACL,UAAIC,GAAW;AACb,YAAMlG,IAAQ8E,EAAK,WAAW;AAC1BA,UAAK,QAAQ,IAAKmB,EAAO,KAAK,IAAI,GAAIjG,MAAU,KAAM,KAAOA,MAAU,IAAK,KAAMA,IAAQ,GAAI,IACzF8E,EAAK,YAAY,IACpB9E,KAAS,KAAMiG,EAAO,KAAK,IAAI,GAAGjG,CAAK,IACpCiG,EAAO,KAAKjG,IAAQ,IAAI,MAAMA,IAAQ,KAAK,MAAMA,IAAQ,EAAE,IAE7DiG,EAAO,KAAK,EAAE;MACvB;AACA,UAAIE,GAAW;AACb,YAAMnG,IAAQ8E,EAAK,WAAW;AAC1BA,UAAK,QAAQ,IAAKmB,EAAO,KAAK,IAAI,GAAIjG,MAAU,KAAM,KAAOA,MAAU,IAAK,KAAMA,IAAQ,GAAI,IACzF8E,EAAK,YAAY,IACpB9E,KAAS,KAAMiG,EAAO,KAAK,IAAI,GAAGjG,CAAK,IACpCiG,EAAO,KAAKjG,IAAQ,IAAI,OAAOA,IAAQ,KAAK,MAAMA,IAAQ,EAAE,IAE9DiG,EAAO,KAAK,EAAE;MACvB;AACIG,YACEtB,EAAK,UAAU,MAAMV,EAAQ,UAAU,KAAK6B,EAAO,KAAKnB,EAAK,UAAU,IAAI,IAAI,EAAE,GACjFA,EAAK,OAAO,MAAMV,EAAQ,OAAO,KAAK6B,EAAO,KAAKnB,EAAK,OAAO,IAAI,IAAI,EAAE,GACxEA,EAAK,YAAY,MAAMV,EAAQ,YAAY,KAAK6B,EAAO,KAAKnB,EAAK,YAAY,IAAI,IAAI,EAAE,GACvFA,EAAK,WAAW,MAAMV,EAAQ,WAAW,KAAK6B,EAAO,KAAKnB,EAAK,WAAW,IAAI,KAAK,EAAE,GACrFA,EAAK,QAAQ,MAAMV,EAAQ,QAAQ,KAAK6B,EAAO,KAAKnB,EAAK,QAAQ,IAAI,IAAI,EAAE,GAC3EA,EAAK,YAAY,MAAMV,EAAQ,YAAY,KAAK6B,EAAO,KAAKnB,EAAK,YAAY,IAAI,IAAI,EAAE,GACvFA,EAAK,SAAS,MAAMV,EAAQ,SAAS,KAAK6B,EAAO,KAAKnB,EAAK,SAAS,IAAI,IAAI,EAAE,GAC9EA,EAAK,MAAM,MAAMV,EAAQ,MAAM,KAAK6B,EAAO,KAAKnB,EAAK,MAAM,IAAI,IAAI,EAAE,GACrEA,EAAK,gBAAgB,MAAMV,EAAQ,gBAAgB,KAAK6B,EAAO,KAAKnB,EAAK,gBAAgB,IAAI,IAAI,EAAE;IAE3G;AAGF,WAAOmB;EACT;EAEU,UAAUnB,GAAmBV,GAAsBK,GAAaI,GAAmB;AAI3F,QAF0BC,EAAK,SAAS,MAAM,EAG5C;AAIF,QAAMuB,IAAcvB,EAAK,SAAS,MAAM,IAElCmB,IAAS,KAAK,WAAWnB,GAAM,KAAK,YAAY;AAStD,QALqBuB,IAAc,CAACnB,EAAQ,KAAK,cAAcJ,CAAI,IAAImB,EAAO,SAAS,GAKrE;AAEZ,WAAK,iBAAiB,MAEnBf,EAAQ,KAAK,cAAc,KAAK,eAAe,MAClD,KAAK,eAAe,QAAU,KAAK,cAAc,MAGnD,KAAK,eAAe,QAAU,KAAK,cAAc,KACjD,KAAK,iBAAiB,IAGxB,KAAK,wBAAwB,KAAK,iBAAiBT,GACnD,KAAK,wBAAwB,KAAK,iBAAiBI,GAEnD,KAAK,eAAe,QAAUoB,EAAO,KAAK,GAAG,CAAC;AAG9C,UAAMvB,IAAO,KAAK,QAAQ,QAAQD,CAAG;AACjCC,YAAS,WACXA,EAAK,QAAQG,GAAK,KAAK,YAAY,GACnC,KAAK,kBAAkBJ,GACvB,KAAK,kBAAkBI;IAE3B;AAKIwB,QACF,KAAK,kBAAkBvB,EAAK,SAAS,KAEjC,KAAK,iBAAiB,MAIpBI,EAAQ,KAAK,cAAc,KAAK,eAAe,IACjD,KAAK,eAAe,QAAU,KAAK,cAAc,OAEjD,KAAK,eAAe,QAAU,KAAK,cAAc,KACjD,KAAK,eAAe,QAAU,KAAK,cAAc,MAEnD,KAAK,iBAAiB,IAGxB,KAAK,eAAeJ,EAAK,SAAS,GAGlC,KAAK,wBAAwB,KAAK,iBAAiBL,GACnD,KAAK,wBAAwB,KAAK,iBAAiBI,IAAMC,EAAK,SAAS;EAE3E;EAEU,iBAAiBb,GAA6C;AACtE,QAAIqC,IAAS,KAAK,SAAS;AAIvB,SAAK,QAAQ,SAAS,KAAK,aAAa,KAAK,UAAU,SACzDA,IAAS,KAAK,wBAAwB,IAAI,KAAK,WAC/C,KAAK,iBAAiB,KAAK,uBAC3B,KAAK,iBAAiB,KAAK;AAG7B,QAAIC,IAAU;AAEd,aAAS,IAAI,GAAG,IAAID,GAAQ,IAC1BC,MAAW,KAAK,SAAS,CAAC,GACtB,IAAI,IAAID,MACVC,KAAW,KAAK,kBAAkB,CAAC;AAKvC,QAAI,CAACtC,GAA4B;AAC/B,UAAMuC,IAAgB,KAAK,QAAQ,QAAQ,KAAK,QAAQ,SAClDC,IAAgB,KAAK,QAAQ,SAE7BC,IAAeF,MAAkB,KAAK,kBAAkBC,MAAkB,KAAK,gBAE/EE,IAAaC,OAAyB;AACtCA,YAAS,IACXL,KAAW,QAAUK,CAAM,MAClBA,IAAS,MAClBL,KAAW,QAAU,CAACK,CAAM;MAEhC;AASIF,aARcE,OAAyB;AACrCA,YAAS,IACXL,KAAW,QAAUK,CAAM,MAClBA,IAAS,MAClBL,KAAW,QAAU,CAACK,CAAM;MAEhC,GAGWJ,IAAgB,KAAK,cAAc,GAC5CG,EAAUF,IAAgB,KAAK,cAAc;IAEjD;AAKA,QAAMI,IAA+B,KAAK,UAAkB,MAAM,cAAc,cAC1EZ,IAAS,KAAK,WAAWY,GAAa,KAAK,YAAY;AAC7D,WAAIZ,EAAO,SAAS,MAClBM,KAAW,QAAUN,EAAO,KAAK,GAAG,CAAC,MAGhCM;EACT;AACF;AAtUA,IAwUaO,IAAN,MAA+D;EAG7D,SAASC,GAA0B;AACxC,SAAK,YAAYA;EACnB;EAEQ,6BAA6BA,GAAoB1B,GAAiB2B,GAA6B;AACrG,QAAMC,IAAU5B,EAAO,QACjB6B,IAAeF,MAAe,SAAaC,IAAUvD,EAAUsD,IAAaD,EAAS,MAAM,GAAGE,CAAO;AAC3G,WAAO,KAAK,wBAAwBF,GAAU1B,GAAQ,EACpD,OAAO4B,IAAUC,GACjB,KAAKD,IAAU,EACjB,GAAG,KAAK;EACV;EAEQ,wBAAwBF,GAAoB1B,GAAiBrB,GAAwBC,GAA6C;AAExI,WADgB,IAAImB,EAAuBC,GAAQ0B,CAAQ,EAC5C,UAAU,EACvB,OAAO,EAAE,GAAG,GAAe,GAAG,OAAO/C,EAAM,SAAU,WAAWA,EAAM,QAAQA,EAAM,MAAM,KAAK,GAC/F,KAAO,EAAE,GAAG+C,EAAS,MAAM,GAAG,OAAO/C,EAAM,OAAU,WAAWA,EAAM,MAAQA,EAAM,IAAI,KAAO,EACjG,GAAGC,CAA0B;EAC/B;EAEQ,uBAAuB8C,GAAoBI,GAAiD;AAClG,QAAM9B,IAAS0B,EAAS,OAAO,QACzBK,IAAU,IAAIC,EAAqBhC,GAAQ0B,GAAUI,CAAO,GAC5DG,IAAgBH,EAAQ,iBAAiB,OACzCnD,IAAQmD,EAAQ;AACtB,QAAInD,EACF,QAAOoD,EAAQ,UAAU,EACvB,OAAO,EAAE,GAAGpD,EAAM,UAAsB,IAAG,OAAOA,EAAM,aAAc,UAAWA,EAAM,WAA4B,GACnH,KAAO,EAAE,GAAG+C,EAAS,MAAM,IAAG,OAAO/C,EAAM,WAAc,UAAWA,EAAM,SAA4B,EACxG,CAAC;AAEH,QAAI,CAACsD,GAAe;AAClB,UAAML,IAAU5B,EAAO,QACjB2B,IAAaG,EAAQ,YACrBD,IAAeF,MAAe,SAAaC,IAAUvD,EAAUsD,IAAaD,EAAS,MAAM,GAAGE,CAAO;AAC3G,aAAOG,EAAQ,UAAU,EACvB,OAAO,EAAE,GAAG,GAAe,GAAGH,IAAUC,EAAY,GACpD,KAAO,EAAE,GAAGH,EAAS,MAAM,GAAGE,IAAU,EAAY,EACtD,CAAC;IACH;AAEA,QAAMM,IAAY,KAAK,WAAW,qBAAqB;AACvD,WAAIA,MAAc,SACTH,EAAQ,UAAU,EACvB,OAAO,EAAE,GAAGG,EAAU,MAAM,GAAG,GAAGA,EAAU,MAAM,EAAE,GACpD,KAAO,EAAE,GAAGA,EAAU,IAAI,GAAK,GAAGA,EAAU,IAAI,EAAI,EACtD,CAAC,IAGI;EACT;EAEQ,gBAAgBR,GAA4B;AAClD,QAAIR,IAAU,IACRiB,IAAQT,EAAS;AAgBvB,QAbIS,EAAM,8BAA2BjB,KAAW,aAC5CiB,EAAM,0BAAuBjB,KAAW,cACxCiB,EAAM,uBAAoBjB,KAAW,gBACrCiB,EAAM,eAAYjB,KAAW,YAC7BiB,EAAM,eAAYjB,KAAW,aAC7BiB,EAAM,0BAAuBjB,KAAW,cACxCiB,EAAM,kBAAejB,KAAW,gBAIhCiB,EAAM,mBAAmB,UAAOjB,KAAW,aAG3CiB,EAAM,sBAAsB,OAC9B,SAAQA,EAAM,mBAAmB;MAC/B,KAAK;AAAOjB,aAAW;AAAY;MACnC,KAAK;AAASA,aAAW;AAAe;MACxC,KAAK;AAAQA,aAAW;AAAe;MACvC,KAAK;AAAOA,aAAW;AAAe;IACxC;AAGF,WAAOA;EACT;EAEO,UAAUY,GAAqC;AAEpD,QAAI,CAAC,KAAK,UACR,OAAM,IAAI,MAAM,2CAA2C;AAI7D,QAAIZ,IAAUY,GAAS,QACnB,KAAK,wBAAwB,KAAK,WAAW,KAAK,UAAU,OAAO,QAAQA,EAAQ,OAAO,IAAI,IAC9F,KAAK,6BAA6B,KAAK,WAAW,KAAK,UAAU,OAAO,QAAQA,GAAS,UAAU;AAGvG,QAAI,CAACA,GAAS,oBACR,KAAK,UAAU,OAAO,OAAO,SAAS,aAAa;AACrD,UAAMM,IAA2B,KAAK,6BAA6B,KAAK,WAAW,KAAK,UAAU,OAAO,WAAW,MAAS;AAC7HlB,WAAW,oBAAwBkB,CAAwB;IAC7D;AAIF,WAAKN,GAAS,iBACZZ,KAAW,KAAK,gBAAgB,KAAK,SAAS,IAGzCA;EACT;EAEO,gBAAgBY,GAAkD;AACvE,QAAI,CAAC,KAAK,UACR,OAAM,IAAI,MAAM,2CAA2C;AAG7D,WAAO,KAAK,uBAAuB,KAAK,WAAWA,KAAW,CAAC,CAAC;EAClE;EAEO,UAAgB;EAAE;AAC3B;AAlcA,IAocaE,IAAN,cAAmCvD,EAAqB;EAO7D,YACEuB,GACiBC,GACAoC,GACjB;AACA,UAAMrC,CAAM;AAHK,SAAA,YAAAC;AACA,SAAA,WAAAoC;AATnB,SAAQ,cAAsB;AAE9B,SAAQ,eAAe;AAYhBpC,MAAkB,MAAM,gBAC3B,KAAK,cAAeA,EAAkB,MAAM,cAAc,OAAO,OAGjE,KAAK,cAAchC;EAEvB;EAEQ,UAAUqE,GAAgBC,GAAsBC,GAA2B;AAGjF,WAFAD,IAAeA,KAAgB,GAC/BC,IAAYA,KAAa,KACrBF,EAAO,SAASC,IACXD,KAGTC,KAAgBD,EAAO,QACnBC,IAAeC,EAAU,WAC3BA,KAAaA,EAAU,OAAOD,IAAeC,EAAU,MAAM,IAExDA,EAAU,MAAM,GAAGD,CAAY,IAAID;EAC5C;EAEU,iBAAiB3C,GAAcO,GAAeC,GAAmB;AACzE,SAAK,gBAAgB;AAErB,QAAIsC,IAAa,WACbC,IAAa;AAAA,KACb,KAAK,SAAS,2BAA2B,WAC3CD,IAAa,KAAK,UAAU,QAAQ,OAAO,cAAc,WACzDC,IAAa,KAAK,UAAU,QAAQ,OAAO,cAAc;AAG3D,QAAMC,IAAyB,CAAC;AAChCA,MAAuB,KAAK,YAAYF,IAAa,GAAG,GACxDE,EAAuB,KAAK,uBAAuBD,IAAa,GAAG,GACnEC,EAAuB,KAAK,kBAAkB,KAAK,UAAU,QAAQ,aAAa,GAAG,GACrFA,EAAuB,KAAK,gBAAgB,KAAK,UAAU,QAAQ,WAAW,KAAK,GACnF,KAAK,gBAAgB,iBAAkBA,EAAuB,KAAK,GAAG,IAAI;EAC5E;EAEU,kBAAwB;AAChC,SAAK,gBAAgB,UACrB,KAAK,gBAAgB;EACvB;EAEU,QAAQvD,GAAaM,GAA0B;AACvD,SAAK,gBAAgB,gBAAgB,KAAK,cAAc,iBACxD,KAAK,cAAc;EACrB;EAEQ,aAAaD,GAAmBmD,GAAmC;AACzE,QAAMjI,IAAQiI,IAAOnD,EAAK,WAAW,IAAIA,EAAK,WAAW;AACzD,QAAImD,IAAOnD,EAAK,QAAQ,IAAIA,EAAK,QAAQ,EAMvC,QAAO,MALK,CACT9E,KAAS,KAAM,KACfA,KAAU,IAAK,KACfA,IAAe,GAClB,EACiB,IAAIkI,OAAK,KAAK,UAAUA,EAAE,SAAS,EAAE,GAAG,GAAG,GAAG,CAAC,EAAE,KAAK,EAAE;AAE3E,QAAID,IAAOnD,EAAK,YAAY,IAAIA,EAAK,YAAY,EAC/C,QAAO,KAAK,YAAY9E,CAAK,EAAE;EAGnC;EAEQ,WAAW8E,GAAmBV,GAA4C;AAChF,QAAMmC,IAAoB,CAAC,GAErBL,IAAY,CAACjB,EAAQH,GAAMV,CAAO,GAClC+B,IAAY,CAACjB,EAAQJ,GAAMV,CAAO,GAClCgC,IAAe,CAACjB,EAAWL,GAAMV,CAAO;AAE9C,QAAI8B,KAAaC,KAAaC,GAAc;AAC1C,UAAM+B,IAAa,KAAK,aAAarD,GAAM,IAAI;AAC3CqD,WACF5B,EAAQ,KAAK,YAAY4B,IAAa,GAAG;AAG3C,UAAMC,IAAa,KAAK,aAAatD,GAAM,KAAK;AAChD,aAAIsD,KACF7B,EAAQ,KAAK,uBAAuB6B,IAAa,GAAG,GAGlDtD,EAAK,UAAU,KAAKyB,EAAQ,KAAK,4CAA4C,GAC7EzB,EAAK,OAAO,KAAKyB,EAAQ,KAAK,oBAAoB,GAClDzB,EAAK,YAAY,KAAKA,EAAK,WAAW,IAAKyB,EAAQ,KAAK,sCAAsC,IACzFzB,EAAK,YAAY,IAAKyB,EAAQ,KAAK,6BAA6B,IAChEzB,EAAK,WAAW,KAAKyB,EAAQ,KAAK,4BAA4B,GACnEzB,EAAK,QAAQ,KAAKyB,EAAQ,KAAK,yBAAyB,GACxDzB,EAAK,YAAY,KAAKyB,EAAQ,KAAK,qBAAqB,GACxDzB,EAAK,SAAS,KAAKyB,EAAQ,KAAK,qBAAqB,GACrDzB,EAAK,MAAM,KAAKyB,EAAQ,KAAK,eAAe,GAC5CzB,EAAK,gBAAgB,KAAKyB,EAAQ,KAAK,gCAAgC,GAEpEA;IACT;EAGF;EAEU,UAAUzB,GAAmBV,GAAsBK,GAAaI,GAAmB;AAG3F,QAD0BC,EAAK,SAAS,MAAM,EAE5C;AAIF,QAAMuB,IAAcvB,EAAK,SAAS,MAAM,IAElCuD,IAAmB,KAAK,WAAWvD,GAAMV,CAAO;AAGlDiE,UACF,KAAK,eAAeA,EAAiB,WAAW,IAC9C,kBACA,yBAA0BA,EAAiB,KAAK,GAAG,IAAI,OAIvDhC,IACF,KAAK,eAAe,MAEpB,KAAK,eAAexC,EAAeiB,EAAK,SAAS,CAAC;EAEtD;EAEU,mBAA2B;AACnC,WAAO,KAAK;EACd;AACF;;;ACpqBO,IAAM,SAAS;AAAA,EACpB,UAAU;AAAA,EACV,UAAU;AAAA,EACV,QAAQ;AAAA,EACR,gBAAgB;AAAA,EAChB,eAAe;AAAA,EACf,WAAW;AAAA,EACX,gBAAgB;AAClB;AAEO,IAAM,wBAAwB;AAAA,EACnC,gBAAgB;AAAA,EAChB,mBAAmB;AAAA,EACnB,kBAAkB;AAAA,EAClB,cAAc;AAChB;AAmBO,IAAM,kBAAN,cAA8B,MAAM;AAAA;AAAA,EAEhC;AAAA,EACT,YAAY,SAAiB,QAAgB;AAC3C,UAAM,OAAO;AACb,SAAK,SAAS;AAAA,EAChB;AACF;AAKO,SAAS,WAAW,QAA6B;AACtD,MAAI,OAAO,WAAW,GAAG;AACvB,UAAM,IAAI;AAAA,MACR;AAAA,MACA,sBAAsB;AAAA,IACxB;AAAA,EACF;AACA,QAAM,SAAS,OAAO,UAAU,CAAC;AACjC,UAAQ,QAAQ;AAAA,IACd,KAAK,OAAO,UAAU;AAGpB,YAAM,OAAO,OAAO,SAAS,SAAS,CAAC;AACvC,aAAO,EAAE,MAAM,YAAY,KAAK;AAAA,IAClC;AAAA,IACA,KAAK,OAAO,UAAU;AACpB,YAAM,OAAO,OAAO,SAAS,SAAS,CAAC;AACvC,UAAI;AACJ,UAAI;AACF,iBAAS,KAAK,MAAM,IAAI;AAAA,MAC1B,SAAS,KAAK;AACZ,cAAM,IAAI;AAAA,UACR,0BAA2B,IAAc,OAAO;AAAA,UAChD,sBAAsB;AAAA,QACxB;AAAA,MACF;AACA,UAAI,CAAC,UAAU,OAAO,WAAW,YAAY,MAAM,QAAQ,MAAM,GAAG;AAClE,cAAM,IAAI;AAAA,UACR;AAAA,UACA,sBAAsB;AAAA,QACxB;AAAA,MACF;AACA,aAAO,EAAE,MAAM,YAAY,MAAM,OAAkC;AAAA,IACrE;AAAA,IACA,KAAK,OAAO,QAAQ;AAClB,UAAI,OAAO,SAAS,GAAG;AACrB,cAAM,IAAI;AAAA,UACR;AAAA,UACA,sBAAsB;AAAA,QACxB;AAAA,MACF;AACA,YAAM,OAAO,OAAO,aAAa,CAAC;AAClC,YAAM,OAAO,OAAO,aAAa,CAAC;AAClC,aAAO,EAAE,MAAM,UAAU,MAAM,KAAK;AAAA,IACtC;AAAA,IACA,KAAK,OAAO,gBAAgB;AAE1B,UAAI,OAAO,SAAS,GAAG;AACrB,cAAM,IAAI;AAAA,UACR;AAAA,UACA,sBAAsB;AAAA,QACxB;AAAA,MACF;AACA,YAAM,QAAQ,OAAO,aAAa,CAAC;AACnC,YAAM,QAAQ,IAAI;AAClB,UAAI,QAAQ,OAAO,QAAQ;AACzB,cAAM,IAAI;AAAA,UACR;AAAA,UACA,sBAAsB;AAAA,QACxB;AAAA,MACF;AACA,YAAM,YAAY,OAAO,SAAS,SAAS,GAAG,KAAK;AACnD,UAAI,QAAQ,IAAI,OAAO,QAAQ;AAC7B,cAAM,IAAI;AAAA,UACR;AAAA,UACA,sBAAsB;AAAA,QACxB;AAAA,MACF;AACA,YAAM,UAAU,OAAO,aAAa,KAAK;AACzC,YAAM,UAAU,QAAQ,IAAI;AAC5B,UAAI,UAAU,OAAO,QAAQ;AAC3B,cAAM,IAAI;AAAA,UACR;AAAA,UACA,sBAAsB;AAAA,QACxB;AAAA,MACF;AACA,YAAM,OAAO,OAAO,SAAS,SAAS,QAAQ,GAAG,OAAO;AACxD,aAAO,EAAE,MAAM,kBAAkB,WAAW,KAAK;AAAA,IACnD;AAAA,IACA,KAAK,OAAO,eAAe;AAIzB,UAAI,OAAO,SAAS,IAAI;AACtB,cAAM,IAAI;AAAA,UACR;AAAA,UACA,sBAAsB;AAAA,QACxB;AAAA,MACF;AACA,YAAM,MAAM,OAAO,aAAa,CAAC;AACjC,YAAM,OAAO,OAAO,aAAa,CAAC;AAClC,YAAM,OAAO,OAAO,aAAa,CAAC;AAElC,YAAM,UAAU,OAAO,aAAa,OAAO,SAAS,CAAC;AACrD,YAAM,UAAU,OAAO,aAAa,OAAO,SAAS,CAAC;AACrD,YAAM,QAAQ,OAAO,SAAS,GAAG,OAAO,SAAS,CAAC;AAClD,aAAO,EAAE,MAAM,iBAAiB,KAAK,MAAM,MAAM,OAAO,SAAS,QAAQ;AAAA,IAC3E;AAAA,IACA,KAAK,OAAO,WAAW;AAGrB,UAAI,OAAO,SAAS,IAAI;AACtB,cAAM,IAAI;AAAA,UACR;AAAA,UACA,sBAAsB;AAAA,QACxB;AAAA,MACF;AACA,YAAM,MAAM,OAAO,aAAa,CAAC;AACjC,YAAM,UAAU,OAAO,aAAa,CAAC;AAErC,YAAM,MAAM,OAAO,SAAS,CAAC;AAC7B,aAAO,EAAE,MAAM,aAAa,KAAK,SAAS,IAAI;AAAA,IAChD;AAAA,IACA,KAAK,OAAO,gBAAgB;AAC1B,UAAI,OAAO,SAAS,GAAG;AACrB,cAAM,IAAI;AAAA,UACR;AAAA,UACA,sBAAsB;AAAA,QACxB;AAAA,MACF;AACA,aAAO,EAAE,MAAM,kBAAkB,QAAQ,OAAO,UAAU,CAAC,EAAE;AAAA,IAC/D;AAAA,IACA;AACE,YAAM,IAAI;AAAA,QACR,oBAAoB,OAAO,SAAS,EAAE,CAAC;AAAA,QACvC,sBAAsB;AAAA,MACxB;AAAA,EACJ;AACF;AAeO,SAAS,cAAc,MAAsB;AAClD,QAAM,OAAO,OAAO,KAAK,KAAK,UAAU,IAAI,GAAG,OAAO;AACtD,QAAM,QAAQ,OAAO,MAAM,IAAI,KAAK,MAAM;AAC1C,QAAM,WAAW,OAAO,UAAU,CAAC;AACnC,OAAK,KAAK,OAAO,CAAC;AAClB,SAAO;AACT;AAKO,SAAS,oBAAoB,QAAwB;AAC1D,QAAM,QAAQ,OAAO,MAAM,CAAC;AAC5B,QAAM,WAAW,OAAO,gBAAgB,CAAC;AACzC,QAAM,WAAW,SAAS,KAAM,CAAC;AACjC,SAAO;AACT;AAcO,SAAS,oBAAoB,WAAmB,MAAsB;AAC3E,QAAM,UAAU,OAAO,KAAK,WAAW,OAAO;AAC9C,QAAM,YAAY,OAAO,KAAK,MAAM,OAAO;AAC3C,QAAM,OAAO,OAAO,MAAM,CAAC;AAC3B,OAAK,WAAW,OAAO,gBAAgB,CAAC;AACxC,OAAK,cAAc,QAAQ,WAAW,GAAG,CAAC;AAC1C,QAAM,UAAU,OAAO,MAAM,CAAC;AAC9B,UAAQ,cAAc,UAAU,WAAW,GAAG,CAAC;AAC/C,SAAO,OAAO,OAAO,CAAC,MAAM,SAAS,SAAS,SAAS,CAAC;AAC1D;AAKO,SAAS,iBAAyB;AACvC,SAAO,cAAc,EAAE,MAAM,aAAa,eAAe,iBAAiB,CAAC;AAC7E;AAcO,SAAS,mBACd,KACA,MACA,MACA,OACA,SACA,SACQ;AACR,QAAM,OAAO,OAAO,MAAM,CAAC;AAC3B,OAAK,WAAW,OAAO,eAAe,CAAC;AACvC,OAAK,cAAc,QAAQ,GAAG,CAAC;AAC/B,OAAK,cAAc,OAAO,OAAQ,CAAC;AACnC,OAAK,cAAc,OAAO,OAAQ,CAAC;AACnC,QAAM,OAAO,OAAO,MAAM,CAAC;AAC3B,OAAK,cAAc,UAAU,OAAQ,CAAC;AACtC,OAAK,cAAc,UAAU,OAAQ,CAAC;AACtC,SAAO,OAAO,OAAO,CAAC,MAAM,OAAO,IAAI,CAAC;AAC1C;AAaO,SAAS,eACd,KACA,SACA,WACQ;AACR,QAAM,OAAO,OAAO,MAAM,CAAC;AAC3B,OAAK,WAAW,OAAO,WAAW,CAAC;AACnC,OAAK,cAAc,QAAQ,GAAG,CAAC;AAC/B,OAAK,cAAc,YAAY,GAAG,CAAC;AACnC,SAAO,OAAO,OAAO,CAAC,MAAM,SAAS,CAAC;AACxC;;;AJ9RA,IAAM,EAAE,SAAS,IAAI;AAIrB,IAAM,WAAW;AACjB,IAAM,WAAW;AAeV,IAAM,eAAN,MAAmB;AAAA;AAAA;AAAA,EAGhB;AAAA,EACA;AAAA,EACA,MAAM;AAAA,EACN,mBAAmB;AAAA,EACnB,QAAQ,oBAAI,IAAY;AAAA;AAAA,EAEhC,YAAY,aAAqB,aAAqB;AACpD,UAAM,OAAO,KAAK,IAAI,KAAK,IAAI,GAAG,WAAW,GAAG,QAAQ;AACxD,UAAM,OAAO,KAAK,IAAI,KAAK,IAAI,GAAG,WAAW,GAAG,QAAQ;AACxD,SAAK,OAAO,IAAI,SAAS,EAAE,MAAM,MAAM,kBAAkB,KAAK,CAAC;AAK/D,SAAK,aAAa,IAAI,EAAe;AACrC,SAAK,KAAK,UAAU,KAAK,UAAiB;AAG1C,SAAK,KAAK,cAAc,MAAM,KAAK,cAAc,CAAC;AAAA,EACpD;AAAA;AAAA,EAGA,IAAI,OAAe;AACjB,WAAO,KAAK,KAAK;AAAA,EACnB;AAAA,EAEA,IAAI,OAAe;AACjB,WAAO,KAAK,KAAK;AAAA,EACnB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EASA,MAAM,OAAwB,UAA6B;AACzD,SAAK,KAAK;AAAA,MACR,OAAO,UAAU,WAAW,QAAQ,MAAM,SAAS,MAAM;AAAA,MACzD;AAAA,IACF;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAWA,OAAO,MAAc,MAA4B;AAC/C,UAAM,IAAI,KAAK,IAAI,KAAK,IAAI,GAAG,IAAI,GAAG,QAAQ;AAC9C,UAAM,IAAI,KAAK,IAAI,KAAK,IAAI,GAAG,IAAI,GAAG,QAAQ;AAC9C,UAAM,SAAS,MAAM,QAAQ,MAAM;AACnC,SAAK,KAAK,OAAO,GAAG,CAAC;AACrB,SAAK,cAAc;AACnB,WAAO,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO;AAAA,EACpC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQA,WAAmB;AACjB,SAAK;AACL,SAAK,mBAAmB,KAAK;AAC7B,SAAK,MAAM,MAAM;AACjB,UAAM,QAAQ,KAAK,eAAe;AAClC,UAAM,MAAM,KAAK,KAAK,OAAO;AAC7B,WAAO;AAAA,MACL,KAAK;AAAA,MACL,KAAK,KAAK;AAAA,MACV,KAAK,KAAK;AAAA,MACV;AAAA,MACA,IAAI;AAAA,MACJ,IAAI;AAAA,IACN;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,gBAA+B;AAC7B,QAAI,KAAK,MAAM,SAAS,EAAG,QAAO;AAClC,SAAK;AACL,UAAM,MAAM,KAAK,kBAAkB;AACnC,UAAM,UAAU,KAAK;AACrB,SAAK,mBAAmB,KAAK;AAC7B,SAAK,MAAM,MAAM;AACjB,WAAO,eAAe,KAAK,KAAK,SAAS,GAAG;AAAA,EAC9C;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAYA,gBAAwB;AACtB,QAAI;AAIF,aAAO,KAAK,WAAW,UAAU,EAAE,YAAY,EAAE,CAAC;AAAA,IACpD,QAAQ;AACN,aAAO;AAAA,IACT;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAYA,eAKE;AACA,UAAM,MAAM,KAAK,KAAK,OAAO;AAC7B,QAAI,gBAAgB;AACpB,QAAI,SAAS;AACb,aAAS,IAAI,GAAG,IAAI,KAAK,KAAK,MAAM,KAAK;AACvC,YAAM,OAAO,IAAI,QAAQ,CAAC;AAC1B,UAAI,CAAC,KAAM;AACX,YAAM,UAAU,KAAK,kBAAkB,IAAI,EAAE,KAAK;AAClD,UAAI,QAAQ,WAAW,EAAG;AAC1B;AACA,iBAAW,MAAM,QAAS,KAAI,OAAO,IAAK;AAAA,IAC5C;AACA,WAAO,EAAE,SAAS,IAAI,MAAM,eAAe,MAAM,KAAK,KAAK,MAAM,OAAO;AAAA,EAC1E;AAAA;AAAA,EAGA,UAAgB;AACd,SAAK,KAAK,QAAQ;AAClB,SAAK,MAAM,MAAM;AAAA,EACnB;AAAA;AAAA;AAAA,EAKQ,gBAAsB;AAC5B,aAAS,IAAI,GAAG,IAAI,KAAK,KAAK,MAAM,KAAK;AACvC,eAAS,IAAI,GAAG,IAAI,KAAK,KAAK,MAAM,KAAK;AACvC,aAAK,MAAM,IAAI,GAAG,CAAC,IAAI,CAAC,EAAE;AAAA,MAC5B;AAAA,IACF;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAWQ,iBAAyB;AAC/B,UAAM,MAAM,KAAK,KAAK,OAAO;AAC7B,UAAM,MAAgB,CAAC;AACvB,aAAS,IAAI,GAAG,IAAI,KAAK,KAAK,MAAM,KAAK;AACvC,YAAM,OAAO,IAAI,QAAQ,CAAC;AAC1B,UAAI,CAAC,KAAM;AACX,eAAS,IAAI,GAAG,IAAI,KAAK,KAAK,MAAM,KAAK;AACvC,cAAM,OAAO,KAAK,QAAQ,CAAC;AAC3B,YAAI,CAAC,KAAM;AACX,cAAM,KAAK,KAAK,SAAS,KAAK;AAC9B,cAAM,UAAU,OAAO,KAAK,IAAI,MAAM;AACtC,YAAI,KAAK,GAAG,OAAO;AACnB,YAAI,KAAK,KAAK,eAAe,KAAK,WAAW,GAAG,CAAC,CAAC;AAClD,YAAI,KAAK,KAAK,eAAe,KAAK,WAAW,GAAG,CAAC,CAAC;AAClD,YAAI,KAAK,KAAK,YAAY,IAAI,CAAC;AAAA,MACjC;AAAA,IACF;AACA,WAAO,OAAO,KAAK,GAAG;AAAA,EACxB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOQ,oBAA4B;AAClC,UAAM,MAAM,KAAK,KAAK,OAAO;AAC7B,UAAM,UAAU,KAAK,MAAM;AAC3B,UAAM,OAAO,OAAO,MAAM,CAAC;AAC3B,SAAK,cAAc,UAAU,OAAQ,CAAC;AACtC,UAAM,UAAoB,CAAC;AAC3B,eAAW,OAAO,KAAK,OAAO;AAC5B,YAAM,CAAC,IAAI,EAAE,IAAI,IAAI,MAAM,GAAG;AAC9B,YAAM,IAAI,SAAS,IAAI,EAAE;AACzB,YAAM,IAAI,SAAS,IAAI,EAAE;AACzB,YAAM,OAAO,IAAI,QAAQ,CAAC;AAC1B,UAAI,CAAC,KAAM;AACX,YAAM,OAAO,KAAK,QAAQ,CAAC;AAC3B,UAAI,CAAC,KAAM;AACX,YAAM,KAAK,KAAK,SAAS,KAAK;AAC9B,YAAM,UAAU,OAAO,KAAK,IAAI,MAAM;AACtC,YAAM,QAAQ,OAAO,MAAM,CAAC;AAC5B,YAAM,cAAc,IAAI,OAAQ,CAAC;AACjC,YAAM,cAAc,IAAI,OAAQ,CAAC;AACjC,cAAQ,KAAK,GAAG,KAAK;AACrB,cAAQ,KAAK,GAAG,OAAO;AACvB,cAAQ,KAAK,KAAK,eAAe,KAAK,WAAW,GAAG,CAAC,CAAC;AACtD,cAAQ,KAAK,KAAK,eAAe,KAAK,WAAW,GAAG,CAAC,CAAC;AACtD,cAAQ,KAAK,KAAK,YAAY,IAAI,CAAC;AAAA,IACrC;AACA,WAAO,OAAO,OAAO,CAAC,MAAM,OAAO,KAAK,OAAO,CAAC,CAAC;AAAA,EACnD;AAAA;AAAA;AAAA;AAAA;AAAA,EAMQ,eAAe,OAAe,UAA0B;AAC9D,QAAI,QAAQ,EAAG,QAAO;AACtB,WAAO,QAAQ;AAAA,EACjB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQQ,YAAY,MAAqD;AACvE,QAAI,IAAI;AACR,QAAI;AACF,UAAI,KAAK,OAAO,EAAG,MAAK;AACxB,UAAI,KAAK,SAAS,EAAG,MAAK;AAC1B,UAAI,KAAK,YAAY,EAAG,MAAK;AAC7B,UAAI,KAAK,UAAU,EAAG,MAAK;AAAA,IAC7B,QAAQ;AAAA,IAER;AACA,WAAO;AAAA,EACT;AACF;;;AK5QA,IAAMwD,OAAM,aAAa,KAAK;AA2CvB,IAAM,sBAAN,MAA0B;AAAA,EACd;AAAA,EACA,WAAW,oBAAI,IAAyB;AAAA,EACxC,kBAAkB,oBAAI,IAAoB;AAAA,EAC1C,aAAa,oBAAI,IAAuB;AAAA,EACjD,eAAsD;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAM7C,YAAY,oBAAI,IAA0B;AAAA;AAAA,EAG1C;AAAA,EAEjB,YAAY,MAAwB,OAAoD,CAAC,GAAG;AAC1F,SAAK,OAAO;AACZ,SAAK,QAAQ,KAAK,OAAO,KAAK;AAC9B,UAAM,SAAS,KAAK,cAAc;AAClC,SAAK,eAAe,YAAY,MAAM,KAAK,SAAS,GAAG,MAAM;AAAA,EAC/D;AAAA;AAAA,EAGA,UAAgB;AACd,QAAI,KAAK,cAAc;AACrB,oBAAc,KAAK,YAAY;AAC/B,WAAK,eAAe;AAAA,IACtB;AACA,SAAK,SAAS,MAAM;AACpB,SAAK,gBAAgB,MAAM;AAC3B,SAAK,WAAW,MAAM;AACtB,eAAW,MAAM,KAAK,UAAU,OAAO,EAAG,IAAG,QAAQ;AACrD,SAAK,UAAU,MAAM;AAAA,EACvB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAYA,UAAU,WAAmB,cAAc,IAAI,cAAc,IAAU;AACrE,UAAM,QAAQ,KAAK,KAAK,gBAAgB;AACxC,QAAI,SAAS,eAAe;AAC1B,YAAM,SAAS,KAAK,cAAc,SAAS;AAC3C,UAAI,QAAQ;AACV,QAAAA,KAAI,KAAK,yBAAyB,KAAK,QAAQ,aAAa,qBAAgB,MAAM,EAAE;AAEpF,aAAK,gBAAgB,QAAQ,QAAQ,EAAE,QAAQ,OAAO,gBAAgB,KAAK,CAAC;AAC5E,aAAK,KAAK,aAAa,MAAM;AAAA,MAC/B;AAAA,IACF;AACA,SAAK,oBAAoB,SAAS;AAClC,UAAM,MAAM,KAAK,MAAM;AACvB,SAAK,SAAS,IAAI,WAAW;AAAA,MAC3B,YAAY;AAAA,MACZ,cAAc;AAAA,MACd,aAAa;AAAA,MACb,iBAAiB,MAAM;AAAA,IACzB,CAAC;AAGD,QAAI,CAAC,KAAK,UAAU,IAAI,SAAS,GAAG;AAClC,WAAK,UAAU,IAAI,WAAW,IAAI,aAAa,aAAa,WAAW,CAAC;AAAA,IAC1E;AAAA,EACF;AAAA;AAAA,EAGA,eAAe,WAAyB;AACtC,UAAM,OAAO,KAAK,SAAS,IAAI,SAAS;AACxC,QAAI,CAAC,KAAM;AACX,UAAM,MAAM,KAAK,MAAM;AACvB,QAAI,MAAM,KAAK,eAAe,IAAM;AACpC,SAAK,cAAc,MAAM,GAAG;AAC5B,SAAK,gBAAgB,WAAW,KAAK,cAAc,IAAI,CAAC;AAAA,EAC1D;AAAA;AAAA,EAGA,YAAY,WAAyB;AACnC,UAAM,OAAO,KAAK,SAAS,IAAI,SAAS;AACxC,QAAI,CAAC,KAAM;AACX,SAAK,cAAc,MAAM,KAAK,MAAM,CAAC;AACrC,SAAK,gBAAgB,WAAW,KAAK,cAAc,IAAI,CAAC;AAAA,EAC1D;AAAA;AAAA,EAGA,SAAS,WAAyB;AAChC,QAAI,OAAO,KAAK,SAAS,IAAI,SAAS;AACtC,UAAM,MAAM,KAAK,MAAM;AACvB,QAAI,CAAC,MAAM;AAGT,aAAO;AAAA,QACL,YAAY;AAAA,QACZ,cAAc;AAAA,QACd,aAAa;AAAA,QACb,iBAAiB,MAAM;AAAA,MACzB;AACA,WAAK,SAAS,IAAI,WAAW,IAAI;AAAA,IACnC,OAAO;AACL,WAAK,aAAa;AAClB,WAAK,eAAe;AACpB,WAAK,kBAAkB,MAAM;AAAA,IAC/B;AACA,SAAK,gBAAgB,WAAW,KAAK,cAAc,IAAI,GAAG,EAAE,gBAAgB,KAAK,CAAC;AAAA,EACpF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOQ,cAAc,MAAmB,KAAmB;AAC1D,SAAK,eAAe;AACpB,SAAK,kBAAkB,MAAM;AAAA,EAC/B;AAAA;AAAA,EAGA,SAAS,WAAyB;AAChC,UAAM,OAAO,KAAK,SAAS,IAAI,SAAS;AACxC,QAAI,CAAC,KAAM;AACX,SAAK,gBAAgB,WAAW,KAAK,cAAc,IAAI,GAAG,EAAE,gBAAgB,KAAK,CAAC;AAAA,EACpF;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,aAAa,WAAyB;AACpC,UAAM,MAAM,KAAK,MAAM;AACvB,UAAM,OAAO,KAAK,SAAS,IAAI,SAAS;AACxC,UAAM,UAAuB;AAAA,MAC3B,YAAY;AAAA,MACZ,cAAc;AAAA,MACd,cAAc,MAAM,eAAe,KAAK;AAAA,MACxC,iBAAiB,MAAM;AAAA,IACzB;AACA,SAAK,gBAAgB,WAAW,QAAQ,EAAE,QAAQ,WAAW,gBAAgB,KAAK,CAAC;AACnF,SAAK,SAAS,IAAI,WAAW,OAAO;AAAA,EACtC;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,aAAa,WAAmB,SAAqB,WAAiB;AACpE,QAAI,KAAK,SAAS,IAAI,SAAS,KAAK,KAAK,WAAW,IAAI,SAAS,GAAG;AAClE,WAAK,gBAAgB,WAAW,QAAQ,EAAE,QAAQ,gBAAgB,KAAK,CAAC;AAAA,IAC1E;AACA,SAAK,oBAAoB,SAAS;AAAA,EACpC;AAAA;AAAA,EAGA,UAAU,WAAyB;AACjC,SAAK,aAAa,WAAW,SAAS;AACtC,SAAK,gBAAgB,SAAS;AAAA,EAChC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAaA,UAAU,WAAmB,OAA8B;AACzD,UAAM,KAAK,KAAK,UAAU,IAAI,SAAS;AACvC,QAAI,GAAI,IAAG,MAAM,KAAK;AAAA,EACxB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQA,iBAAiB,WAAmB,MAAqC;AACvE,UAAM,KAAK,KAAK,UAAU,IAAI,SAAS;AACvC,QAAI,GAAI,MAAK,GAAG,SAAS,CAAC;AAAA,EAC5B;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EASA,kBAAkB,WAA2B;AAC3C,UAAM,KAAK,KAAK,UAAU,IAAI,SAAS;AACvC,WAAO,KAAK,GAAG,cAAc,IAAI;AAAA,EACnC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,iBAAiB,WAKR;AACP,UAAM,KAAK,KAAK,UAAU,IAAI,SAAS;AACvC,WAAO,KAAK,GAAG,aAAa,IAAI;AAAA,EAClC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAWA,SAAS,WAAmB,MAAc,MAAmC;AAC3E,UAAM,KAAK,KAAK,UAAU,IAAI,SAAS;AACvC,QAAI,CAAC,GAAI,QAAO;AAChB,WAAO,GAAG,OAAO,MAAM,IAAI;AAAA,EAC7B;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,cAAc,WAAmB,MAAqC;AACpE,UAAM,KAAK,KAAK,UAAU,IAAI,SAAS;AACvC,QAAI,GAAI,MAAK,GAAG,SAAS,CAAC;AAAA,EAC5B;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAUA,UAAU,WAAmB,WAA0C;AACrE,UAAM,KAAK,KAAK,UAAU,IAAI,SAAS;AACvC,QAAI,CAAC,GAAI;AACT,UAAM,OAAO,GAAG,cAAc;AAC9B,QAAI,KAAM,WAAU,IAAI;AAAA,EAC1B;AAAA;AAAA;AAAA;AAAA;AAAA,EAMQ,gBAAgB,WAAyB;AAC/C,UAAM,KAAK,KAAK,UAAU,IAAI,SAAS;AACvC,QAAI,IAAI;AACN,SAAG,QAAQ;AACX,WAAK,UAAU,OAAO,SAAS;AAAA,IACjC;AAAA,EACF;AAAA;AAAA,EAGA,gBAAgB,WAAyB;AACvC,SAAK,oBAAoB,SAAS;AAAA,EACpC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EASQ,oBAAoB,WAAyB;AACnD,SAAK,SAAS,OAAO,SAAS;AAC9B,SAAK,gBAAgB,OAAO,SAAS;AACrC,SAAK,WAAW,OAAO,SAAS;AAChC,SAAK,gBAAgB,SAAS;AAAA,EAChC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EASA,gBAAgB,WAAuC;AACrD,WAAO,KAAK,SAAS,IAAI,SAAS,GAAG;AAAA,EACvC;AAAA;AAAA,EAGQ,cAAc,MAA8B;AAClD,UAAM,MAAM,KAAK,MAAM;AACvB,WAAO,MAAM,KAAK,eAAe,4BAA4B,YAAY;AAAA,EAC3E;AAAA;AAAA;AAAA,EAIQ,cAAc,kBAAyC;AAC7D,QAAI,SAAwB;AAC5B,QAAI,SAAS,OAAO;AACpB,eAAW,CAAC,IAAI,IAAI,KAAK,KAAK,UAAU;AACtC,UAAI,OAAO,iBAAkB;AAC7B,YAAM,SAAS,KAAK,IAAI,KAAK,YAAY,KAAK,YAAY;AAC1D,UAAI,SAAS,QAAQ;AACnB,iBAAS;AACT,iBAAS;AAAA,MACX;AAAA,IACF;AACA,WAAO;AAAA,EACT;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQQ,gBACN,WACA,QACA,OAA0D,CAAC,GACrD;AACN,UAAM,MAAM,KAAK,MAAM;AACvB,UAAM,SAAS,KAAK,gBAAgB,IAAI,SAAS,KAAK;AACtD,UAAM,WAAW,KAAK,WAAW,IAAI,SAAS;AAC9C,UAAM,aAAa,aAAa;AAChC,QAAI,CAAC,KAAK,kBAAkB,cAAc,MAAM,SAAS,kCAAkC;AACzF;AAAA,IACF;AACA,SAAK,gBAAgB,IAAI,WAAW,GAAG;AACvC,SAAK,WAAW,IAAI,WAAW,MAAM;AACrC,QAAI;AACF,YAAM,MACJ,WAAW,UAAU,KAAK,SACtB,EAAE,MAAM,cAAc,WAAW,QAAQ,QAAQ,KAAK,OAAO,IAC7D,EAAE,MAAM,cAAc,WAAW,OAAO;AAC9C,WAAK,KAAK,UAAU,GAAG;AAAA,IACzB,SAAS,KAAK;AACZ,MAAAA,KAAI,KAAK,oCAAoC,EAAE,WAAW,QAAQ,KAAM,IAAc,QAAQ,CAAC;AAAA,IACjG;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA,EAMQ,WAAiB;AACvB,UAAM,MAAM,KAAK,MAAM;AAEvB,UAAM,WAAW,CAAC,GAAG,KAAK,SAAS,QAAQ,CAAC;AAC5C,eAAW,CAAC,IAAI,IAAI,KAAK,UAAU;AACjC,UAAI,KAAK,kBAAkB,IAAK;AAEhC,UAAI,KAAK,KAAK,aAAa,EAAE,EAAG;AAChC,YAAM,SAAS,KAAK,IAAI,KAAK,YAAY,KAAK,YAAY;AAC1D,UAAI,MAAM,SAAS,qBAAqB;AACtC,QAAAA,KAAI,KAAK,sCAAsC,EAAE,UAAU,MAAM,UAAU,GAAI,GAAG;AAClF,aAAK,gBAAgB,IAAI,QAAQ,EAAE,QAAQ,QAAQ,gBAAgB,KAAK,CAAC;AACzE,aAAK,KAAK,aAAa,EAAE;AAAA,MAC3B,OAAO;AAEL,aAAK,kBAAkB,MAAM;AAC7B,aAAK,gBAAgB,IAAI,KAAK,cAAc,IAAI,CAAC;AAAA,MACnD;AAAA,IACF;AAAA,EACF;AAAA;AAAA;AAAA,EAKA,gBAAgB,WAA4C;AAC1D,WAAO,KAAK,SAAS,IAAI,SAAS;AAAA,EACpC;AAAA;AAAA,EAGA,kBAAkB,WAA0C;AAC1D,WAAO,KAAK,WAAW,IAAI,SAAS;AAAA,EACtC;AACF;;;ACvcA,IAAMC,OAAM,aAAa,WAAW;AAwG7B,IAAM,oBAAN,MAAM,mBAAkB;AAAA;AAAA,EAE7B,OAAwB,kBAAkB;AAAA;AAAA,EAE1C,OAAwB,sBAAsB;AAAA;AAAA,EAE9C,OAAwB,0BAA0B;AAAA;AAAA;AAAA;AAAA;AAAA,EAKlD,OAAwB,uBAAuB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAa/C,OAAwB,sBAAsB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAc9C,OAAwB,0BAA0B;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAalD,OAAwB,+BAA+B;AAAA,EAE/C,SAAS,oBAAI,IAA4B;AAAA,EACzC,cAAc,oBAAI,IAAqB;AAAA,EACvC,mBAAmB,oBAAI,IAQ7B;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAMM,wBAAwB,oBAAI,IAAoB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAehD,mBAAmB,oBAAI,IAAqB;AAAA,EAEnC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOjB,YAAY,MAA6B;AACvC,SAAK,OAAO;AACZ,SAAK,YAAY,KAAK,aAAa,CAAC,IAAI,OAAO,WAAW,IAAI,EAAE;AAChE,SAAK,cACH,KAAK,eAAe,OAAK,aAAa,CAAkC;AAC1E,SAAK,kBACH,KAAK,kBAAkB,QAAQ,IAAI,sBAAsB;AAAA,EAC7D;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EASA,aACE,WACA,QACS;AACT,QAAI,WAAW,kBAAmB,QAAO;AACzC,QAAI,KAAK,iBAAiB,IAAI,SAAS,EAAG,QAAO;AACjD,UAAM,QAAQ,KAAK,OAAO,IAAI,SAAS,KAAK,EAAE,QAAQ,UAAmB;AACzE,QAAI,MAAM,WAAW,UAAW,QAAO,WAAW;AAClD,WAAO,WAAW;AAAA,EACpB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,kBAAkB,WAAmB,MAAc,MAAoB;AACrE,UAAM,OAAO,KAAK,OAAO,IAAI,SAAS,KAAK,EAAE,QAAQ,UAAmB;AACxE,QAAI,KAAK,WAAW,WAAW;AAE7B,WAAK,OAAO,IAAI,WAAW;AAAA,QACzB,QAAQ;AAAA,QACR,iBAAiB,EAAE,MAAM,KAAK;AAAA,MAChC,CAAC;AAAA,IACH,OAAO;AAGL,WAAK,OAAO,IAAI,WAAW,EAAE,GAAG,MAAM,MAAM,KAAK,CAAC;AAAA,IACpD;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAYA,eACE,WACA,UACA,UACA,UACM;AAIN,QAAI,KAAK,iBAAiB;AACxB,YAAM,gBAAgB,KAAK,sBAAsB,IAAI,SAAS,KAAK;AACnE,UAAI,KAAK,IAAI,IAAI,eAAe;AAC9B,aAAK,qBAAqB,WAAW,YAAY;AAAA,UAC/C;AAAA,UACA,QAAQ;AAAA,QACV,CAAC;AACD;AAAA,MACF;AAAA,IACF;AAEA,UAAM,QAAQ,KAAK,OAAO,IAAI,SAAS,KAAK,EAAE,QAAQ,UAAmB;AAGzE,UAAM,QAAQ,KAAK,YAAY,IAAI,SAAS;AAC5C,QACE,UAAU,UACV,MAAM,WAAW,YACjB,MAAM,mBAAmB,UACzB;AACA,WAAK,YAAY,KAAK;AACtB,WAAK,YAAY,OAAO,SAAS;AACjC,WAAK,KAAK;AAAA,QACR,EAAE,MAAM,4BAA4B,WAAW,OAAO,cAAc;AAAA,QACpE,EAAE,YAAY,KAAK;AAAA,MACrB;AACA,UAAI,KAAK,iBAAiB;AAGxB,aAAK,OAAO,IAAI,WAAW;AAAA,UACzB,GAAG;AAAA,UACH,MAAM,YAAY,MAAM;AAAA,UACxB,MAAM,YAAY,MAAM;AAAA,UACxB,gBAAgB,KAAK,IAAI;AAAA,QAC3B,CAAC;AACD,aAAK,qBAAqB,WAAW,cAAc;AAAA,UACjD,MAAM,YAAY,MAAM;AAAA,UACxB,MAAM,YAAY,MAAM;AAAA,QAC1B,CAAC;AAAA,MACH;AACA;AAAA,IACF;AAGA,QAAI,KAAK,iBAAiB;AAKxB,YAAM,eAAe,KAAK,iBAAiB,IAAI,SAAS;AACxD,UAAI,cAAc;AAChB,aAAK,YAAY,aAAa,KAAK;AACnC,aAAK,iBAAiB,OAAO,SAAS;AAAA,MACxC;AAOA,UAAI,KAAK,YAAY,IAAI,SAAS,GAAG;AACnC,QAAAA,KAAI;AAAA,UACF,4DAA4D,SAAS,cACtD,QAAQ,oBAAoB,MAAM,WAAW,WAAW,MAAM,iBAAiB,KAAK,+CACxD,mBAAkB,eAAe;AAAA,QAC9E;AAAA,MACF;AAMA,MAAAA,KAAI;AAAA,QACF,gEAAgE,SAAS,WAC7D,QAAQ,eAAe,MAAM,MAAM;AAAA,MACjD;AAEA,YAAM,WACJ,aAAa,MAAM,WAAW,WAAW,MAAM,OAAO;AACxD,YAAM,WACJ,aAAa,MAAM,WAAW,WAAW,MAAM,OAAO;AAExD,YAAM,OAAuB;AAAA,QAC3B,QAAQ;AAAA,QACR,gBAAgB;AAAA,QAChB,MAAM;AAAA,QACN,MAAM;AAAA,QACN,YAAY,KAAK,IAAI;AAAA,QACrB,gBAAgB,KAAK,IAAI;AAAA,MAC3B;AACA,WAAK,OAAO,IAAI,WAAW,IAAI;AAC/B,WAAK,qBAAqB,WAAW,cAAc;AAAA,QACjD,MAAM;AAAA,QACN,MAAM;AAAA,MACR,CAAC;AACD,WAAK;AAAA,QACH;AAAA,QACA;AAAA,QACA,YAAY;AAAA,QACZ,YAAY;AAAA,QACZ;AAAA,MACF;AAAA,IACF,OAAO;AAEL,YAAM,eACJ,MAAM,WAAW,WAAW,MAAM,OAAO,MAAM,iBAAiB;AAClE,YAAM,eACJ,MAAM,WAAW,WAAW,MAAM,OAAO,MAAM,iBAAiB;AAClE,WAAK;AAAA,QACH;AAAA,QACA;AAAA,QACA,YAAY,gBAAgB;AAAA,QAC5B,YAAY,gBAAgB;AAAA,MAC9B;AAAA,IACF;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQA,eAAe,WAAmB,UAAwB;AACxD,UAAM,QAAQ,KAAK,OAAO,IAAI,SAAS;AACvC,QACE,CAAC,SACD,MAAM,WAAW,YACjB,MAAM,mBAAmB,UACzB;AACA;AAAA,IACF;AAEA,SAAK,KAAK;AAAA,MACR,EAAE,MAAM,4BAA4B,WAAW,OAAO,eAAe;AAAA,MACrE,EAAE,YAAY,KAAK;AAAA,IACrB;AAGA,UAAM,aAAa,KAAK,IAAI,IAAI,mBAAkB;AAClD,SAAK,qBAAqB,WAAW,aAAa,EAAE,WAAW,CAAC;AAChE,UAAM,QAAQ,KAAK;AAAA,MACjB,MAAM,KAAK,YAAY,SAAS;AAAA,MAChC,mBAAkB;AAAA,IACpB;AACA,SAAK,YAAY,IAAI,WAAW,KAAK;AAAA,EACvC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQA,oBAAoB,WAAmB,QAAuB;AAC5D,UAAM,UAAU,KAAK,iBAAiB,IAAI,SAAS;AACnD,QAAI,CAAC,QAAS;AACd,SAAK,YAAY,QAAQ,KAAK;AAC9B,SAAK,iBAAiB,OAAO,SAAS;AAMtC,UAAM,QAAQ,KAAK,YAAY,IAAI,SAAS;AAC5C,QAAI,UAAU,QAAW;AACvB,WAAK,YAAY,KAAK;AACtB,WAAK,YAAY,OAAO,SAAS;AAAA,IACnC;AAEA,QAAI,QAAQ;AAEV,YAAM,MAAM,KAAK,IAAI;AACrB,YAAM,OAAuB;AAAA,QAC3B,QAAQ;AAAA,QACR,gBAAgB,QAAQ;AAAA,QACxB,MAAM,QAAQ;AAAA,QACd,MAAM,QAAQ;AAAA,QACd,YAAY;AAAA,QACZ,gBAAgB;AAAA,MAClB;AACA,WAAK,OAAO,IAAI,WAAW,IAAI;AAC/B,WAAK;AAAA,QACH;AAAA,QACA;AAAA,QACA,KAAK,QAAQ;AAAA,QACb,KAAK,QAAQ;AAAA,QACb,KAAK;AAAA,MACP;AAEA,WAAK,qBAAqB,WAAW,cAAc;AAAA,QACjD,MAAM,KAAK;AAAA,QACX,MAAM,KAAK;AAAA,MACb,CAAC;AAAA,IACH,OAAO;AAEL,YAAM,MAAM,KAAK,OAAO,IAAI,SAAS,KAAK,EAAE,QAAQ,UAAmB;AAGvE,YAAM,eACJ,IAAI,WAAW,WAAW,IAAI,OAAO,IAAI,iBAAiB;AAC5D,YAAM,eACJ,IAAI,WAAW,WAAW,IAAI,OAAO,IAAI,iBAAiB;AAC5D,WAAK,OAAO,IAAI,WAAW;AAAA,QACzB,QAAQ;AAAA,QACR,iBACE,iBAAiB,UAAa,iBAAiB,SAC3C,EAAE,MAAM,cAAc,MAAM,aAAa,IACzC;AAAA,MACR,CAAC;AACD,WAAK;AAAA,QACH;AAAA,QACA;AAAA,QACA,gBAAgB;AAAA,QAChB,gBAAgB;AAAA,QAChB;AAAA,MACF;AAAA,IACF;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAUA,wBAAwB,WAAmB,UAAwB;AACjE,UAAM,QAAQ,KAAK,OAAO,IAAI,SAAS,KAAK,EAAE,QAAQ,UAAmB;AACzE,QAAI,MAAM,WAAW,YAAY,MAAM,mBAAmB,SAAU;AACpE,UAAM,eACJ,MAAM,WAAW,WAAW,MAAM,OAAO,MAAM,iBAAiB;AAClE,UAAM,eACJ,MAAM,WAAW,WAAW,MAAM,OAAO,MAAM,iBAAiB;AAClE,SAAK;AAAA,MACH;AAAA,MACA;AAAA,MACA,gBAAgB;AAAA,MAChB,gBAAgB;AAAA,IAClB;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,SAAS,WAA6C;AACpD,WAAO,KAAK,OAAO,IAAI,SAAS,KAAK,EAAE,QAAQ,UAAmB;AAAA,EACpE;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,cAAc,WAAyB;AACrC,UAAM,QAAQ,KAAK,YAAY,IAAI,SAAS;AAC5C,QAAI,UAAU,QAAW;AACvB,WAAK,YAAY,KAAK;AACtB,WAAK,YAAY,OAAO,SAAS;AAAA,IACnC;AACA,UAAM,UAAU,KAAK,iBAAiB,IAAI,SAAS;AACnD,QAAI,YAAY,QAAW;AACzB,WAAK,YAAY,QAAQ,KAAK;AAC9B,WAAK,iBAAiB,OAAO,SAAS;AAAA,IACxC;AACA,SAAK,OAAO,OAAO,SAAS;AAE5B,SAAK,sBAAsB,OAAO,SAAS;AAAA,EAC7C;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,UAAgB;AACd,eAAW,UAAU,KAAK,YAAY,OAAO,GAAG;AAC9C,WAAK,YAAY,MAAM;AAAA,IACzB;AACA,SAAK,YAAY,MAAM;AACvB,eAAW,SAAS,KAAK,iBAAiB,OAAO,GAAG;AAClD,WAAK,YAAY,MAAM,KAAK;AAAA,IAC9B;AACA,SAAK,iBAAiB,MAAM;AAC5B,SAAK,OAAO,MAAM;AAElB,SAAK,sBAAsB,MAAM;AAEjC,eAAW,UAAU,KAAK,iBAAiB,OAAO,GAAG;AACnD,WAAK,YAAY,MAAM;AAAA,IACzB;AACA,SAAK,iBAAiB,MAAM;AAAA,EAC9B;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAcA,mBAAmB,WAAyB;AAC1C,UAAM,QAAQ,KAAK,YAAY,IAAI,SAAS;AAC5C,QAAI,UAAU,QAAW;AACvB,WAAK,YAAY,KAAK;AACtB,WAAK,YAAY,OAAO,SAAS;AAAA,IACnC;AACA,UAAM,UAAU,KAAK,iBAAiB,IAAI,SAAS;AACnD,QAAI,YAAY,QAAW;AACzB,WAAK,YAAY,QAAQ,KAAK;AAC9B,WAAK,iBAAiB,OAAO,SAAS;AAAA,IACxC;AAKA,UAAM,OAAO,KAAK,OAAO,IAAI,SAAS;AACtC,UAAM,kBACJ,MAAM,WAAW,YAAY,KAAK,kBAAkB;AACtD,SAAK,OAAO,IAAI,WAAW;AAAA,MACzB,QAAQ;AAAA,MACR;AAAA,IACF,CAAC;AACD,SAAK,sBAAsB;AAAA,MACzB;AAAA,MACA,KAAK,IAAI,IAAI,mBAAkB;AAAA,IACjC;AACA,SAAK,qBAAqB,WAAW,aAAa;AAElD,SAAK;AAAA,MACH;AAAA,MACA;AAAA,MACA,iBAAiB,QAAQ;AAAA,MACzB,iBAAiB,QAAQ;AAAA,MACzB;AAAA,IACF;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAWA,oBAAoB,WAAmB,UAAwB;AAC7D,UAAM,IAAI,KAAK,OAAO,IAAI,SAAS;AACnC,QAAI,CAAC,KAAK,EAAE,WAAW,YAAY,EAAE,mBAAmB,UAAU;AAahE,YAAM,aACJ,KAAK,IAAI,KAAK,KAAK,sBAAsB,IAAI,SAAS,KAAK;AAC7D,UAAI,KAAK,mBAAmB,CAAC,YAAY;AAIvC,cAAM,WACJ,MAAM,SACF,SACA,EAAE,WAAW,WACX,EAAE,OACF,EAAE,iBAAiB;AAC3B,cAAM,WACJ,MAAM,SACF,SACA,EAAE,WAAW,WACX,EAAE,OACF,EAAE,iBAAiB;AAC3B,QAAAA,KAAI;AAAA,UACF,mEAAmE,SAAS,eAC5D,QAAQ,eAAe,GAAG,UAAU,MAAM,eAC1C,KAAK,EAAE,WAAW,WAAW,EAAE,iBAAiB,KAAK;AAAA,QACvE;AACA,aAAK,eAAe,WAAW,UAAU,UAAU,QAAQ;AAC3D;AAAA,MACF;AAKA,MAAAA,KAAI;AAAA,QACF,6DAA6D,SAAS,eACtD,QAAQ,WAAW,GAAG,UAAU,MAAM,gBACrC,KAAK,EAAE,WAAW,WAAW,EAAE,iBAAiB,KAAK;AAAA,MACxE;AACA;AAAA,IACF;AAGA,IAAAA,KAAI;AAAA,MACF,+BAA+B,SAAS,WAAW,QAAQ,MAAM,EAAE,cAAc;AAAA,IACnF;AACA,SAAK,OAAO,IAAI,WAAW,EAAE,GAAG,GAAG,gBAAgB,KAAK,IAAI,EAAE,CAAC;AAAA,EACjE;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAUA,iBAAiB,WAAyB;AACxC,UAAM,IAAI,KAAK,OAAO,IAAI,SAAS;AACnC,QAAI,CAAC,KAAK,EAAE,WAAW,SAAU;AACjC,SAAK,qBAAqB,WAAW,SAAS,EAAE,QAAQ,cAAc,CAAC;AAAA,EACzE;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,qBAAqB,WAA4B;AAC/C,WAAO,KAAK,IAAI,KAAK,KAAK,sBAAsB,IAAI,SAAS,KAAK;AAAA,EACpE;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EASA,yBAAyB,WAAuC;AAC9D,WAAO,KAAK,sBAAsB,IAAI,SAAS;AAAA,EACjD;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,mBAAmB,WAAmB,MAAe,MAAqB;AACxE,SAAK,qBAAqB,WAAW,aAAa,EAAE,MAAM,KAAK,CAAC;AAAA,EAClE;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAWQ,gBACN,WACA,UACA,MACA,MACM;AACN,UAAM,OAAO,KAAK,iBAAiB,IAAI,SAAS;AAChD,QAAI,MAAM;AACR,WAAK,YAAY,KAAK,KAAK;AAE3B,YAAM,MAAM,KAAK,OAAO,IAAI,SAAS,KAAK,EAAE,QAAQ,UAAmB;AACvE,YAAM,UACJ,IAAI,WAAW,WAAW,IAAI,OAAO,IAAI,iBAAiB;AAC5D,YAAM,UACJ,IAAI,WAAW,WAAW,IAAI,OAAO,IAAI,iBAAiB;AAC5D,WAAK;AAAA,QACH;AAAA,QACA,IAAI;AAAA,QACJ,WAAW;AAAA,QACX,WAAW;AAAA,QACX,IAAI,WAAW,WAAW,IAAI,iBAAiB;AAAA,MACjD;AAAA,IACF;AAEA,UAAM,QAAQ,KAAK;AAAA,MACjB,MACE,KAAK;AAAA,QACH;AAAA,QACA,mBAAkB;AAAA,MACpB;AAAA,MACF,mBAAkB;AAAA,IACpB;AACA,SAAK,iBAAiB,IAAI,WAAW;AAAA,MACnC;AAAA,MACA,mBAAmB;AAAA,MACnB,eAAe;AAAA,MACf,eAAe;AAAA,IACjB,CAAC;AAED,SAAK,KAAK;AAAA,MACR;AAAA,QACE,MAAM;AAAA,QACN;AAAA,QACA,eAAe;AAAA,QACf,eAAe;AAAA,QACf,WAAW,mBAAkB;AAAA,MAC/B;AAAA,MACA,EAAE,YAAY,KAAK;AAAA,IACrB;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAgBQ,YAAY,WAAyB;AAC3C,SAAK,YAAY,OAAO,SAAS;AAMjC,UAAM,MAAM,KAAK,OAAO,IAAI,SAAS;AASrC,IAAAA,KAAI;AAAA,MACF,iCAAiC,SAAS,eAAe,KAAK,UAAU,MAAM,eAC9D,OAAO,IAAI,WAAW,WAAW,IAAI,iBAAiB,KAAK,qBACrD,KAAK,WAAW,SAAS;AAAA,IACjD;AACA,QAAI,OAAO,IAAI,WAAW,WAAW;AACnC;AAAA,IACF;AAEA,SAAK,KAAK;AAAA,MACR,EAAE,MAAM,4BAA4B,WAAW,OAAO,UAAU;AAAA,MAChE,EAAE,YAAY,KAAK;AAAA,IACrB;AAEA,UAAM,OAAO,KAAK,KAAK,mBAAmB,SAAS;AACnD,UAAM,WAAW,MAAM,QAAQ;AAC/B,UAAM,WAAW,MAAM,QAAQ;AAE/B,SAAK,OAAO,IAAI,WAAW;AAAA,MACzB,QAAQ;AAAA,MACR,iBAAiB,OAAO,EAAE,MAAM,UAAU,MAAM,SAAS,IAAI;AAAA,IAC/D,CAAC;AACD,SAAK;AAAA,MACH;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,IACF;AAEA,SAAK,qBAAqB,WAAW,aAAa;AAElD,QAAI,MAAM;AACR,UAAI;AACF,aAAK,KAAK;AAAA,UACR;AAAA,UACA,KAAK;AAAA,UACL,KAAK;AAAA,UACL;AAAA,QACF;AAAA,MACF,SAAS,KAAK;AAEZ,QAAAA,KAAI;AAAA,UACF,8CAA8C,SAAS;AAAA,UACvD;AAAA,QACF;AAAA,MACF;AAAA,IACF;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EASQ,qBACN,WACA,QACA,MACA,MACA,gBACM;AACN,UAAM,UAAyB;AAAA,MAC7B,MAAM;AAAA,MACN;AAAA,MACA;AAAA,MACA,aAAa;AAAA;AAAA,MACb;AAAA,MACA;AAAA,IACF;AACA,SAAK,KAAK,UAAU,SAAS;AAAA,MAC3B,cAAc,eAAa;AACzB,YAAI,UAAU,SAAS,YAAY;AACjC,iBAAO,EAAE,aAAa,WAAW,UAAU;AAAA,QAC7C;AAEA,YACE,WAAW,YACX,mBAAmB,UACnB,UAAU,aAAa,gBACvB;AACA,iBAAO,EAAE,aAAa,KAAK;AAAA,QAC7B;AACA,eAAO,EAAE,aAAa,MAAM;AAAA,MAC9B;AAAA,IACF,CAAC;AAAA,EACH;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAgBQ,qBACN,WACA,MAOA,MAOM;AACN,QAAI,CAAC,KAAK,gBAAiB;AAI3B,QAAI,SAAS,cAAc;AACzB,WAAK,uBAAuB,SAAS;AAAA,IACvC,OAAO;AACL,WAAK,qBAAqB,SAAS;AAAA,IACrC;AACA,UAAM,IAAI,KAAK,OAAO,IAAI,SAAS;AACnC,UAAM,aACJ,GAAG,WAAW,WACV;AAAA,MACE,MAAM,EAAE;AAAA,MACR,MAAM,EAAE;AAAA,MACR,YAAY,EAAE;AAAA,MACd,gBAAgB,EAAE;AAAA,IACpB,IACA;AAEN,IAAAA,KAAI;AAAA,MACF,iCAAiC,SAAS,SAAS,IAAI,MACpD,aACG,mBAAmB,WAAW,cAAc,SAAS,WAAW,IAAI,SAAS,WAAW,IAAI,KAC5F,OACH,MAAM,aAAa,eAAe,KAAK,UAAU,KAAK,OACtD,MAAM,gBAAgB,kBAAkB,KAAK,aAAa,KAAK,OAC/D,MAAM,SAAS,WAAW,KAAK,MAAM,KAAK;AAAA,IAC/C;AACA,SAAK,KAAK;AAAA,MACR;AAAA,QACE,MAAM;AAAA,QACN;AAAA,QACA;AAAA,QACA;AAAA,QACA,YAAY,MAAM;AAAA,QAClB,eAAe,MAAM;AAAA,QACrB,QAAQ,MAAM;AAAA,MAChB;AAAA,MACA,EAAE,YAAY,KAAK;AAAA,IACrB;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAKQ,MAAc;AACpB,WAAO,KAAK,KAAK,MAAM,KAAK,KAAK,IAAI;AAAA,EACvC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAeQ,uBAAuB,WAAyB;AACtD,QAAI,KAAK,iBAAiB,IAAI,SAAS,EAAG;AAC1C,UAAM,OAAO,MAAY;AACvB,YAAM,IAAI,KAAK,OAAO,IAAI,SAAS;AACnC,UAAI,CAAC,KAAK,EAAE,WAAW,UAAU;AAC/B,aAAK,iBAAiB,OAAO,SAAS;AACtC;AAAA,MACF;AAYA,YAAM,MAAM,KAAK,IAAI;AACrB,YAAM,SAAS,MAAM,EAAE;AACvB,YAAM,YACJ,OAAO,KAAK,KAAK,qBAAqB,SAAS,KAAK,EAAE;AACxD,YAAM,OAAO,KAAK,KAAK,gBAAgB,SAAS,KAAK;AACrD,YAAM,eACJ,aAAa,mBAAkB,uBAAuB;AAMxD,YAAM,cAAc,OAChB,mBAAkB,+BAClB,mBAAkB;AACtB,YAAM,UACJ,SAAS,mBAAkB,wBAC1B,CAAC,gBAAgB,SAAS;AAC7B,UAAI,SAAS;AACX,QAAAA,KAAI;AAAA,UACF,uCAAuC,SAAS,WACpC,MAAM,cAAc,SAAS,SAAS,IAAI,YACzC,WAAW;AAAA,QAC1B;AACA,aAAK,YAAY,SAAS;AAC1B;AAAA,MACF;AAGA,WAAK,qBAAqB,WAAW,YAAY;AASjD,WAAK;AAAA,QACH;AAAA,QACA;AAAA,QACA,EAAE,QAAQ;AAAA,QACV,EAAE,QAAQ;AAAA,QACV,EAAE;AAAA,MACJ;AACA,YAAM,OAAO,KAAK,UAAU,MAAM,GAAI;AACtC,WAAK,iBAAiB,IAAI,WAAW,IAAI;AAAA,IAC3C;AACA,UAAM,SAAS,KAAK,UAAU,MAAM,GAAI;AACxC,SAAK,iBAAiB,IAAI,WAAW,MAAM;AAAA,EAC7C;AAAA;AAAA;AAAA;AAAA;AAAA,EAMQ,qBAAqB,WAAyB;AACpD,UAAM,IAAI,KAAK,iBAAiB,IAAI,SAAS;AAC7C,QAAI,MAAM,OAAW;AACrB,SAAK,YAAY,CAAC;AAClB,SAAK,iBAAiB,OAAO,SAAS;AAAA,EACxC;AACF;;;ACviCO,SAAS,eAAe,OAAe,OAAqB;AACjE,MAAI,CAAC,sBAAsB,KAAK,KAAK,GAAG;AACtC,UAAM,IAAI,MAAM,WAAW,KAAK,KAAK,KAAK,EAAE;AAAA,EAC9C;AACF;AAkBO,SAAS,oBAAoB,SAAqC;AACvE,QAAMC,KAAI,QAAQ,YAAY;AAC9B,MAAIA,GAAE,SAAS,kBAAkB,EAAG,QAAO;AAC3C,MAAIA,GAAE,SAAS,aAAa,KAAKA,GAAE,SAAS,kBAAkB,GAAG;AAC/D,WAAO;AAAA,EACT;AACA,MAAIA,GAAE,SAAS,WAAW,KAAKA,GAAE,SAAS,gBAAgB,GAAG;AAC3D,WAAO;AAAA,EACT;AACA,SAAO;AACT;AAWO,SAAS,gBAAgB,QAAmB,KAAqB;AACtE,QAAM,KAAK,MAAM,KAAK,UAAU,GAAG,CAAC;AACpC,UAAQ,OAAO,MAAM;AAAA,IACnB,KAAK;AACH,aACE,GAAG,EAAE;AAAA,IAOT,KAAK;AACH,aAAO,GAAG,EAAE,oBAAoB,KAAK,UAAU,OAAO,MAAM,CAAC;AAAA,IAC/D,KAAK;AACH,UAAI,OAAO,UAAU;AACnB,eAAO,GAAG,EAAE,uBAAuB,KAAK,UAAU,OAAO,IAAI,CAAC;AAAA,MAChE;AACA,aAAO,GAAG,EAAE,kBAAkB,KAAK,UAAU,OAAO,IAAI,CAAC;AAAA,IAC3D,KAAK;AAIH,aAAO,GAAG,EAAE,kBAAkB,OAAO,QAAQ,OAAO,IAAI,IAAI,KAAK,UAAU,OAAO,MAAM,CAAC;AAAA,IAC3F,KAAK;AACH,aAAO,GAAG,EAAE;AAAA,IACd,KAAK;AACH,aAAO,GAAG,EAAE;AAAA,IACd,KAAK,OAAO;AAEV,YAAM,QAAQ,KAAK,IAAI,GAAG,KAAK,IAAI,OAAO,QAAQ,GAAG,GAAG,CAAC;AACzD,YAAM,MAAM;AACZ,aAAO,GAAG,EAAE,gCAAgC,GAAG,QAAQ,KAAK;AAAA,IAC9D;AAAA,IACA,SAAS;AACP,YAAM,cAAqB;AAC3B,YAAM,IAAI;AAAA,QACR,2BAA4B,YAA0B,IAAI;AAAA,MAC5D;AAAA,IACF;AAAA,EACF;AACF;;;ACvGA,SAAS,YAAY;AAKrB,IAAMC,OAAM,aAAa,gBAAgB;AAgBzC,eAAsB,mBACpB,KAC2B;AAC3B,MAAI,CAAC,OAAO,UAAU,GAAG,KAAK,OAAO,EAAG,QAAO;AAC/C,MAAI;AACF,UAAM,QACJ,QAAQ,aAAa,UACjB,MAAM,2BAA2B,GAAG,IACpC,MAAM,wBAAwB,GAAG;AACvC,UAAM,WAAW,cAAc,KAAK;AACpC,IAAAA,KAAI;AAAA,MACF,wBAAwB,GAAG,gBAAgB,MAAM,MAAM,OAAO,QAAQ;AAAA,IACxE;AACA,WAAO;AAAA,EACT,SAAS,KAAK;AACZ,IAAAA,KAAI;AAAA,MACF,+BAA+B,GAAG,KAAM,IAAc,OAAO;AAAA,IAC/D;AACA,WAAO;AAAA,EACT;AACF;AAQO,SAAS,cAAc,cAA0C;AACtE,QAAM,SAAS,aAAa,KAAK,IAAI,EAAE,YAAY;AAGnD,MAAI,aAAa,KAAK,MAAM,KAAK,OAAO,SAAS,QAAQ,EAAG,QAAO;AACnE,MAAI,OAAO,SAAS,OAAO,EAAG,QAAO;AACrC,MAAI,OAAO,SAAS,QAAQ,EAAG,QAAO;AACtC,SAAO;AACT;AAQA,eAAe,wBAAwB,KAAgC;AACrE,QAAM,MAAM,MAAM,SAAS,yBAAyB;AACpD,QAAM,OAAqD,CAAC;AAC5D,aAAW,QAAQ,IAAI,MAAM,IAAI,GAAG;AAClC,UAAMC,KAAI,KAAK,MAAM,2BAA2B;AAChD,QAAI,CAACA,GAAG;AACR,SAAK,KAAK,EAAE,KAAK,OAAOA,GAAE,CAAC,CAAC,GAAG,MAAM,OAAOA,GAAE,CAAC,CAAC,GAAG,KAAKA,GAAE,CAAC,EAAE,CAAC;AAAA,EAChE;AACA,SAAO,sBAAsB,KAAK,IAAI;AACxC;AAQA,eAAe,2BAA2B,KAAgC;AACxE,QAAM,MAAM,MAAM;AAAA,IAChB;AAAA,EACF;AACA,QAAM,OAAqD,CAAC;AAC5D,QAAM,QAAQ,IACX,MAAM,IAAI,EACV,IAAI,OAAK,EAAE,KAAK,CAAC,EACjB,OAAO,OAAO;AAEjB,aAAW,QAAQ,OAAO;AACxB,QAAI,KAAK,WAAW,OAAO,EAAG;AAE9B,UAAM,QAAQ,KAAK,MAAM,GAAG;AAC5B,QAAI,MAAM,SAAS,EAAG;AACtB,UAAM,SAAS,OAAO,MAAM,MAAM,SAAS,CAAC,CAAC;AAC7C,UAAM,WAAW,OAAO,MAAM,MAAM,SAAS,CAAC,CAAC;AAC/C,UAAM,MAAM,MAAM,MAAM,GAAG,MAAM,SAAS,CAAC,EAAE,KAAK,GAAG;AACrD,QAAI,CAAC,OAAO,UAAU,MAAM,EAAG;AAG/B,QAAI,aAAa,OAAO,WAAW,KAAK;AACtC,MAAAD,KAAI;AAAA,QACF,8BAA8B,GAAG,sBAAsB,MAAM,WAAW,QAAQ,SAAS,IAAI,MAAM,GAAG,EAAE,CAAC;AAAA,MAC3G;AAAA,IACF;AACA,SAAK,KAAK,EAAE,KAAK,QAAQ,MAAM,UAAU,IAAI,CAAC;AAAA,EAChD;AACA,SAAO,sBAAsB,KAAK,IAAI;AACxC;AASO,SAAS,sBACd,SACA,MACU;AACV,QAAM,mBAAmB,oBAAI,IAAyB;AACtD,aAAW,KAAK,MAAM;AACpB,UAAM,MAAM,iBAAiB,IAAI,EAAE,IAAI,KAAK,CAAC;AAC7C,QAAI,KAAK,CAAC;AACV,qBAAiB,IAAI,EAAE,MAAM,GAAG;AAAA,EAClC;AACA,QAAM,OAAiB,CAAC;AACxB,QAAM,QAAQ,CAAC,GAAI,iBAAiB,IAAI,OAAO,KAAK,CAAC,CAAE;AACvD,QAAM,OAAO,oBAAI,IAAY;AAC7B,WAAS,IAAI,GAAG,IAAI,MAAM,QAAQ,KAAK;AACrC,UAAM,OAAO,MAAM,CAAC;AACpB,QAAI,KAAK,IAAI,KAAK,GAAG,EAAG;AACxB,SAAK,IAAI,KAAK,GAAG;AACjB,SAAK,KAAK,KAAK,GAAG;AAClB,UAAM,OAAO,iBAAiB,IAAI,KAAK,GAAG;AAC1C,QAAI,KAAM,OAAM,KAAK,GAAG,IAAI;AAAA,EAC9B;AACA,SAAO;AACT;AAGA,SAAS,SAAS,SAAkC;AAClD,SAAO,IAAI,QAAQ,CAAC,SAAS,WAAW;AACtC;AAAA,MACE;AAAA,MACA,EAAE,SAAS,MAAM,WAAW,IAAI,OAAO,KAAK;AAAA,MAC5C,CAAC,KAAK,WAAW;AACf,YAAI,IAAK,QAAO,GAAG;AAAA,YACd,SAAQ,MAAM;AAAA,MACrB;AAAA,IACF;AAAA,EACF,CAAC;AACH;;;AC5JA,OAAO,QAAQ;AACf,OAAOE,SAAQ;AACf,OAAOC,WAAU;AAMjB,IAAMC,OAAM,aAAa,gBAAgB;AAGzC,IAAM,sBAAsB;AA2BrB,SAAS,kBAAkB,KAAa,QAAQ,IAAwB;AAC7E,MAAI;AACF,UAAM,OAAO,mBAAmB;AAChC,UAAM,OAAO,WAAW,GAAG;AAC3B,UAAM,MAAMC,MAAK,KAAK,MAAM,IAAI;AAChC,IAAAD,KAAI,KAAK,mCAAmC,GAAG,SAAS,IAAI,EAAE;AAC9D,QAAI,CAAC,GAAG,WAAW,GAAG,GAAG;AACvB,MAAAA,KAAI,KAAK,qCAAqC,GAAG,EAAE;AAEnD,UAAI;AACF,cAAM,WAAW,GAAG,WAAW,IAAI,IAAI,GAAG,YAAY,IAAI,IAAI,CAAC;AAC/D,cAAM,OAAO,SAAS,OAAO,OAAK;AAChC,gBAAM,IAAI,EAAE,YAAY,EAAE,QAAQ,MAAM,EAAE;AAC1C,gBAAME,KAAI,KAAK,YAAY,EAAE,QAAQ,MAAM,EAAE;AAC7C,iBAAO,EAAE,SAASA,GAAE,MAAM,GAAG,CAAC,KAAKA,GAAE,SAAS,EAAE,MAAM,GAAG,CAAC;AAAA,QAC5D,CAAC;AACD,QAAAF,KAAI;AAAA,UACF,+BAA+B,SAAS,MAAM,uBAAuB,KAAK,UAAU,IAAI,CAAC;AAAA,QAC3F;AAAA,MACF,QAAQ;AAAA,MAER;AACA,aAAO,CAAC;AAAA,IACV;AACA,UAAM,UAAU,GACb,YAAY,GAAG,EACf,OAAO,OAAK,EAAE,SAAS,QAAQ,CAAC,EAChC,IAAI,OAAK;AACR,YAAM,OAAOC,MAAK,KAAK,KAAK,CAAC;AAC7B,YAAM,OAAO,GAAG,SAAS,IAAI;AAC7B,aAAO;AAAA,QACL,IAAI,EAAE,QAAQ,YAAY,EAAE;AAAA,QAC5B,OAAO,KAAK;AAAA,QACZ;AAAA,MACF;AAAA,IACF,CAAC,EACA,KAAK,CAAC,GAAGC,OAAMA,GAAE,QAAQ,EAAE,KAAK,EAChC,MAAM,GAAG,KAAK;AAEjB,WAAO,QAAQ,IAAI,QAAM;AAAA,MACvB,IAAI,EAAE;AAAA,MACN,OAAO,KAAK,MAAM,EAAE,KAAK;AAAA,MACzB,SAAS,iBAAiB,EAAE,IAAI;AAAA,IAClC,EAAE;AAAA,EACJ,SAAS,KAAK;AACZ,IAAAF,KAAI,KAAK,0CAA0C,GAAG,KAAM,IAAc,OAAO,EAAE;AACnF,WAAO,CAAC;AAAA,EACV;AACF;AAkBO,SAAS,qBACd,MACA,YACA,KACoB;AACpB,QAAM,OAAO,IAAI,IAAI,KAAK,IAAI,OAAK,EAAE,EAAE,CAAC;AACxC,QAAM,SAAS,CAAC,GAAG,IAAI;AACvB,aAAW,MAAM,YAAY;AAC3B,QAAI,KAAK,IAAI,EAAE,EAAG;AAClB,SAAK,IAAI,EAAE;AACX,WAAO,KAAK,EAAE,IAAI,OAAO,KAAK,SAAS,GAAG,CAAC;AAAA,EAC7C;AACA,SAAO,OAAO,KAAK,CAAC,GAAGE,OAAMA,GAAE,QAAQ,EAAE,KAAK;AAChD;AAoBO,SAAS,sBAAsB,MAIvB;AACb,QAAM,UAAgD,CAAC;AACvD,QAAM,SAAS,MAAY;AACzB,eAAW,KAAK,QAAS,cAAa,CAAC;AACvC,YAAQ,SAAS;AAAA,EACnB;AACA,aAAW,SAAS,KAAK,UAAU;AACjC,YAAQ;AAAA,MACN,WAAW,MAAM;AACf,cAAM,WAAW,KAAK,OAAO;AAC7B,YAAI,SAAS,SAAS,GAAG;AACvB,eAAK,WAAW,QAAQ;AACxB,iBAAO;AAAA,QACT;AAAA,MACF,GAAG,KAAK;AAAA,IACV;AAAA,EACF;AACA,SAAO;AACT;AAiBO,SAAS,sBACd,KACA,WACA,QAAQ,IACa;AACrB,MAAI;AACF,UAAM,OAAOD,MAAK,KAAK,mBAAmB,GAAG,WAAW,GAAG,GAAG,GAAG,SAAS,QAAQ;AAClF,QAAI,CAAC,GAAG,WAAW,IAAI,GAAG;AACxB,MAAAD,KAAI,KAAK,wCAAwC,IAAI,EAAE;AACvD,aAAO,CAAC;AAAA,IACV;AACA,UAAM,MAAM,GAAG,aAAa,MAAM,OAAO;AACzC,UAAM,MAA2B,CAAC;AAClC,eAAW,QAAQ,IAAI,MAAM,OAAO,GAAG;AACrC,YAAM,UAAU,KAAK,KAAK;AAC1B,UAAI,CAAC,QAAS;AACd,UAAI;AACJ,UAAI;AACF,cAAM,KAAK,MAAM,OAAO;AAAA,MAC1B,QAAQ;AACN;AAAA,MACF;AACA,YAAM,OACJ,IAAI,SAAS,SAAS,SAAS,IAAI,SAAS,cAAc,cAAc;AAC1E,UAAI,CAAC,KAAM;AACX,YAAM,UAAU,IAAI;AACpB,UAAI,OAAO,mBAAmB,SAAS,OAAO,EAAE,KAAK;AACrD,UAAI,CAAC,KAAM;AACX,UAAI,KAAK,SAAS,qBAAqB;AACrC,eAAO,GAAG,KAAK,MAAM,GAAG,mBAAmB,CAAC;AAAA,MAC9C;AACA,YAAM,QAAQ,IAAI;AAClB,YAAM,KAAK,OAAO,UAAU,WAAW,KAAK,MAAM,KAAK,KAAK,IAAI;AAChE,UAAI,KAAK,EAAE,MAAM,MAAM,GAAG,CAAC;AAAA,IAC7B;AACA,UAAM,SAAS,IAAI,SAAS,QAAQ,IAAI,MAAM,IAAI,SAAS,KAAK,IAAI;AACpE,IAAAA,KAAI;AAAA,MACF,2CAA2C,SAAS,WAAW,IAAI,MAAM,aAAa,OAAO,MAAM;AAAA,IACrG;AACA,WAAO;AAAA,EACT,SAAS,KAAK;AACZ,IAAAA,KAAI;AAAA,MACF,kDAAkD,SAAS,KAAM,IAAc,OAAO;AAAA,IACxF;AACA,WAAO,CAAC;AAAA,EACV;AACF;AAuFA,SAAS,mBAAmB,SAA0B;AACpD,MAAI,OAAO,YAAY,SAAU,QAAO;AACxC,MAAI,CAAC,MAAM,QAAQ,OAAO,EAAG,QAAO;AACpC,SAAO,QACJ,IAAI,OAAK;AACR,QAAI,KAAK,OAAO,MAAM,YAAa,EAAyB,SAAS,QAAQ;AAC3E,YAAM,OAAQ,EAAyB;AACvC,aAAO,OAAO,SAAS,WAAW,OAAO;AAAA,IAC3C;AACA,WAAO;AAAA,EACT,CAAC,EACA,OAAO,OAAO,EACd,KAAK,IAAI;AACd;AASA,SAAS,qBAA6B;AACpC,SAAOG,MAAK,KAAKC,IAAG,QAAQ,GAAG,WAAW,UAAU;AACtD;AAgBO,SAAS,WAAW,KAAqB;AAC9C,SAAO,IAAI,QAAQ,iBAAiB,GAAG;AACzC;AAQA,SAAS,iBAAiB,UAA0B;AAClD,MAAI,KAAoB;AACxB,MAAI;AACF,SAAK,GAAG,SAAS,UAAU,GAAG;AAC9B,UAAM,MAAM,OAAO,MAAM,KAAK,IAAI;AAClC,UAAM,QAAQ,GAAG,SAAS,IAAI,KAAK,GAAG,IAAI,QAAQ,CAAC;AACnD,UAAM,OAAO,IAAI,SAAS,SAAS,GAAG,KAAK;AAC3C,eAAW,QAAQ,KAAK,MAAM,IAAI,GAAG;AACnC,YAAM,UAAU,KAAK,KAAK;AAC1B,UAAI,CAAC,QAAS;AACd,UAAI;AACJ,UAAI;AACF,cAAM,KAAK,MAAM,OAAO;AAAA,MAC1B,QAAQ;AACN;AAAA,MACF;AACA,UAAI,IAAI,SAAS,OAAQ;AACzB,YAAM,UAAU,IAAI;AACpB,YAAM,UAAU,SAAS;AACzB,YAAM,OACJ,OAAO,YAAY,WACf,UACA,MAAM,QAAQ,OAAO,IACnB,QACG;AAAA,QAAI,OACH,KAAK,OAAO,MAAM,YAAY,UAAU,IACpC,OAAQ,EAAwB,IAAI,IACpC;AAAA,MACN,EACC,KAAK,GAAG,IACX;AACR,YAAM,UAAU,KAAK,QAAQ,QAAQ,GAAG,EAAE,KAAK;AAC/C,UAAI,QAAS,QAAO,QAAQ,MAAM,GAAG,EAAE;AAAA,IACzC;AACA,WAAO;AAAA,EACT,QAAQ;AACN,WAAO;AAAA,EACT,UAAE;AACA,QAAI,OAAO,MAAM;AACf,UAAI;AACF,WAAG,UAAU,EAAE;AAAA,MACjB,QAAQ;AAAA,MAER;AAAA,IACF;AAAA,EACF;AACF;;;AChYO,SAAS,aAAa,KAAqB;AAChD,MAAIC,KAAI,IAAI,KAAK,EAAE,QAAQ,OAAO,GAAG;AACrC,EAAAA,KAAIA,GAAE,QAAQ,kBAAkB,IAAI;AACpC,EAAAA,KAAIA,GAAE,QAAQ,QAAQ,EAAE;AACxB,MAAI,QAAQ,aAAa,QAAS,CAAAA,KAAIA,GAAE,YAAY;AACpD,SAAOA;AACT;;;ACFO,IAAM,qBAAN,MAAmD;AAAA,EAC/C,OAAO;AAAA,EACP,UAAU;AAAA;AAAA,EAGnB,MAAM,eAAe,YAAoB,MAAqC;AAG5E,WAAO;AAAA,MACL,OAAO,KAAK;AAAA,MACZ,MAAM,KAAK,QAAQ,CAAC;AAAA,MACpB,KAAK,KAAK;AAAA,MACV,KAAK,KAAK;AAAA,MACV,MAAM,KAAK;AAAA,MACX,MAAM,KAAK;AAAA,IACb;AAAA,EACF;AAAA;AAAA,EAGA,MAAM,OAAO,YAAmC;AAAA,EAEhD;AAAA;AAAA,EAGA,MAAM,KAAK,YAAmC;AAAA,EAE9C;AAAA,EAEA,MAAM,OAAqC;AACzC,WAAO,CAAC;AAAA,EACV;AAAA;AAAA,EAGA,MAAM,OAAO,YAAoB,OAAe,OAAe,SAAsC;AAAA,EAErG;AAAA,EAEA,MAAM,QAA8B;AAClC,WAAO,EAAE,SAAS,MAAM,SAAS,KAAK;AAAA,EACxC;AACF;;;ACpBO,SAAS,qBACd,SACA,QAAqC,CAAC,GACtB;AAChB,MAAI,WAAW,YAAY,UAAU,YAAY,QAAQ;AACvD,YAAQ,KAAK,0BAA0B,OAAO,0GAA8C;AAAA,EAC9F;AACA,SAAO,IAAI,mBAAmB;AAChC;;;AjBFA,IAAM,SAAS,aAAa,KAAK;AACjC,IAAM,SAAS,aAAa,KAAK;AAEjC,IAAM,UAAUC,IAAG,QAAQ;AAUpB,SAAS,cAAcC,IAAoB;AAChD,MAAI;AACF,WAAOC,IAAG,SAASD,EAAC,EAAE,YAAY;AAAA,EACpC,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAIA,IAAM,WAAWE,MAAK,KAAK,SAAS,oBAAoB;AACxD,IAAM,gBAAgBA,MAAK,KAAK,UAAU,eAAe;AACzD,IAAM,eAAeA,MAAK,KAAK,UAAU,aAAa;AACtD,IAAM,YAAY;AAOlB,SAAS,4BAAoC;AAC3C,QAAM,UACJ,QAAQ,aAAa,UAChB,QAAQ,IAAI,gBAAgBA,MAAK,KAAK,SAAS,WAAW,OAAO,IACjE,QAAQ,IAAI,kBAAkBA,MAAK,KAAK,SAAS,UAAU,OAAO;AACzE,SAAOA,MAAK,KAAK,SAAS,UAAU,YAAY;AAClD;AASA,IAAI,YAA2B;AAE/B,SAAS,kBAA0B;AACjC,MAAI,UAAW,QAAO;AACtB,MAAI;AACF,gBAAYD,IAAG,aAAa,YAAY;AACxC,WAAO;AAAA,EACT,QAAQ;AACN,QAAI,CAACA,IAAG,WAAW,QAAQ,EAAG,CAAAA,IAAG,UAAU,UAAU,EAAE,WAAW,KAAK,CAAC;AACxE,UAAM,MAAME,QAAO,YAAY,EAAE;AACjC,IAAAF,IAAG,cAAc,cAAc,KAAK,EAAE,MAAM,IAAM,CAAC;AACnD,gBAAY;AACZ,WAAO;AAAA,EACT;AACF;AAEA,SAAS,gBAAgB,UAA0B;AACjD,QAAM,MAAM,gBAAgB;AAC5B,QAAM,KAAKE,QAAO,YAAY,EAAE;AAChC,QAAM,SAASA,QAAO,eAAe,WAAW,KAAK,EAAE;AACvD,QAAM,YAAY,OAAO,OAAO;AAAA,IAC9B,OAAO,OAAO,UAAU,MAAM;AAAA,IAC9B,OAAO,MAAM;AAAA,EACf,CAAC;AACD,QAAM,MAAM,OAAO,WAAW;AAC9B,SAAO,OAAO,OAAO,CAAC,IAAI,KAAK,SAAS,CAAC,EAAE,SAAS,QAAQ;AAC9D;AAEA,SAAS,gBACP,SACA,WACoB;AACpB,MAAI;AACF,UAAM,MAAM,gBAAgB;AAC5B,UAAM,MAAM,OAAO,KAAK,SAAS,QAAQ;AACzC,UAAM,KAAK,IAAI,SAAS,GAAG,EAAE;AAC7B,UAAM,MAAM,IAAI,SAAS,IAAI,EAAE;AAC/B,UAAM,aAAa,IAAI,SAAS,EAAE;AAClC,UAAM,WAAWA,QAAO,iBAAiB,WAAW,KAAK,EAAE;AAC3D,aAAS,WAAW,GAAG;AACvB,WAAO,SAAS,OAAO,UAAU,EAAE,SAAS,MAAM,IAAI,SAAS,MAAM,MAAM;AAAA,EAC7E,QAAQ;AACN,WAAO;AAAA,MACL,6CAA6C,SAAS;AAAA,IACxD;AACA,WAAO;AAAA,EACT;AACF;AAEA,SAAS,wBAA0C;AACjD,MAAI;AACF,UAAM,MAAMF,IAAG,aAAa,eAAe,OAAO;AAClD,UAAM,SAA6B,KAAK,MAAM,GAAG;AACjD,QAAI,CAAC,MAAM,QAAQ,MAAM,EAAG,QAAO;AACnC,WAAO,OAAO,IAAI,CAAC,EAAE,mBAAmB,GAAG,KAAK,MAAM;AACpD,YAAM,WAAW,oBACb,gBAAgB,mBAAmB,KAAK,EAAE,IAC1C;AACJ,YAAM,cAAc,KAAK,SAAS,eAAe,KAAK,SAAS;AAC/D,YAAM,cAAc,KAAK,gBAAgB,cAAc,UAAU;AACjE,aAAO;AAAA,QACL,GAAG;AAAA,QACH;AAAA,QACA,QAAQ;AAAA,QACR,GAAI,qBAAqB,CAAC,WAAW,EAAE,cAAc,KAAK,IAAI,CAAC;AAAA,QAC/D,GAAI,WAAW,EAAE,SAAS,IAAI,CAAC;AAAA,MACjC;AAAA,IACF,CAAC;AAAA,EACH,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAEA,SAAS,sBAAsB,UAA2B;AACxD,MAAI;AACF,QAAI,CAACA,IAAG,WAAW,QAAQ,GAAG;AAC5B,MAAAA,IAAG,UAAU,UAAU,EAAE,WAAW,KAAK,CAAC;AAAA,IAC5C;AACA,UAAM,SAA6B,SAAS;AAAA,MAC1C,CAAC,EAAE,QAAQ,IAAI,UAAU,cAAc,KAAK,GAAG,KAAK,OAAO;AAAA,QACzD,GAAG;AAAA,QACH,GAAI,WAAW,EAAE,mBAAmB,gBAAgB,QAAQ,EAAE,IAAI,CAAC;AAAA,MACrE;AAAA,IACF;AACA,IAAAA,IAAG,cAAc,eAAe,KAAK,UAAU,QAAQ,MAAM,CAAC,GAAG,OAAO;AAAA,EAC1E,SAAS,KAAK;AACZ,WAAO,MAAM,0CAA0C,GAAG;AAAA,EAC5D;AACF;AAEA,IAAM,gBAAgB;AAAA,EACpBC,MAAK,KAAK,SAAS,QAAQ,YAAY;AAAA,EACvCA,MAAK,KAAK,SAAS,QAAQ,UAAU;AAAA,EACrCA,MAAK,KAAK,SAAS,QAAQ,QAAQ;AACrC;AAEA,IAAM,kBAAkB,oBAAI,IAAI;AAAA,EAC9B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,CAAC;AAED,SAAS,iBAAgC;AACvC,aAAW,WAAW,eAAe;AACnC,QAAI;AACF,aAAOD,IAAG,aAAa,OAAO;AAAA,IAChC,QAAQ;AAAA,IAER;AAAA,EACF;AACA,SAAO;AACT;AAUA,IAAM,2BAA2B;AAIjC,IAAM,kCAAkC,CAAC,KAAK,MAAM,GAAI;AAIjD,IAAM,eAAe;AACrB,IAAM,eAAe;AACrB,IAAM,eAAe;AACrB,IAAM,eAAe;AAErB,SAAS,UAAUG,IAAmB;AAC3C,QAAM,IAAI,KAAK,MAAMA,EAAC;AACtB,MAAI,CAAC,OAAO,SAAS,CAAC,KAAK,KAAK,EAAG,QAAO;AAC1C,SAAO,KAAK,IAAI,KAAK,IAAI,GAAG,YAAY,GAAG,YAAY;AACzD;AAEO,SAAS,UAAUA,IAAmB;AAC3C,QAAM,IAAI,KAAK,MAAMA,EAAC;AACtB,MAAI,CAAC,OAAO,SAAS,CAAC,KAAK,KAAK,EAAG,QAAO;AAC1C,SAAO,KAAK,IAAI,KAAK,IAAI,GAAG,YAAY,GAAG,YAAY;AACzD;AAoBO,SAAS,gBAAgB,MAAc,MAAuB;AACnE,SAAO,QAAQ,MAAM,QAAQ;AAC/B;AAMO,SAAS,iBACd,QACA,cACA,cAKA;AACA,MAAI,CAAC;AACH,WAAO,EAAE,MAAM,cAAc,MAAM,cAAc,QAAQ,WAAW;AACtE,MAAI,CAAC,gBAAgB,OAAO,MAAM,OAAO,IAAI,GAAG;AAC9C,WAAO;AAAA,MACL,MAAM;AAAA,MACN,MAAM;AAAA,MACN,QAAQ;AAAA,IACV;AAAA,EACF;AACA,SAAO,EAAE,MAAM,OAAO,MAAM,MAAM,OAAO,MAAM,QAAQ,SAAS;AAClE;AAEO,SAAS,kBACd,OACA,YACsB;AACtB,QAAM,OAAO,UAAU,OAAO,UAAU,MAAM,QAAQ;AACtD,MAAI,SAAS,IAAK,QAAO;AACzB,MAAI,SAAS,IAAK,QAAO;AACzB,MAAI,SAAS,OAAO,eAAe,IAAK,QAAO;AAC/C,MAAI,SAAS,IAAK,QAAO;AACzB,MAAI,SAAS,IAAK,QAAO;AACzB,SAAO;AACT;AASO,SAAS,0BACd,SACoB;AACpB,SAAO,QAAQ,eAAe;AAChC;AAEO,IAAM,aAAN,MAAiB;AAAA,EACd,WAAsB,CAAC;AAAA,EACvB,WAAW,oBAAI,IAA2B;AAAA,EAC1C,gBAAgB,oBAAI,IAAwB;AAAA,EAC5C,oBAAoB,oBAAI,IAAoB;AAAA,EAC5C,WAAW,oBAAI,IAA4C;AAAA;AAAA;AAAA,EAG3D,qBAAqB,oBAAI,IAAwB;AAAA,EACjD,qBAAqB,oBAAI,IAAY;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAMrC,kBAAkB,oBAAI,IAA4C;AAAA,EAC1D;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAMR,sBAA+D;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAM/D,wBAAkE;AAAA;AAAA;AAAA,EAGlE,kBAAkB,oBAAI,IAAwC;AAAA;AAAA;AAAA;AAAA,EAI9D,cAAc,oBAAI,IAAiC;AAAA;AAAA;AAAA;AAAA,EAInD;AAAA;AAAA;AAAA,EAGA,qBAAqB,oBAAI,IAAY;AAAA,EACrC;AAAA;AAAA;AAAA;AAAA,EAIQ;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAYR;AAAA,EACA,aAAa;AAAA,EACb,SAAS,oBAAI,IAAoB;AAAA,EACjC,eAAe,oBAAI,IAA4C;AAAA,EAC/D,cAAc,oBAAI,IAAuC;AAAA;AAAA;AAAA,EAGzD,4BAA4B,oBAAI,IAAwB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAahE,iBAAiB,WAAmB,MAAc,MAAoB;AACpE,SAAK,SAAS,IAAI,WAAW,EAAE,MAAM,KAAK,CAAC;AAAA,EAC7C;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAWA,WAAW,WAA+D;AACxE,WAAO,KAAK,SAAS,IAAI,SAAS;AAAA,EACpC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAWA,mBAAmB,IAAsD;AACvE,SAAK,kBAAkB;AAAA,EACzB;AAAA,EAEA,YAAY,WAAwB;AAClC,SAAK,YAAY;AACjB,SAAK,YAAY,IAAI,oBAAoB;AAAA,MACvC,iBAAiB,MACf,KAAK,SAAS,OACd,KAAK,cAAc,OACnB,KAAK,mBAAmB;AAAA,MAC1B,cAAc,QAAM,KAAK,WAAW,EAAE;AAAA,MACtC,cAAc,QAAM,KAAK,mBAAmB,IAAI,EAAE;AAAA;AAAA,MAElD,WAAW,SAAO,KAAK,UAAU,GAAG;AAAA,IACtC,CAAC;AAGD,SAAK,YAAY,IAAI,kBAAkB;AAAA,MACrC,WAAW,KAAK;AAAA,MAChB,aAAa,CAAC,KAAK,MAAM,MAAM,WAC7B,KAAK,OAAO,KAAK,MAAM,MAAM,MAAM;AAAA,MACrC,oBAAoB,SAAO,KAAK,gBAAgB,IAAI,GAAG;AAAA;AAAA;AAAA;AAAA,MAIvD,oBAAoB,SAAO,KAAK,UAAU,gBAAgB,GAAG;AAAA,MAC7D,eAAe,SAAO,KAAK,sBAAsB,GAAG,KAAK;AAAA,IAC3D,CAAC;AACD,SAAK,YAAY,IAAI,oBAAoB,0BAA0B,CAAC;AACpE,UAAM,QAAQ,sBAAsB;AACpC,QAAI,SAAS,MAAM,SAAS,GAAG;AAC7B,WAAK,WAAW;AAAA,IAClB;AAEA,UAAM,WAAW,KAAK,SAAS,KAAK,CAAAC,OAAKA,GAAE,gBAAgB,OAAO;AAClE,QAAI,CAAC,UAAU;AACb,WAAK,WAAW;AAAA,QACd,MAAM;AAAA,QACN,MAAM;AAAA,QACN,MAAM;AAAA,QACN,UAAUN,IAAG,SAAS,EAAE;AAAA,QACxB,aAAa;AAAA,MACf,CAAC;AAAA,IACH;AAIA,QAAI;AACF,YAAM,QAAQ,IAAI,IAAI,KAAK,SAAS,IAAI,CAAAM,OAAKA,GAAE,EAAE,CAAC;AAClD,WAAK,UAAU,aAAa,KAAK;AAAA,IACnC,QAAQ;AAAA,IAER;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EASA,uBAAuB,IAA0C;AAC/D,SAAK,sBAAsB;AAAA,EAC7B;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQA,yBAAyB,IAA2C;AAClE,SAAK,wBAAwB;AAAA,EAC/B;AAAA,EAEA,cAAyB;AACvB,WAAO,KAAK;AAAA,EACd;AAAA,EAEA,oBAA0B;AACxB,SAAK,UAAU,EAAE,MAAM,gBAAgB,UAAU,KAAK,SAAS,CAAC;AAAA,EAClE;AAAA,EAEA,mBAAyB;AACvB,SAAK,UAAU,EAAE,MAAM,YAAY,MAAM,KAAK,kBAAkB,EAAE,CAAC;AAAA,EACrE;AAAA,EAEA,oBAA8B;AAC5B,QAAI;AACF,YAAM,MAAMH,MAAK,KAAK,SAAS,MAAM;AACrC,aAAOD,IACJ,YAAY,GAAG,EACf,OAAO,OAAK,CAAC,EAAE,SAAS,MAAM,KAAK,CAAC,gBAAgB,IAAI,CAAC,CAAC,EAC1D,IAAI,OAAKC,MAAK,KAAK,KAAK,CAAC,CAAC;AAAA,IAC/B,QAAQ;AACN,aAAO,CAAC;AAAA,IACV;AAAA,EACF;AAAA,EAEA,YAAY,MAAwC;AAClD,QAAI;AAEJ,SAAK,WAAW,KAAK,SAAS,IAAI,UAAQ;AACxC,UAAI,KAAK,OAAO,KAAK,IAAI;AACvB,kBAAU,EAAE,GAAG,MAAM,GAAG,KAAK;AAC7B,eAAO;AAAA,MACT;AACA,aAAO;AAAA,IACT,CAAC;AAED,QAAI,CAAC,SAAS;AACZ,YAAM,IAAI,MAAM,mBAAmB;AAAA,IACrC;AAEA,0BAAsB,KAAK,QAAQ;AACnC,WAAO;AAAA,EACT;AAAA,EAEA,WAAW,MAA+C;AACxD,UAAM,UAAmB;AAAA,MACvB,GAAG;AAAA,MACH,IAAIC,QAAO,WAAW;AAAA,MACtB,QAAQ;AAAA,IACV;AACA,SAAK,SAAS,KAAK,OAAO;AAC1B,0BAAsB,KAAK,QAAQ;AACnC,WAAO;AAAA,EACT;AAAA,EAEA,cAAc,IAAkB;AAC9B,SAAK,WAAW,EAAE;AAClB,SAAK,WAAW,KAAK,SAAS,OAAO,CAAAE,OAAKA,GAAE,OAAO,EAAE;AACrD,SAAK,SAAS,OAAO,EAAE;AAGvB,SAAK,UAAU,gBAAgB,EAAE;AAGjC,SAAK,UAAU,cAAc,EAAE;AAC/B,SAAK,gBAAgB,OAAO,EAAE;AAC9B,0BAAsB,KAAK,QAAQ;AAAA,EACrC;AAAA,EAEA,QACE,WACA,EAAE,iBAAiB,MAAM,IAA8B,CAAC,GAClD;AACN,UAAM,UAAU,KAAK,SAAS,KAAK,CAAAA,OAAKA,GAAE,OAAO,SAAS;AAE1D,QAAI,CAAC,QAAS;AAEd,QAAI,QAAQ,WAAW,eAAe,mBAAmB,OAAO;AAC9D;AAAA,IACF;AAKA,SAAK,UAAU,UAAU,SAAS;AAElC,QAAI,QAAQ,gBAAgB,SAAS;AAEnC,WAAK,KAAK,aAAa,WAAW,OAAO;AACzC;AAAA,IACF;AAKA,QAAI,KAAK,SAAS,IAAI,SAAS,GAAG;AAEhC,WAAK,aAAa,WAAW,WAAW;AACxC;AAAA,IACF;AAEA,UAAM,iBAAgC;AAAA,MACpC,MAAM,QAAQ;AAAA,MACd,MAAM,QAAQ;AAAA,MACd,UAAU,QAAQ;AAAA,MAClB,cAAc,MAAM;AAAA,IACtB;AAEA,QAAI,QAAQ,eAAe,YAAY;AACrC,UAAI,CAAC,QAAQ,UAAU;AACrB,aAAK,aAAa,WAAW,SAAS,sBAAsB;AAC5D;AAAA,MACF;AACA,qBAAe,WAAW,QAAQ;AAAA,IACpC,OAAO;AACL,UAAI,QAAQ,gBAAgB;AAC1B,YAAI;AACF,yBAAe,aAAaJ,IAAG,aAAa,QAAQ,cAAc;AAAA,QACpE,QAAQ;AACN,eAAK;AAAA,YACH;AAAA,YACA;AAAA,YACA,uBAAuB,QAAQ,cAAc;AAAA,UAC/C;AACA;AAAA,QACF;AAAA,MACF,OAAO;AACL,cAAM,aAAa,eAAe;AAClC,YAAI,CAAC,YAAY;AACf,eAAK;AAAA,YACH;AAAA,YACA;AAAA,YACA;AAAA,UACF;AACA;AAAA,QACF;AACA,uBAAe,aAAa;AAAA,MAC9B;AAAA,IACF;AAEA,SAAK,aAAa,WAAW,YAAY;AACzC,UAAM,SAAS,IAAI,OAAO;AAC1B,SAAK,SAAS,IAAI,WAAW,EAAE,QAAQ,QAAQ,KAAK,CAAC;AAErD,WAAO,GAAG,SAAS,YAAY;AAI7B,YAAM,KAAK;AAAA,QACT;AAAA,QACA,QAAQ,uBAAuB;AAAA,MACjC;AAEA,YAAM,iBAAiB,KAAK,SAAS,IAAI,SAAS;AAClD,UAAI,CAAC,kBAAkB,eAAe,WAAW,OAAQ;AACzD,UAAI,KAAK,mBAAmB,IAAI,SAAS,GAAG;AAC1C,aAAK,mBAAmB,OAAO,SAAS;AACxC;AAAA,MACF;AACA,YAAM,SAAS,KAAK,SAAS,IAAI,SAAS;AAG1C,YAAM;AAAA,QACJ,MAAM;AAAA,QACN,MAAM;AAAA,QACN;AAAA,MACF,IAAI;AAAA,QACF;AAAA,QACA,QAAQ,qBAAqB;AAAA,QAC7B,QAAQ,qBAAqB;AAAA,MAC/B;AACA,UAAI,WAAW,wBAAwB;AACrC,eAAO;AAAA,UACL,kDAAmC,SAAS,WAAW,QAAQ,IAAI,IAAI,QAAQ,IAAI,oBAAe,SAAS,IAAI,SAAS;AAAA,QAC1H;AAAA,MACF;AACA,aAAO;AAAA,QACL,2BAA2B,SAAS,IAAI,SAAS,IAAI,SAAS,WAAW,MAAM;AAAA,MACjF;AAEA,aAAO;AAAA,QACL,EAAE,MAAM,kBAAkB,MAAM,WAAW,MAAM,UAAU;AAAA,QAC3D,CAAC,KAAK,WAAW;AACf,cAAI,KAAK;AACP,iBAAK,aAAa,WAAW,SAAS,IAAI,OAAO;AACjD,iBAAK,SAAS,OAAO,SAAS;AAC9B;AAAA,UACF;AAEA,gBAAM,UAAU,KAAK,SAAS,IAAI,SAAS;AAC3C,cAAI,QAAS,SAAQ,SAAS;AAE9B,eAAK,aAAa,WAAW,WAAW;AACxC,eAAK,iBAAiB,SAAS;AAI/B,eAAK,UAAU;AAAA,YACb,MAAM;AAAA,YACN;AAAA,YACA,MAAM;AAAA,YACN,MAAM;AAAA,UACR,CAAC;AAID,cAAI,UAAU;AACd,cAAI,cAAoD;AAKxD,gBAAM,gBAAgB,IAAI,cAAc,MAAM;AAC9C,gBAAM,gBAAgB,IAAI,cAAc,MAAM;AAI9C,gBAAM,aAAa,KAAK,sBAAsB,SAAS;AAEvD,gBAAM,YAAY,MAAM;AACtB,iBAAK,UAAU,EAAE,MAAM,iBAAiB,WAAW,MAAM,QAAQ,CAAC;AAIlE,gBAAI,KAAK,iBAAiB;AACxB,mBAAK,UAAU;AAAA,gBAAU;AAAA,gBAAW,WAClC,KAAK,gBAAiB,WAAW,KAAK;AAAA,cACxC;AAAA,YACF;AACA,sBAAU;AACV,0BAAc;AAAA,UAChB;AAEA,iBAAO,GAAG,QAAQ,CAAC,SAAiB;AAClC,kBAAM,MAAM,cAAc,MAAM,IAAI;AACpC,iBAAK,UAAU,KAAK,SAAS;AAC7B,uBAAW,OAAO,GAAG;AACrB,iBAAK,UAAU,cAAc,WAAW,UAAU;AAClD,iBAAK,UAAU,eAAe,SAAS;AAGvC,iBAAK,UAAU,UAAU,WAAW,GAAG;AACvC,uBAAW;AACX,gBAAI,CAAC,YAAa,eAAc,WAAW,WAAW,EAAE;AAAA,UAC1D,CAAC;AAED,iBAAO,OAAO,GAAG,QAAQ,CAAC,SAAiB;AACzC,kBAAM,MAAM,cAAc,MAAM,IAAI;AACpC,uBAAW,OAAO,GAAG;AACrB,iBAAK,UAAU,cAAc,WAAW,UAAU;AAClD,iBAAK,UAAU,eAAe,SAAS;AAEvC,iBAAK,UAAU,UAAU,WAAW,GAAG;AACvC,uBAAW;AACX,gBAAI,CAAC,YAAa,eAAc,WAAW,WAAW,EAAE;AAAA,UAC1D,CAAC;AAED,iBAAO,GAAG,SAAS,MAAM;AAGvB,mBAAO,KAAK,0BAA0B,SAAS,EAAE;AAIjD,iBAAK,UAAU,iBAAiB,SAAS;AACzC,iBAAK,UAAU,UAAU,SAAS;AAElC,kBAAM,OAAO,cAAc,IAAI,IAAI,cAAc,IAAI;AACrD,gBAAI,KAAM,YAAW;AACrB,gBAAI,WAAW,aAAa;AAC1B,kBAAI,aAAa;AACf,6BAAa,WAAW;AACxB,8BAAc;AAAA,cAChB;AACA,kBAAI;AACF,qBAAK,UAAU;AAAA,kBACb,MAAM;AAAA,kBACN;AAAA,kBACA,MAAM;AAAA,gBACR,CAAC;AACH,wBAAU;AAAA,YACZ;AACA,iBAAK,iBAAiB,SAAS;AAC/B,iBAAK,aAAa,WAAW,cAAc;AAC3C,iBAAK,SAAS,OAAO,SAAS;AAAA,UAChC,CAAC;AAAA,QACH;AAAA,MACF;AAAA,IACF,CAAC;AAED,WAAO,GAAG,SAAS,SAAO;AACxB,WAAK,aAAa,WAAW,SAAS,IAAI,OAAO;AACjD,WAAK,SAAS,OAAO,SAAS;AAAA,IAChC,CAAC;AAED,WAAO,QAAQ,cAAc;AAAA,EAC/B;AAAA,EAEA,MAAc,aACZ,WACA,SACe;AAIf,QAAI,KAAK,cAAc,IAAI,SAAS,GAAG;AACrC,WAAK,aAAa,WAAW,WAAW;AACxC;AAAA,IACF;AAEA,SAAK,aAAa,WAAW,YAAY;AAEzC,QAAI;AACF,YAAM,YAAY,QAAQ;AAC1B,YAAM,WAAW,QAAQ,IAAI;AAC7B,YAAM,QACJ,aAAa,UAAU,SAAS,IAC5B,YACA,YAAY,SAAS,SAAS,IAC5B,WACA,QAAQ,aAAa,UAClB,QAAQ,IAAI,WAAW,YACxB;AAEV,YAAM,MAA8B;AAAA,QAClC,MAAM;AAAA,QACN,SAASF,IAAG,SAAS,EAAE;AAAA,QACvB,MAAMA,IAAG,SAAS,EAAE;AAAA,QACpB,OAAO;AAAA,QACP,GAAI,QAAQ;AAAA,QACZ,MAAM;AAAA,QACN,WAAW;AAAA,MACb;AACA,UAAI,QAAQ,aAAa,SAAS;AAChC,cAAM,aAAa,MAAM,YAAY;AACrC,YACE,WAAW,SAAS,MAAM,KAC1B,eAAe,SACf,eAAe,WACf;AACA,gBAAM,aAAa;AACnB,cAAI,OAAO,GAAG,UAAU,IAAI,IAAI,QAAQ,IAAI,QAAQ,EAAE;AAAA,QACxD;AAAA,MACF,OAAO;AACL,cAAM,eAAe;AACrB,YAAI,OAAO,GAAG,IAAI,QAAQ,EAAE,IAAI,YAAY;AAAA,MAC9C;AAIA,YAAM,KAAK;AAAA,QACT;AAAA,QACA,QAAQ,uBAAuB;AAAA,MACjC;AAEA,UAAI,KAAK,mBAAmB,IAAI,SAAS,GAAG;AAC1C,aAAK,mBAAmB,OAAO,SAAS;AACxC;AAAA,MACF;AACA,YAAM,SAAS,KAAK,SAAS,IAAI,SAAS;AAE1C,YAAM;AAAA,QACJ,MAAM;AAAA,QACN,MAAM;AAAA,QACN,QAAQ;AAAA,MACV,IAAI;AAAA,QACF;AAAA,QACA,QAAQ,qBAAqB;AAAA,QAC7B,QAAQ,qBAAqB;AAAA,MAC/B;AACA,UAAI,gBAAgB,wBAAwB;AAC1C,eAAO;AAAA,UACL,oDAAqC,SAAS,WAAW,QAAQ,IAAI,IAAI,QAAQ,IAAI,oBAAe,SAAS,IAAI,SAAS;AAAA,QAC5H;AAAA,MACF;AASA,YAAM,eAAe,QAAQ,UAAU,KAAK;AAC5C,UAAI,eAAe,gBAAgB;AACnC,UAAI,iBAAgC;AACpC,UAAI,gBAAgB,CAAC,cAAc,YAAY,GAAG;AAChD,uBAAe;AACf,eAAO;AAAA,UACL,uCAA6B,SAAS,QAAQ,YAAY,mBAAc,OAAO;AAAA,QACjF;AACA,yBACE;AAAA,wDAA0B,YAAY;AAAA,0BACrB,OAAO;AAAA;AAAA,MAC5B;AAEA,YAAM,aAAa,KAAK,kBAAkB,OAAO;AACjD,YAAM,OAAO;AAAA,QACX;AAAA,QACA,MAAM,QAAQ;AAAA,QACd,KAAK;AAAA,QACL;AAAA,QACA,MAAM;AAAA,QACN,MAAM;AAAA,MACR;AACA,YAAM,OAAO,MAAM,WAAW,eAAe,WAAW,IAAI;AAC5D,aAAO;AAAA,QACL,6BAA6B,SAAS,IAAI,SAAS,IAAI,SAAS,QAAQ,WAAW,IAAI,WAAW,WAAW;AAAA,MAC/G;AAEA,YAAM,OAAO,cAAc;AAAA,QACzB,OAAO,KAAK;AAAA,QACZ,MAAM,KAAK;AAAA,QACX,MAAM,KAAK;AAAA,QACX,MAAM,KAAK;AAAA,QACX,KAAK,KAAK;AAAA,QACV,KAAK,KAAK;AAAA,MACZ,CAAC;AACA,MAAC,KAA+B,WAAW,KAAK,IAAI;AAErD,WAAK,cAAc,IAAI,WAAW,IAAI;AACtC,WAAK,kBAAkB,OAAO,SAAS;AACvC,WAAK,aAAa,WAAW,WAAW;AACxC,WAAK,iBAAiB,SAAS;AAG/B,WAAK,UAAU;AAAA,QACb,MAAM;AAAA,QACN;AAAA,QACA,MAAM;AAAA,QACN,MAAM;AAAA,MACR,CAAC;AAID,UAAI,UAAU;AACd,UAAI,cAAoD;AAGxD,YAAM,aAAa,KAAK,sBAAsB,SAAS;AAKvD,UAAI,gBAAgB;AAClB,mBAAW,OAAO,cAAc;AAChC,aAAK,UAAU,EAAE,MAAM,iBAAiB,WAAW,MAAM,eAAe,CAAC;AAAA,MAC3E;AAEA,YAAM,YAAY,MAAM;AACtB,aAAK,UAAU,EAAE,MAAM,iBAAiB,WAAW,MAAM,QAAQ,CAAC;AAElE,YAAI,KAAK,iBAAiB;AACxB,eAAK,UAAU;AAAA,YAAU;AAAA,YAAW,WAClC,KAAK,gBAAiB,WAAW,KAAK;AAAA,UACxC;AAAA,QACF;AACA,kBAAU;AACV,sBAAc;AAAA,MAChB;AAEA,WAAK,OAAO,CAAC,SAAiB;AAC5B,aAAK,UAAU,MAAM,SAAS;AAC9B,mBAAW,OAAO,IAAI;AACtB,aAAK,UAAU,cAAc,WAAW,UAAU;AAClD,aAAK,UAAU,eAAe,SAAS;AAEvC,aAAK,UAAU,UAAU,WAAW,IAAI;AACxC,mBAAW;AACX,YAAI,CAAC,YAAa,eAAc,WAAW,WAAW,EAAE;AAAA,MAC1D,CAAC;AAED,WAAK,OAAO,CAAC,UAAkB;AAK7B,cAAM,UAAW,KAA+B,YAAY,KAAK,IAAI;AACrE,cAAM,WAAW,KAAK,IAAI,IAAI;AAC9B,eAAO;AAAA,UACL,4BAA4B,SAAS,SAAS,KAAK,WAAW,QAAQ;AAAA,QACxE;AAGA,aAAK,UAAU,iBAAiB,SAAS;AACzC,aAAK,UAAU,UAAU,SAAS;AAClC,YAAI,aAAa;AACf,uBAAa,WAAW;AACxB,wBAAc;AAAA,QAChB;AACA,aAAK,iBAAiB,SAAS;AAC/B,aAAK,cAAc,OAAO,SAAS;AAMnC,cAAMO,cAAa,KAAK,4BAA4B,SAAS;AAC7D,YAAIA,aAAY;AACd,cAAI;AACF,iBAAK,QAAQ,QAAQA,YAAW,KAAK,SAAS,CAAC,EAAE;AAAA,cAC/C,MAAM;AAAA,YACR;AAAA,UACF,QAAQ;AAAA,UAER;AAAA,QACF;AAEA,YAAI,KAAK,mBAAmB,IAAI,SAAS,GAAG;AAC1C,eAAK,mBAAmB,OAAO,SAAS;AACxC;AAAA,QACF;AAEA,cAAM,iBAAiB,KAAK,SAAS,KAAK,CAAAD,OAAKA,GAAE,OAAO,SAAS;AACjE,YAAI,CAAC,eAAgB;AAErB,cAAM,YAAY,KAAK,kBAAkB,IAAI,SAAS,KAAK,KAAK;AAChE,YAAI,YAAY,GAAG;AACjB,eAAK,kBAAkB,OAAO,SAAS;AACvC,eAAK;AAAA,YACH;AAAA,YACA;AAAA,YACA,wCAAe,KAAK;AAAA,UACtB;AACA;AAAA,QACF;AACA,aAAK,kBAAkB,IAAI,WAAW,QAAQ;AAC9C,aAAK,aAAa,WAAW,cAAc;AAC3C,mBAAW,MAAM;AACf,gBAAMA,KAAI,KAAK,SAAS,KAAK,CAAAA,OAAKA,GAAE,OAAO,SAAS;AACpD,cAAIA,GAAG,MAAK,KAAK,aAAa,WAAWA,EAAC;AAAA,QAC5C,GAAG,GAAI;AAAA,MACT,CAAC;AAAA,IACH,SAAS,KAAK;AACZ,YAAM,YAAY,QAAQ;AAC1B,YAAM,WAAW,QAAQ,IAAI;AAC7B,YAAM,cACJ,aACA,aACC,QAAQ,aAAa,UAAU,YAAY;AAC9C,aAAO,MAAM,2BAAiB,WAAW,MAAO,IAAc,OAAO,EAAE;AACvE,WAAK;AAAA,QACH;AAAA,QACA;AAAA,QACA,2BAAiB,WAAW,MAAO,IAAc,OAAO;AAAA,MAC1D;AAAA,IACF;AAAA,EACF;AAAA,EAEA,WAAW,WAAyB;AAClC,SAAK,iBAAiB,SAAS;AAG/B,SAAK,UAAU,aAAa,WAAW,SAAS;AAGhD,SAAK,0BAA0B,SAAS;AAKxC,UAAM,OAAO,KAAK,mBAAmB,IAAI,SAAS;AAClD,UAAM,YAAY,KAAK,cAAc,IAAI,SAAS;AAClD,UAAM,UAAU,KAAK,SAAS,IAAI,SAAS;AAC3C,UAAM,cACJ,SAAS,UAAa,cAAc,UAAa,YAAY;AAE/D,QAAI,aAAa;AACf,WAAK,mBAAmB,IAAI,SAAS;AAAA,IACvC;AACA,QAAI,KAAM,MAAK;AAKf,SAAK,YAAY,OAAO,SAAS;AACjC,QAAI;AACF,WAAK,UAAU,OAAO,SAAS;AAAA,IACjC,QAAQ;AAAA,IAER;AAEA,QAAI,WAAW;AACb,gBAAU,KAAK;AACf,WAAK,cAAc,OAAO,SAAS;AACnC,WAAK,aAAa,WAAW,cAAc;AAC3C;AAAA,IACF;AAEA,QAAI,SAAS;AACX,cAAQ,QAAQ,IAAI;AACpB,cAAQ,OAAO,IAAI;AACnB,WAAK,SAAS,OAAO,SAAS;AAAA,IAChC;AACA,SAAK,aAAa,WAAW,cAAc;AAAA,EAC7C;AAAA;AAAA;AAAA,EAKA,WAAW,WAA4B;AACrC,WAAO,KAAK,cAAc,IAAI,SAAS,KAAK,KAAK,SAAS,IAAI,SAAS;AAAA,EACzE;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAaA,oBAA4B;AAC1B,WAAO,KAAK,SAAS,OAAO,KAAK,cAAc;AAAA,EACjD;AAAA;AAAA,EAGA,cAAc,WAA+C;AAC3D,WAAO,KAAK,YAAY,IAAI,SAAS,KAAK;AAAA,EAC5C;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,WAAW,WAAyB;AAClC,QAAI,KAAK,mBAAmB,IAAI,SAAS,GAAG;AAC1C,aAAO;AAAA,QACL,wDAAwD,SAAS;AAAA,MACnE;AACA;AAAA,IACF;AACA,SAAK,mBAAmB,IAAI,SAAS;AACrC,QAAI;AACF,aAAO,KAAK,oCAAoC,SAAS,EAAE;AAC3D,YAAM,UAAU,KAAK,SAAS,KAAK,CAAAA,OAAKA,GAAE,OAAO,SAAS;AAC1D,UAAI,CAAC,QAAS;AAGd,YAAM,YAAY,KAAK,cAAc,IAAI,SAAS;AAClD,UAAI,WAAW;AACb,kBAAU,KAAK;AACf,aAAK,cAAc,OAAO,SAAS;AAAA,MACrC;AACA,YAAM,UAAU,KAAK,SAAS,IAAI,SAAS;AAC3C,UAAI,SAAS;AACX,gBAAQ,QAAQ,IAAI;AACpB,gBAAQ,OAAO,IAAI;AACnB,aAAK,SAAS,OAAO,SAAS;AAAA,MAChC;AAGA,WAAK,YAAY,OAAO,SAAS;AACjC,UAAI;AACF,aAAK,UAAU,OAAO,SAAS;AAAA,MACjC,QAAQ;AAAA,MAER;AAGA,WAAK,UAAU,aAAa,SAAS;AAGrC,cAAQ,SAAS;AACjB,WAAK,QAAQ,WAAW,EAAE,gBAAgB,KAAK,CAAC;AAAA,IAClD,UAAE;AAEA,iBAAW,MAAM,KAAK,mBAAmB,OAAO,SAAS,GAAG,GAAG;AAAA,IACjE;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EASQ,sBAAsB,WAAwC;AACpE,QAAI,MAAM,KAAK,YAAY,IAAI,SAAS;AACxC,QAAI,CAAC,KAAK;AAKR,YAAM,UAAU,KAAK,SAAS,KAAK,CAAAA,OAAKA,GAAE,OAAO,SAAS;AAC1D,YAAM,MAAM,SAAS,mBAAmB;AAExC,YAAM,WAAW,KAAK,UAAU,YAAY,SAAS;AACrD,UAAI,UAAU;AACZ,YAAI;AACF,gBAAM,oBAAoB,YAAY,KAAK,QAAQ;AAAA,QACrD,SAAS,KAAK;AAEZ,iBAAO;AAAA,YACL,+CAA+C,SAAS,KAAM,IAAc,OAAO;AAAA,UACrF;AACA,gBAAM,IAAI,oBAAoB,GAAG;AAAA,QACnC;AAAA,MACF,OAAO;AACL,cAAM,IAAI,oBAAoB,GAAG;AAAA,MACnC;AACA,WAAK,YAAY,IAAI,WAAW,GAAG;AAAA,IACrC;AACA,WAAO;AAAA,EACT;AAAA,EAEA,UAAU,WAAmB,MAAoB;AAC/C,UAAM,YAAY,KAAK,cAAc,IAAI,SAAS;AAClD,QAAI,WAAW;AACb,gBAAU,MAAM,IAAI;AACpB,WAAK,UAAU,YAAY,SAAS;AACpC;AAAA,IACF;AAEA,UAAM,UAAU,KAAK,SAAS,IAAI,SAAS;AAC3C,QAAI,SAAS,QAAQ;AACnB,cAAQ,OAAO,MAAM,IAAI;AACzB,WAAK,UAAU,YAAY,SAAS;AAAA,IACtC;AAAA,EACF;AAAA,EAEA,OACE,WACA,MACA,MACA,SAAmD,WAC7C;AACN,UAAM,WAAW,UAAU,IAAI;AAC/B,UAAM,WAAW,UAAU,IAAI;AAK/B,QAAI,CAAC,KAAK,UAAU,aAAa,WAAW,MAAM,GAAG;AACnD,UAAI,QAAQ,IAAI,aAAa,eAAe;AAC1C,eAAO;AAAA,UACL,sDAAsD,SAAS,WAAW,MAAM,IAAI,QAAQ,IAAI,QAAQ;AAAA,QAC1G;AAAA,MACF;AACA;AAAA,IACF;AAIA,QAAI,WAAW,WAAW;AACxB,WAAK,gBAAgB,IAAI,WAAW,EAAE,MAAM,UAAU,MAAM,SAAS,CAAC;AAAA,IACxE;AAEA,UAAM,OAAO,KAAK,SAAS,IAAI,SAAS;AAExC,UAAM,SAAS,MAAM,SAAS,YAAY,MAAM,SAAS;AAEzD,QAAI,QAAQ,IAAI,aAAa,eAAe;AAC1C,aAAO,KAAK,gBAAgB;AAAA,QAC1B;AAAA,QACA;AAAA,QACA,KAAK,EAAE,MAAM,KAAK;AAAA,QAClB,MAAM,EAAE,MAAM,UAAU,MAAM,SAAS;AAAA,QACvC;AAAA,QACA;AAAA,MACF,CAAC;AAAA,IACH;AAIA,QAAI,CAAC,QAAQ;AACX,WAAK,SAAS,IAAI,WAAW,EAAE,MAAM,UAAU,MAAM,SAAS,CAAC;AAC/D,aAAO;AAAA,QACL,wBAAwB,SAAS,WAAW,MAAM,IAAI,QAAQ,IAAI,QAAQ,UAAU,MAAM,QAAQ,GAAG,IAAI,MAAM,QAAQ,GAAG;AAAA,MAC5H;AAKA,WAAK,UAAU,SAAS,WAAW,UAAU,QAAQ;AAAA,IACvD;AAKA,UAAM,OAAO,KAAK,mBAAmB,IAAI,SAAS;AAClD,QAAI,KAAM,MAAK;AAEf,QAAI,QAAQ;AAIV;AAAA,IACF;AAEA,UAAM,YAAY,KAAK,cAAc,IAAI,SAAS;AAClD,QAAI,WAAW;AACb,gBAAU,OAAO,UAAU,QAAQ;AAAA,IACrC,OAAO;AACL,YAAM,UAAU,KAAK,SAAS,IAAI,SAAS;AAC3C,eAAS,QAAQ,UAAU,UAAU,UAAU,GAAG,CAAC;AAAA,IACrD;AAIA,UAAM,MAAM,KAAK,4BAA4B,SAAS;AACtD,QAAI,KAAK;AAEP,WAAK,IAAI,OAAO,WAAW,UAAU,UAAU,MAAM,EAAE,MAAM,MAAM;AAAA,MAEnE,CAAC;AAAA,IACH;AAKA,SAAK,UAAU,kBAAkB,WAAW,UAAU,QAAQ;AAC9D,SAAK,UAAU;AAAA,MACb,MAAM;AAAA,MACN;AAAA,MACA,MAAM;AAAA,MACN,MAAM;AAAA,IACR,CAAC;AAAA,EACH;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAUQ,4BACN,YAC4B;AAC5B,QAAI,KAAK,gBAAgB,SAAS,EAAG,QAAO;AAC5C,WAAO,KAAK,gBAAgB,OAAO,EAAE,KAAK,EAAE;AAAA,EAC9C;AAAA;AAAA;AAAA;AAAA;AAAA,EAMQ,kBAAkB,SAAkC;AAC1D,UAAM,UAAU,0BAA0B,OAAO;AACjD,UAAM,QAAQ,QAAQ,oBAAoB;AAC1C,UAAM,WAAW,GAAG,OAAO,UAAU,KAAK;AAC1C,UAAM,SAAS,KAAK,gBAAgB,IAAI,QAAQ;AAChD,QAAI,OAAQ,QAAO;AACnB,UAAM,MAAM,qBAAqB,OAAO;AACxC,SAAK,gBAAgB,IAAI,UAAU,GAAG;AACtC,WAAO;AAAA,EACT;AAAA;AAAA;AAAA;AAAA;AAAA,EAMQ,mBACN,WACA,YAAY,KACG;AACf,QAAI,KAAK,SAAS,IAAI,SAAS,EAAG,QAAO,QAAQ,QAAQ;AACzD,UAAM,OAAO,KAAK,mBAAmB,IAAI,SAAS;AAClD,QAAI,KAAM,MAAK;AACf,WAAO,IAAI,QAAc,aAAW;AAClC,UAAI,OAAO;AACX,YAAM,SAAS,MAAM;AACnB,YAAI,KAAM;AACV,eAAO;AACP,aAAK,mBAAmB,OAAO,SAAS;AACxC,qBAAa,KAAK;AAClB,gBAAQ;AAAA,MACV;AACA,YAAM,QAAQ,WAAW,QAAQ,SAAS;AAC1C,WAAK,mBAAmB,IAAI,WAAW,MAAM;AAAA,IAC/C,CAAC;AAAA,EACH;AAAA,EAEA,gBAAgB,WAAyB;AACvC,UAAM,UAAU,KAAK,SAAS,KAAK,CAAAA,OAAKA,GAAE,OAAO,SAAS;AAC1D,QAAI,CAAC,QAAS;AACd,QAAI,QAAQ,cAAc,QAAQ;AAChC,WAAK;AAAA,QACH;AAAA,QACA,QAAQ,aAAa,IAAI,aAAW,QAAQ,KAAK,CAAC,EAAE,KAAK,MAAM,IAAI;AAAA,MACrE;AAAA,IACF;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAoBA,gBAAgB,WAAqC;AACnD,WAAO,KAAK,SAAS,KAAK,CAAAA,OAAKA,GAAE,OAAO,SAAS,GAAG,gBAAgB;AAAA,EACtE;AAAA,EAEA,MAAM,+BAA+B,WAAkC;AACrE,UAAM,OAAO,KAAK,cAAc,IAAI,SAAS;AAC7C,UAAM,UAAU,KAAK,SAAS,KAAK,CAAAA,OAAKA,GAAE,OAAO,SAAS;AAC1D,QAAI,CAAC,QAAS;AAEd,QAAI,WAA6B;AACjC,QAAI,QAAQ,OAAO,KAAK,QAAQ,UAAU;AACxC,iBAAW,MAAM,mBAAmB,KAAK,GAAG;AAAA,IAC9C;AACA,QAAI,QAAQ,iBAAiB,UAAU;AACrC,aAAO;AAAA,QACL,0CAA0C,SAAS,IAAI,QAAQ,gBAAgB,MAAM,WAAM,QAAQ;AAAA,MACrG;AACA,cAAQ,eAAe;AACvB,WAAK,kBAAkB;AAIvB,UAAI,aAAa,UAAU;AACzB,aAAK,uBAAuB,SAAS;AAAA,MACvC;AAAA,IACF;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAUA,aAAa,WAAyB;AACpC,WAAO,KAAK,kCAAkC,SAAS,EAAE;AAEzD,SAAK,UAAU,WAAW,GAAM;AAChC,eAAW,MAAM,KAAK,UAAU,WAAW,GAAM,GAAG,GAAG;AAAA,EACzD;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EASA,uBAAuB,WAAyB;AAE9C,SAAK,0BAA0B,SAAS;AAExC,UAAM,MAAM,KAAK,gBAAgB,SAAS;AAC1C,UAAM,OAAO,kBAAkB,GAAG;AAIlC,UAAM,aAAa,KAAK,wBAAwB,SAAS,KAAK,CAAC;AAC/D,UAAM,WAAW,qBAAqB,MAAM,YAAY,KAAK,IAAI,CAAC;AAClE,WAAO;AAAA,MACL,oCAAoC,SAAS,QAAQ,GAAG,SAAS,KAAK,MAAM,YAAY,WAAW,MAAM,UAAU,SAAS,MAAM;AAAA,IACpI;AACA,SAAK,UAAU,EAAE,MAAM,kBAAkB,WAAW,KAAK,SAAS,CAAC;AAKnE,UAAM,eAAe,KAAK,SAAS,KAAK,CAAAA,OAAKA,GAAE,OAAO,SAAS,GAAG;AAClE,QAAI,SAAS,WAAW,KAAK,iBAAiB,UAAU;AACtD,YAAM,SAAS,sBAAsB;AAAA,QACnC,UAAU;AAAA,QACV,QAAQ,MAAM,kBAAkB,KAAK,gBAAgB,SAAS,CAAC;AAAA,QAC/D,YAAY,cAAY;AACtB,gBAAM,cAAc,KAAK,gBAAgB,SAAS;AAClD,iBAAO;AAAA,YACL,2CAA2C,SAAS,UAAU,SAAS,MAAM;AAAA,UAC/E;AACA,eAAK,UAAU;AAAA,YACb,MAAM;AAAA,YACN;AAAA,YACA,KAAK;AAAA,YACL,UAAU;AAAA,UACZ,CAAC;AACD,eAAK,0BAA0B,OAAO,SAAS;AAAA,QACjD;AAAA,MACF,CAAC;AACD,WAAK,0BAA0B,IAAI,WAAW,MAAM;AAAA,IACtD;AAAA,EACF;AAAA;AAAA,EAGQ,0BAA0B,WAAyB;AACzD,UAAM,SAAS,KAAK,0BAA0B,IAAI,SAAS;AAC3D,QAAI,QAAQ;AACV,aAAO;AACP,WAAK,0BAA0B,OAAO,SAAS;AAAA,IACjD;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EASA,2BAA2B,WAAmB,WAAyB;AACrE,UAAM,MAAM,KAAK,gBAAgB,SAAS;AAC1C,UAAM,WAAW,sBAAsB,KAAK,SAAS;AACrD,WAAO;AAAA,MACL,8CAA8C,SAAS,YAAY,SAAS,UAAU,SAAS,MAAM;AAAA,IACvG;AACA,SAAK,UAAU,EAAE,MAAM,4BAA4B,WAAW,WAAW,SAAS,CAAC;AAAA,EACrF;AAAA,EAEA,UAAgB;AACd,SAAK,aAAa;AAElB,eAAW,CAAC,EAAE,KAAK,KAAK,cAAc;AACpC,WAAK,iBAAiB,EAAE;AAAA,IAC1B;AAGA,eAAW,CAAC,EAAE,MAAM,KAAK,KAAK,2BAA2B;AACvD,aAAO;AAAA,IACT;AACA,SAAK,0BAA0B,MAAM;AAErC,eAAW,CAAC,EAAE,KAAK,KAAK,eAAe;AACrC,WAAK,mBAAmB,IAAI,EAAE;AAAA,IAChC;AAIA,eAAW,CAAC,EAAE,IAAI,KAAK,KAAK,oBAAoB;AAC9C,WAAK;AAAA,IACP;AACA,SAAK,mBAAmB,MAAM;AAE9B,eAAW,CAAC,EAAE,IAAI,KAAK,KAAK,eAAe;AACzC,WAAK,KAAK;AAAA,IACZ;AACA,SAAK,cAAc,MAAM;AAEzB,eAAW,CAAC,EAAE,KAAK,KAAK,UAAU;AAChC,WAAK,WAAW,EAAE;AAAA,IACpB;AAIA,QAAI;AACF,WAAK,UAAU,aAAa,KAAK,WAAW;AAAA,IAC9C,QAAQ;AAAA,IAER;AACA,SAAK,YAAY,MAAM;AACvB,SAAK,mBAAmB,MAAM;AAE9B,SAAK,UAAU,QAAQ;AAEvB,SAAK,UAAU,QAAQ;AAIvB,eAAW,OAAO,KAAK,gBAAgB,OAAO,GAAG;AAC/C,WAAK,IAAI,UAAU,EAAE,MAAM,MAAM,MAAS;AAAA,IAC5C;AACA,SAAK,gBAAgB,MAAM;AAAA,EAC7B;AAAA,EAEQ,UAAU,MAAc,WAAyB;AACvD,UAAM,QAAQ,KAAK,MAAM,8CAA8C;AACvE,QAAI,OAAO;AACT,WAAK,OAAO,IAAI,WAAW,mBAAmB,MAAM,CAAC,CAAC,CAAC;AAAA,IACzD;AAAA,EACF;AAAA,EAEQ,iBAAiB,WAAyB;AAChD,SAAK,iBAAiB,SAAS;AAC/B,QAAI,CAAC,KAAK,YAAY,IAAI,SAAS,GAAG;AACpC,WAAK,YAAY,IAAI,WAAW,OAAO,CAAC,CAAC;AAAA,IAC3C;AACA,UAAM,QAAQ,KAAK,YAAY,IAAI,SAAS;AAI5C,QAAI,OAAO;AACX,UAAM,QAAQ,YAAY,MAAM;AAC9B,UAAI,EAAE,OAAO,0BAA0B;AACrC,eAAO;AACP,cAAM,MAAM,KAAK,iBAAiB,WAAW,EAAE,MAAM,SAAS,CAAC,CAAC,EAAE;AAAA,UAChE,MAAM;AAAA,UAAC;AAAA,QACT;AAAA,MACF;AAEA,WAAK,KAAK,+BAA+B,SAAS,EAAE,MAAM,MAAM;AAAA,MAAC,CAAC;AAAA,IACpE,GAAG,GAAI;AACP,SAAK,aAAa,IAAI,WAAW,KAAK;AAAA,EACxC;AAAA,EAEQ,iBAAiB,WAAyB;AAChD,UAAM,QAAQ,KAAK,aAAa,IAAI,SAAS;AAC7C,QAAI,OAAO;AACT,oBAAc,KAAK;AACnB,WAAK,aAAa,OAAO,SAAS;AAAA,IACpC;AACA,SAAK,YAAY,OAAO,SAAS;AAAA,EACnC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAYA,gBAAgB,WAA2B;AACzC,UAAM,UAAU,KAAK,OAAO,IAAI,SAAS;AACzC,QAAI,QAAS,QAAO;AACpB,UAAM,UAAU,KAAK,SAAS,KAAK,CAAAA,OAAKA,GAAE,OAAO,SAAS;AAC1D,QAAI,SAAS,SAAU,QAAO,QAAQ;AACtC,QAAI;AACF,YAAM,UAAU,QAAQ,IAAI;AAC5B,UAAI,QAAS,QAAO;AAAA,IACtB,QAAQ;AAAA,IAER;AACA,WAAO;AAAA,EACT;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAcA,wBAAwB,KAA4B;AAClD,UAAM,SAAS,aAAa,GAAG;AAC/B,eAAW,WAAW,KAAK,UAAU;AACnC,UAAI,QAAQ,gBAAgB,MAAO;AACnC,UAAI,aAAa,KAAK,gBAAgB,QAAQ,EAAE,CAAC,MAAM,QAAQ;AAC7D,eAAO,QAAQ;AAAA,MACjB;AAAA,IACF;AACA,WAAO;AAAA,EACT;AAAA;AAAA,EAGA,MAAc,mBAAmB,WAA2C;AAC1E,QAAI;AACF,YAAM,MAAM,UAAU,EAAE,SAAS,UAAU,CAAC;AAC5C,YAAM,OAAO,MAAM,IAAI,SAAS,CAAC,iBAAiB,CAAC,GAAG,KAAK;AAC3D,aAAO,OAAO;AAAA,IAChB,QAAQ;AACN,aAAO;AAAA,IACT;AAAA,EACF;AAAA,EAEA,MAAM,iBAAiB,WAAmB,QAAkC;AAC1E,UAAM,UAAU,KAAK,SAAS,KAAK,CAAAA,OAAKA,GAAE,OAAO,SAAS;AAC1D,QAAI,CAAC,QAAS;AAEd,QAAI,QAAQ,gBAAgB,SAAS;AACnC,YAAM,YAAY,KAAK,gBAAgB,SAAS;AAChD,YAAM,UAAU,MAAM,KAAK,mBAAmB,SAAS;AACvD,YAAME,OAAM,WAAW;AACvB,aAAO,KAAK;AAAA,QACV;AAAA,QACA;AAAA,QACAA;AAAA,QACA,YAAY;AAAA,MACd;AAAA,IACF;AAGA,UAAM,MAAM,KAAK,OAAO,IAAI,SAAS,KAAK;AAE1C,QAAI;AAKF,UAAI,OAAO,SAAS,WAAY,gBAAe,OAAO,QAAQ,QAAQ;AACtE,UAAI,OAAO,SAAS,gBAAiB,gBAAe,OAAO,MAAM,MAAM;AACvE,UAAI,OAAO,SAAS,iBAAiB;AACnC,uBAAe,OAAO,QAAQ,QAAQ;AAKtC,cAAM,MAAM,MAAM,KAAK,wBAAwB,WAAW,GAAG;AAC7D,YAAI,OAAO,QAAQ,OAAO,QAAQ;AAChC,iBAAO,KAAK;AAAA,YACV;AAAA,YACA,QAAQ;AAAA,YACR;AAAA,YACA,QAAQ,OAAO;AAAA,YACf,SAAS;AAAA,UACX,CAAC;AACD,eAAK,UAAU;AAAA,YACb,MAAM;AAAA,YACN;AAAA,YACA,QAAQ,OAAO;AAAA,YACf,SAAS;AAAA,YACT,OAAO,0CAA0C,OAAO,MAAM;AAAA,YAC9D,WAAW;AAAA,UACb,CAAC;AACD;AAAA,QACF;AAAA,MACF;AACA,YAAM,MAAM,gBAAgB,QAAQ,GAAG;AACvC,YAAM,SAAS,MAAM,KAAK,QAAQ,WAAW,GAAG;AAChD,UAAI,OAAO,SAAS,UAAU;AAC5B,cAAM,OAAO,KAAK,eAAe,MAAM;AACvC,aAAK,MAAM;AACX,eAAO,KAAK;AAAA,UACV;AAAA,UACA,QAAQ;AAAA,UACR;AAAA,UACA,WAAW,KAAK;AAAA,UAChB,OAAO,KAAK,MAAM;AAAA,QACpB,CAAC;AACD,aAAK,UAAU;AAAA,UACb,MAAM;AAAA,UACN;AAAA,UACA,QAAQ,OAAO;AAAA,UACf,SAAS;AAAA,UACT;AAAA,QACF,CAAC;AAAA,MACH,WAAW,OAAO,SAAS,OAAO;AAChC,cAAM,UAAU,KAAK,YAAY,MAAM;AACvC,cAAM,OAAsB;AAAA,UAC1B,QAAQ;AAAA,UACR,OAAO;AAAA,UACP,QAAQ;AAAA,UACR,UAAU,CAAC;AAAA,UACX,OAAO,CAAC;AAAA,UACR,WAAW;AAAA,UACX,cAAc;AAAA,UACd;AAAA,UACA;AAAA,QACF;AACA,eAAO,KAAK;AAAA,UACV;AAAA,UACA,QAAQ;AAAA,UACR;AAAA,UACA,SAAS,QAAQ;AAAA,QACnB,CAAC;AACD,aAAK,UAAU;AAAA,UACb,MAAM;AAAA,UACN;AAAA,UACA,QAAQ,OAAO;AAAA,UACf,SAAS;AAAA,UACT;AAAA,QACF,CAAC;AAAA,MACH,WAAW,OAAO,SAAS,iBAAiB;AAI1C,YAAI,qBAAqB,KAAK,MAAM,GAAG;AACrC,gBAAM,MAAM,OAAO,KAAK;AACxB,iBAAO,KAAK,EAAE,WAAW,QAAQ,iBAAiB,KAAK,OAAO,IAAI,CAAC;AACnE,eAAK,UAAU;AAAA,YACb,MAAM;AAAA,YACN;AAAA,YACA,QAAQ,OAAO;AAAA,YACf,SAAS;AAAA,YACT,OAAO;AAAA,YACP,WAAW,oBAAoB,GAAG;AAAA,UACpC,CAAC;AAAA,QACH,OAAO;AACL,iBAAO,KAAK;AAAA,YACV;AAAA,YACA,QAAQ;AAAA,YACR;AAAA,YACA,QAAQ,OAAO;AAAA,YACf,OAAO,OAAO;AAAA,YACd,SAAS;AAAA,UACX,CAAC;AACD,eAAK,UAAU;AAAA,YACb,MAAM;AAAA,YACN;AAAA,YACA,QAAQ,OAAO;AAAA,YACf,SAAS;AAAA,UACX,CAAC;AACD;AAAA,YACE,MACE,KAAK,iBAAiB,WAAW,EAAE,MAAM,SAAS,CAAC,EAAE;AAAA,cACnD,MAAM;AAAA,cAAC;AAAA,YACT;AAAA,YACF;AAAA,UACF;AAAA,QACF;AAAA,MACF,OAAO;AACL,eAAO,KAAK;AAAA,UACV;AAAA,UACA,QAAQ,OAAO;AAAA,UACf;AAAA,UACA,SAAS;AAAA,QACX,CAAC;AACD,aAAK,UAAU;AAAA,UACb,MAAM;AAAA,UACN;AAAA,UACA,QAAQ,OAAO;AAAA,UACf,SAAS;AAAA,QACX,CAAC;AACD;AAAA,UACE,MACE,KAAK,iBAAiB,WAAW,EAAE,MAAM,SAAS,CAAC,EAAE;AAAA,YACnD,MAAM;AAAA,YAAC;AAAA,UACT;AAAA,UACF;AAAA,QACF;AAAA,MACF;AAAA,IACF,SAAS,KAAK;AACZ,YAAM,UAAW,IAAc;AAC/B,aAAO,KAAK;AAAA,QACV;AAAA,QACA,QAAQ,OAAO;AAAA,QACf;AAAA,QACA,OAAO;AAAA,MACT,CAAC;AACD,WAAK,UAAU;AAAA,QACb,MAAM;AAAA,QACN;AAAA,QACA,QAAQ,OAAO;AAAA,QACf,SAAS;AAAA,QACT,OAAO;AAAA,QACP,WACE,OAAO,SAAS,kBACZ,oBAAoB,OAAO,IAC3B;AAAA,MACR,CAAC;AAAA,IACH;AAAA,EACF;AAAA,EAEA,MAAc,sBACZ,WACA,QACA,KACA,WACe;AACf,UAAM,MAAM,UAAU,EAAE,SAAS,IAAI,CAAC;AAGtC,QAAI,CAAC,WAAW;AACd,UAAI,OAAO,SAAS,YAAY,OAAO,SAAS,OAAO;AACrD,cAAM,OAAsB;AAAA,UAC1B,QAAQ;AAAA,UACR,OAAO;AAAA,UACP,QAAQ;AAAA,UACR,UAAU,CAAC;AAAA,UACX,OAAO,CAAC;AAAA,UACR,WAAW;AAAA,UACX,cAAc;AAAA,UACd,SAAS,CAAC;AAAA,UACV;AAAA,QACF;AACA,eAAO,KAAK;AAAA,UACV;AAAA,UACA,QAAQ,OAAO;AAAA,UACf;AAAA,UACA,WAAW;AAAA,QACb,CAAC;AACD,aAAK,UAAU;AAAA,UACb,MAAM;AAAA,UACN;AAAA,UACA,QAAQ,OAAO;AAAA,UACf,SAAS;AAAA,UACT;AAAA,QACF,CAAC;AACD;AAAA,MACF;AAEA,aAAO,KAAK;AAAA,QACV;AAAA,QACA,QAAQ,OAAO;AAAA,QACf;AAAA,QACA,OAAO;AAAA,MACT,CAAC;AACD,WAAK,UAAU;AAAA,QACb,MAAM;AAAA,QACN;AAAA,QACA,QAAQ,OAAO;AAAA,QACf,SAAS;AAAA,QACT,OAAO,yBAAyB,GAAG;AAAA,MACrC,CAAC;AACD;AAAA,IACF;AAEA,QAAI;AACF,UAAI,OAAO,SAAS,UAAU;AAC5B,cAAM,SAAuB,MAAM,IAAI,OAAO;AAC9C,cAAM,gBAA+B,MAAM,IAAI,OAAO,CAAC,MAAM,CAAC;AAE9D,cAAM,QAAuB,OAAO,MAAM,IAAI,QAAM;AAAA,UAClD,MAAM,EAAE;AAAA,UACR,OAAO,kBAAkB,EAAE,OAAO,EAAE,WAAW;AAAA,UAC/C,UAAU,EAAE,UAAU,OAAO,EAAE,UAAU;AAAA,QAC3C,EAAE;AAEF,cAAM,WAA4B,OAAO;AAAA,UACvC,cAAc;AAAA,QAChB,EAAE,IAAI,CAAAC,QAAM;AAAA,UACV,MAAMA,GAAE,KAAK,QAAQ,YAAY,EAAE;AAAA,UACnC,UAAUA,GAAE,KAAK,WAAW,UAAU;AAAA,UACtC,UAAUA,GAAE;AAAA,UACZ,gBAAgBA,GAAE,UAAU;AAAA,UAC5B,OAAOA,GAAE,UAAU,OAAO,QAAQ;AAAA,UAClC,QAAQA,GAAE,UAAU,OAAO,SAAS;AAAA,QACtC,EAAE;AAEF,cAAM,OAAsB;AAAA,UAC1B,QAAQ,OAAO,WAAW;AAAA,UAC1B,OAAO,OAAO;AAAA,UACd,QAAQ,OAAO;AAAA,UACf;AAAA,UACA;AAAA,UACA,WAAW;AAAA,UACX,cAAc,MAAM;AAAA,UACpB;AAAA,QACF;AAEA,eAAO,KAAK;AAAA,UACV;AAAA,UACA,QAAQ;AAAA,UACR;AAAA,UACA,WAAW;AAAA,UACX,QAAQ,KAAK;AAAA,UACb,OAAO,MAAM;AAAA,QACf,CAAC;AACD,aAAK,UAAU;AAAA,UACb,MAAM;AAAA,UACN;AAAA,UACA,QAAQ;AAAA,UACR,SAAS;AAAA,UACT;AAAA,QACF,CAAC;AAAA,MACH,WAAW,OAAO,SAAS,OAAO;AAChC,cAAM,QAAQ,KAAK,IAAI,GAAG,KAAK,IAAI,OAAO,QAAQ,GAAG,GAAG,CAAC;AACzD,cAAM,MAAM;AACZ,cAAM,SAAS,MAAM,IAAI,IAAI;AAAA,UAC3B;AAAA,UACA,mBAAmB,GAAG;AAAA,UACtB;AAAA,UACA,OAAO,KAAK;AAAA,QACd,CAAC;AACD,cAAM,UAAU,KAAK,YAAY,MAAM;AACvC,cAAM,OAAsB;AAAA,UAC1B,QAAQ;AAAA,UACR,OAAO;AAAA,UACP,QAAQ;AAAA,UACR,UAAU,CAAC;AAAA,UACX,OAAO,CAAC;AAAA,UACR,WAAW;AAAA,UACX,cAAc;AAAA,UACd;AAAA,UACA;AAAA,QACF;AACA,eAAO,KAAK;AAAA,UACV;AAAA,UACA,QAAQ;AAAA,UACR;AAAA,UACA,SAAS,QAAQ;AAAA,QACnB,CAAC;AACD,aAAK,UAAU;AAAA,UACb,MAAM;AAAA,UACN;AAAA,UACA,QAAQ;AAAA,UACR,SAAS;AAAA,UACT;AAAA,QACF,CAAC;AAAA,MACH,OAAO;AACL,gBAAQ,OAAO,MAAM;AAAA,UACnB,KAAK;AACH,kBAAM,IAAI,SAAS,OAAO,MAAM;AAChC;AAAA,UACF,KAAK;AACH,gBAAI,OAAO,UAAU;AACnB,oBAAM,IAAI,oBAAoB,OAAO,IAAI;AAAA,YAC3C,OAAO;AACL,oBAAM,IAAI,OAAO,CAAC,OAAO,IAAI,CAAC;AAAA,YAChC;AACA;AAAA,UACF,KAAK,iBAAiB;AAGpB,kBAAM,MAAM,MAAM,KAAK,0BAA0B,GAAG;AACpD,gBAAI,OAAO,QAAQ,OAAO,QAAQ;AAChC,qBAAO,KAAK;AAAA,gBACV;AAAA,gBACA,QAAQ;AAAA,gBACR;AAAA,gBACA,QAAQ,OAAO;AAAA,gBACf,SAAS;AAAA,cACX,CAAC;AACD,oBAAM,IAAI;AAAA,gBACR,0CAA0C,OAAO,MAAM;AAAA,cACzD;AAAA,YACF;AAGA,gBAAI,OAAO,OAAO;AAChB,oBAAM,IAAI,IAAI,CAAC,UAAU,MAAM,OAAO,MAAM,CAAC;AAC7C,qBAAO,KAAK;AAAA,gBACV;AAAA,gBACA,QAAQ;AAAA,gBACR;AAAA,gBACA,QAAQ,OAAO;AAAA,gBACf,OAAO;AAAA,cACT,CAAC;AAAA,YACH,OAAO;AACL,oBAAM,IAAI,kBAAkB,OAAO,MAAM;AAAA,YAC3C;AACA;AAAA,UACF;AAAA,UACA,KAAK;AACH,kBAAM,IAAI,KAAK;AACf;AAAA,UACF,KAAK;AACH,kBAAM,IAAI,KAAK;AACf;AAAA,UACF,SAAS;AAIP,kBAAM,cAAqB;AAC3B,kBAAM,IAAI;AAAA,cACR,2BAA4B,YAA0B,IAAI;AAAA,YAC5D;AAAA,UACF;AAAA,QACF;AACA,eAAO,KAAK;AAAA,UACV;AAAA,UACA,QAAQ,OAAO;AAAA,UACf;AAAA,UACA,SAAS;AAAA,QACX,CAAC;AACD,aAAK,UAAU;AAAA,UACb,MAAM;AAAA,UACN;AAAA,UACA,QAAQ,OAAO;AAAA,UACf,SAAS;AAAA,QACX,CAAC;AACD;AAAA,UACE,MACE,KAAK,iBAAiB,WAAW,EAAE,MAAM,SAAS,CAAC,EAAE;AAAA,YACnD,MAAM;AAAA,YAAC;AAAA,UACT;AAAA,UACF;AAAA,QACF;AAAA,MACF;AAAA,IACF,SAAS,KAAK;AACZ,YAAM,UAAW,IAAc;AAC/B,aAAO,KAAK;AAAA,QACV;AAAA,QACA,QAAQ,OAAO;AAAA,QACf;AAAA,QACA,OAAO;AAAA,MACT,CAAC;AACD,WAAK,UAAU;AAAA,QACb,MAAM;AAAA,QACN;AAAA,QACA,QAAQ,OAAO;AAAA,QACf,SAAS;AAAA,QACT,OAAO;AAAA;AAAA;AAAA,QAGP,WACE,OAAO,SAAS,kBACZ,oBAAoB,OAAO,IAC3B;AAAA,MACR,CAAC;AAAA,IACH;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQA,MAAc,0BACZ,KACwB;AACxB,QAAI;AACF,YAAM,MAAM,MAAM,IAAI,IAAI;AAAA,QACxB;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,MACF,CAAC;AACD,aAAO,IAAI,KAAK,EAAE,QAAQ,aAAa,EAAE,KAAK;AAAA,IAChD,QAAQ;AACN,aAAO;AAAA,IACT;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,MAAc,wBACZ,WACA,KACwB;AACxB,QAAI;AACF,YAAM,MAAM,MAAM,KAAK;AAAA,QACrB;AAAA,QACA,MAAM,KAAK,UAAU,GAAG,CAAC;AAAA,MAC3B;AACA,aAAO,IAAI,KAAK,EAAE,QAAQ,aAAa,EAAE,KAAK;AAAA,IAChD,QAAQ;AACN,aAAO;AAAA,IACT;AAAA,EACF;AAAA,EAEQ,YAAY,QAAiC;AACnD,UAAM,UAA2B,CAAC;AAClC,QAAI,UAAU;AACd,eAAW,QAAQ,OAAO,MAAM,IAAI,GAAG;AACrC,UAAI,CAAC,KAAM;AACX,YAAM,QAAQ,KAAK,MAAM,GAAI;AAC7B,UAAI,MAAM,SAAS,GAAG;AACpB,mBAAW;AACX;AAAA,MACF;AACA,YAAM,CAAC,MAAM,WAAW,QAAQ,cAAc,SAAS,GAAG,YAAY,IACpE;AACF,YAAM,UAAU,aAAa,KAAK,GAAI;AACtC,YAAM,OAAO,UACT,QACG,MAAM,GAAG,EACT,IAAI,OAAK,EAAE,KAAK,CAAC,EACjB,OAAO,OAAO,IACjB,CAAC;AACL,cAAQ,KAAK;AAAA,QACX,MAAM,QAAQ;AAAA,QACd,WAAW,cAAc,OAAO,KAAK,MAAM,GAAG,CAAC,IAAI;AAAA,QACnD,SAAS,WAAW;AAAA,QACpB,QAAQ,UAAU;AAAA,QAClB,cAAc,gBAAgB;AAAA,QAC9B;AAAA,MACF,CAAC;AAAA,IACH;AACA,QAAI,UAAU,GAAG;AACf,aAAO,KAAK,uCAAuC;AAAA,QACjD;AAAA,QACA,QAAQ,QAAQ;AAAA,MAClB,CAAC;AAAA,IACH;AACA,WAAO;AAAA,EACT;AAAA,EAEQ,eAAe,QAA+B;AACpD,UAAM,WAAmC,CAAC;AAC1C,QAAI,UAAU;AACd,eAAW,QAAQ,OAAO,MAAM,IAAI,GAAG;AACrC,UAAI,KAAK,WAAW,KAAK,KAAK,KAAK,SAAS,KAAK,GAAG;AAClD,kBAAU,KAAK,MAAM,GAAG,EAAE;AAC1B,iBAAS,OAAO,IAAI;AAAA,MACtB,WAAW,SAAS;AAClB,iBAAS,OAAO,KACb,SAAS,OAAO,IAAI,SAAS,OAAO,IAAI,OAAO,MAAM;AAAA,MAC1D;AAAA,IACF;AAEA,UAAM,aAAa,SAAS,UAAU,KAAK,IAAI,KAAK,MAAM;AAC1D,QAAI,CAAC,WAAW;AACd,aAAO;AAAA,QACL,QAAQ;AAAA,QACR,OAAO;AAAA,QACP,QAAQ;AAAA,QACR,UAAU,CAAC;AAAA,QACX,OAAO,CAAC;AAAA,QACR,WAAW;AAAA,QACX,cAAc;AAAA,MAChB;AAAA,IACF;AAEA,UAAM,UAAU,SAAS,QAAQ,KAAK,IAAI,KAAK;AAE/C,UAAM,QAAuB,CAAC;AAC9B,eAAW,SAAS,SAAS,QAAQ,KAAK,IAAI,MAAM,IAAI,GAAG;AACzD,UAAI,KAAK,SAAS,EAAG;AACrB,YAAM,KAAK,KAAK,MAAM,GAAG,CAAC;AAC1B,YAAM,WAAW,KAAK,MAAM,CAAC,EAAE,KAAK;AACpC,YAAM,WAAW,GAAG,CAAC,MAAM,OAAO,GAAG,CAAC,MAAM;AAC5C,UAAI,QAA8B;AAClC,YAAM,OAAO,WAAW,GAAG,CAAC,IAAI,GAAG,CAAC;AACpC,UAAI,SAAS,IAAK,SAAQ;AAAA,eACjB,SAAS,IAAK,SAAQ;AAAA,eACtB,SAAS,IAAK,SAAQ;AAAA,eACtB,SAAS,IAAK,SAAQ;AAAA,eACtB,SAAS,IAAK,SAAQ;AAC/B,YAAM,KAAK,EAAE,MAAM,UAAU,OAAO,SAAS,CAAC;AAAA,IAChD;AAEA,UAAM,WAA4B,CAAC;AACnC,eAAW,SAAS,SAAS,UAAU,KAAK,IAAI,MAAM,IAAI,GAAG;AAC3D,UAAI,CAAC,KAAK,KAAK,EAAG;AAClB,YAAM,WAAW,KAAK,WAAW,GAAG;AACpC,YAAM,WAAW,KAAK,SAAS,UAAU;AACzC,YAAM,QAAQ,KAAK,QAAQ,WAAW,EAAE,EAAE,MAAM,KAAK;AACrD,YAAM,OAAO,MAAM,CAAC,GAAG,QAAQ,YAAY,EAAE,KAAK;AAClD,YAAM,iBACJ,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,EAAE,WAAW,IAAI,IAAI,MAAM,CAAC,IAAI;AACtD,UAAI,KAAM,UAAS,KAAK,EAAE,MAAM,UAAU,UAAU,eAAe,CAAC;AAAA,IACtE;AAEA,QAAI,QAAQ;AACZ,QAAI,SAAS;AACb,UAAM,UAAU,SAAS,cAAc,KAAK,IAAI,KAAK;AACrD,QAAI,QAAQ;AACV,YAAM,OAAO,OAAO,MAAM,KAAK;AAC/B,cAAQ,SAAS,KAAK,CAAC,KAAK,KAAK,EAAE,KAAK;AACxC,eAAS,SAAS,KAAK,CAAC,KAAK,KAAK,EAAE,KAAK;AAAA,IAC3C;AAEA,WAAO;AAAA,MACL;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA,WAAW;AAAA,MACX,cAAc,MAAM;AAAA,IACtB;AAAA,EACF;AAAA,EAEQ,QAAQ,WAAmB,KAA8B;AAC/D,WAAO,IAAI,QAAQ,CAAC,SAAS,WAAW;AACtC,YAAM,UAAU,KAAK,SAAS,IAAI,SAAS;AAC3C,UAAI,CAAC,SAAS;AACZ,eAAO,IAAI,MAAM,gBAAgB,CAAC;AAClC;AAAA,MACF;AACA,cAAQ,OAAO,KAAK,KAAK,CAAC,KAAK,WAAW;AACxC,YAAI,KAAK;AACP,iBAAO,GAAG;AACV;AAAA,QACF;AACA,YAAI,MAAM;AACV,eAAO,GAAG,QAAQ,CAAC,MAAc;AAC/B,iBAAO,EAAE,SAAS;AAAA,QACpB,CAAC;AACD,eAAO,OAAO,GAAG,QAAQ,CAAC,MAAc;AACtC,iBAAO,EAAE,SAAS;AAAA,QACpB,CAAC;AACD,eAAO,GAAG,SAAS,MAAM,QAAQ,GAAG,CAAC;AAAA,MACvC,CAAC;AAAA,IACH,CAAC;AAAA,EACH;AAAA,EAEQ,aACN,WACA,QACA,OACM;AACN,QAAI,KAAK,WAAY;AACrB,UAAM,UAAU,KAAK,SAAS,KAAK,CAAAH,OAAKA,GAAE,OAAO,SAAS;AAC1D,QAAI,SAAS;AACX,cAAQ,SAAS;AACjB,UAAI,OAAO;AACT,gBAAQ,eAAe;AAAA,MACzB,WAAW,WAAW,aAAa;AACjC,gBAAQ,eAAe;AACvB,gBAAQ,gBAAgB,oBAAI,KAAK;AAAA,MACnC;AACA,WAAK,UAAU,EAAE,MAAM,qBAAqB,WAAW,QAAQ,MAAM,CAAC;AAAA,IACxE;AAAA,EACF;AACF;;;AkBnvEA,OAAOI,SAAQ;AACf,OAAOC,WAAU;AACjB,OAAOC,SAAQ;AACf,SAAS,YAAY,mBAAmB;AACxC,SAAS,iBAAiB,iBAAiB;;;ACQpC,IAAM,oBAAoB;AAW1B,IAAM,yBAAyB;AAG/B,IAAM,kBAAkB;AAExB,IAAM,gCAAgC;AAEtC,IAAM,0BAA0B;AAEhC,IAAM,0BAA0B;AAShC,SAAS,iBACd,QACA,QACU;AACV,MAAI,UAAU,OAAO,SAAS,EAAG,QAAO;AACxC,MAAI,QAAQ;AACV,UAAM,OAAO,OACV,MAAM,GAAG,EACT,IAAI,CAAC,MAAM,EAAE,KAAK,CAAC,EACnB,OAAO,OAAO;AACjB,QAAI,KAAK,SAAS,EAAG,QAAO;AAAA,EAC9B;AACA,SAAO,CAAC,iBAAiB;AAC3B;;;AClCO,SAAS,UACd,aACA,WAAmB,wBACX;AACR,QAAM,OAAO,SAAS,QAAQ,QAAQ,EAAE;AACxC,SAAO,GAAG,IAAI,WAAW,mBAAmB,WAAW,CAAC;AAC1D;;;AC1BA,SAAS,aAAgC;;;ACclC,SAAS,eAAeC,OAAuC;AACpE,QAAM,OAAO,OAAO,KAAKA,KAAI;AAC7B,MAAI,KAAK,WAAW,EAAG,QAAO;AAC9B,MAAI,KAAK,WAAW,EAAG,QAAO,GAAGA,MAAK,KAAK,CAAC,CAAC,CAAC;AAC9C,SAAO,KAAK,IAAI,CAAAC,OAAK,GAAGA,EAAC,IAAID,MAAKC,EAAC,CAAC,EAAE,EAAE,KAAK,IAAI;AACnD;AAQO,SAAS,wBAAwB,SAA0B;AAChE,MAAI,WAAW,KAAM,QAAO;AAC5B,MAAI,OAAO,YAAY,SAAU,QAAO;AACxC,MAAI,MAAM,QAAQ,OAAO,GAAG;AAC1B,UAAM,QAAkB,CAAC;AACzB,eAAW,QAAQ,SAAS;AAC1B,UAAI,QAAQ,OAAO,SAAS,UAAU;AACpC,cAAM,IAAK,KAA4B;AACvC,YAAI,MAAM,QAAQ;AAChB,gBAAM,OAAQ,KAA4B;AAC1C,gBAAM,KAAK,OAAO,SAAS,WAAW,OAAO,EAAE;AAAA,QACjD,OAAO;AACL,gBAAM,KAAK,KAAK,UAAU,IAAI,CAAC;AAAA,QACjC;AAAA,MACF,OAAO;AACL,cAAM,KAAK,OAAO,IAAI,CAAC;AAAA,MACzB;AAAA,IACF;AACA,WAAO,MAAM,KAAK,IAAI;AAAA,EACxB;AACA,SAAO,OAAO,OAAO;AACvB;AASO,SAAS,mBAAmB,QAAyB;AAC1D,SACE,OAAO,SAAS,8BAA8B,KAC9C,OAAO,SAAS,iBAAiB;AAErC;AAOO,SAAS,eAAe,WAA2B;AACxD,SAAO,UAAU,WAAW,IAAI,YAAY;AAC9C;;;AC7BO,IAAM,sBAAN,MAA0B;AAAA,EACd;AAAA;AAAA,EAET,aAAa;AAAA;AAAA,EAEb,sBAAqC;AAAA;AAAA,EAErC,+BAA+B;AAAA;AAAA,EAE/B,sBAAqC;AAAA,EACrC,iBAAgC;AAAA,EAExC,YAAY,IAA4B;AACtC,SAAK,KAAK;AAAA,EACZ;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,KAAK,OAAqB;AACxB,SAAK,cAAc;AACnB,UAAM,QAAQ,KAAK,WAAW,MAAM,IAAI;AAExC,SAAK,aAAa,MAAM,IAAI,KAAK;AACjC,eAAW,QAAQ,OAAO;AACxB,YAAM,UAAU,KAAK,KAAK;AAC1B,UAAI,QAAQ,WAAW,EAAG;AAC1B,WAAK,UAAU,OAAO;AAAA,IACxB;AAAA,EACF;AAAA;AAAA,EAGA,QAAc;AACZ,UAAM,YAAY,KAAK,WAAW,KAAK;AACvC,SAAK,aAAa;AAClB,QAAI,UAAU,SAAS,EAAG,MAAK,UAAU,SAAS;AAClD,SAAK,qBAAqB;AAAA,EAC5B;AAAA;AAAA,EAGQ,UAAU,MAAoB;AACpC,QAAI;AACJ,QAAI;AACF,YAAM,KAAK,MAAM,IAAI;AAAA,IACvB,SAAS,GAAG;AACV,WAAK,GAAG,cAAc,MAAM,CAAC;AAC7B;AAAA,IACF;AACA,UAAM,OAAO,OAAO,IAAI,SAAS,WAAW,IAAI,OAAO;AACvD,QAAI,SAAS,MAAM;AACjB,WAAK,GAAG,gBAAgB,gBAAgB;AACxC;AAAA,IACF;AACA,QAAI;AACF,cAAQ,MAAM;AAAA,QACZ,KAAK,UAAU;AAEb,gBAAM,UACJ,OAAO,IAAI,YAAY,WAAW,IAAI,UAAU;AAClD,cAAI,YAAY,QAAQ;AACtB,kBAAM,MACJ,OAAO,IAAI,eAAe,WAAW,IAAI,aAAa;AACxD,gBAAI,IAAK,MAAK,GAAG,cAAc,GAAG;AAAA,UACpC,OAAO;AACL,iBAAK,GAAG,gBAAgB,UAAU,OAAO,EAAE;AAAA,UAC7C;AACA;AAAA,QACF;AAAA,QACA,KAAK;AACH,eAAK,kBAAkB,GAAG;AAC1B;AAAA,QACF,KAAK;AACH,eAAK,qBAAqB,GAAG;AAC7B;AAAA,QACF,KAAK;AACH,eAAK,kBAAkB,GAAG;AAC1B;AAAA,QACF,KAAK;AACH,eAAK,aAAa,GAAG;AACrB;AAAA,QACF;AACE,eAAK,GAAG,gBAAgB,IAAI;AAAA,MAChC;AAAA,IACF,SAAS,GAAG;AACV,WAAK,GAAG,cAAc,MAAM,CAAC;AAAA,IAC/B;AAAA,EACF;AAAA;AAAA,EAGQ,kBAAkB,KAAoC;AAC5D,UAAM,QAAQ,IAAI;AAClB,QAAI,CAAC,SAAS,OAAO,UAAU,SAAU;AACzC,UAAM,KAAK;AACX,UAAM,YAAY,OAAO,GAAG,SAAS,WAAW,GAAG,OAAO;AAC1D,YAAQ,WAAW;AAAA,MACjB,KAAK;AAAA,MACL,KAAK;AACH;AAAA;AAAA,MACF,KAAK;AAEH,aAAK,qBAAqB;AAC1B;AAAA,MACF,KAAK;AAAA,MACL,KAAK;AAAA,MACL,KAAK;AACH,aAAK,mBAAmB,WAAW,EAAE;AACrC;AAAA,MACF;AACE,aAAK,GAAG,gBAAgB,gBAAgB,aAAa,QAAQ,EAAE;AAAA,IACnE;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOQ,mBACN,WACA,OACM;AACN,QAAI,cAAc,uBAAuB;AACvC,YAAM,QAAQ,MAAM;AACpB,UAAI,SAAS,OAAO,UAAU,UAAU;AACtC,cAAM,YAAa,MAA6B;AAChD,YAAI,cAAc,YAAY;AAE5B,gBAAM,OAAQ,MAA6B;AAC3C,eAAK,iBAAiB,OAAO,SAAS,WAAW,OAAO;AACxD,eAAK,sBAAsB;AAAA,QAC7B,WAAW,cAAc,QAAQ;AAC/B,eAAK,sBAAsB;AAAA,QAC7B;AAAA,MAEF;AACA;AAAA,IACF;AACA,QAAI,cAAc,uBAAuB;AACvC,YAAM,QAAQ,MAAM;AACpB,UAAI,CAAC,SAAS,OAAO,UAAU,SAAU;AACzC,YAAM,IAAI;AACV,YAAM,YAAY,OAAO,EAAE,SAAS,WAAW,EAAE,OAAO;AACxD,UAAI,cAAc,cAAc;AAC9B,cAAM,OAAO,OAAO,EAAE,SAAS,WAAW,EAAE,OAAO;AACnD,YAAI,SAAS,QAAQ,KAAK,wBAAwB,MAAM;AACtD,eAAK,uBAAuB;AAG5B,eAAK,GAAG,OAAO;AAAA,YACb,MAAM;AAAA,YACN,MAAM,KAAK;AAAA,YACX,aAAa;AAAA,UACf,CAAC;AACD,eAAK,+BAA+B;AAAA,QACtC;AAAA,MACF,WAAW,cAAc,oBAAoB;AAC3C,cAAM,UACJ,OAAO,EAAE,iBAAiB,WAAW,EAAE,eAAe;AACxD,YAAI,YAAY,QAAQ,KAAK,wBAAwB,MAAM;AACzD,eAAK,uBAAuB;AAAA,QAC9B;AAAA,MACF;AAEA;AAAA,IACF;AAEA,QAAI,KAAK,mBAAmB,QAAQ,KAAK,wBAAwB,MAAM;AACrE,UAAIC;AACJ,UAAI;AACF,QAAAA,QACE,KAAK,oBAAoB,WAAW,IAChC,CAAC,IACA,KAAK,MAAM,KAAK,mBAAmB;AAAA,MAC5C,QAAQ;AACN,QAAAA,QAAO,EAAE,SAAS,KAAK,oBAAoB;AAAA,MAC7C;AACA,WAAK,GAAG,OAAO;AAAA,QACb,MAAM;AAAA,QACN,UAAU,KAAK;AAAA,QACf,MAAM,eAAeA,KAAI;AAAA,MAC3B,CAAC;AACD,WAAK,iBAAiB;AACtB,WAAK,sBAAsB;AAAA,IAC7B;AAEA,SAAK,qBAAqB;AAAA,EAC5B;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQQ,qBAAqB,KAAoC;AAC/D,UAAM,UAAU,IAAI;AACpB,QAAI,CAAC,WAAW,OAAO,YAAY,SAAU;AAC7C,UAAM,UAAW,QAAkC;AACnD,QAAI,CAAC,MAAM,QAAQ,OAAO,EAAG;AAC7B,eAAW,QAAQ,SAAS;AAC1B,UAAI,CAAC,QAAQ,OAAO,SAAS,SAAU;AACvC,YAAM,WAAY,KAA4B;AAC9C,UAAI,aAAa,QAAQ;AACvB,YAAI,KAAK,6BAA8B;AACvC,cAAM,OAAQ,KAA4B;AAC1C,YAAI,OAAO,SAAS,YAAY,KAAK,SAAS,GAAG;AAC/C,eAAK,GAAG,OAAO,EAAE,MAAM,aAAa,MAAM,aAAa,MAAM,CAAC;AAAA,QAChE;AAAA,MACF,WAAW,aAAa,YAAY;AAClC,cAAM,OAAQ,KAA4B;AAC1C,cAAM,QAAS,KAA6B;AAC5C,aAAK,GAAG,OAAO;AAAA,UACb,MAAM;AAAA,UACN,UAAU,OAAO,SAAS,WAAW,OAAO;AAAA,UAC5C,MACE,SAAS,OAAO,UAAU,WACtB,eAAe,KAAgC,IAC/C,GAAG,KAAK;AAAA,QAChB,CAAC;AAAA,MACH;AAAA,IAEF;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA,EAMQ,kBAAkB,KAAoC;AAC5D,UAAM,UAAU,IAAI;AACpB,QAAI,CAAC,WAAW,OAAO,YAAY,SAAU;AAC7C,UAAM,UAAW,QAAkC;AACnD,QAAI,CAAC,MAAM,QAAQ,OAAO,EAAG;AAC7B,eAAW,QAAQ,SAAS;AAC1B,UAAI,CAAC,QAAQ,OAAO,SAAS,SAAU;AACvC,UAAK,KAA4B,SAAS,cAAe;AACzD,YAAM,MAAO,KAAmC;AAChD,YAAM,YAAY,OAAO,QAAQ,WAAW,MAAM;AAClD,YAAM,UAAW,KAAgC,aAAa;AAC9D,YAAM,SAAS;AAAA,QACZ,KAA+B;AAAA,MAClC;AAGA,UAAI,WAAW,mBAAmB,MAAM,GAAG;AACzC,aAAK,GAAG,OAAO;AAAA,UACb,MAAM;AAAA,UACN,QAAQ;AAAA,UACR,SAAS,CAAC;AAAA,UACV,MAAM;AAAA,UACN,gBAAgB;AAAA,QAClB,CAAC;AACD;AAAA,MACF;AACA,WAAK,GAAG,OAAO;AAAA,QACb,MAAM;AAAA,QACN,UAAU,eAAe,SAAS;AAAA,QAClC;AAAA,QACA,WAAW;AAAA;AAAA,MACb,CAAC;AAAA,IACH;AAAA,EACF;AAAA;AAAA,EAGQ,aAAa,KAAoC;AACvD,SAAK,qBAAqB;AAC1B,SAAK,GAAG,OAAO;AAAA,MACb,MAAM;AAAA,MACN,SAAS,OAAO,IAAI,YAAY,WAAW,IAAI,UAAU;AAAA,MACzD,YACE,OAAO,IAAI,gBAAgB,WAAW,IAAI,cAAc;AAAA,MAC1D,UAAU,OAAO,IAAI,cAAc,WAAW,IAAI,YAAY;AAAA,MAC9D,cACE,OAAO,IAAI,mBAAmB,WAAW,IAAI,iBAAiB;AAAA,MAChE,SAAS,IAAI,aAAa;AAAA,IAC5B,CAAC;AAED,SAAK,+BAA+B;AAAA,EACtC;AAAA;AAAA,EAGQ,uBAA6B;AACnC,UAAM,OAAO,KAAK;AAClB,QAAI,SAAS,QAAQ,KAAK,SAAS,GAAG;AACpC,WAAK,GAAG,OAAO,EAAE,MAAM,aAAa,MAAM,aAAa,MAAM,CAAC;AAE9D,WAAK,+BAA+B;AAAA,IACtC;AACA,SAAK,sBAAsB;AAAA,EAC7B;AACF;;;AFrUA,IAAMC,OAAM,aAAa,cAAc;AAsEhC,IAAM,cAAN,MAAkB;AAAA;AAAA,EAEf,YAAY,oBAAI,IAAiC;AAAA;AAAA,EAGjD,WAAW,oBAAI,IAAmC;AAAA;AAAA;AAAA;AAAA;AAAA,EAMlD;AAAA;AAAA;AAAA;AAAA;AAAA,EAWA;AAAA,EAER,YACE,MAMA,SACA;AACA,SAAK,OAAO;AACZ,SAAK,UAAU;AAAA,EACjB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EASA,IACE,WACA,WACA,MACA,KACM;AACN,QAAI,KAAK,UAAU,IAAI,SAAS,GAAG;AACjC,MAAAA,KAAI,KAAK,kDAAkD,SAAS,EAAE;AACtE;AAAA,IACF;AAIA,UAAM,KAAK,kBAAkB,MAAM,KAAK,QAAQ;AAChD,QAAI,CAAC,GAAG,IAAI;AACV,MAAAA,KAAI;AAAA,QACF,kBAAkB,GAAG,KAAK,iBAAiB,SAAS,WAAW,GAAG,MAAM;AAAA,MAC1E;AACA,WAAK,KAAK,WAAW,WAAW,UAAU;AAAA,QACxC,MACE,GAAG,UAAU,aACT,uBAAuB,GAAG,MAAM;AAAA,IAChC,iBAAiB,GAAG,MAAM;AAAA;AAAA,MAClC,CAAC;AACD,WAAK,KAAK,WAAW,WAAW,QAAQ,EAAE,UAAU,EAAE,CAAC;AACvD;AAAA,IACF;AACA,UAAM,OAAO,GAAG;AAEhB,IAAAA,KAAI;AAAA,MACF,8BAA8B,SAAS,cAAc,SAAS,QAAQ,OAAO,WAAW,SAAS,KAAK,UAAU,IAAI,CAAC;AAAA,IACvH;AAEA,UAAM,QAA6B,EAAE,WAAW,WAAW,KAAK;AAGhE,QAAI,KAAK,SAAS;AAChB,YAAM,UAAU,KAAK;AACrB,YAAM,SAAS,IAAI,oBAAoB;AAAA,QACrC,QAAQ,UAAQ,QAAQ,OAAO,WAAW,WAAW,UAAU,IAAI;AAAA,QACnE,aAAa,SAAO,QAAQ,YAAY,WAAW,WAAW,GAAG;AAAA,QACjE,aAAa,CAAC,MAAM,QAClBA,KAAI;AAAA,UACF,4BAA4B,SAAS,KAAM,IAAc,OAAO,SAAS,KAAK,MAAM,GAAG,GAAG,CAAC;AAAA,QAC7F;AAAA,MACJ,CAAC;AAAA,IACH;AACA,SAAK,UAAU,IAAI,WAAW,KAAK;AAEnC,SAAK,QAAQ,YAAY,OAAO;AAChC,SAAK,QAAQ,GAAG,QAAQ,CAAC,UAAkB;AACzC,WAAK,KAAK,WAAW,WAAW,UAAU,EAAE,MAAM,MAAM,CAAC;AAEzD,YAAM,QAAQ,KAAK,KAAK;AAAA,IAC1B,CAAC;AACD,SAAK,QAAQ,YAAY,OAAO;AAChC,SAAK,QAAQ,GAAG,QAAQ,CAAC,UAAkB;AACzC,WAAK,KAAK,WAAW,WAAW,UAAU,EAAE,MAAM,MAAM,CAAC;AAAA,IAC3D,CAAC;AACD,SAAK,GAAG,SAAS,CAAC,QAAe;AAC/B,WAAK,KAAK,WAAW,WAAW,UAAU;AAAA,QACxC,MAAM,kBAAkB,IAAI,OAAO;AAAA;AAAA,MACrC,CAAC;AAMD,UAAI,KAAK,UAAU,IAAI,SAAS,GAAG;AACjC,aAAK,UAAU,OAAO,SAAS;AAC/B,aAAK,KAAK,WAAW,WAAW,QAAQ,EAAE,UAAU,EAAE,CAAC;AAAA,MACzD;AAAA,IACF,CAAC;AACD,SAAK,GAAG,QAAQ,CAAC,SAAwB;AAEvC,UAAI,CAAC,KAAK,UAAU,IAAI,SAAS,EAAG;AACpC,WAAK,UAAU,OAAO,SAAS;AAE/B,YAAM,QAAQ,MAAM;AACpB,MAAAA,KAAI;AAAA,QACF,+BAA+B,SAAS,cAAc,SAAS,SAAS,QAAQ,EAAE;AAAA,MACpF;AACA,WAAK,KAAK,WAAW,WAAW,QAAQ,EAAE,UAAU,QAAQ,GAAG,CAAC;AAAA,IAClE,CAAC;AAAA,EACH;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,KAAK,WAAmB,WAA0B;AAChD,QAAI,WAAW;AACb,YAAM,QAAQ,KAAK,UAAU,IAAI,SAAS;AAC1C,UAAI,OAAO;AACT,QAAAA,KAAI,KAAK,iCAAiC,SAAS,EAAE;AACrD,cAAM,KAAK,KAAK,SAAS;AAAA,MAC3B;AACA;AAAA,IACF;AACA,eAAW,SAAS,KAAK,UAAU,OAAO,GAAG;AAC3C,UAAI,MAAM,cAAc,WAAW;AACjC,QAAAA,KAAI;AAAA,UACF,8CAA8C,MAAM,SAAS;AAAA,QAC/D;AACA,cAAM,KAAK,KAAK,SAAS;AAAA,MAC3B;AAAA,IACF;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAiBA,YACE,WACA,WACA,KACA,QACM;AACN,QAAI,KAAK,SAAS,IAAI,SAAS,GAAG;AAChC,MAAAA,KAAI;AAAA,QACF,0DAA0D,SAAS;AAAA,MACrE;AACA;AAAA,IACF;AAOA,UAAM,OAAO;AAAA,MACX;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA,SAAS,aAAa;AAAA,MACtB;AAAA,IACF;AACA,UAAM,KAAK,kBAAkB,MAAM,KAAK,MAAM;AAC9C,QAAI,CAAC,GAAG,IAAI;AACV,MAAAA,KAAI;AAAA,QACF,8BAA8B,GAAG,KAAK,iBAAiB,SAAS,cAAc,SAAS,WAAW,GAAG,MAAM;AAAA,MAC7G;AACA,WAAK,KAAK,WAAW,WAAW,UAAU;AAAA,QACxC,MAAM,8BAA8B,GAAG,MAAM;AAAA;AAAA,MAC/C,CAAC;AACD,WAAK,KAAK,WAAW,WAAW,QAAQ,EAAE,UAAU,EAAE,CAAC;AACvD;AAAA,IACF;AACA,UAAM,OAAO,GAAG;AAEhB,IAAAA,KAAI;AAAA,MACF,sCAAsC,SAAS,cAAc,SAAS,QAAQ,OAAO,WAAW,WAAW,UAAU,KAAK;AAAA,IAC5H;AAEA,UAAM,QAA+B,EAAE,WAAW,WAAW,KAAK;AAClE,QAAI,KAAK,SAAS;AAChB,YAAM,UAAU,KAAK;AACrB,YAAM,SAAS,IAAI,oBAAoB;AAAA,QACrC,QAAQ,UAAQ,QAAQ,OAAO,WAAW,WAAW,UAAU,IAAI;AAAA,QACnE,aAAa,SAAO,QAAQ,YAAY,WAAW,WAAW,GAAG;AAAA,QACjE,aAAa,CAAC,MAAM,QAClBA,KAAI;AAAA,UACF,gCAAgC,SAAS,KAAM,IAAc,OAAO,SAAS,KAAK,MAAM,GAAG,GAAG,CAAC;AAAA,QACjG;AAAA,MACJ,CAAC;AAAA,IACH;AACA,SAAK,SAAS,IAAI,WAAW,KAAK;AAElC,SAAK,QAAQ,YAAY,OAAO;AAChC,SAAK,QAAQ,GAAG,QAAQ,CAAC,UAAkB;AAEzC,YAAM,QAAQ,KAAK,KAAK;AAAA,IAC1B,CAAC;AACD,SAAK,QAAQ,YAAY,OAAO;AAChC,SAAK,QAAQ,GAAG,QAAQ,CAAC,UAAkB;AACzC,MAAAA,KAAI;AAAA,QACF,2CAA2C,SAAS,KAAK,MAAM,QAAQ,CAAC;AAAA,MAC1E;AAAA,IACF,CAAC;AACD,SAAK,GAAG,SAAS,CAAC,QAAe;AAE/B,MAAAA,KAAI;AAAA,QACF,0CAA0C,SAAS,KAAK,IAAI,OAAO;AAAA,MACrE;AACA,UAAI,KAAK,SAAS,IAAI,SAAS,GAAG;AAChC,aAAK,SAAS,OAAO,SAAS;AAC9B,aAAK,KAAK,WAAW,WAAW,QAAQ,EAAE,UAAU,EAAE,CAAC;AAAA,MACzD;AAAA,IACF,CAAC;AACD,SAAK,GAAG,QAAQ,CAAC,SAAwB;AACvC,UAAI,CAAC,KAAK,SAAS,IAAI,SAAS,EAAG;AACnC,WAAK,SAAS,OAAO,SAAS;AAC9B,YAAM,QAAQ,MAAM;AACpB,MAAAA,KAAI;AAAA,QACF,uCAAuC,SAAS,cAAc,SAAS,SAAS,QAAQ,EAAE;AAAA,MAC5F;AACA,WAAK,KAAK,WAAW,WAAW,QAAQ,EAAE,UAAU,QAAQ,GAAG,CAAC;AAAA,IAClE,CAAC;AAAA,EACH;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EASA,cAAc,WAAmB,MAAoB;AACnD,UAAM,QAAQ,KAAK,SAAS,IAAI,SAAS;AACzC,QAAI,CAAC,OAAO;AACV,MAAAA,KAAI,KAAK,sDAAsD,SAAS,EAAE;AAC1E;AAAA,IACF;AACA,UAAM,WAAW,KAAK,UAAU;AAAA,MAC9B,MAAM;AAAA,MACN,SAAS,EAAE,MAAM,QAAQ,SAAS,CAAC,EAAE,MAAM,QAAQ,KAAK,CAAC,EAAE;AAAA,IAC7D,CAAC;AACD,UAAM,KAAK,OAAO,MAAM,GAAG,QAAQ;AAAA,CAAI;AAAA,EACzC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAUA,aAAa,WAAoB,WAA0B;AACzD,QAAI,WAAW;AACb,YAAM,QAAQ,KAAK,SAAS,IAAI,SAAS;AACzC,UAAI,OAAO;AACT,QAAAA,KAAI,KAAK,yCAAyC,SAAS,EAAE;AAC7D,cAAM,KAAK,KAAK,SAAS;AAAA,MAC3B;AACA;AAAA,IACF;AACA,QAAI,WAAW;AACb,iBAAW,SAAS,KAAK,SAAS,OAAO,GAAG;AAC1C,YAAI,MAAM,cAAc,WAAW;AACjC,UAAAA,KAAI;AAAA,YACF,oDAAoD,SAAS,cAAc,MAAM,SAAS;AAAA,UAC5F;AACA,gBAAM,KAAK,KAAK,SAAS;AAAA,QAC3B;AAAA,MACF;AAAA,IACF;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EASA,oBAAoB,WAA6B;AAC/C,UAAM,MAAgB,CAAC;AACvB,eAAW,SAAS,KAAK,SAAS,OAAO,GAAG;AAC1C,UAAI,MAAM,cAAc,UAAW,KAAI,KAAK,MAAM,SAAS;AAAA,IAC7D;AACA,WAAO;AAAA,EACT;AAAA;AAAA,EAGA,UAAgB;AACd,eAAW,SAAS,KAAK,UAAU,OAAO,GAAG;AAC3C,UAAI;AACF,cAAM,KAAK,KAAK,SAAS;AAAA,MAC3B,QAAQ;AAAA,MAER;AAAA,IACF;AACA,SAAK,UAAU,MAAM;AACrB,eAAW,SAAS,KAAK,SAAS,OAAO,GAAG;AAC1C,UAAI;AACF,cAAM,KAAK,KAAK,SAAS;AAAA,MAC3B,QAAQ;AAAA,MAER;AAAA,IACF;AACA,SAAK,SAAS,MAAM;AAAA,EACtB;AACF;AAiBA,SAAS,kBACP,MACA,KACA,OAG6D;AAC7D,QAAM,aAAa,kBAAkB,IAAI;AACzC,MAAI,CAAC,WAAW,IAAI;AAClB,WAAO;AAAA,MACL,IAAI;AAAA,MACJ,OAAO;AAAA,MACP,QAAQ,WAAW,UAAU;AAAA,IAC/B;AAAA,EACF;AACA,QAAM,QAAQ,QAAQ,aAAa;AACnC,QAAM,UAAU,QAAS,QAAQ,IAAI,WAAW,YAAa,KAAK,CAAC;AACnE,QAAMC,QAAO,QAAQ,CAAC,MAAM,MAAM,MAAM,GAAG,IAAI,IAAI,KAAK,MAAM,CAAC;AAC/D,MAAI;AACF,UAAM,OAAO,MAAM,SAASA,OAAM;AAAA;AAAA,MAEhC;AAAA,MACA,KAAK,EAAE,GAAG,QAAQ,KAAK,MAAM,QAAQ,MAAM,cAAc,EAAE;AAAA,MAC3D,OAAO,CAAC,OAAO,QAAQ,MAAM;AAAA,MAC7B,aAAa;AAAA;AAAA,MAEb,0BAA0B;AAAA,IAC5B,CAAC;AACD,WAAO,EAAE,IAAI,MAAM,KAAK;AAAA,EAC1B,SAAS,KAAK;AACZ,WAAO,EAAE,IAAI,OAAO,OAAO,SAAS,QAAS,IAAc,QAAQ;AAAA,EACrE;AACF;AAYA,SAAS,gBAAwB;AAC/B,QAAM,MAAM,QAAQ,aAAa,UAAU,MAAM;AACjD,QAAM,UAAU,QAAQ,IAAI,QAAQ,QAAQ,IAAI,QAAQ;AACxD,MAAI,QAAQ,aAAa,SAAU,QAAO;AAC1C,QAAM,OAAO,QAAQ,IAAI,QAAQ;AACjC,QAAM,QAAQ;AAAA,IACZ;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA,OAAO,GAAG,IAAI,gBAAgB;AAAA,IAC9B,OAAO,GAAG,IAAI,qBAAqB;AAAA,EACrC,EAAE,OAAO,OAAO;AAChB,QAAM,OAAO,IAAI,IAAI,QAAQ,MAAM,GAAG,CAAC;AACvC,QAAM,UAAU,MAAM,OAAO,CAAAC,OAAK,CAAC,KAAK,IAAIA,EAAC,CAAC;AAC9C,SAAO,QAAQ,SAAS,IAAI,GAAG,OAAO,GAAG,GAAG,GAAG,QAAQ,KAAK,GAAG,CAAC,KAAK;AACvE;AAeO,SAAS,kBAAkB,MAGhC;AACA,MAAI,CAAC,MAAM,QAAQ,IAAI,KAAK,KAAK,WAAW,GAAG;AAC7C,WAAO,EAAE,IAAI,OAAO,QAAQ,aAAa;AAAA,EAC3C;AACA,MAAI,KAAK,SAAS,IAAI;AACpB,WAAO,EAAE,IAAI,OAAO,QAAQ,gBAAgB;AAAA,EAC9C;AACA,QAAM,WAAW,KAAK,OAAO,CAAC,GAAG,MAAM,KAAK,GAAG,UAAU,IAAI,CAAC;AAC9D,MAAI,WAAW,OAAO;AACpB,WAAO,EAAE,IAAI,OAAO,QAAQ,gBAAgB;AAAA,EAC9C;AACA,aAAW,KAAK,MAAM;AACpB,QAAI,OAAO,MAAM,UAAU;AACzB,aAAO,EAAE,IAAI,OAAO,QAAQ,iBAAiB;AAAA,IAC/C;AAAA,EACF;AACA,QAAM,iBAAiB,oBAAI,IAAI,CAAC,UAAU,SAAS,QAAQ,CAAC;AAC5D,MAAI,CAAC,eAAe,IAAI,KAAK,CAAC,CAAC,GAAG;AAChC,WAAO,EAAE,IAAI,OAAO,QAAQ,6BAA6B,KAAK,CAAC,CAAC,IAAI;AAAA,EACtE;AACA,SAAO,EAAE,IAAI,KAAK;AACpB;;;AG3hBA,OAAOC,SAAQ;AACf,OAAOC,WAAU;AASV,IAAM,2BAA2B,KAAK,OAAO;AAGpD,IAAM,wBAAwB;AAI9B,IAAM,gBAAgB;AAMtB,IAAM,yBAAyB;AAG/B,IAAM,yBAAyB;AAG/B,IAAM,gBAAgB;AAaf,SAAS,mBAAmB,MAAsB;AAEvD,QAAM,WAAW,OAAO,QAAQ,EAAE,EAAE,MAAM,OAAO;AACjD,QAAM,OAAO,SAAS,SAAS,SAAS,CAAC,KAAK;AAC9C,MAAI,UAAU,KACX,QAAQ,eAAe,EAAE,EACzB,QAAQ,uBAAuB,GAAG,EAClC,KAAK;AAER,YAAU,QAAQ,QAAQ,UAAU,EAAE;AACtC,MAAI,YAAY,MAAM,YAAY,OAAO,YAAY,KAAM,QAAO;AAElE,MAAI,uBAAuB,KAAK,OAAO,EAAG,WAAU,IAAI,OAAO;AAC/D,SAAO;AACT;AAWO,SAAS,qBACd,KACA,MACA,QACQ;AACR,QAAM,QAAQA,MAAK,KAAK,KAAK,IAAI;AACjC,MAAI,CAAC,OAAO,KAAK,EAAG,QAAO;AAE3B,QAAM,MAAMA,MAAK,QAAQ,IAAI;AAC7B,QAAM,OAAO,KAAK,MAAM,GAAG,KAAK,SAAS,IAAI,MAAM;AACnD,WAAS,IAAI,GAAG,KAAK,KAAM,KAAK;AAC9B,UAAM,YAAYA,MAAK,KAAK,KAAK,GAAG,IAAI,KAAK,CAAC,IAAI,GAAG,EAAE;AACvD,QAAI,CAAC,OAAO,SAAS,EAAG,QAAO;AAAA,EACjC;AAEA,SAAOA,MAAK,KAAK,KAAK,GAAG,IAAI,KAAK,KAAK,IAAI,CAAC,IAAI,GAAG,EAAE;AACvD;AAiCO,IAAM,qBAAN,MAAyB;AAAA,EACb;AAAA,EACA;AAAA,EACA,UAAU,oBAAI,IAAyB;AAAA;AAAA;AAAA;AAAA;AAAA,EAMxD,YAAY,MAGT;AACD,SAAK,aAAa,KAAK;AACvB,SAAK,WAAW,KAAK,YAAY;AAAA,EACnC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAWA,MACE,WACA,UACA,MACA,MACA,aACqC;AACrC,SAAK,aAAa;AAClB,QAAI,mBAAmB,IAAI,MAAM,GAAI,QAAO,EAAE,IAAI,OAAO,OAAO,WAAW;AAE3E,QAAI,CAAC,OAAO,UAAU,IAAI,KAAK,OAAO,KAAK,OAAO,KAAK,UAAU;AAC/D,aAAO,EAAE,IAAI,OAAO,OAAO,YAAY;AAAA,IACzC;AACA,QAAI,CAAC,OAAO,UAAU,WAAW,KAAK,cAAc,GAAG;AACrD,aAAO,EAAE,IAAI,OAAO,OAAO,WAAW;AAAA,IACxC;AAEA,QACE,CAAC,KAAK,QAAQ,IAAI,QAAQ,KAC1B,KAAK,QAAQ,QAAQ,wBACrB;AACA,aAAO,EAAE,IAAI,OAAO,OAAO,WAAW;AAAA,IACxC;AACA,SAAK,QAAQ,IAAI,UAAU;AAAA,MACzB;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA,UAAU;AAAA,MACV,QAAQ,oBAAI,IAAI;AAAA,MAChB,SAAS;AAAA,MACT,aAAa,KAAK,IAAI;AAAA,IACxB,CAAC;AACD,WAAO;AAAA,EACT;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EASA,MAAM,UAAkB,OAAe,SAAuB;AAC5D,UAAM,QAAQ,KAAK,QAAQ,IAAI,QAAQ;AACvC,QAAI,CAAC,SAAS,MAAM,QAAS;AAE7B,QACE,CAAC,OAAO,UAAU,KAAK,KACvB,QAAQ,KACR,SAAS,MAAM,eACf,MAAM,OAAO,IAAI,KAAK,GACtB;AACA,YAAM,UAAU;AAChB,YAAM,OAAO,MAAM;AACnB;AAAA,IACF;AACA,UAAM,MAAM,OAAO,KAAK,OAAO,WAAW,EAAE,GAAG,QAAQ;AACvD,QAAI,MAAM,WAAW,IAAI,SAAS,KAAK,UAAU;AAC/C,YAAM,UAAU;AAChB,YAAM,OAAO,MAAM;AACnB;AAAA,IACF;AACA,UAAM,OAAO,IAAI,OAAO,GAAG;AAC3B,UAAM,YAAY,IAAI;AACtB,UAAM,cAAc,KAAK,IAAI;AAAA,EAC/B;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAWA,MAAM,IACJ,UACyD;AACzD,UAAM,QAAQ,KAAK,QAAQ,IAAI,QAAQ;AACvC,QAAI,CAAC,MAAO,QAAO,EAAE,IAAI,OAAO,OAAO,WAAW;AAClD,QAAI;AACF,UAAI,MAAM,QAAS,QAAO,EAAE,IAAI,OAAO,OAAO,eAAe;AAG7D,UACE,MAAM,OAAO,SAAS,MAAM,eAC5B,MAAM,aAAa,MAAM,MACzB;AACA,eAAO,EAAE,IAAI,OAAO,OAAO,eAAe;AAAA,MAC5C;AACA,YAAM,OAAO,mBAAmB,MAAM,IAAI;AAC1C,UAAI,SAAS,GAAI,QAAO,EAAE,IAAI,OAAO,OAAO,WAAW;AAEvD,YAAM,MAAM,KAAK,WAAW,MAAM,SAAS;AAI3C,YAAMD,IAAG,SAAS,MAAM,KAAK,EAAE,WAAW,KAAK,CAAC;AAChD,YAAM,YAAY,qBAAqB,KAAK,MAAM,CAAAE,OAAKF,IAAG,WAAWE,EAAC,CAAC;AAEvE,YAAM,WAAW,GAAG,SAAS,IAAI,QAAQ;AACzC,UAAI;AAGF,cAAM,KAAK,MAAMF,IAAG,SAAS,KAAK,UAAU,GAAG;AAC/C,YAAI;AACF,mBAAS,IAAI,GAAG,IAAI,MAAM,aAAa,KAAK;AAC1C,kBAAM,GAAG,MAAM,MAAM,OAAO,IAAI,CAAC,CAAW;AAAA,UAC9C;AAAA,QACF,UAAE;AACA,gBAAM,GAAG,MAAM;AAAA,QACjB;AACA,cAAMA,IAAG,SAAS,OAAO,UAAU,SAAS;AAAA,MAC9C,QAAQ;AAEN,YAAI;AACF,gBAAMA,IAAG,SAAS,GAAG,UAAU,EAAE,OAAO,KAAK,CAAC;AAAA,QAChD,QAAQ;AAAA,QAER;AACA,eAAO,EAAE,IAAI,OAAO,OAAO,eAAe;AAAA,MAC5C;AACA,aAAO,EAAE,IAAI,MAAM,MAAM,UAAU;AAAA,IACrC,UAAE;AACA,WAAK,QAAQ,OAAO,QAAQ;AAAA,IAC9B;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,MAAM,UAAwB;AAC5B,SAAK,QAAQ,OAAO,QAAQ;AAAA,EAC9B;AAAA;AAAA,EAGA,WAAiB;AACf,SAAK,QAAQ,MAAM;AAAA,EACrB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOQ,eAAqB;AAC3B,UAAM,MAAM,KAAK,IAAI;AACrB,eAAW,CAAC,IAAI,KAAK,KAAK,KAAK,SAAS;AACtC,UAAI,MAAM,MAAM,cAAc,cAAe,MAAK,QAAQ,OAAO,EAAE;AAAA,IACrE;AAAA,EACF;AACF;;;ACxRO,IAAM,gBAAN,MAAoB;AAAA,EAGzB,YAA6BG,SAA6B;AAA7B,kBAAAA;AAC3B,SAAK,UAAUA,QAAO,aAAa;AAAA,EACrC;AAAA,EAF6B;AAAA,EAFrB;AAAA;AAAA,EAOR,SAAS,OAAe,UAAkB,QAAsB;AAC9D,QAAI,CAAC,MAAO;AACZ,UAAM,MAAM,KAAK,QAAQ,UAAU,CAAC,MAAM,EAAE,UAAU,KAAK;AAC3D,QAAI,OAAO,EAAG,MAAK,QAAQ,GAAG,IAAI,EAAE,OAAO,UAAU,OAAO;AAAA,QACvD,MAAK,QAAQ,KAAK,EAAE,OAAO,UAAU,OAAO,CAAC;AAClD,SAAK,OAAO,aAAa,KAAK,OAAO;AAAA,EACvC;AAAA;AAAA,EAGA,WAAW,OAAqB;AAC9B,UAAM,OAAO,KAAK,QAAQ,OAAO,CAAC,MAAM,EAAE,UAAU,KAAK;AACzD,QAAI,KAAK,WAAW,KAAK,QAAQ,OAAQ;AACzC,SAAK,UAAU;AACf,SAAK,OAAO,aAAa,KAAK,OAAO;AAAA,EACvC;AAAA;AAAA,EAGA,SAAmB;AACjB,WAAO,KAAK,QAAQ,IAAI,CAAC,MAAM,EAAE,KAAK;AAAA,EACxC;AACF;;;APRA,IAAMC,OAAM,aAAa,IAAI;AAI7B,IAAM,gBAAgB,aAAa,aAAa;AAShD,IAAM,6BAA6B,MACjC,QAAQ,IAAI,iCAAiC;AAS/C,IAAM,8BAA8B;AAYpC,IAAM,oBAAoB,CAAC,OAAgB,QACzC,OAAO,SAAS,EAAE,EACf,MAAM,GAAG,GAAG,EAEZ,QAAQ,oBAAoB,OAAK,MAAM,EAAE,WAAW,CAAC,EAAE,SAAS,EAAE,EAAE,SAAS,GAAG,GAAG,CAAC,EAAE;AAY3F,IAAM,oBAAoB;AAC1B,IAAMC,YAAWC,MAAK,KAAKC,IAAG,QAAQ,GAAG,iBAAiB;AAC1D,IAAM,qBAAqBD,MAAK,KAAKD,WAAU,mBAAmB;AAElE,SAAS,2BAAmC;AAC1C,MAAI;AACF,UAAM,WAAWG,IAAG,aAAa,oBAAoB,OAAO,EAAE,KAAK;AACnE,QAAI,SAAS,WAAW,GAAI,QAAO;AAAA,EACrC,QAAQ;AAAA,EAER;AACA,QAAM,QAAQ,OAAO,WAAW;AAChC,MAAI;AACF,QAAI,CAACA,IAAG,WAAWH,SAAQ,EAAG,CAAAG,IAAG,UAAUH,WAAU,EAAE,WAAW,KAAK,CAAC;AACxE,IAAAG,IAAG,cAAc,oBAAoB,OAAO,EAAE,MAAM,IAAM,CAAC;AAAA,EAC7D,SAAS,KAAK;AACZ,YAAQ,MAAM,yDAAqC,GAAG;AAAA,EACxD;AACA,SAAO;AACT;AAEA,IAAM,gBAAgB,yBAAyB;AAK/C,IAAI,0BAA0B;AAe9B,SAAS,YACP,UACA,WACA,SACe;AACf,QAAM,aAAa,CAAC;AACpB,MAAI,WAAY,2BAA0B;AAC1C,SAAO;AAAA,IACL,MAAM;AAAA,IACN;AAAA,IACA,YAAY;AAAA,IACZ,UAAU;AAAA,IACV,UAAU;AAAA,IACV,kBAAkB;AAAA,IAClB,eAAe;AAAA,IACf,UAAU,QAAQ;AAAA,IAClB,YAAY,QAAQ;AAAA;AAAA,IAEpB,GAAI,aAAa,EAAE,mBAAmB,KAAK,IAAI,CAAC;AAAA,EAClD;AACF;AAEA,SAAS,aAAqB;AAC5B,QAAM,aAAaD,IAAG,kBAAkB;AACxC,aAAW,SAAS,OAAO,OAAO,UAAU,GAAG;AAC7C,QAAI,CAAC,MAAO;AACZ,eAAW,QAAQ,OAAO;AACxB,UAAI,KAAK,WAAW,UAAU,CAAC,KAAK,UAAU;AAC5C,eAAO,KAAK;AAAA,MACd;AAAA,IACF;AAAA,EACF;AACA,SAAO;AACT;AAgBO,IAAM,YAAN,MAAgB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAwFrB,YACE,KACA,WACiBE,SACAC,WACjB;AAFiB,kBAAAD;AACA,oBAAAC;AAEjB,SAAK,MAAM;AACX,SAAK,gBAAgB,IAAI,cAAcD,OAAM;AAI7C,QAAI;AAAA,MAAmB,CAAC,WAAW,UACjC,KAAK,mBAAmB,WAAW,KAAK;AAAA,IAC1C;AACA,SAAK,YAAY;AAEjB,SAAK,cAAc,IAAI;AAAA,MACrB,CAAC,WAAW,WAAW,QAAQ,YAAY;AAEzC,YAAI,WAAW,UAAU;AACvB,UAAAL,KAAI;AAAA,YACF,8CAAyC,SAAS,QAAQ,SAAS,iBAAiB,QAAQ,MAAM,UAAU,CAAC;AAAA,UAC/G;AAAA,QACF,OAAO;AACL,UAAAA,KAAI;AAAA,YACF,8CAAyC,SAAS,QAAQ,SAAS,IAAI,MAAM,MAC1E,WAAW,SAAS,SAAS,QAAQ,QAAQ,KAAK,KAAK,QAAQ,MAAM,KAAK,KAAK,EAAE;AAAA,UACtF;AAAA,QACF;AACA,cAAM,MAAM;AAAA,UACV,MAAM;AAAA,UACN;AAAA,UACA;AAAA,UACA;AAAA,UACA,MAAM,QAAQ;AAAA,UACd,UAAU,QAAQ;AAAA,QACpB;AAIA,YAAI,WAAW,QAAQ;AACrB,eAAK,UAAU,GAAG;AAAA,QACpB,OAAO;AACL,eAAK,kBAAkB,GAAG;AAAA,QAC5B;AAAA,MACF;AAAA;AAAA;AAAA,MAGA;AAAA,QACE,QAAQ,CAAC,WAAW,WAAW,OAAO,SAAS;AAC7C,eAAK,UAAU;AAAA,YACb,MAAM;AAAA,YACN;AAAA,YACA;AAAA,YACA,IAAI,KAAK,IAAI;AAAA,YACb;AAAA,YACA;AAAA,UACF,CAAC;AAAA,QACH;AAAA,QACA,aAAa,CAAC,WAAW,WAAW,cAAc;AAChD,eAAK,UAAU;AAAA,YACb,MAAM;AAAA,YACN;AAAA,YACA;AAAA,YACA;AAAA,UACF,CAAC;AAAA,QACH;AAAA,MACF;AAAA,IACF;AAIA,SAAK,IAAI,yBAAyB,SAAO,KAAK,YAAY,oBAAoB,GAAG,CAAC;AAGlF,SAAK,aAAa,IAAI,mBAAmB;AAAA,MACvC,YAAY,CAAC,QAAQ,KAAK,IAAI,gBAAgB,GAAG;AAAA,IACnD,CAAC;AAED,UAAM,SAAS,KAAK,OAAO,UAAU;AACrC,UAAM,cAAc,KAAK,OAAO,uBAAuB;AACvD,UAAM,SAAS,KAAK,OAAO,iBAAiB;AAC5C,UAAM,SAAS,KAAK,OAAO,iBAAiB;AAE5C,IAAAA,KAAI;AAAA,MACF,6BAA6B,MAAM,cAAc,WAAW,SAAS,MAAM,SAAS,MAAM;AAAA,IAC5F;AAEA,SAAK,SAAS;AACd,SAAK,sBAAsB;AAC3B,SAAK,gBAAgB;AACrB,SAAK,iBAAiB,QAAQ,MAAM;AACpC,SAAK,eAAe;AAAA,EACtB;AAAA,EA1FmB;AAAA,EACA;AAAA,EA3FX;AAAA,EACA;AAAA;AAAA;AAAA;AAAA;AAAA,EAKA;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,sBAAsB,oBAAI,IAA2C;AAAA,EAErE,gBAAgB,oBAAI,IAAe;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQnC,wBAAwB,oBAAI,IAAuB;AAAA,EACnD,aAAa,oBAAI,QAAqC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAUtD,mBAAmB,oBAAI,IAAyC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAMhE,mBAAmB,oBAAI,QAA6B;AAAA,EACpD,MAA8B;AAAA,EAE9B,UAA4B;AAAA,EAC5B,aAA4B;AAAA,EAC5B,kBAAkB;AAAA,EAClB,cAA2B;AAAA,EAC3B,gBAAgB;AAAA,EAChB,YAAY;AAAA,EAEZ,SAAiB;AAAA,EACjB,sBAA8B;AAAA,EAC9B,gBAAwB;AAAA;AAAA,EAGxB,iBAAwC;AAAA,EACxC,YAAmC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQnC,wBAAuC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAO9B,uBAAuB,oBAAI,IAA4B;AAAA;AAAA,EAGvD;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAgHjB,kBACE,KACA,cACM;AACN,eAAW,MAAM,KAAK,eAAe;AACnC,UAAI,GAAG,eAAe,UAAU,KAAM;AACtC,UAAI,cAAc;AAChB,cAAM,OAAO,KAAK,WAAW,IAAI,EAAE;AACnC,cAAM,aAAa;AAAA,UACjB,GAAG;AAAA,UACH,GAAG,aAAa,EAAE,MAAM,UAAU,UAAU,MAAM,SAAS,CAAC;AAAA,QAC9D;AACA,WAAG,KAAK,cAAc,UAAU,CAAC;AAAA,MACnC,OAAO;AACL,WAAG,KAAK,cAAc,GAAG,CAAC;AAAA,MAC5B;AAAA,IACF;AACA,QAAI,KAAK,SAAS,eAAe,UAAU,MAAM;AAC/C,UAAI,cAAc;AAChB,cAAM,OAAO,KAAK,WAAW,IAAI,KAAK,OAAO;AAC7C,cAAM,aAAa;AAAA,UACjB,GAAG;AAAA,UACH,GAAG,aAAa,EAAE,MAAM,UAAU,UAAU,MAAM,SAAS,CAAC;AAAA,QAC9D;AACA,aAAK,QAAQ,KAAK,cAAc,UAAU,CAAC;AAAA,MAC7C,OAAO;AACL,aAAK,QAAQ,KAAK,cAAc,GAAG,CAAC;AAAA,MACtC;AAAA,IACF;AAAA,EACF;AAAA;AAAA;AAAA,EAKA,eAAyB;AACvB,WAAO,KAAK,cAAc,OAAO;AAAA,EACnC;AAAA;AAAA,EAGA,gBAAgB,WAA4B;AAC1C,WAAO,KAAK,sBAAsB,IAAI,SAAS;AAAA,EACjD;AAAA;AAAA,EAGA,YAAY,KAA0B;AACpC,QAAI,KAAK,SAAS,eAAe,UAAU,MAAM;AAC/C,WAAK,QAAQ,KAAK,cAAc,GAAG,CAAC;AAAA,IACtC;AAAA,EACF;AAAA;AAAA,EAIQ,oBAAoB,KAA0B;AACpD,QAAI,KAAK,UAAW;AACpB,SAAK,UAAU,GAAG;AAAA,EACpB;AAAA;AAAA,EAIQ,eAAe,QAA2B;AAChD,SAAK,cAAc;AACnB,IAAAA,KAAI;AAAA,MACF,kBAAkB,MAAM,eAAe,KAAK,eAAe,QAAO,oBAAI,KAAK,GAAE,YAAY,CAAC;AAAA,IAC5F;AAEA,SAAK,oBAAoB;AAAA,MACvB,MAAM;AAAA,MACN;AAAA,MACA,YAAY,KAAK;AAAA,IACnB,CAAC;AAED,SAAK,kBAAkB;AAAA,MACrB,MAAM;AAAA,MACN;AAAA,MACA,YAAY,KAAK;AAAA,IACnB,CAAC;AAAA,EACH;AAAA;AAAA,EAGA,8BAAoC;AAClC,SAAK,oBAAoB;AAAA,MACvB,MAAM;AAAA,MACN,QAAQ,KAAK;AAAA,MACb,YAAY,KAAK;AAAA,IACnB,CAAC;AAAA,EACH;AAAA;AAAA,EAIA,YAAkB;AAChB,UAAM,OAAO,KAAK,gBAAgB;AAClC,SAAK,UAAU,EAAE,MAAM,WAAW,GAAG,KAAK,CAAC;AAAA,EAC7C;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAmBA,sBAAsB,WAAyB;AAC7C,IAAAA,KAAI,KAAK,yCAAyC,SAAS,EAAE;AAM7D,SAAK,UAAU;AAAA,MACb,MAAM;AAAA,MACN;AAAA,MACA,QAAQ;AAAA,IACV,CAAC;AACD,UAAM,KAAK,KAAK,sBAAsB,IAAI,SAAS;AAEnD,QAAI,MAAM,OAAO,KAAK,SAAS;AAC7B,UAAI;AACF,WAAG,MAAM,MAAM,kBAAkB;AAAA,MACnC,SAAS,KAAK;AACZ,QAAAA,KAAI;AAAA,UACF,2DAA2D,SAAS;AAAA,UACpE;AAAA,QACF;AAAA,MACF;AAAA,IACF;AACA,SAAK,IAAI,UAAU,mBAAmB,SAAS;AAAA,EACjD;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAcA,mBACE,KAcM;AACN,YAAQ,IAAI,MAAM;AAAA,MAChB,KAAK;AACH,QAAAA,KAAI;AAAA,UACF,kCAAkC,IAAI,SAAS,cAAc,IAAI,SAAS,SAAS,KAAK,UAAU,IAAI,IAAI,CAAC;AAAA,QAC7G;AACA,aAAK,YAAY,IAAI,IAAI,WAAW,IAAI,WAAW,IAAI,MAAM,IAAI,GAAG;AACpE;AAAA,MACF,KAAK;AACH,QAAAA,KAAI;AAAA,UACF,mCAAmC,IAAI,SAAS,cAAc,IAAI,aAAa,OAAO;AAAA,QACxF;AACA,YAAI,IAAI,WAAW;AAEjB,eAAK,YAAY,KAAK,IAAI,WAAW,IAAI,SAAS;AAAA,QACpD,OAAO;AAEL,eAAK,IAAI,aAAa,IAAI,SAAS;AAAA,QACrC;AACA;AAAA,MACF,KAAK;AAEH,QAAAA,KAAI,KAAK,4CAA4C,IAAI,SAAS,EAAE;AACpE,aAAK,IAAI,uBAAuB,IAAI,SAAS;AAC7C;AAAA,MACF,KAAK;AAEH,QAAAA,KAAI;AAAA,UACF,qDAAqD,IAAI,SAAS,YAAY,IAAI,SAAS;AAAA,QAC7F;AACA,aAAK,IAAI,2BAA2B,IAAI,WAAW,IAAI,SAAS;AAChE;AAAA,MACF,KAAK,sBAAsB;AAIzB,cAAM,MAAM,IAAI,OAAO,KAAK,IAAI,gBAAgB,IAAI,SAAS;AAK7D,YAAI,SAAS,IAAI;AACjB,YAAI,SAAS;AACb,cAAM,WAAW,kBAAkB,GAAG;AACtC,YAAI,IAAI,gBAAgB,IAAI,QAAQ;AAElC,gBAAM,SACJ,IAAI,UAAU,SAAS,KAAK,OAAK,EAAE,OAAO,IAAI,SAAS,IACnD,IAAI,YACJ,SAAS,CAAC,GAAG;AACnB,cAAI,QAAQ;AACV,qBAAS;AACT,qBAAS;AAAA,UACX;AAAA,QACF;AAMA,cAAM,WAAW,SAAS,sBAAsB,KAAK,MAAM,IAAI,CAAC;AAChE,aAAK,UAAU;AAAA,UACb,MAAM;AAAA,UACN,WAAW,IAAI;AAAA,UACf,WAAW;AAAA,UACX;AAAA,QACF,CAAC;AAGD,aAAK,UAAU;AAAA,UACb,MAAM;AAAA,UACN,WAAW,IAAI;AAAA,UACf;AAAA,UACA;AAAA,QACF,CAAC;AACD,QAAAA,KAAI;AAAA,UACF,2CAA2C,IAAI,SAAS,YAAY,MAAM,QAAQ,GAAG,WAAW,MAAM,iBAAiB,IAAI,gBAAgB,KAAK,cAAc,IAAI,UAAU,KAAK,qBAAqB,IAAI,oBAAoB,KAAK;AAAA,QACrO;AACA,YAAI,IAAI,kBAAkB;AAKxB,gBAAM,MAAM,IAAI;AAChB,gBAAM,OAAO,KAAK,oBAAoB,IAAI,GAAG;AAC7C,cAAI,KAAM,cAAa,IAAI;AAC3B,eAAK,oBAAoB;AAAA,YACvB;AAAA,YACA,WAAW,MAAM;AACf,mBAAK,oBAAoB,OAAO,GAAG;AACnC,mBAAK,YAAY,YAAY,KAAK,QAAQ,KAAK,MAAM;AAAA,YACvD,GAAG,GAAG;AAAA,UACR;AACA,eAAK,IAAI,aAAa,GAAG;AAAA,QAC3B,OAAO;AACL,eAAK,YAAY,YAAY,IAAI,WAAW,QAAQ,KAAK,MAAM;AAAA,QACjE;AACA;AAAA,MACF;AAAA,MACA,KAAK;AAEH,aAAK,YAAY,cAAc,IAAI,WAAW,IAAI,IAAI;AACtD;AAAA,MACF,KAAK,uBAAuB;AAG1B,QAAAA,KAAI;AAAA,UACF,4CAA4C,IAAI,SAAS,YAAY,IAAI,aAAa,cAAc;AAAA,QACtG;AAGA,cAAM,UAAU,KAAK,oBAAoB,IAAI,IAAI,SAAS;AAC1D,YAAI,SAAS;AACX,uBAAa,OAAO;AACpB,eAAK,oBAAoB,OAAO,IAAI,SAAS;AAAA,QAC/C;AACA,aAAK,YAAY,aAAa,IAAI,WAAW,IAAI,SAAS;AAC1D;AAAA,MACF;AAAA,MACA,KAAK,mCAAmC;AAItC,cAAM,MAAM,IAAI;AAChB,cAAM,MAAM,IAAI;AAChB,cAAM,YAAY;AAIhB,gBAAM,KAAK,IAAI,+BAA+B,GAAG;AACjD,eAAK,YAAY,aAAa,QAAW,GAAG;AAC5C,cAAI,KAAK,IAAI,gBAAgB,GAAG,MAAM,UAAU;AAC9C,YAAAA,KAAI;AAAA,cACF,kFAA6E,GAAG;AAAA,YAClF;AACA;AAAA,UACF;AACA,UAAAA,KAAI;AAAA,YACF,wDAAwD,GAAG,YAAY,GAAG;AAAA,UAC5E;AAIA,cAAI,CAAC,mBAAmB,KAAK,GAAG,GAAG;AACjC,YAAAA,KAAI;AAAA,cACF,yEAAoE,GAAG;AAAA,YACzE;AACA;AAAA,UACF;AAKA,eAAK,IAAI,UAAU,KAAK,GAAM;AAC9B,qBAAW,MAAM;AACf,iBAAK,IAAI,UAAU,KAAK,mBAAmB,GAAG;AAAA,CAAI;AAAA,UACpD,GAAG,GAAG;AAAA,QACR,GAAG;AACH;AAAA,MACF;AAAA,IACF;AAAA,EACF;AAAA,EAEQ,kBAIN;AACA,QAAI,KAAK,YAAY;AACnB,aAAO;AAAA;AAAA;AAAA,QAGL,KAAK,UAAU,KAAK,UAAU;AAAA,QAC9B,MAAM;AAAA,QACN,SAAS;AAAA,UACP,YAAY,cAAc,MAAM,GAAG,CAAC;AAAA,UACpC,WAAW,KAAK;AAAA,QAClB;AAAA,MACF;AAAA,IACF;AAEA,UAAM,QAAQ,WAAW;AACzB,WAAO;AAAA,MACL,KAAK,QAAQ,KAAK,IAAI,KAAK,MAAM;AAAA,MACjC,MAAM;AAAA,MACN,SAAS,EAAE,OAAO,WAAW,KAAK,OAAO;AAAA,IAC3C;AAAA,EACF;AAAA;AAAA,EAIQ,iBAAuB;AAC7B,SAAK,cAAc;AACnB,SAAK,iBAAiB,YAAY,MAAM;AACtC,UAAI,KAAK,SAAS,eAAe,UAAU,KAAM;AACjD,WAAK,QAAQ,KAAK;AAClB,WAAK,YAAY,WAAW,MAAM;AAChC,QAAAA,KAAI;AAAA,UACF,8BAA6B,oBAAI,KAAK,GAAE,YAAY,CAAC;AAAA,QACvD;AACA,aAAK,SAAS,UAAU;AACxB,aAAK,UAAU;AAAA,MACjB,GAAG,KAAK,aAAa;AAAA,IACvB,GAAG,KAAK,mBAAmB;AAAA,EAC7B;AAAA,EAEQ,gBAAsB;AAC5B,QAAI,KAAK,gBAAgB;AACvB,oBAAc,KAAK,cAAc;AACjC,WAAK,iBAAiB;AAAA,IACxB;AACA,QAAI,KAAK,WAAW;AAClB,mBAAa,KAAK,SAAS;AAC3B,WAAK,YAAY;AAAA,IACnB;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA,EAOQ,cACN,KACA,SAA2B,MACrB;AACN,YAAQ,IAAI,MAAM;AAAA,MAChB,KAAK;AACH,aAAK,IAAI,kBAAkB;AAE3B;AAAA,MACF,KAAK;AACH,aAAK,IAAI,WAAW,IAAI,OAAO;AAC/B,aAAK,IAAI,kBAAkB;AAC3B;AAAA,MACF,KAAK;AACH,aAAK,IAAI,YAAY,IAAI,OAAO;AAChC,aAAK,IAAI,kBAAkB;AAC3B;AAAA,MACF,KAAK;AACH,aAAK,IAAI,cAAc,IAAI,SAAS;AACpC,aAAK,IAAI,kBAAkB;AAC3B;AAAA,MACF,KAAK;AAOH,aAAK,wBAAwB,IAAI;AACjC,aAAK,IAAI,QAAQ,IAAI,SAAS;AAC9B;AAAA,MACF,KAAK;AAEH,aAAK,IAAI,WAAW,IAAI,SAAS;AACjC;AAAA,MACF,KAAK,kBAAkB;AAIrB,cAAM,OAAO,IAAI,QAAQ;AACzB,cAAM,QAAQ,CAAC,GAAG,IAAI,EAAE,IAAI,OAAK,EAAE,WAAW,CAAC,CAAC;AAChD,cAAM,WAAW,MAAM,SAAS,CAAI;AACpC,cAAM,QAAQ,KAAK;AAAA,UACjB;AAAA,UACA,OAAK,MAAM,EAAE,WAAW,CAAC,EAAE,SAAS,EAAE,EAAE,SAAS,GAAG,GAAG,CAAC;AAAA,QAC1D;AACA,YAAI,UAAU;AACZ,UAAAA,KAAI;AAAA,YACF,qDAAgD,IAAI,SAAS,UAAU,MAAM,MAAM,WAAW,KAAK;AAAA,UACrG;AAAA,QACF,WAAW,KAAK,SAAS,GAAG;AAC1B,UAAAA,KAAI;AAAA,YACF,uCAAuC,IAAI,SAAS,UAAU,MAAM,MAAM,WAAW,MAAM,MAAM,GAAG,EAAE,CAAC;AAAA,UACzG;AAAA,QACF;AACA,aAAK,IAAI,UAAU,IAAI,WAAW,IAAI,IAAI;AAG1C,YAAI,QAAQ;AACV,gBAAM,OAAO,KAAK,WAAW,IAAI,MAAM;AACvC,cAAI,MAAM,UAAU;AAClB,iBAAK,IAAI,UAAU,oBAAoB,IAAI,WAAW,KAAK,QAAQ;AAAA,UACrE;AAAA,QACF;AACA;AAAA,MACF;AAAA,MACA,KAAK;AAEH,aAAK,cAAc,SAAS,IAAI,OAAO,IAAI,UAAU,IAAI,MAAM;AAC/D;AAAA,MACF,KAAK;AACH,aAAK,cAAc,WAAW,IAAI,KAAK;AACvC;AAAA,MACF,KAAK;AAQH,aAAK,IAAI,OAAO,IAAI,WAAW,IAAI,MAAM,IAAI,MAAM,QAAQ;AAC3D;AAAA,MACF,KAAK;AACH,aAAK,UAAU;AACf;AAAA,MACF,KAAK;AACH,aAAK,IAAI,iBAAiB;AAC1B;AAAA,MACF,KAAK;AACH,aAAK,IAAI,gBAAgB,IAAI,SAAS;AACtC;AAAA,MACF,KAAK;AACH,aAAK,UAAU,EAAE,MAAM,kBAAkB,WAAW,IAAI,UAAU,CAAC;AACnE;AAAA,MACF,KAAK;AACH,aAAK,UAAU;AACf;AAAA,MACF,KAAK;AACH,aAAK,IAAI,iBAAiB,IAAI,WAAW,IAAI,MAAM;AACnD;AAAA;AAAA,MAEF,KAAK;AAEH,aAAK,wBAAwB,IAAI;AACjC,aAAK,oBAAoB,IAAI,WAAW,QAAQ,IAAI,MAAM,IAAI,IAAI;AAClE;AAAA,MACF,KAAK;AAGH,QAAAA,KAAI,KAAK,uCAAuC,IAAI,SAAS,EAAE;AAC/D,aAAK,IAAI,UAAU,SAAS,IAAI,SAAS;AAQzC,YAAI,QAAQ;AACV,gBAAM,OAAO,KAAK,WAAW,IAAI,MAAM;AACvC,cAAI,MAAM,UAAU;AAClB,iBAAK,IAAI,UAAU,eAAe,IAAI,WAAW,KAAK,QAAQ;AAAA,UAChE;AAAA,QACF;AACA;AAAA,MACF,KAAK;AAMH,YAAI,QAAQ;AACV,gBAAM,OAAO,KAAK,WAAW,IAAI,MAAM;AACvC,cAAI,MAAM,UAAU;AAClB,iBAAK,IAAI,UAAU,oBAAoB,IAAI,WAAW,KAAK,QAAQ;AAAA,UACrE;AAAA,QACF;AACA;AAAA,MACF,KAAK,mBAAmB;AAItB,cAAM,OAAO,SAAS,KAAK,WAAW,IAAI,MAAM,IAAI;AACpD,cAAM,SAAS,MAAM,YAAY;AAEjC,cAAM,SAAS,kBAAkB,IAAI,OAAO,EAAE;AAC9C,cAAM,UAAU,kBAAkB,IAAI,QAAQ,EAAE;AAChD,cAAM,WAAW,kBAAkB,IAAI,SAAS,GAAI;AACpD,cAAM,WAAW,IAAI,UAAU,WAAM,kBAAkB,IAAI,SAAS,GAAI,CAAC,KAAK;AAC9E,cAAM,MAAM,kBAAkB,IAAI,IAAI,EAAE;AACxC,cAAM,WAAW,IAAI,YAAY,kBAAkB,IAAI,WAAW,EAAE,IAAI;AACxE,cAAM,OACJ,IAAI,MAAM,MAAM,OAAO,KAAK,QAAQ,GAAG,QAAQ,cACjC,GAAG,YAAY,QAAQ,WAAW,MAAM;AAIxD,cAAM,WAAmB,IAAI;AAC7B,YAAI,aAAa,QAAS,eAAc,MAAM,IAAI;AAAA,iBACzC,aAAa,UAAW,eAAc,KAAK,IAAI;AAAA,iBAC/C,aAAa,OAAQ,eAAc,KAAK,IAAI;AAAA,YAChD,eAAc,KAAK,IAAI;AAC5B;AAAA,MACF;AAAA,MACA,KAAK,qBAAqB;AAExB,QAAAA,KAAI;AAAA,UACF,0CAA0C,IAAI,SAAS,OAAO,IAAI,QAAQ,SAAS,IAAI,IAAI,WAAW,IAAI,WAAW;AAAA,QACvH;AACA,cAAM,WAAW,KAAK,WAAW;AAAA,UAC/B,IAAI;AAAA,UACJ,IAAI;AAAA,UACJ,IAAI;AAAA,UACJ,IAAI;AAAA,UACJ,IAAI;AAAA,QACN;AACA,YAAI,UAAU;AACZ,UAAAA,KAAI;AAAA,YACF,mDAAmD,IAAI,SAAS,OAAO,IAAI,QAAQ,UAAU,SAAS,KAAK;AAAA,UAC7G;AACA,eAAK,aAAa,QAAQ;AAAA,YACxB,MAAM;AAAA,YACN,WAAW,IAAI;AAAA,YACf,UAAU,IAAI;AAAA,YACd,IAAI;AAAA,YACJ,OAAO,SAAS;AAAA,UAClB,CAAC;AAAA,QACH;AACA;AAAA,MACF;AAAA,MACA,KAAK;AAEH,aAAK,WAAW,MAAM,IAAI,UAAU,IAAI,OAAO,IAAI,IAAI;AACvD;AAAA,MACF,KAAK,mBAAmB;AAGtB,cAAM,EAAE,WAAW,SAAS,IAAI;AAChC,QAAAA,KAAI,KAAK,wCAAwC,SAAS,OAAO,QAAQ,EAAE;AAC3E,aAAK,WACF,IAAI,QAAQ,EACZ,KAAK,SAAO;AACX,UAAAA,KAAI;AAAA,YACF,yCAAyC,SAAS,OAAO,QAAQ,OAAO,IAAI,EAAE,MAC3E,IAAI,KAAK,SAAS,IAAI,IAAI,KAAK,UAAU,IAAI,KAAK;AAAA,UACvD;AACA,eAAK,aAAa,QAAQ;AAAA,YACxB,MAAM;AAAA,YACN;AAAA,YACA;AAAA,YACA,GAAG;AAAA,UACL,CAAC;AAAA,QACH,CAAC,EACA,MAAM,SAAO;AAGZ,UAAAA,KAAI,MAAM,0CAA0C,QAAQ,KAAK,GAAG,EAAE;AACtE,eAAK,aAAa,QAAQ;AAAA,YACxB,MAAM;AAAA,YACN;AAAA,YACA;AAAA,YACA,IAAI;AAAA,YACJ,OAAO;AAAA,UACT,CAAC;AAAA,QACH,CAAC;AACH;AAAA,MACF;AAAA,MACA,KAAK;AAEH,aAAK,IAAI,WAAW,IAAI,SAAS;AACjC;AAAA,MACF,KAAK,uCAAuC;AAG1C,YAAI,QAAQ;AACV,gBAAM,OAAO,KAAK,WAAW,IAAI,MAAM;AACvC,cAAI,MAAM,UAAU;AAClB,iBAAK,IAAI,UAAU;AAAA,cACjB,IAAI;AAAA,cACJ,KAAK;AAAA,YACP;AAAA,UACF;AAAA,QACF;AACA;AAAA,MACF;AAAA;AAAA,MAEA,KAAK;AAAA,MACL,KAAK;AAAA,MACL,KAAK;AAAA,MACL,KAAK;AAAA,MACL,KAAK;AAAA,MACL,KAAK;AAAA,MACL,KAAK;AAAA,MACL,KAAK;AACH,aAAK,mBAAmB,GAAG;AAC3B;AAAA,IACJ;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAYQ,oBACN,WACA,QACA,MACA,MACM;AACN,IAAAA,KAAI;AAAA,MACF,uCAAuC,SAAS,eAChC,KAAK,IAAI,WAAW,SAAS,CAAC,eAC9B,QAAQ,GAAG,IAAI,QAAQ,GAAG;AAAA,IAC5C;AAKA,QAAI,UAAU,KAAK,IAAI,UAAU,qBAAqB,SAAS,GAAG;AAChE,MAAAA,KAAI;AAAA,QACF,2DAA2D,SAAS;AAAA,MACtE;AAIA,WAAK,UAAU;AAAA,QACb,MAAM;AAAA,QACN;AAAA,QACA,QAAQ;AAAA,QACR,eAAe,KAAK,IAAI,UAAU,yBAAyB,SAAS;AAAA,MACtE,CAAC;AACD,UAAI,WAAW,KAAK,SAAS;AAC3B,YAAI;AACF,iBAAO,MAAM,MAAM,mBAAmB;AAAA,QACxC,QAAQ;AAAA,QAER;AAAA,MACF;AAGA,YAAM,OAAO,KAAK,WAAW,IAAI,MAAM;AACvC,UAAI,MAAM,UAAU;AAClB,aAAK,IAAI,UAAU,eAAe,WAAW,KAAK,UAAU,MAAM,IAAI;AAAA,MACxE;AACA;AAAA,IACF;AAGA,QAAI,QAAQ;AACV,WAAK,sBAAsB,IAAI,WAAW,MAAM;AAAA,IAClD;AAKA,SAAK,IAAI,UAAU,mBAAmB,WAAW,MAAM,IAAI;AAI3D,UAAM,aAAa,KAAK,IAAI,WAAW,SAAS;AAMhD,UAAM,UAAU,KAAK,IAAI,WAAW,SAAS;AAC7C,UAAM,mBACJ,WAAW,QAAQ,OAAO,KAAK,QAAQ,OAAO,IAC1C,EAAE,MAAM,QAAQ,MAAM,MAAM,QAAQ,KAAK,IACzC,CAAC;AAIP,QACE,OAAO,SAAS,YAChB,OAAO,SAAS,YAChB,OAAO,KACP,OAAO,GACP;AACA,UAAI,CAAC,KAAK,IAAI,WAAW,SAAS,GAAG;AAEnC,aAAK,IAAI,iBAAiB,WAAW,MAAM,IAAI;AAAA,MACjD,OAAO;AAIL,aAAK,IAAI,OAAO,WAAW,MAAM,MAAM,QAAQ;AAAA,MACjD;AAAA,IACF;AAGA,SAAK,IAAI,QAAQ,SAAS;AAG1B,SAAK,IAAI,UAAU,SAAS,SAAS;AAIrC,QAAI,QAAQ;AACV,YAAM,OAAO,KAAK,WAAW,IAAI,MAAM;AACvC,UAAI,MAAM,UAAU;AAClB,aAAK,IAAI,UAAU,eAAe,WAAW,KAAK,UAAU,MAAM,IAAI;AAAA,MACxE;AAAA,IACF;AAQA,QACE,cACA,OAAO,SAAS,YAChB,OAAO,SAAS,YAChB,OAAO,KACP,OAAO,GACP;AACA,WAAK,yBAAyB,WAAW,MAAM,IAAI;AAAA,IACrD;AAUA,QACE,2BAA2B,KAC3B,cACA,OAAO,SAAS,YAChB,OAAO,SAAS,YAChB,OAAO,KACP,OAAO,GACP;AACA,YAAM,QAAQ,KAAK,mBAAmB,SAAS;AAC/C,UAAI,OAAO;AACT,cAAM,OAAO,KAAK,IAAI,UAAU,iBAAiB,SAAS;AAC1D,QAAAA,KAAI;AAAA,UACF,8CAA8C,SAAS,UAAU,MAAM,MAAM,MAC1E,OACG,YAAY,KAAK,OAAO,kBAAkB,KAAK,aAAa,IAAI,KAAK,IAAI,WAAW,KAAK,MAAM,KAC/F;AAAA,QACR;AACA,YAAI,UAAU,OAAO,eAAe,UAAU,MAAM;AAClD,iBAAO,KAAK,KAAK;AAAA,QACnB,OAAO;AAGL,eAAK,oBAAoB,WAAW,KAAK;AAAA,QAC3C;AAAA,MACF;AAAA,IACF;AAEA,UAAM,SAAS,KAAK,IAAI,cAAc,SAAS;AAC/C,QAAI,CAAC,UAAU,OAAO,QAAQ,GAAG;AAE/B,WAAK,aAAa,QAAQ;AAAA,QACxB,MAAM;AAAA,QACN;AAAA,QACA,YAAY;AAAA,QACZ,aAAa;AAAA,QACb,MAAM;AAAA,QACN,GAAG;AAAA,MACL,CAAC;AACD;AAAA,IACF;AAMA,UAAM,SAAS,OAAO;AAAA,MACpB;AAAA,MACA;AAAA,IACF;AACA,UAAM,QAAQ,OAAO;AACrB,IAAAA,KAAI;AAAA,MACF,uCAAuC,SAAS,UAAU,OAAO,KAAK,CAAC,WAC3D,KAAK,QAAQ,OAAO,SAAS,CAAC,SAAS,wBAAwB;AAAA,IAC7E;AACA,aAAS,IAAI,GAAG,IAAI,OAAO,KAAK;AAC9B,WAAK,aAAa,QAAQ;AAAA,QACxB,MAAM;AAAA,QACN;AAAA,QACA,YAAY;AAAA,QACZ,aAAa;AAAA,QACb,MAAM,OAAO,CAAC;AAAA,QACd,GAAG;AAAA,MACL,CAAC;AAAA,IACH;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EASQ,mBAAmB,WAAkC;AAC3D,UAAM,OAAO,KAAK,IAAI,UAAU,kBAAkB,SAAS;AAC3D,QAAI,CAAC,QAAQ,KAAK,WAAW,EAAG,QAAO;AACvC,WAAO,oBAAoB,WAAW,IAAI;AAAA,EAC5C;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAaQ,4BAA4B,WAAyB;AAC3D,QAAI,CAAC,2BAA2B,EAAG;AACnC,UAAM,WAAW,KAAK,qBAAqB,IAAI,SAAS;AACxD,QAAI,SAAU,cAAa,QAAQ;AACnC,SAAK,qBAAqB;AAAA,MACxB;AAAA,MACA,WAAW,MAAM;AACf,aAAK,qBAAqB,OAAO,SAAS;AAC1C,cAAM,QAAQ,KAAK,mBAAmB,SAAS;AAC/C,YAAI,OAAO;AACT,gBAAM,OAAO,KAAK,IAAI,UAAU,iBAAiB,SAAS;AAC1D,UAAAA,KAAI;AAAA,YACF,8CAA8C,SAAS,UAAU,MAAM,MAAM,MAC1E,OACG,YAAY,KAAK,OAAO,kBAAkB,KAAK,aAAa,IAAI,KAAK,IAAI,WAAW,KAAK,MAAM,KAC/F;AAAA,UACR;AACA,eAAK,oBAAoB,WAAW,KAAK;AAAA,QAC3C;AAAA,MACF,GAAG,2BAA2B;AAAA,IAChC;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EA4BQ,yBACN,WACA,MACA,MACM;AACN,UAAM,MAAM,OAAO,eAAe,OAAO,IAAI,eAAe;AAC5D,SAAK,IAAI,OAAO,WAAW,MAAM,KAAK,QAAQ;AAC9C,SAAK,IAAI,OAAO,WAAW,MAAM,MAAM,QAAQ;AAAA,EACjD;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOQ,aAAa,QAA0B,KAA0B;AACvE,QAAI,UAAU,OAAO,eAAe,UAAU,MAAM;AAClD,aAAO,KAAK,cAAc,GAAG,CAAC;AAAA,IAChC,OAAO;AACL,WAAK,kBAAkB,GAAG;AAAA,IAC5B;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAgBA,mBAAmB,WAAmB,OAAqB;AACzD,eAAW,MAAM,KAAK,eAAe;AACnC,UACE,KAAK,iBAAiB,IAAI,EAAE,MAAM,eAClC,KAAK,WAAW,IAAI,EAAE,GAAG,sBAAsB,aAC/C,GAAG,eAAe,UAAU,MAC5B;AACA,WAAG,KAAK,KAAK;AAAA,MACf;AAAA,IACF;AACA,QACE,KAAK,WACL,KAAK,iBAAiB,IAAI,KAAK,OAAO,MAAM,eAC5C,KAAK,WAAW,IAAI,KAAK,OAAO,GAAG,sBAAsB,aACzD,KAAK,QAAQ,eAAe,UAAU,MACtC;AACA,WAAK,QAAQ,KAAK,KAAK;AAAA,IACzB;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EASQ,oBAAoB,WAAmB,OAAqB;AAClE,eAAW,MAAM,KAAK,eAAe;AACnC,UACE,KAAK,WAAW,IAAI,EAAE,GAAG,sBAAsB,aAC/C,GAAG,eAAe,UAAU,MAC5B;AACA,WAAG,KAAK,KAAK;AAAA,MACf;AAAA,IACF;AACA,QACE,KAAK,WACL,KAAK,WAAW,IAAI,KAAK,OAAO,GAAG,sBAAsB,aACzD,KAAK,QAAQ,eAAe,UAAU,MACtC;AACA,WAAK,QAAQ,KAAK,KAAK;AAAA,IACzB;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAUQ,oBAAoB,IAAwB;AAClD,UAAM,MAAM,KAAK,IAAI;AACrB,UAAM,SAAS,MAAM;AACrB,UAAM,QAAQ,KAAK,iBAAiB,IAAI,EAAE,KAAK,CAAC;AAChD,UAAM,SAAS,MAAM,OAAO,OAAK,IAAI,MAAM;AAC3C,QAAI,OAAO,UAAU,GAAG;AACtB,WAAK,iBAAiB,IAAI,IAAI,MAAM;AACpC,aAAO;AAAA,IACT;AACA,WAAO,KAAK,GAAG;AACf,SAAK,iBAAiB,IAAI,IAAI,MAAM;AACpC,WAAO;AAAA,EACT;AAAA;AAAA,EAIA,YAAkB;AAChB,QAAI,KAAK,UAAW;AACpB,IAAAA,KAAI,KAAK,oCAAoC;AAC7C,SAAK,cAAc;AACnB,QAAI,KAAK,SAAS;AAChB,WAAK,QAAQ,UAAU;AACvB,WAAK,UAAU;AAAA,IACjB;AACA,SAAK,cAAc;AACnB,SAAK,kBAAkB;AACvB,SAAK,eAAe;AAAA,EACtB;AAAA;AAAA,EAIQ,iBAAiB,QAAgB,QAAsB;AAC7D,QAAI;AAMF,YAAM,MAAM,IAAI,gBAAgB,EAAE,MAAM,QAAQ,MAAM,UAAU,CAAC;AACjE,WAAK,MAAM;AAEX,UAAI,GAAG,SAAS,CAAC,QAA+B;AAI9C,cAAM,UACJ,IAAI,SAAS,eACT,kBAAkB,MAAM,gDACxB,mBAAmB,IAAI,QAAQ,IAAI,OAAO;AAChD,QAAAA,KAAI,MAAM,2CAA2C,MAAM,KAAK,IAAI,OAAO,EAAE;AAC7E,YAAI,KAAK,QAAQ,IAAK,MAAK,MAAM;AAEjC,aAAK,UAAU,EAAE,MAAM,SAAS,QAAQ,GAAG,EAAE,YAAY,KAAK,CAAC;AAAA,MACjE,CAAC;AAKD,UAAI,GAAG,aAAa,MAAM;AACxB,QAAAA,KAAI,KAAK,0CAA0C,MAAM,EAAE;AAAA,MAC7D,CAAC;AAED,UAAI,GAAG,cAAc,CAAC,IAAe,QAAyB;AAC5D,cAAM,WAAW,IAAI,QAAQ;AAC7B,YAAI,CAAC,UAAU;AAEb,aAAG,MAAM,MAAM,mBAAmB;AAClC;AAAA,QACF;AAIA,YAAI,gBAAgB;AACpB,cAAM,QAAQ,YAAY,EAAE,EAAE,SAAS,KAAK;AAC5C,WAAG;AAAA,UACD,cAAc;AAAA,YACZ,MAAM;AAAA,YACN;AAAA,UACF,CAAyB;AAAA,QAC3B;AAEA,cAAM,YAAY,WAAW,MAAM;AACjC,cAAI,CAAC,eAAe;AAClB,eAAG;AAAA,cACD,cAAc;AAAA,gBACZ,MAAM;AAAA,gBACN,QAAQ;AAAA,cACV,CAAyB;AAAA,YAC3B;AACA,eAAG,MAAM;AAAA,UACX;AAAA,QACF,GAAG,MAAM;AAET,WAAG,GAAG,WAAW,SAAO;AAGtB,cAAI,OAAO,QAAQ,UAAU;AAC3B,eAAG;AAAA,cACD,oBAAoB,sBAAsB,iBAAiB;AAAA,YAC7D;AACA,eAAG,MAAM,MAAM,kBAAkB;AACjC;AAAA,UACF;AACA,gBAAM,SAAS,OAAO,SAAS,GAAG,IAC9B,MACA,MAAM,QAAQ,GAAG,IACf,OAAO,OAAO,GAAuC,IACrD,OAAO,KAAK,GAAkB;AAEpC,cAAI;AACJ,cAAI;AACF,qBAAS,WAAW,MAAM;AAAA,UAC5B,SAAS,KAAK;AACZ,kBAAM,SACJ,eAAe,kBACX,IAAI,SACJ,sBAAsB;AAC5B,eAAG,KAAK,oBAAoB,MAAM,CAAC;AACnC;AAAA,UACF;AAGA,cAAI,OAAO,SAAS,UAAU;AAC5B,gBAAI,CAAC,cAAe;AACpB,kBAAM,YACJ,KAAK,WAAW,IAAI,EAAE,GAAG,qBACzB,KAAK,yBACL;AACF,gBAAI,WAAW;AACb,mBAAK,IAAI,OAAO,WAAW,OAAO,MAAM,OAAO,MAAM,QAAQ;AAE7D,mBAAK,4BAA4B,SAAS;AAAA,YAC5C,OAAO;AACL,cAAAA,KAAI;AAAA,gBACF;AAAA,cACF;AAAA,YACF;AACA;AAAA,UACF;AAGA,cAAI,OAAO,SAAS,kBAAkB;AACpC,YAAAA,KAAI;AAAA,cACF,+CAA+C,OAAO,OAAO,SAAS,EAAE,CAAC;AAAA,YAC3E;AACA,eAAG,MAAM;AACT;AAAA,UACF;AAGA,cAAI,OAAO,SAAS,WAAY;AAEhC,cAAI;AACF,kBAAM,MAAM,OAAO;AAEnB,gBAAI,CAAC,eAAe;AAGlB,kBAAK,IAAY,SAAS,SAAS;AACjC;AAAC,gBAAC,GAAW,eAAe;AAC5B;AAAA,cACF;AACA,kBAAI,IAAI,SAAS,QAAQ;AACvB,sBAAM,WAAW,WAAW,UAAU,aAAa,EAChD,OAAO,KAAK,EACZ,OAAO,KAAK;AACf,oBAAI,IAAI,UAAU,UAAU;AAC1B,kCAAgB;AAChB,+BAAa,SAAS;AACtB,qBAAG;AAAA,oBACD,cAAc,EAAE,MAAM,UAAU,CAAyB;AAAA,kBAC3D;AAEA,sBAAK,GAAW,cAAc;AAC5B,uBAAG,KAAK,eAAe,CAAC;AAAA,kBAC1B;AAEA,uBAAK,cAAc,IAAI,EAAE;AAGzB,uBAAK,WAAW,IAAI,IAAI;AAAA,oBACtB,UAAU,OAAO,KAAK,IAAI,CAAC,IAAI,KAAK,OAAO,EAAE,SAAS,EAAE,EAAE,MAAM,GAAG,CAAC,CAAC;AAAA,kBACvE,CAAC;AAED,uBAAK,iBAAiB,IAAI,IAAI,UAAU;AACxC,qBAAG;AAAA,oBACD,cAAc;AAAA,sBACZ,MAAM;AAAA,sBACN,UAAU,KAAK,IAAI,YAAY;AAAA,oBACjC,CAAC;AAAA,kBACH;AACA,qBAAG;AAAA,oBACD,cAAc;AAAA,sBACZ,MAAM;AAAA,sBACN,QAAQ,KAAK;AAAA,sBACb,YAAY,KAAK;AAAA,oBACnB,CAAC;AAAA,kBACH;AAAA,gBACF,OAAO;AACL,qBAAG;AAAA,oBACD,cAAc;AAAA,sBACZ,MAAM;AAAA,sBACN,QAAQ;AAAA,oBACV,CAAyB;AAAA,kBAC3B;AACA,qBAAG,MAAM;AAAA,gBACX;AAAA,cACF;AACA;AAAA,YACF;AAGA,gBAAK,IAAY,SAAS,SAAS;AACjC,iBAAG,KAAK,eAAe,CAAC;AACxB;AAAA,YACF;AAIA,gBACE,IAAI,SAAS,oBACb,OAAQ,IAAY,cAAc,UAClC;AACA,oBAAM,MAAO,IAAY;AACzB,oBAAM,WAAW,KAAK,WAAW,IAAI,EAAE;AACvC,kBAAI,SAAU,UAAS,oBAAoB;AAG3C,kBAAI,KAAK,iBAAiB,IAAI,EAAE,MAAM,aAAa;AACjD,gBAAAA,KAAI;AAAA,kBACF,mFAA8E,GAAG;AAAA,gBACnF;AACA,qBAAK,IAAI,UAAU;AAAA,kBAAiB;AAAA,kBAAK,WACvC,GAAG,KAAK,KAAK;AAAA,gBACf;AAAA,cACF;AAAA,YACF;AAQA,gBAAK,IAAY,SAAS,WAAW;AACnC,oBAAM,aAAe,IAAY,eAA4B,CAAC;AAC9D,oBAAM,WAAqC,WAAW;AAAA,gBACpD;AAAA,cACF,IACI,cACA;AACJ,mBAAK,iBAAiB,IAAI,IAAI,QAAQ;AACtC,iBAAG;AAAA,gBACD;AAAA,kBACE,YAAY,UAAU,KAAK,OAAO,aAAa,GAAG,KAAK,SAAS,OAAO;AAAA,gBACzE;AAAA,cACF;AACA;AAAA,YACF;AAMA,gBAAK,IAAY,SAAS,gBAAgB;AACxC,kBAAI,KAAK,oBAAoB,EAAE,GAAG;AAChC,gBAAAA,KAAI;AAAA,kBACF;AAAA,gBACF;AACA;AAAA,cACF;AACA,oBAAM,YAAY,KAAK,WAAW,IAAI,EAAE,GAAG;AAC3C,kBAAI,CAAC,UAAW;AAChB,oBAAM,SAAU,IAAY,UAAU;AACtC,cAAAA,KAAI;AAAA,gBACF,uCAAuC,SAAS,WAAW,MAAM;AAAA,cACnE;AACA,mBAAK,IAAI,UAAU;AAAA,gBAAc;AAAA,gBAAW,WAC1C,GAAG,KAAK,KAAK;AAAA,cACf;AACA;AAAA,YACF;AAEA,iBAAK,cAAc,KAAK,EAAE;AAAA,UAC5B,QAAQ;AACN,eAAG;AAAA,cACD,cAAc;AAAA,gBACZ,MAAM;AAAA,gBACN,SAAS;AAAA,cACX,CAAC;AAAA,YACH;AAAA,UACF;AAAA,QACF,CAAC;AAED,WAAG,GAAG,SAAS,MAAM;AACnB,uBAAa,SAAS;AAGtB,gBAAM,OAAO,KAAK,WAAW,IAAI,EAAE;AACnC,eAAK,cAAc,OAAO,EAAE;AAC5B,eAAK,iBAAiB,OAAO,EAAE;AAE/B,cAAI,MAAM,YAAY,KAAK,mBAAmB;AAI5C,gBAAI,KAAK,sBAAsB,IAAI,KAAK,iBAAiB,MAAM,IAAI;AACjE,mBAAK,sBAAsB,OAAO,KAAK,iBAAiB;AAAA,YAC1D;AACA,iBAAK,IAAI,UAAU;AAAA,cACjB,KAAK;AAAA,cACL,KAAK;AAAA,YACP;AAAA,UACF;AAAA,QACF,CAAC;AAAA,MACH,CAAC;AAAA,IACH,SAAS,KAAK;AACZ,cAAQ;AAAA,QACN,sCAAsC,KAAK,MAAM;AAAA,QACjD;AAAA,MACF;AAAA,IACF;AAAA,EACF;AAAA;AAAA,EAIQ,iBAAuB;AAE7B,QACE,KAAK,aACL,KAAK,gBAAgB,gBACrB,KAAK,gBAAgB;AAErB;AAEF,UAAM,YAAY,KAAK,OAAO,aAAa;AAC3C,UAAM,UAAU,UAAU,KAAK,gBAAgB,UAAU,MAAM;AAI/D,UAAM,YAAY,QAAQ,QAAQ,QAAQ,EAAE;AAE5C,QAAI;AACF,WAAK,IAAI;AAAA,QACP,UACG,QAAQ,aAAa,UAAU,EAC/B,QAAQ,YAAY,SAAS;AAAA,MAClC;AAAA,IACF,QAAQ;AACN,cAAQ;AAAA,QACN;AAAA,QACA;AAAA,MACF;AACA;AAAA,IACF;AAEA,SAAK;AACL,SAAK,eAAe,YAAY;AAEhC,UAAM,UAAU,GAAG,SAAS,SAAS,aAAa;AAClD,UAAM,YAAY,GAAG,SAAS,WAAW,aAAa;AAEtD,IAAAA,KAAI;AAAA,MACF,8BAA8B,KAAK,eAAe,aAAa,KAAK,aAAa,UAAU,cAAc,MAAM,GAAG,CAAC,CAAC,WAAU,oBAAI,KAAK,GAAE,YAAY,CAAC;AAAA,IACxJ;AAIA,IAAAA,KAAI,MAAM,qBAAqB,OAAO,iBAAiB,SAAS,UAAU,SAAS,wBAAwB,SAAS,iBAAiB;AAErI,UAAM,cAAc,KAAK,OAAO,oBAAoB;AAEpD,UAAM,KAAK,IAAI,UAAU,SAAS;AAAA,MAChC,SAAS,EAAE,eAAe,UAAU,WAAW,GAAG;AAAA,IACpD,CAAC;AACD,QAAI,WAAW;AAEf,OAAG,GAAG,QAAQ,MAAM;AAClB,WAAK,UAAU;AACf,WAAK,aAAa;AAClB,WAAK,kBAAkB;AACvB,WAAK,gBAAgB;AAMrB,WAAK,WAAW,IAAI,IAAI;AAAA,QACtB,UAAU,aAAa,KAAK,IAAI,CAAC,IAAI,KAAK,OAAO,EAAE,SAAS,EAAE,EAAE,MAAM,GAAG,CAAC,CAAC;AAAA,MAC7E,CAAC;AACD,WAAK,eAAe,WAAW;AAC/B,WAAK,UAAU;AAAA,QACb,MAAM;AAAA;AAAA,QAEN,KAAK,UAAU,SAAS;AAAA,QACxB,MAAM;AAAA,QACN,SAAS;AAAA,UACP,YAAY,cAAc,MAAM,GAAG,CAAC;AAAA,UACpC,WAAW,KAAK;AAAA,QAClB;AAAA,MACF,CAAC;AACD,WAAK,eAAe;AAAA,IACtB,CAAC;AAED,OAAG,GAAG,QAAQ,MAAM;AAClB,UAAI,KAAK,WAAW;AAClB,qBAAa,KAAK,SAAS;AAC3B,aAAK,YAAY;AAAA,MACnB;AAAA,IACF,CAAC;AAED,OAAG,GAAG,WAAW,SAAO;AAEtB,UAAI,OAAO,QAAQ,UAAU;AAC3B,QAAAA,KAAI,KAAK,2DAAsD;AAC/D;AAAA,MACF;AACA,YAAM,SAAS,OAAO,SAAS,GAAG,IAC9B,MACA,MAAM,QAAQ,GAAG,IACf,OAAO,OAAO,GAAuC,IACrD,OAAO,KAAK,GAAkB;AAEpC,UAAI;AACJ,UAAI;AACF,iBAAS,WAAW,MAAM;AAAA,MAC5B,SAAS,KAAK;AACZ,QAAAA,KAAI,KAAK,+BAAgC,IAAc,OAAO,EAAE;AAChE;AAAA,MACF;AAEA,UAAI,OAAO,SAAS,UAAU;AAC5B,cAAM,aACH,KAAK,WACJ,KAAK,WAAW,IAAI,KAAK,OAAO,GAAG,sBACrC,KAAK,yBACL;AACF,YAAI,WAAW;AACb,eAAK,IAAI,OAAO,WAAW,OAAO,MAAM,OAAO,MAAM,QAAQ;AAE7D,eAAK,4BAA4B,SAAS;AAAA,QAC5C,OAAO;AACL,UAAAA,KAAI;AAAA,YACF;AAAA,UACF;AAAA,QACF;AACA;AAAA,MACF;AAEA,UAAI,OAAO,SAAS,WAAY;AAEhC,UAAI;AACF,cAAM,MAAM,OAAO;AAEnB,YAAK,IAAY,SAAS,SAAS;AAIjC,UAAAA,KAAI,MAAM,iGAAuF;AACjG,eAAK,SAAS,KAAK,eAAe,CAAC;AACnC;AAAA,QACF;AAGA,YACE,IAAI,SAAS,oBACb,OAAQ,IAAY,cAAc,UAClC;AACA,gBAAM,MAAO,IAAY;AACzB,cAAI,KAAK,SAAS;AAChB,kBAAM,WAAW,KAAK,WAAW,IAAI,KAAK,OAAO;AACjD,gBAAI,SAAU,UAAS,oBAAoB;AAAA,UAC7C;AAEA,cACE,KAAK,WACL,KAAK,iBAAiB,IAAI,KAAK,OAAO,MAAM,aAC5C;AACA,YAAAA,KAAI;AAAA,cACF,8EAAyE,GAAG;AAAA,YAC9E;AACA,iBAAK,IAAI,UAAU;AAAA,cAAiB;AAAA,cAAK,WACvC,KAAK,SAAS,KAAK,KAAK;AAAA,YAC1B;AAAA,UACF;AAAA,QACF;AAGA,YAAK,IAAY,SAAS,aAAa,KAAK,SAAS;AACnD,gBAAM,aAAe,IAAY,eAA4B,CAAC;AAC9D,gBAAM,WAAqC,WAAW;AAAA,YACpD;AAAA,UACF,IACI,cACA;AACJ,eAAK,iBAAiB,IAAI,KAAK,SAAS,QAAQ;AAChD,eAAK,QAAQ;AAAA,YACX;AAAA,cACE,YAAY,UAAU,KAAK,OAAO,aAAa,GAAG,KAAK,SAAS,OAAO;AAAA,YACzE;AAAA,UACF;AACA;AAAA,QACF;AACA,YAAK,IAAY,SAAS,kBAAkB,KAAK,SAAS;AACxD,cAAI,KAAK,oBAAoB,KAAK,OAAO,GAAG;AAC1C,YAAAA,KAAI,KAAK,oDAAoD;AAC7D;AAAA,UACF;AACA,gBAAM,YAAY,KAAK,WAAW,IAAI,KAAK,OAAO,GAC9C;AACJ,cAAI,CAAC,UAAW;AAChB,gBAAM,SAAU,IAAY,UAAU;AACtC,UAAAA,KAAI;AAAA,YACF,kCAAkC,SAAS,WAAW,MAAM;AAAA,UAC9D;AAEA,eAAK,IAAI,UAAU;AAAA,YAAc;AAAA,YAAW,WAC1C,KAAK,SAAS,KAAK,KAAK;AAAA,UAC1B;AACA;AAAA,QACF;AAMA,aAAK,cAAc,KAAK,KAAK,OAAO;AAAA,MACtC,QAAQ;AACN,QAAAA,KAAI,KAAK,yCAAyC;AAAA,MACpD;AAAA,IACF,CAAC;AAED,OAAG,GAAG,SAAS,CAAC,MAAM,cAAc;AAClC,YAAM,SAAS,WAAW,SAAS,KAAK;AACxC,MAAAA,KAAI;AAAA,QACF,sBAAsB,IAAI,WAAW,MAAM,QAAO,oBAAI,KAAK,GAAE,YAAY,CAAC;AAAA,MAC5E;AACA,WAAK,UAAU;AACf,WAAK,aAAa;AAClB,WAAK,cAAc;AACnB,WAAK,eAAe,cAAc;AAElC,UAAI,YAAY,UAAU,SAAS,GAAG;AAEpC,cAAM,aAAa,KAAK,gBAAgB,KAAK,UAAU;AACvD,cAAM,cAAc,cAAc;AAClC,aAAK,gBAAgB;AACrB,QAAAA,KAAI;AAAA,UACF,oCAA+B,SAAS,KAAK,UAAU,SAAS,CAAC,SAAQ,oBAAI,KAAK,GAAE,YAAY,CAAC;AAAA,QACnG;AACA,YAAI,CAAC,KAAK,WAAW;AAEnB,gBAAMO,SAAQ,cACV,KAAK,IAAI,MAAQ,KAAK,KAAK,IAAI,KAAK,iBAAiB,CAAC,GAAG,GAAM,IAC/D;AACJ,qBAAW,MAAM,KAAK,eAAe,GAAGA,MAAK;AAAA,QAC/C;AACA;AAAA,MACF;AAEA,YAAM,QAAQ,KAAK;AAAA,QACjB,MAAQ,KAAK,KAAK,IAAI,KAAK,iBAAiB,CAAC;AAAA,QAC7C;AAAA,MACF;AACA,MAAAP,KAAI;AAAA,QACF,+CAA0C,QAAQ,GAAI,cAAc,KAAK,kBAAkB,CAAC,SAAQ,oBAAI,KAAK,GAAE,YAAY,CAAC;AAAA,MAC9H;AACA,UAAI,CAAC,KAAK,UAAW,YAAW,MAAM,KAAK,eAAe,GAAG,KAAK;AAAA,IACpE,CAAC;AAED,OAAG,GAAG,SAAS,OAAK;AAClB,iBAAW;AACX,MAAAA,KAAI;AAAA,QACF,0BAA0B,EAAE,OAAO,SAAQ,oBAAI,KAAK,GAAE,YAAY,CAAC;AAAA,MACrE;AAAA,IACF,CAAC;AAED,OAAG,GAAG,uBAAuB,CAAC,MAAM,QAAQ;AAC1C,MAAAA,KAAI;AAAA,QACF,sCAAsC,IAAI,UAAU,kBAAkB,IAAI,aAAa,QAAO,oBAAI,KAAK,GAAE,YAAY,CAAC;AAAA,MACxH;AAAA,IACF,CAAC;AAAA,EACH;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAcA,6BAAqC;AACnC,QAAI,IAAI,KAAK,cAAc;AAC3B,QAAI,KAAK,SAAS,eAAe,UAAU,KAAM,MAAK;AACtD,WAAO;AAAA,EACT;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAaA,MAAM,iBAAiB,WAAkC;AACvD,QAAI,KAAK,UAAW;AAEpB,UAAM,UAAU,KAAK,SAAS;AAC9B,IAAAA,KAAI;AAAA,MACF,+CAA+C,SAAS,kBACrC,KAAK,cAAc,IAAI,UAAU,KAAK,SAAS,eAAe,UAAU,IAAI;AAAA,IACjG;AACA,QAAI;AACF,WAAK,UAAU;AAAA,QACb,MAAM;AAAA,QACN,SAAS;AAAA,QACT;AAAA,MACF,CAAC;AAAA,IACH,SAAS,KAAK;AACZ,MAAAA,KAAI,KAAK,kDAAkD,GAAG;AAAA,IAChE;AACA,UAAM,IAAI,QAAc,aAAW,WAAW,SAAS,SAAS,CAAC;AACjE,IAAAA,KAAI;AAAA,MACF;AAAA,IACF;AAAA,EACF;AAAA;AAAA,EAIA,UAAgB;AACd,SAAK,YAAY;AAEjB,eAAW,SAAS,KAAK,oBAAoB,OAAO,EAAG,cAAa,KAAK;AACzE,SAAK,oBAAoB,MAAM;AAC/B,SAAK,YAAY,QAAQ;AACzB,SAAK,cAAc;AACnB,SAAK,SAAS,UAAU;AACxB,SAAK,UAAU;AACf,eAAW,MAAM,KAAK,eAAe;AACnC,SAAG,MAAM;AAAA,IACX;AACA,SAAK,cAAc,MAAM;AAEzB,SAAK,sBAAsB,MAAM;AACjC,SAAK,KAAK,MAAM;AAChB,SAAK,MAAM;AAAA,EACb;AACF;;;AQr8DO,SAAS,gBAAgB,MAAmC;AAEjE,kBAAgB,KAAK,MAAM;AAI3B,MAAI;AAEJ,QAAM,YAAyB,CAAC,KAAK,UAAU;AAC7C,QAAI,CAAC,OAAO,cAAc;AACxB,YAAM,YAAgC,EAAE,MAAM,WAAW;AACzD,YAAM,UAAU,OAAO,eAClB,EAAE,GAAG,KAAK,GAAG,MAAM,aAAa,SAAS,EAAE,IAC5C;AACJ,WAAK,cAAc,OAAO;AAAA,IAC5B;AACA,QAAI,CAAC,OAAO,WAAY,KAAI,kBAAkB,KAAK,OAAO,YAAY;AAAA,EACxE;AAEA,QAAM,MAAM,IAAI,WAAW,SAAS;AACpC,OAAK,IAAI,UAAU,KAAK,WAAW,KAAK,QAAQ,KAAK,QAAQ;AAI7D,MAAI,YAAY;AAEhB,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA;AAAA,IACA,UAAU;AACR,UAAI,UAAW;AACf,kBAAY;AACZ,UAAI,QAAQ;AACZ,UAAI,QAAQ;AAAA,IACd;AAAA,EACF;AACF;;;ACxEA,IAAMQ,OAAM,aAAa,eAAe;;;ACRxC,IAAMC,OAAM,aAAa,cAAc;;;A9BFvC,SAAS,cAAc;;;A+BJvB,SAAS,kBAAkB;AAC3B,OAAOC,SAAQ;AACf,OAAOC,WAAU;;;ACFjB,OAAOC,SAAQ;AACf,OAAOC,WAAU;AAsBV,SAAS,UAAU,MAAkC;AAC1D,MAAI;AACF,WAAO,KAAK,MAAMD,IAAG,aAAa,MAAM,MAAM,CAAC;AAAA,EACjD,QAAQ;AACN,WAAO,CAAC;AAAA,EACV;AACF;AAQO,SAAS,WAAW,MAAc,MAAgC;AACvE,EAAAA,IAAG,UAAUC,MAAK,QAAQ,IAAI,GAAG,EAAE,WAAW,MAAM,MAAM,IAAM,CAAC;AACjE,EAAAD,IAAG,cAAc,MAAM,KAAK,UAAU,MAAM,MAAM,CAAC,GAAG,EAAE,UAAU,QAAQ,MAAM,IAAM,CAAC;AACzF;;;ADzBA,IAAM,aAAaE,MAAK,KAAKC,IAAG,QAAQ,GAAG,yBAAyB,qBAAqB;AAYlF,IAAM,sBAAN,MAAsD;AAAA,EAG3D,YAA6B,OAAe,YAAY;AAA3B;AAC3B,SAAK,OAAO,UAAU,IAAI;AAC1B,QAAI,CAAC,KAAK,KAAK,YAAY;AACzB,WAAK,KAAK,aAAa,WAAW;AAClC,iBAAW,MAAM,KAAK,IAAI;AAAA,IAC5B;AAAA,EACF;AAAA,EAN6B;AAAA,EAFrB;AAAA,EAUR,eAAyB;AAIvB,WAAO;AAAA,MACL,KAAK,KAAK;AAAA,MACV,QAAQ,IAAI,uBAAuB,QAAQ,IAAI,kBAAkB;AAAA,IACnE;AAAA,EACF;AAAA,EAEA,YAAoB;AAClB,WAAO,KAAK,KAAK,WAAW;AAAA,EAC9B;AAAA,EAEA,yBAAiC;AAC/B,WAAO,KAAK,KAAK,yBAAyB;AAAA,EAC5C;AAAA,EAEA,mBAA2B;AACzB,WAAO,KAAK,KAAK,mBAAmB;AAAA,EACtC;AAAA,EAEA,mBAA2B;AACzB,WAAO,KAAK,KAAK,mBAAmB;AAAA,EACtC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EASA,aAAa,MAAsB;AACjC,SAAK,KAAK,aAAa;AACvB,eAAW,KAAK,MAAM,KAAK,IAAI;AAAA,EACjC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,UAAU,MAAoB;AAC5B,SAAK,KAAK,UAAU;AACpB,eAAW,KAAK,MAAM,KAAK,IAAI;AAAA,EACjC;AAAA;AAAA,EAGA,uBAAuB,IAAkB;AACvC,SAAK,KAAK,wBAAwB;AAClC,eAAW,KAAK,MAAM,KAAK,IAAI;AAAA,EACjC;AAAA;AAAA,EAGA,iBAAiB,IAAkB;AACjC,SAAK,KAAK,kBAAkB;AAC5B,eAAW,KAAK,MAAM,KAAK,IAAI;AAAA,EACjC;AAAA;AAAA,EAGA,iBAAiB,IAAkB;AACjC,SAAK,KAAK,kBAAkB;AAC5B,eAAW,KAAK,MAAM,KAAK,IAAI;AAAA,EACjC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,WACE,OAMM;AACN,WAAO,OAAO,KAAK,MAAM,KAAK;AAC9B,eAAW,KAAK,MAAM,KAAK,IAAI;AAAA,EACjC;AAAA,EAEA,eAAuB;AAErB,WAAO,KAAK,KAAK;AAAA,EACnB;AAAA,EAEA,eAAgC;AAC9B,WAAO,KAAK,KAAK,cAAc,CAAC;AAAA,EAClC;AAAA,EAEA,aAAa,SAAgC;AAC3C,SAAK,KAAK,aAAa;AACvB,eAAW,KAAK,MAAM,KAAK,IAAI;AAAA,EACjC;AAAA,EAEA,sBAA8B;AAG5B,WACE,QAAQ,IAAI,gCACZ,QAAQ,IAAI,2BACZ;AAAA,EAEJ;AACF;;;AEhJA,OAAOC,SAAQ;AACf,OAAOC,SAAQ;AACf,OAAOC,WAAU;AASV,IAAM,mBAAwC,oBAAI,IAAI;AAAA,EAC3D;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,CAAC;AAYM,SAAS,kBAAkB,OAA8B;AAC9D,MAAI,MAAMA,MAAK,QAAQ,KAAK;AAC5B,aAAS;AACP,UAAM,YAAYA,MAAK,KAAK,KAAK,MAAM;AACvC,QAAI;AACF,MAAAF,IAAG,WAAW,SAAS;AACvB,aAAO;AAAA,IACT,QAAQ;AAAA,IAER;AACA,UAAM,SAASE,MAAK,QAAQ,GAAG;AAC/B,QAAI,WAAW,IAAK,QAAO;AAC3B,UAAM;AAAA,EACR;AACF;AAMA,SAAS,kBAA4B;AACnC,QAAM,QAAkB,CAAC;AACzB,QAAM,UAAU,kBAAkB,QAAQ,IAAI,CAAC;AAC/C,MAAI,QAAS,OAAM,KAAK,OAAO;AAC/B,QAAM,KAAKA,MAAK,KAAKD,IAAG,QAAQ,GAAG,yBAAyB,MAAM,CAAC;AACnE,SAAO;AACT;AAeO,SAAS,WACd,QAAkB,gBAAgB,GAClC,UAA+B,kBACrB;AACV,QAAM,SAAmB,CAAC;AAC1B,aAAW,QAAQ,OAAO;AACxB,QAAI;AACJ,QAAI;AACF,aAAOD,IAAG,aAAa,MAAM,MAAM;AAAA,IACrC,QAAQ;AACN;AAAA,IACF;AACA,eAAW,OAAO,KAAK,MAAM,OAAO,GAAG;AACrC,YAAM,OAAO,IAAI,KAAK,EAAE,QAAQ,cAAc,EAAE;AAChD,UAAI,CAAC,QAAQ,KAAK,WAAW,GAAG,EAAG;AACnC,YAAM,KAAK,KAAK,QAAQ,GAAG;AAC3B,UAAI,MAAM,EAAG;AACb,YAAM,MAAM,KAAK,MAAM,GAAG,EAAE,EAAE,KAAK;AACnC,UAAI,CAAC,QAAQ,IAAI,GAAG,EAAG;AACvB,UAAI,OAAO,QAAQ,IAAK;AACxB,UAAI,MAAM,KAAK,MAAM,KAAK,CAAC,EAAE,KAAK;AAElC,UACE,IAAI,UAAU,MACZ,IAAI,WAAW,GAAG,KAAK,IAAI,SAAS,GAAG,KACtC,IAAI,WAAW,GAAG,KAAK,IAAI,SAAS,GAAG,IAC1C;AACA,cAAM,IAAI,MAAM,GAAG,EAAE;AAAA,MACvB;AACA,cAAQ,IAAI,GAAG,IAAI;AAAA,IACrB;AACA,WAAO,KAAK,IAAI;AAAA,EAClB;AACA,SAAO;AACT;;;ACvGA;AAAA,EACE,MAAQ;AAAA,EACR,SAAW;AAAA,EACX,MAAQ;AAAA,EACR,aAAe;AAAA,EACf,KAAO;AAAA,IACL,OAAS;AAAA,EACX;AAAA,EACA,OAAS;AAAA,IACP;AAAA,EACF;AAAA,EACA,eAAiB;AAAA,IACf,QAAU;AAAA,EACZ;AAAA,EACA,SAAW;AAAA,IACT,MAAQ;AAAA,EACV;AAAA,EACA,YAAc;AAAA,IACZ,MAAQ;AAAA,IACR,KAAO;AAAA,IACP,WAAa;AAAA,EACf;AAAA,EACA,SAAW;AAAA,IACT,OAAS;AAAA,IACT,OAAS;AAAA,IACT,KAAO;AAAA,IACP,WAAa;AAAA,IACb,MAAQ;AAAA,IACR,SAAW;AAAA,EACb;AAAA,EACA,cAAgB;AAAA,IACd,mBAAmB;AAAA,IACnB,gBAAgB;AAAA,IAChB,KAAO;AAAA,IACP,oBAAoB;AAAA,IACpB,kBAAkB;AAAA,IAClB,YAAY;AAAA,IACZ,WAAW;AAAA,IACX,mBAAmB;AAAA,IACnB,OAAS;AAAA,IACT,cAAc;AAAA,IACd,MAAQ;AAAA,IACR,IAAM;AAAA,EACR;AAAA,EACA,iBAAmB;AAAA,IACjB,cAAc;AAAA,IACd,gBAAgB;AAAA,IAChB,eAAe;AAAA,IACf,gBAAgB;AAAA,IAChB,uBAAuB;AAAA,IACvB,MAAQ;AAAA,IACR,KAAO;AAAA,IACP,YAAc;AAAA,IACd,uBAAuB;AAAA,IACvB,QAAU;AAAA,EACZ;AACF;;;AChDO,IAAM,WAA0B,EAAE,SAAS,gBAAI,QAAQ;;;ACR9D,OAAOG,UAAQ;AACf,OAAOC,UAAQ;AACf,OAAOC,YAAU;;;ACFjB,OAAOC,SAAQ;AACf,OAAOC,SAAQ;AACf,OAAOC,WAAU;AAQjB,IAAMC,sBAAqBC,MAAK,KAAKC,IAAG,QAAQ,GAAG,yBAAyB,mBAAmB;AASxF,SAAS,mBAAkC;AAChD,MAAI;AACF,UAAM,IAAIC,IAAG,aAAaH,qBAAoB,OAAO,EAAE,KAAK;AAC5D,WAAO,EAAE,WAAW,KAAK,IAAI;AAAA,EAC/B,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAWO,SAAS,aAAa,WAAqB,OAAqC;AACrF,QAAM,UAAU,UAAU,CAAC;AAC3B,MAAI,CAAC,SAAS,CAAC,QAAS,QAAO;AAC/B,QAAM,YAAY,QAAQ,QAAQ,QAAQ,EAAE;AAC5C,SAAO,UAAU,GAAG,SAAS,WAAW,KAAK,EAAE;AACjD;AAcO,SAAS,eAAe,WAAoC;AACjE,SAAO,aAAa,WAAW,iBAAiB,CAAC;AACnD;;;AC1DA,SAAS,iBAAiB;AAC1B,OAAOI,SAAQ;AACf,OAAOC,SAAQ;AACf,OAAOC,YAAU;AAQV,SAAS,qBAA8B;AAC5C,SAAO,QAAQ,aAAa;AAC9B;AAGA,IAAI;AASG,SAAS,mBAA4B;AAC1C,MAAI,2BAA2B,QAAW;AACxC,UAAM,IAAI,UAAU,aAAa,CAAC,UAAU,WAAW,GAAG,EAAE,OAAO,SAAS,CAAC;AAC7E,6BAAyB,CAAC,EAAE,SAAS,EAAE,WAAW;AAAA,EACpD;AACA,SAAO;AACT;AAGO,SAAS,cAAsB;AACpC,SAAOD,IAAG,SAAS,EAAE;AACvB;AAmBO,SAAS,cAA8B;AAC5C,QAAM,WAAW,QAAQ;AACzB,QAAM,YAAY,QAAQ,KAAK,CAAC,KAAK;AACrC,MAAI,aAAa;AACjB,MAAI;AACF,iBAAaD,IAAG,aAAaE,OAAK,QAAQ,SAAS,CAAC;AAAA,EACtD,QAAQ;AACN,iBAAaA,OAAK,QAAQ,SAAS;AAAA,EACrC;AAEA,QAAM,aAAa,WAAW,MAAMA,OAAK,GAAG,EAAE,SAAS,MAAM;AAC7D,SAAO,EAAE,UAAU,YAAY,WAAW;AAC5C;;;ACnEA,SAAS,aAAAC,kBAAiB;AAC1B,OAAOC,SAAQ;;;ACDf,OAAOC,SAAQ;AACf,OAAOC,YAAU;AAMV,IAAM,eAAe;AAGrB,IAAM,iBAAiB,GAAG,YAAY;AAoBtC,SAAS,WAAWC,IAAuB;AAChD,QAAMC,QAAO,GAAG,cAAcD,GAAE,QAAQ,CAAC,IAAI,cAAcA,GAAE,UAAU,CAAC;AACxE,SAAO,GAAG;AAAA,IACR;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA,aAAaC,KAAI;AAAA,IACjB;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF,EAAE,KAAK,IAAI,CAAC;AAAA;AACd;AAGA,SAAS,cAAcD,IAAmB;AACxC,SAAO,KAAK,KAAKA,EAAC,IAAI,IAAIA,EAAC,MAAMA;AACnC;AAGO,SAAS,cAAsB;AACpC,SAAOD,OAAK,KAAKD,IAAG,QAAQ,GAAG,WAAW,WAAW,MAAM;AAC7D;AAGO,SAAS,eAAuB;AACrC,SAAOC,OAAK,KAAK,YAAY,GAAG,cAAc;AAChD;;;ADxBA,SAAS,cAAcG,OAAiE;AACtF,QAAM,IAAIC,WAAU,aAAa,CAAC,UAAU,GAAGD,KAAI,GAAG,EAAE,UAAU,QAAQ,CAAC;AAC3E,SAAO;AAAA,IACL,IAAI,CAAC,EAAE,SAAS,EAAE,WAAW;AAAA,IAC7B,SAAS,EAAE,UAAU,IAAI,KAAK;AAAA,IAC9B,SAAS,EAAE,UAAU,IAAI,KAAK;AAAA,EAChC;AACF;AAQO,SAAS,kBAA2B;AACzC,MAAI,QAAQ,aAAa,QAAS,QAAO;AACzC,SAAO,cAAc,CAAC,aAAa,cAAc,CAAC,EAAE,WAAW;AACjE;AAGA,SAAS,cAAc,MAAuB;AAC5C,QAAM,IAAIC,WAAU,YAAY,CAAC,aAAa,MAAM,mBAAmB,GAAG;AAAA,IACxE,UAAU;AAAA,EACZ,CAAC;AACD,MAAI,EAAE,MAAO,QAAO;AACpB,UAAQ,EAAE,UAAU,IAAI,SAAS,YAAY;AAC/C;AAGO,SAAS,cAAc,MAAsB;AAClD,SAAO,+BAA+B,IAAI;AAC5C;AAGO,SAAS,gBAA+B;AAC7C,QAAM,OAAO,YAAY;AACzB,QAAM,YAAYC,IAAG,WAAW,aAAa,CAAC;AAC9C,MAAI,EAAE,mBAAmB,KAAK,iBAAiB,IAAI;AACjD,WAAO,EAAE,WAAW,OAAO,WAAW,QAAQ,OAAO,SAAS,OAAO,QAAQ,OAAO,KAAK;AAAA,EAC3F;AACA,SAAO;AAAA,IACL,WAAW;AAAA,IACX;AAAA,IACA,QAAQ,gBAAgB;AAAA,IACxB,SAAS,cAAc,CAAC,cAAc,cAAc,CAAC,EAAE,WAAW;AAAA,IAClE,QAAQ,cAAc,IAAI;AAAA,IAC1B;AAAA,EACF;AACF;AAaO,SAAS,eAAeC,OAAqC;AAClE,QAAM,OAAO,YAAY;AACzB,QAAM,WAAW,aAAa;AAC9B,QAAM,WAAqB,CAAC;AAE5B,EAAAD,IAAG,UAAU,YAAY,GAAG,EAAE,WAAW,KAAK,CAAC;AAC/C,EAAAA,IAAG;AAAA,IACD;AAAA,IACA,WAAW,EAAE,UAAUC,MAAK,UAAU,YAAYA,MAAK,WAAW,CAAC;AAAA,IACnE,EAAE,MAAM,IAAM;AAAA,EAChB;AACA,WAAS,KAAK,iBAAiB,QAAQ,EAAE;AAEzC,QAAM,SAAS,cAAc,CAAC,eAAe,CAAC;AAC9C,MAAI,CAAC,OAAO,IAAI;AAGd,aAAS,KAAK,gCAAgC,OAAO,MAAM,EAAE;AAC7D,WAAO;AAAA,MACL,IAAI;AAAA,MACJ;AAAA,MACA,QAAQ,cAAc,IAAI;AAAA,MAC1B,eAAe,cAAc,IAAI;AAAA,MACjC;AAAA,IACF;AAAA,EACF;AAEA,QAAM,SAAS,cAAc,CAAC,UAAU,cAAc,CAAC;AACvD,MAAI,OAAO,GAAI,UAAS,KAAK,wBAAwB;AAAA,MAChD,UAAS,KAAK,yBAAyB,OAAO,MAAM,EAAE;AAE3D,SAAO;AAAA,IACL,IAAI,OAAO;AAAA,IACX;AAAA,IACA,QAAQ,cAAc,IAAI;AAAA,IAC1B,eAAe,cAAc,IAAI;AAAA,IACjC;AAAA,EACF;AACF;AAGO,SAAS,eAAwB;AACtC,SAAO,cAAc,CAAC,SAAS,cAAc,CAAC,EAAE;AAClD;AAGO,SAAS,mBAAwD;AACtE,QAAM,WAAqB,CAAC;AAC5B,QAAM,UAAU,cAAc,CAAC,WAAW,SAAS,cAAc,CAAC;AAClE,MAAI,CAAC,QAAQ,MAAM,QAAQ,OAAQ,UAAS,KAAK,qBAAqB,QAAQ,MAAM,EAAE;AACtF,MAAI;AACF,IAAAD,IAAG,OAAO,aAAa,CAAC;AACxB,aAAS,KAAK,cAAc;AAAA,EAC9B,QAAQ;AAAA,EAER;AACA,gBAAc,CAAC,eAAe,CAAC;AAC/B,SAAO,EAAE,IAAI,MAAM,SAAS;AAC9B;AAGO,SAAS,iBAA0B;AACxC,SAAO,cAAc,CAAC,WAAW,cAAc,CAAC,EAAE;AACpD;AAGO,SAAS,cAAuB;AACrC,SAAO,cAAc,CAAC,QAAQ,cAAc,CAAC,EAAE;AACjD;;;AHpIO,SAAS,kBACd,QACAE,SACQ;AACR,MAAI,CAAC,mBAAmB,GAAG;AACzB,YAAQ,IAAI,+HAAoD;AAChE,YAAQ,IAAI,qCAAY,QAAQ,QAAQ,yDAA2B;AACnE,WAAO;AAAA,EACT;AACA,MAAI,CAAC,iBAAiB,GAAG;AACvB,YAAQ,IAAI,6IAAuE;AACnF,WAAO;AAAA,EACT;AACA,UAAQ,QAAQ;AAAA,IACd,KAAK;AACH,aAAO,UAAU;AAAA,IACnB,KAAK;AACH,aAAO,YAAY;AAAA,IACrB,KAAK;AACH,aAAO,SAASA,OAAM;AAAA,IACxB;AACE,cAAQ,IAAI,qDAAqD;AACjE,aAAO;AAAA,EACX;AACF;AAGA,SAAS,YAAoB;AAC3B,QAAMC,QAAO,YAAY;AACzB,MAAIA,MAAK,YAAY;AACnB,YAAQ,IAAI,uLAA2C;AACvD,YAAQ,IAAI,iFAAqB;AACjC,YAAQ,IAAI,kCAAkC;AAC9C,YAAQ,IAAI,2BAA2B;AACvC,WAAO;AAAA,EACT;AAEA,QAAM,MAAM,eAAeA,KAAI;AAC/B,aAAWC,MAAK,IAAI,SAAU,SAAQ,IAAIA,EAAC;AAC3C,MAAI,CAAC,IAAI,GAAI,QAAO;AAKpB,MAAI,eAAe,EAAG,SAAQ,IAAI,iBAAiB;AAAA,MAC9C,SAAQ,IAAI,0FAAsE;AAEvF,UAAQ,IAAI,EAAE;AACd,MAAI,IAAI,QAAQ;AACd,YAAQ,IAAI,yKAAyC;AAAA,EACvD,OAAO;AACL,YAAQ,IAAI,iKAA8C;AAC1D,YAAQ,IAAI,OAAO,IAAI,aAAa,EAAE;AACtC,YAAQ,IAAI,0IAA2C;AAAA,EACzD;AAEA,MAAI,CAAC,yBAAyB,GAAG;AAC/B,YAAQ,IAAI,EAAE;AACd,YAAQ,IAAI,6JAA0C;AACtD,YAAQ,IAAI,kJAAwD;AACpE,YAAQ,IAAI,6FAAwF;AACpG,YAAQ,IAAI,uDAAwC;AAAA,EACtD;AACA,SAAO;AACT;AAGA,SAAS,cAAsB;AAC7B,QAAM,MAAM,iBAAiB;AAC7B,aAAWA,MAAK,IAAI,SAAU,SAAQ,IAAIA,EAAC;AAC3C,UAAQ,IAAI,oFAAmB;AAC/B,SAAO;AACT;AAGA,SAAS,SAASF,SAAqC;AACrD,QAAM,IAAI,cAAc;AACxB,UAAQ,IAAI,gBAAgB,QAAQ,QAAQ,gBAAgB,EAAE,SAAS,GAAG;AAC1E,UAAQ,IAAI,gBAAgB,EAAE,SAAS,EAAE;AACzC,UAAQ,IAAI,gBAAgB,EAAE,MAAM,EAAE;AACtC,UAAQ,IAAI,gBAAgB,EAAE,OAAO,EAAE;AACvC,UAAQ,IAAI,gBAAgB,EAAE,MAAM,WAAW,EAAE,IAAI,GAAG;AAExD,QAAM,UAAU,eAAeA,QAAO,aAAa,CAAC;AACpD,MAAI,QAAS,SAAQ,IAAI,gBAAgB,OAAO,EAAE;AAElD,MAAI,EAAE,aAAa,CAAC,EAAE,QAAQ;AAC5B,YAAQ,IAAI,EAAE;AACd,YAAQ,IAAI,8EAAuB,cAAc,EAAE,IAAI,CAAC,EAAE;AAAA,EAC5D;AACA,SAAO;AACT;AAUA,SAAS,2BAAoC;AAC3C,QAAM,UAAUG,OAAK,KAAKC,KAAG,QAAQ,GAAG,yBAAyB,MAAM;AACvE,MAAI;AACF,UAAM,OAAOC,KAAG,aAAa,SAAS,OAAO;AAE7C,WAAO,gFAAgF,KAAK,IAAI;AAAA,EAClG,QAAQ;AACN,WAAO;AAAA,EACT;AACF;;;AKlJA,SAAS,oBAAoB;AAmC7B,IAAM,WAAW;AAYV,IAAM,gBAAN,cAA4B,aAAa;AAAA,EACtC,QAAyB;AAAA,IAC/B,aAAa;AAAA,IACb,YAAY;AAAA,IACZ,IAAI;AAAA,IACJ,UAAU,CAAC;AAAA,IACX,UAAU,CAAC;AAAA,IACX,MAAM,CAAC;AAAA,EACT;AAAA,EAEA,WAA4B;AAC1B,WAAO,KAAK;AAAA,EACd;AAAA;AAAA,EAGA,MAAM,KAA0B;AAC9B,YAAQ,IAAI,MAAM;AAAA,MAChB,KAAK;AACH,aAAK,QAAQ,EAAE,GAAG,KAAK,OAAO,aAAa,IAAI,QAAQ,YAAY,IAAI,WAAW;AAClF,aAAK,QAAQ,WAAW,IAAI,MAAM,GAAG,IAAI,aAAa,WAAW,IAAI,UAAU,MAAM,EAAE,EAAE;AACzF;AAAA,MACF,KAAK;AACH,aAAK,QAAQ,EAAE,GAAG,KAAK,OAAO,IAAI,EAAE,KAAK,IAAI,KAAK,MAAM,IAAI,MAAM,SAAS,IAAI,QAAQ,EAAE;AACzF,aAAK,QAAQ,QAAQ,IAAI,IAAI,EAAE;AAC/B;AAAA,MACF,KAAK;AACH,aAAK,QAAQ;AAAA,UACX,GAAG,KAAK;AAAA,UACR,UAAU,EAAE,GAAG,KAAK,MAAM,UAAU,CAAC,IAAI,SAAS,GAAG,IAAI,OAAO;AAAA,QAClE;AACA,aAAK,QAAQ,SAAS,IAAI,SAAS,WAAM,IAAI,MAAM,GAAG,IAAI,MAAM,QAAQ,IAAI,GAAG,KAAK,EAAE,EAAE;AACxF;AAAA,MACF,KAAK;AAEH,aAAK,QAAQ;AAAA,UACX,GAAG,KAAK;AAAA,UACR,UAAU,IAAI,SAAS,IAAI,CAAAC,QAAM,EAAE,IAAIA,GAAE,IAAI,MAAMA,GAAE,KAAK,EAAE;AAAA,QAC9D;AACA,aAAK,QAAQ,cAAc,IAAI,SAAS,MAAM,EAAE;AAChD;AAAA,MACF,KAAK;AACH,aAAK,QAAQ,UAAU,IAAI,SAAS,WAAM,IAAI,MAAM,EAAE;AACtD;AAAA,MACF,KAAK;AACH,aAAK,QAAQ,WAAW,IAAI,OAAO,EAAE;AACrC;AAAA;AAAA,MAEF,KAAK;AAAA,MACL,KAAK;AAAA,MACL,KAAK;AAAA,MACL,KAAK;AACH;AAAA,MACF;AACE,aAAK,QAAQ,QAAK,IAAI,IAAI,EAAE;AAAA,IAChC;AAAA,EACF;AAAA;AAAA,EAGA,IAAI,MAAoB;AACtB,SAAK,QAAQ,IAAI;AAAA,EACnB;AAAA,EAEQ,QAAQ,MAAoB;AAClC,UAAM,MAAK,oBAAI,KAAK,GAAE,YAAY,EAAE,MAAM,IAAI,EAAE;AAChD,UAAM,OAAO,CAAC,GAAG,KAAK,MAAM,MAAM,GAAG,EAAE,IAAI,IAAI,EAAE,EAAE,MAAM,CAAC,QAAQ;AAClE,SAAK,QAAQ,EAAE,GAAG,KAAK,OAAO,KAAK;AACnC,SAAK,KAAK,QAAQ;AAAA,EACpB;AACF;;;ACnHA,SAAS,OAAAC,MAAK,QAAAC,OAAM,cAAc;AAClC,SAAgB,aAAAC,YAAW,YAAAC,iBAAgB;;;ACD3C,OAAOC,UAAQ;AACf,OAAOC,YAAU;AAEjB,SAAS,KAAK,MAAM,gBAAgB;AACpC,SAAgB,WAAW,gBAAgB;;;ACHpC,IAAM,UAAU,SAAI,OAAO,EAAE;;;ACc7B,SAAS,UAAU,KAAqB;AAC7C,MAAI;AACF,WAAO,mBAAmB,GAAG;AAAA,EAC/B,QAAQ;AACN,WAAO;AAAA,EACT;AACF;;;AFmBI,SA6DI,UA5DF,KADF;AAzBJ,IAAM,eAAuC;AAAA,EAC3C,WAAW;AAAA,EACX,YAAY;AAAA,EACZ,cAAc;AAAA,EACd,OAAO;AACT;AAMA,SAAS,iBAA2B;AAClC,QAAM,MAAgB,CAAC;AACvB,aAAW,SAAS,OAAO,OAAOC,KAAG,kBAAkB,CAAC,GAAG;AACzD,QAAI,CAAC,MAAO;AACZ,eAAW,QAAQ,OAAO;AACxB,UAAI,KAAK,WAAW,UAAU,CAAC,KAAK,SAAU,KAAI,KAAK,KAAK,OAAO;AAAA,IACrE;AAAA,EACF;AACA,SAAO;AACT;AAGA,SAAS,IAAI,EAAE,OAAO,OAAO,MAAM,GAAqD;AACtF,SACE,qBAAC,OACC;AAAA,wBAAC,QAAK,OAAM,QAAQ,eAAK,MAAM,OAAO,CAAC,CAAC,KAAI;AAAA,IAC5C,oBAAC,QAAK,OAAe,iBAAM;AAAA,KAC7B;AAEJ;AAYO,SAAS,WAAW;AAAA,EACzB;AAAA,EACA,QAAAC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAMG;AACD,QAAM,CAAC,SAAS,UAAU,IAAI,SAAS,CAAC;AAExC,YAAU,MAAM;AACd,UAAM,IAAI,YAAY,MAAM;AAC1B,iBAAW,KAAK,GAAG,2BAA2B,CAAC;AAAA,IACjD,GAAG,GAAI;AACP,WAAO,MAAM,cAAc,CAAC;AAAA,EAC9B,GAAG,CAAC,IAAI,CAAC;AAET;AAAA,IACE,CAAC,QAAQ,QAAQ;AACf,UAAI,IAAI,OAAQ,QAAO;AAAA,IACzB;AAAA,IACA,EAAE,UAAU,OAAO;AAAA,EACrB;AAEA,QAAM,cAAc,aAAa,MAAM,WAAW,KAAK;AACvD,QAAM,QAAQ,MAAM,aAAa,WAAW,MAAM,UAAU,MAAM;AAClE,QAAM,CAAC,SAAS,GAAG,SAAS,IAAIA,QAAO,aAAa;AACpD,QAAM,YAAYA,QAAO,oBAAoB,EAAE,SAAS;AACxD,QAAM,MAAM,eAAe;AAC3B,QAAM,UAAUC,OAAK,KAAKF,KAAG,QAAQ,GAAG,uBAAuB;AAE/D,SACE,qBAAC,OAAI,eAAc,UACjB;AAAA,wBAAC,QAAK,OAAM,QAAQ,mBAAQ;AAAA,IAC5B,oBAAC,QAAK,kBAAI;AAAA,IAEV,oBAAC,QAAK,OAAM,QAAQ,mBAAQ;AAAA,IAC5B,oBAAC,QAAK,OAAM,QAAO,wBAAU;AAAA,IAC5B,MAAM,KACL,iCACE;AAAA,0BAAC,OAAI,OAAM,YAAW,OAAO,UAAU,MAAM,GAAG,GAAG,GAAG,OAAM,QAAO;AAAA,MACnE;AAAA,QAAC;AAAA;AAAA,UACC,OAAM;AAAA,UACN,OAAO,GAAG,MAAM,GAAG,IAAI,GAAG,MAAM,GAAG,QAAQ,aAAa,YAAY,MAAM,GAAG,QAAQ,UAAU,KAAK,EAAE;AAAA;AAAA,MACxG;AAAA,OACF,IAEA,oBAAC,OAAI,OAAM,YAAW,OAAM,uCAAiC,OAAM,UAAS;AAAA,IAG9E,oBAAC,QAAK,OAAM,QAAQ,mBAAQ;AAAA,IAC5B,oBAAC,QAAK,OAAM,QAAO,mBAAK;AAAA,IACxB,oBAAC,OAAI,OAAM,UAAS,OAAO,GAAG,MAAM,WAAW,GAAG,KAAK,IAAI,OAAO,aAAa;AAAA,IAC/E,oBAAC,OAAI,OAAM,UAAS,OAAO,WAAW,KAAK;AAAA,IAC1C,UAAU,IAAI,SACb,oBAAC,OAAc,OAAM,YAAW,OAAO,OAA7B,GAAkC,CAC7C;AAAA,IACD;AAAA,MAAC;AAAA;AAAA,QACC,OAAM;AAAA,QACN,OAAO,YAAY,eAAU;AAAA,QAC7B,OAAO,YAAY,UAAU;AAAA;AAAA,IAC/B;AAAA,IAEA,oBAAC,QAAK,OAAM,QAAQ,mBAAQ;AAAA,IAC5B,oBAAC,QAAK,OAAM,QAAO,0BAAY;AAAA,IAC/B,oBAAC,OAAI,OAAM,WAAU,OAAO,OAAOC,QAAO,UAAU,CAAC,GAAG;AAAA,IACxD,oBAAC,OAAI,OAAM,OAAM,OAAO,IAAI,SAAS,IAAI,IAAI,KAAK,IAAI,IAAI,UAAU;AAAA,IACpE,oBAAC,OAAI,OAAM,WAAU,OAAO,OAAO,OAAO,GAAG,OAAO,UAAU,IAAI,UAAU,QAAQ;AAAA,IAEpF,oBAAC,QAAK,OAAM,QAAQ,mBAAQ;AAAA,IAC5B,oBAAC,QAAK,OAAM,QAAO,mBAAK;AAAA,IACxB,oBAAC,OAAI,OAAM,WAAU,OAAO,SAAS,SAAS;AAAA,IAC9C,oBAAC,OAAI,OAAM,WAAU,OAAO,GAAGA,QAAO,aAAa,EAAE,MAAM,GAAG,CAAC,CAAC,UAAK;AAAA,IACrE,oBAAC,OAAI,OAAM,OAAM,OAAO,OAAOA,QAAO,aAAa,EAAE,MAAM,GAAG;AAAA,IAC9D,oBAAC,OAAI,OAAM,YAAW,OAAO,OAAO,KAAK,IAAI,YAAY,EAAE,MAAM,GAAG;AAAA,IAEpE,oBAAC,QAAK,OAAM,QAAQ,mBAAQ;AAAA,IAC5B,oBAAC,QAAK,OAAM,QAAO,sBAAQ;AAAA,IAC3B;AAAA,MAAC;AAAA;AAAA,QACC,OAAM;AAAA,QACN,OAAO,GAAGA,QAAO,uBAAuB,CAAC,SAAMA,QAAO,iBAAiB,CAAC,SAAMA,QAAO,iBAAiB,CAAC;AAAA;AAAA,IACzG;AAAA,IAEA,oBAAC,QAAK,OAAM,QAAQ,mBAAQ;AAAA,IAC5B,oBAAC,QAAK,OAAM,QAAO,oBAAM;AAAA,IACzB,oBAAC,OAAI,OAAM,MAAK,OAAO,GAAGD,KAAG,SAAS,CAAC,IAAIA,KAAG,KAAK,CAAC,SAAMA,KAAG,SAAS,CAAC,IAAI;AAAA,IAC3E,oBAAC,OAAI,OAAM,QAAO,OAAO,QAAQ,SAAS;AAAA,IAC1C,oBAAC,OAAI,OAAM,QAAO,OAAO,SAAS;AAAA,IAClC,oBAAC,OAAI,OAAM,QAAO,OAAOE,OAAK,KAAK,SAAS,MAAM,GAAG;AAAA,IAErD,oBAAC,QAAK,OAAM,QAAQ,mBAAQ;AAAA,IAC5B,oBAAC,QAAK,OAAM,QAAO,sBAAQ;AAAA,KAC7B;AAEJ;;;AG5JA,SAAS,OAAAC,MAAK,QAAAC,OAAM,YAAAC,iBAAgB;AACpC,SAAgB,YAAAC,iBAAgB;;;ACDhC,SAAS,OAAAC,MAAK,QAAAC,aAAY;AAC1B,OAAO,iBAAiB;AA6BlB,gBAAAC,MACW,QAAAC,aADX;AAjBC,SAAS,cAAc;AAAA,EAC5B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAOG;AACD,SACE,gBAAAA,MAACH,MAAA,EAAI,eAAc,UAAS,aAAY,SAAQ,aAAY,OAAM,UAAU,GAC1E;AAAA,oBAAAE,KAACD,OAAA,EAAM,mBAAQ;AAAA,IACd,UAAU,gBAAAE,MAACF,OAAA,EAAK,OAAM,UAAS;AAAA;AAAA,MAAG;AAAA,OAAQ,IAAU;AAAA,IACrD,gBAAAC,KAACF,MAAA,EAAI,WAAW,GACd,0BAAAE;AAAA,MAAC;AAAA;AAAA,QACC,WAAW;AAAA,QACX,OAAO;AAAA,UACL,EAAE,KAAK,UAAU,OAAO,UAAU,OAAO,SAAS;AAAA,UAClD,EAAE,KAAK,WAAW,OAAO,cAAc,OAAO,UAAU;AAAA,QAC1D;AAAA,QACA,UAAU,UAAS,KAAK,UAAU,YAAY,UAAU,IAAI,SAAS;AAAA;AAAA,IACvE,GACF;AAAA,KACF;AAEJ;;;AC5CA,OAAOE,YAAU;AAEjB,SAAS,OAAAC,MAAK,QAAAC,aAAY;AAC1B,SAAgB,aAAAC,YAAW,SAAS,YAAAC,iBAAgB;;;ACK7C,SAAS,gBAAgB,KAAsB;AACpD,QAAM,IAAI,IAAI,KAAK;AACnB,MAAI,EAAE,EAAE,WAAW,OAAO,KAAK,EAAE,WAAW,QAAQ,GAAI,QAAO;AAC/D,MAAI;AAEF,WAAO,IAAI,IAAI,CAAC,EAAE,KAAK,SAAS;AAAA,EAClC,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAGO,SAAS,YAAY,GAAoB;AAC9C,SAAO,OAAO,UAAU,CAAC,KAAK,KAAK,KAAK,KAAK;AAC/C;AAGO,SAAS,iBAAiB,GAAoB;AACnD,SAAO,OAAO,UAAU,CAAC,KAAK,IAAI;AACpC;AAWO,SAAS,oBAAoB,OAUvB;AACX,QAAM,OAAiB,CAAC;AACxB,MAAI,CAAC,MAAM,KAAK,KAAK,EAAG,MAAK,KAAK,MAAM;AACxC,MAAI,MAAM,gBAAgB,OAAO;AAC/B,QAAI,CAAC,MAAM,KAAK,KAAK,EAAG,MAAK,KAAK,MAAM;AACxC,QAAI,CAAC,MAAM,SAAS,KAAK,EAAG,MAAK,KAAK,UAAU;AAChD,QAAI,CAAC,YAAY,MAAM,IAAI,EAAG,MAAK,KAAK,MAAM;AAC9C,QAAI,MAAM,eAAe,SAAS,EAAE,MAAM,kBAAkB,IAAI,KAAK,EAAG,MAAK,KAAK,KAAK;AACvF,QAAI,MAAM,eAAe,cAAc,MAAM,SAAS,EAAE,MAAM,YAAY,IAAI,KAAK,GAAG;AACpF,WAAK,KAAK,UAAU;AAAA,IACtB;AAAA,EACF;AACA,SAAO;AACT;;;AC7DA,SAAS,OAAAC,MAAK,QAAAC,aAAY;AAC1B,OAAO,eAAe;AAkClB,SAEI,OAAAC,MAFJ,QAAAC,aAAA;AAvBG,SAAS,MAAM;AAAA,EACpB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAYG;AACD,SACE,gBAAAA,MAACH,MAAA,EACC;AAAA,oBAAAE,KAACF,MAAA,EAAI,OAAO,IACV,0BAAAE,KAACD,OAAA,EAAK,OAAO,QAAQ,SAAS,QAAS,iBAAM,GAC/C;AAAA,IACA,gBAAAC;AAAA,MAAC;AAAA;AAAA,QACC;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA;AAAA,IACF;AAAA,IACC,UAAU,gBAAAA,KAACD,OAAA,EAAK,OAAM,OAAM,qBAAE,IAAU;AAAA,KAC3C;AAEJ;;;AClDA,SAAS,OAAAG,MAAK,QAAAC,aAAY;AAC1B,OAAOC,kBAAiB;AAsClB,SAEI,OAAAC,MAFJ,QAAAC,aAAA;AApBC,SAAS,YAA8B;AAAA,EAC5C;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAMG;AACD,QAAM,eAAe,KAAK;AAAA,IACxB;AAAA,IACA,QAAQ,UAAU,OAAK,EAAE,UAAU,KAAK;AAAA,EAC1C;AACA,QAAM,UAAU,QAAQ,KAAK,OAAK,EAAE,UAAU,KAAK;AACnD,SACE,gBAAAA,MAACJ,MAAA,EAAI,eAAc,UACjB;AAAA,oBAAAI,MAACJ,MAAA,EACC;AAAA,sBAAAG,KAACH,MAAA,EAAI,OAAO,IACV,0BAAAG,KAACF,OAAA,EAAK,OAAO,YAAY,SAAS,QAAS,iBAAM,GACnD;AAAA,MACA,gBAAAE,KAACF,OAAA,EAAM,mBAAS,SAAS,OAAM;AAAA,OACjC;AAAA,IACC,YACC,gBAAAE,KAACH,MAAA,EAAI,YAAY,IACf,0BAAAG;AAAA,MAACD;AAAA,MAAA;AAAA,QACC,OAAO,QAAQ,IAAI,QAAM,EAAE,KAAK,EAAE,OAAO,OAAO,EAAE,OAAO,OAAO,EAAE,MAAM,EAAE;AAAA,QAC1E;AAAA,QACA;AAAA,QACA,UAAU,UAAQ,SAAS,KAAK,KAAK;AAAA;AAAA,IACvC,GACF,IACE;AAAA,KACN;AAEJ;;;ACzDA,SAAS,YAAAG,iBAAgB;AACzB,SAAS,YAAAC,iBAAgB;AAclB,SAAS,cACd,OACA,MAC8C;AAC9C,QAAM,CAAC,SAAS,UAAU,IAAIA,UAAS,CAAC;AACxC,QAAM,aAAa,MAAM,KAAK,IAAI,SAAS,MAAM,SAAS,CAAC,CAAC;AAE5D,QAAM,UAAU,CAAC,QAAiB;AAChC,UAAM,IAAI,MAAM,QAAQ,GAAG;AAC3B,QAAI,KAAK,MAAM,SAAS,EAAG,MAAK,SAAS;AAAA,QACpC,YAAW,IAAI,CAAC;AAAA,EACvB;AAEA,EAAAD;AAAA,IACE,CAAC,QAAQ,QAAQ;AACf,UAAI,IAAI,OAAQ,MAAK,SAAS;AAAA,eACrB,IAAI,IAAK,YAAW,QAAM,KAAK,IAAI,GAAG,MAAM,SAAS,CAAC,IAAI,KAAK,MAAM,MAAM;AAAA,IACtF;AAAA,IACA,EAAE,UAAU,KAAK,SAAS;AAAA,EAC5B;AAEA,SAAO,EAAE,YAAY,QAAQ;AAC/B;;;ACrCA,OAAOE,UAAQ;AAoBf,SAAS,UAAU,MAAwC;AACzD,MAAI,CAAC,KAAM,QAAO,CAAC;AACnB,QAAM,EAAE,IAAI,KAAK,QAAQ,SAAS,GAAG,KAAK,IAAI;AAC9C,SAAO;AACT;AAGO,SAAS,cAAcC,IAAqC;AACjE,QAAM,UAAUA,IAAG,gBAAgB;AACnC,SAAO;AAAA,IACL,aAAaA,IAAG,eAAe;AAAA,IAC/B,MAAMA,IAAG,QAAQ;AAAA,IACjB,MAAMA,MAAK,CAAC,UAAUA,GAAE,OAAO;AAAA,IAC/B,MAAMA,KAAI,OAAOA,GAAE,IAAI,IAAI;AAAA,IAC3B,UAAUA,MAAK,CAAC,UAAUA,GAAE,WAAW;AAAA,IACvC,YAAYA,IAAG,cAAc;AAAA,IAC7B,gBAAgBA,IAAG,kBAAkB;AAAA,IACrC,UAAU;AAAA,IACV,YAAYA,IAAG,cAAc;AAAA,IAC7B,UAAUA,IAAG,YAAY;AAAA,EAC3B;AACF;AAUO,SAAS,oBACd,MACA,GACgC;AAChC,QAAM,UAAU,UAAU,IAAI;AAC9B,MAAI,EAAE,gBAAgB,SAAS;AAC7B,WAAO;AAAA,MACL,GAAG;AAAA,MACH,aAAa;AAAA,MACb,MAAM,EAAE,KAAK,KAAK;AAAA,MAClB,MAAM,MAAM,QAAQ;AAAA,MACpB,MAAM,MAAM,QAAQ;AAAA,MACpB,UAAU,MAAM,YAAYD,KAAG,SAAS,EAAE;AAAA,MAC1C,YAAY,EAAE,WAAW,KAAK,KAAK,MAAM;AAAA,MACzC,UAAU,EAAE,SAAS,KAAK,KAAK,MAAM;AAAA,IACvC;AAAA,EACF;AACA,SAAO;AAAA,IACL,GAAG;AAAA,IACH,aAAa;AAAA,IACb,MAAM,EAAE,KAAK,KAAK;AAAA,IAClB,MAAM,EAAE,KAAK,KAAK;AAAA,IAClB,MAAM,OAAO,SAAS,EAAE,MAAM,EAAE;AAAA,IAChC,UAAU,EAAE,SAAS,KAAK;AAAA,IAC1B,YAAY,EAAE;AAAA,IACd,gBAAgB,EAAE,eAAe,QAAQ,EAAE,kBAAkB,SAAY,MAAM;AAAA,IAC/E,UAAU,EAAE,eAAe,aAAa,EAAE,YAAY,MAAM,WAAW,MAAM;AAAA,EAC/E;AACF;;;ALgCQ,SA8DA,YAAAE,WA9DA,OAAAC,MAWA,QAAAC,aAXA;AApFD,SAAS,kBAAkB;AAAA,EAChC,SAAS;AAAA,EACT;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAMG;AACD,QAAM,CAAC,MAAM,OAAO,IAAIC,UAA2B,MAAM,cAAc,IAAI,CAAC;AAC5E,QAAM,CAAC,QAAQ,SAAS,IAAIA,UAAmB,CAAC,CAAC;AACjD,QAAM,OAAO,QAAQ,MAAM,KAAK,IAAI,kBAAkB,GAAG,CAAC,IAAI,CAAC;AAE/D,QAAM,MAAM,CAAC,UAAqC,QAAQ,QAAM,EAAE,GAAG,GAAG,GAAG,MAAM,EAAE;AAGnF,EAAAC,WAAU,MAAM;AACd,QACE,KAAK,gBAAgB,SACrB,KAAK,eAAe,SACpB,CAAC,KAAK,kBACN,KAAK,SAAS,GACd;AACA,UAAI,EAAE,gBAAgB,KAAK,CAAC,EAAE,CAAC;AAAA,IACjC;AAAA,EAEF,GAAG,CAAC,KAAK,aAAa,KAAK,YAAY,IAAI,CAAC;AAG5C,QAAM,QAAkB,CAAC;AACzB,MAAI,CAAC,KAAM,OAAM,KAAK,MAAM;AAC5B,QAAM,KAAK,MAAM;AACjB,MAAI,KAAK,gBAAgB,OAAO;AAC9B,UAAM,KAAK,QAAQ,QAAQ,YAAY,QAAQ,KAAK,eAAe,QAAQ,QAAQ,UAAU;AAAA,EAC/F,OAAO;AACL,UAAM,KAAK,cAAc,UAAU;AAAA,EACrC;AAEA,QAAM,SAAS,MAAM;AACnB,UAAM,OAAO,oBAAoB;AAAA,MAC/B,aAAa,KAAK;AAAA,MAClB,MAAM,KAAK;AAAA,MACX,MAAM,KAAK;AAAA,MACX,MAAM,OAAO,SAAS,KAAK,MAAM,EAAE;AAAA,MACnC,UAAU,KAAK;AAAA,MACf,YAAY,KAAK;AAAA,MACjB,gBAAgB,KAAK;AAAA,MACrB,UAAU,KAAK;AAAA,MACf,OAAO,CAAC;AAAA,IACV,CAAC;AACD,QAAI,KAAK,SAAS,GAAG;AACnB,gBAAU,IAAI;AACd;AAAA,IACF;AAIA,QAAI;AACF,YAAM,UAAU,oBAAoB,MAAM,IAAI;AAC9C,UAAI,KAAM,MAAK,IAAI,YAAY,EAAE,GAAG,SAAS,IAAI,KAAK,GAAG,CAAC;AAAA,UACrD,MAAK,IAAI,WAAW,OAAO;AAChC,WAAK,IAAI,kBAAkB;AAC3B,aAAO;AAAA,IACT,QAAQ;AACN,gBAAU,CAAC,MAAM,CAAC;AAAA,IACpB;AAAA,EACF;AAEA,QAAM,EAAE,YAAY,QAAQ,IAAI,cAAc,OAAO;AAAA,IACnD,UAAU;AAAA,IACV,UAAU;AAAA,IACV,UAAU;AAAA,EACZ,CAAC;AAED,QAAM,UAAU,CAACC,OAAc,OAAO,SAASA,EAAC;AAGhD,QAAM,YACJ,KAAK,eAAe,QAClB,KAAK,SAAS,IACZ,gBAAAJ;AAAA,IAAC;AAAA;AAAA,MACC,OAAM;AAAA,MACN,OAAO,KAAK,kBAAkB,KAAK,CAAC;AAAA,MACpC,WAAW,eAAe;AAAA,MAC1B,SAAS,KAAK,IAAI,CAAAI,QAAM,EAAE,OAAOC,OAAK,SAASD,EAAC,GAAG,OAAOA,GAAE,EAAE;AAAA,MAC9D,UAAU,CAAAE,OAAK;AACb,YAAI,EAAE,gBAAgBA,GAAE,CAAC;AACzB,gBAAQ,KAAK;AAAA,MACf;AAAA;AAAA,EACF,IAEA,gBAAAL,MAACM,MAAA,EACC;AAAA,oBAAAP,KAACO,MAAA,EAAI,OAAO,IACV,0BAAAP,KAACQ,OAAA,EAAK,OAAO,eAAe,QAAQ,SAAS,QAAQ,iBAAG,GAC1D;AAAA,IACA,gBAAAR,KAACQ,OAAA,EAAK,OAAM,UAAS,wDAAqC;AAAA,KAC5D,IAGF,gBAAAR;AAAA,IAAC;AAAA;AAAA,MACC,OAAM;AAAA,MACN,OAAO,KAAK;AAAA,MACZ,UAAU,CAAAM,OAAK,IAAI,EAAE,UAAUA,GAAE,CAAC;AAAA,MAClC,OAAO,eAAe;AAAA,MACtB,UAAU,MAAM,QAAQ,UAAU;AAAA,MAClC,MAAK;AAAA,MACL,aAAa,OAAQ,KAAK,eAAe,2BAAsB,gBAAiB;AAAA,MAChF,SAAS,QAAQ,UAAU;AAAA;AAAA,EAC7B;AAGJ,SACE,gBAAAL,MAACM,MAAA,EAAI,eAAc,UACjB;AAAA,oBAAAP,KAACQ,OAAA,EAAM,iBAAO,QAAQ,KAAK,IAAI,KAAK,eAAc;AAAA,IAClD,gBAAAR,KAACQ,OAAA,EAAK,OAAM,QAAQ,mBAAQ;AAAA,IAE3B,MAAM,SAAS,MAAM,IACpB,gBAAAR;AAAA,MAAC;AAAA;AAAA,QACC,OAAM;AAAA,QACN,OAAO,KAAK;AAAA,QACZ,WAAW,eAAe;AAAA,QAC1B,SAAS;AAAA,UACP,EAAE,OAAO,OAAO,OAAO,MAAM;AAAA,UAC7B,EAAE,OAAO,SAAS,OAAO,QAAQ;AAAA,QACnC;AAAA,QACA,UAAU,CAAAM,OAAK;AACb,cAAI,EAAE,aAAaA,GAAE,CAAC;AACtB,kBAAQ,MAAM;AAAA,QAChB;AAAA;AAAA,IACF,IACE;AAAA,IAEJ,gBAAAN;AAAA,MAAC;AAAA;AAAA,QACC,OAAM;AAAA,QACN,OAAO,KAAK;AAAA,QACZ,UAAU,CAAAM,OAAK,IAAI,EAAE,MAAMA,GAAE,CAAC;AAAA,QAC9B,OAAO,eAAe;AAAA,QACtB,UAAU,MAAM,QAAQ,MAAM;AAAA,QAC9B,SAAS,QAAQ,MAAM;AAAA;AAAA,IACzB;AAAA,IAEC,KAAK,gBAAgB,QACpB,gBAAAL,MAAAF,WAAA,EACE;AAAA,sBAAAC;AAAA,QAAC;AAAA;AAAA,UACC,OAAM;AAAA,UACN,OAAO,KAAK;AAAA,UACZ,UAAU,CAAAM,OAAK,IAAI,EAAE,MAAMA,GAAE,CAAC;AAAA,UAC9B,OAAO,eAAe;AAAA,UACtB,UAAU,MAAM,QAAQ,MAAM;AAAA,UAC9B,SAAS,QAAQ,MAAM;AAAA;AAAA,MACzB;AAAA,MACA,gBAAAN;AAAA,QAAC;AAAA;AAAA,UACC,OAAM;AAAA,UACN,OAAO,KAAK;AAAA,UACZ,UAAU,CAAAM,OAAK,IAAI,EAAE,MAAMA,GAAE,QAAQ,WAAW,EAAE,EAAE,CAAC;AAAA,UACrD,OAAO,eAAe;AAAA,UACtB,UAAU,MAAM,QAAQ,MAAM;AAAA,UAC9B,SAAS,QAAQ,MAAM;AAAA;AAAA,MACzB;AAAA,MACA,gBAAAN;AAAA,QAAC;AAAA;AAAA,UACC,OAAM;AAAA,UACN,OAAO,KAAK;AAAA,UACZ,UAAU,CAAAM,OAAK,IAAI,EAAE,UAAUA,GAAE,CAAC;AAAA,UAClC,OAAO,eAAe;AAAA,UACtB,UAAU,MAAM,QAAQ,UAAU;AAAA,UAClC,SAAS,QAAQ,UAAU;AAAA;AAAA,MAC7B;AAAA,MACA,gBAAAN;AAAA,QAAC;AAAA;AAAA,UACC,OAAM;AAAA,UACN,OAAO,KAAK;AAAA,UACZ,WAAW,eAAe;AAAA,UAC1B,SAAS;AAAA,YACP,EAAE,OAAO,OAAO,OAAO,MAAM;AAAA,YAC7B,EAAE,OAAO,YAAY,OAAO,WAAW;AAAA,UACzC;AAAA,UACA,UAAU,CAAAM,OAAK;AACb,gBAAI,EAAE,YAAYA,GAAE,CAAC;AACrB,oBAAQ,MAAM;AAAA,UAChB;AAAA;AAAA,MACF;AAAA,MACC;AAAA,OACH,IAEA,gBAAAL,MAAAF,WAAA,EACE;AAAA,sBAAAC;AAAA,QAAC;AAAA;AAAA,UACC,OAAM;AAAA,UACN,OAAO,KAAK;AAAA,UACZ,UAAU,CAAAM,OAAK,IAAI,EAAE,YAAYA,GAAE,CAAC;AAAA,UACpC,OAAO,eAAe;AAAA,UACtB,UAAU,MAAM,QAAQ,YAAY;AAAA,UACpC,aAAY;AAAA;AAAA,MACd;AAAA,MACA,gBAAAN;AAAA,QAAC;AAAA;AAAA,UACC,OAAM;AAAA,UACN,OAAO,KAAK;AAAA,UACZ,UAAU,CAAAM,OAAK,IAAI,EAAE,UAAUA,GAAE,CAAC;AAAA,UAClC,OAAO,eAAe;AAAA,UACtB,UAAU,MAAM,QAAQ,UAAU;AAAA,UAClC,aAAY;AAAA;AAAA,MACd;AAAA,OACF;AAAA,IAGF,gBAAAN,KAACQ,OAAA,EAAK,OAAM,QAAQ,mBAAQ;AAAA,IAC3B,OAAO,SAAS,IACf,gBAAAR,KAACQ,OAAA,EAAK,OAAM,OACT,iBAAO,SAAS,MAAM,IACnB,6DACA,0BAA0B,OAAO,KAAK,IAAI,CAAC,IACjD,IACE;AAAA,IACJ,gBAAAR,KAACQ,OAAA,EAAK,OAAM,QAAO,2DAAuC;AAAA,KAC5D;AAEJ;;;AFrNM,gBAAAC,MAOA,QAAAC,aAPA;AArBN,IAAM,OAAO;AAGb,IAAM,aAAsE;AAAA,EAC1E,WAAW,EAAE,KAAK,UAAK,OAAO,QAAQ;AAAA,EACtC,cAAc,EAAE,KAAK,UAAK,OAAO,OAAO;AAAA,EACxC,YAAY,EAAE,KAAK,UAAK,OAAO,SAAS;AAAA,EACxC,OAAO,EAAE,KAAK,UAAK,OAAO,MAAM;AAClC;AAGA,SAAS,SAASC,IAAoB;AACpC,QAAM,UAAUA,GAAE,gBAAgB,WAAWA,GAAE,SAAS,eAAeA,GAAE,SAAS;AAClF,SAAO,UAAU,UAAU,GAAGA,GAAE,QAAQ,IAAIA,GAAE,IAAI,IAAIA,GAAE,IAAI;AAC9D;AAGA,SAAS,WAAW,EAAE,GAAAA,IAAG,SAAS,GAAsC;AACtE,QAAM,IAAI,WAAWA,GAAE,MAAM,KAAK,EAAE,KAAK,QAAK,OAAO,OAAO;AAC5D,SACE,gBAAAD,MAACE,MAAA,EACC;AAAA,oBAAAH,KAACI,OAAA,EAAK,OAAM,QAAQ,qBAAW,YAAO,MAAK;AAAA,IAC3C,gBAAAJ,KAACG,MAAA,EAAI,OAAO,IACV,0BAAAH,KAACI,OAAA,EAAM,UAAAF,GAAE,MAAK,GAChB;AAAA,IACA,gBAAAF,KAACG,MAAA,EAAI,OAAO,IACV,0BAAAH,KAACI,OAAA,EAAK,OAAM,QAAQ,mBAASF,EAAC,GAAE,GAClC;AAAA,IACA,gBAAAD,MAACG,OAAA,EAAK,OAAO,EAAE,OACZ;AAAA,QAAE;AAAA,MAAI;AAAA,MAAEF,GAAE;AAAA,OACb;AAAA,KACF;AAEJ;AAYO,SAAS,eAAe;AAAA,EAC7B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAKG;AACD,QAAM,CAAC,UAAU,WAAW,IAAIG,UAAS,CAAC;AAC1C,QAAM,CAAC,MAAM,OAAO,IAAIA,UAAsC,MAAM;AACpE,QAAM,CAAC,SAAS,UAAU,IAAIA,UAAyB,IAAI;AAE3D,QAAM,QAAQ,SAAS;AAEvB,QAAM,MAAM,KAAK,IAAI,UAAU,KAAK,IAAI,GAAG,QAAQ,CAAC,CAAC;AAErD,EAAAC;AAAA,IACE,CAAC,OAAO,QAAQ;AACd,UAAI,IAAI,QAAQ;AACd,eAAO;AAAA,MACT,WAAW,IAAI,SAAS;AACtB,oBAAY,KAAK,IAAI,GAAG,MAAM,CAAC,CAAC;AAAA,MAClC,WAAW,IAAI,WAAW;AACxB,oBAAY,KAAK,IAAI,QAAQ,GAAG,MAAM,CAAC,CAAC;AAAA,MAC1C,WAAW,UAAU,KAAK;AACxB,mBAAW,IAAI;AACf,gBAAQ,MAAM;AAAA,MAChB,WAAW,UAAU,KAAK;AACxB,YAAI,SAAS,GAAG,GAAG;AACjB,qBAAW,SAAS,GAAG,CAAC;AACxB,kBAAQ,MAAM;AAAA,QAChB;AAAA,MACF,WAAW,UAAU,KAAK;AACxB,YAAI,SAAS,GAAG,EAAG,SAAQ,SAAS;AAAA,MACtC;AAAA,IACF;AAAA,IACA,EAAE,UAAU,UAAU,SAAS,OAAO;AAAA,EACxC;AAGA,EAAAA;AAAA,IACE,CAAC,QAAQ,QAAQ;AACf,UAAI,IAAI,OAAQ,SAAQ,MAAM;AAAA,IAChC;AAAA,IACA,EAAE,UAAU,UAAU,SAAS,UAAU;AAAA,EAC3C;AAEA,MAAI,SAAS,QAAQ;AACnB,WACE,gBAAAN;AAAA,MAAC;AAAA;AAAA,QACC,SAAS;AAAA,QACT;AAAA,QACA;AAAA,QACA,QAAQ,MAAM,QAAQ,MAAM;AAAA,QAC5B,UAAU,MAAM,QAAQ,MAAM;AAAA;AAAA,IAChC;AAAA,EAEJ;AAGA,QAAM,QAAQ,KAAK,IAAI,GAAG,KAAK,IAAI,MAAM,KAAK,MAAM,OAAO,CAAC,GAAG,KAAK,IAAI,GAAG,QAAQ,IAAI,CAAC,CAAC;AACzF,QAAM,UAAU,SAAS,MAAM,OAAO,QAAQ,IAAI;AAClD,QAAM,cAAc;AACpB,QAAM,cAAc,KAAK,IAAI,GAAG,SAAS,QAAQ,KAAK;AAEtD,QAAM,SAAS,SAAS,GAAG;AAC3B,QAAM,aAAa,SAAS,KAAK,IAAI,WAAW,OAAO,EAAE,IAAI;AAE7D,QAAM,gBAAgB,SAClB,OAAO,gBAAgB,UACrB,kDACA,aACE,oDACA,SACJ;AAEJ,SACE,gBAAAC,MAACE,MAAA,EAAI,eAAc,UACjB;AAAA,oBAAAH,KAACI,OAAA,EAAK,OAAM,QAAQ,mBAAQ;AAAA,IAC5B,gBAAAH,MAACG,OAAA,EAAK;AAAA;AAAA,MAAW;AAAA,MAAM;AAAA,OAAC;AAAA,IACvB,cAAc,IAAI,gBAAAH,MAACG,OAAA,EAAK,OAAM,QAAO;AAAA;AAAA,MAAG;AAAA,MAAY;AAAA,OAAK,IAAU;AAAA,IACnE,QAAQ,IAAI,CAACF,IAAG,MACf,gBAAAF,KAAC,cAAsB,GAAGE,IAAG,UAAU,QAAQ,MAAM,OAApCA,GAAE,EAAuC,CAC3D;AAAA,IACA,cAAc,IAAI,gBAAAD,MAACG,OAAA,EAAK,OAAM,QAAO;AAAA;AAAA,MAAG;AAAA,MAAY;AAAA,OAAK,IAAU;AAAA,IACpE,gBAAAJ,KAACI,OAAA,EAAK,OAAM,QAAQ,mBAAQ;AAAA,IAC3B,SAAS,aAAa,SACrB,gBAAAJ;AAAA,MAAC;AAAA;AAAA,QACC,SAAS,WAAW,OAAO,IAAI;AAAA,QAC/B,cAAa;AAAA,QACb,SAAS;AAAA,QACT,UAAU,UAAU,SAAS;AAAA,QAC7B,WAAW,MAAM;AAGf,cAAI;AACF,iBAAK,IAAI,cAAc,OAAO,EAAE;AAChC,iBAAK,IAAI,kBAAkB;AAAA,UAC7B,QAAQ;AAAA,UAER;AAEA,sBAAY,KAAK,IAAI,GAAG,KAAK,IAAI,KAAK,QAAQ,CAAC,CAAC,CAAC;AACjD,kBAAQ,MAAM;AAAA,QAChB;AAAA,QACA,UAAU,MAAM,QAAQ,MAAM;AAAA;AAAA,IAChC,IAEA,gBAAAA,KAACI,OAAA,EAAK,OAAM,QAAO,oFAAgD;AAAA,KAEvE;AAEJ;;;AQ3KA,SAAS,OAAAG,MAAK,QAAAC,OAAM,YAAAC,iBAAgB;AACpC,SAAgB,aAAAC,YAAW,WAAAC,UAAS,YAAAC,iBAAgB;;;ACDpD,OAAO,YAAY;AAMZ,SAAS,SAAS,KAAqB;AAC5C,MAAI,MAAM;AACV,SAAO,SAAS,KAAK,EAAE,OAAO,KAAK,GAAG,OAAK;AACzC,UAAM;AAAA,EACR,CAAC;AACD,SAAO,IAAI,QAAQ,OAAO,EAAE;AAC9B;;;ACaO,SAAS,aAAa,OAAe,QAAgB,MAA4B;AACtF,QAAM,YAAY,KAAK,IAAI,GAAG,QAAQ,IAAI;AAC1C,QAAM,UAAU,KAAK,IAAI,KAAK,IAAI,GAAG,MAAM,GAAG,SAAS;AACvD,QAAM,MAAM,QAAQ;AACpB,QAAM,QAAQ,KAAK,IAAI,GAAG,MAAM,IAAI;AACpC,SAAO,EAAE,OAAO,KAAK,aAAa,OAAO,aAAa,QAAQ,KAAK,UAAU;AAC/E;;;AFuBM,gBAAAC,MASgB,QAAAC,aAThB;AAnCN,IAAM,eAAe;AAGrB,IAAM,YAAsC;AAAA,EAC1C,MAAM;AAAA,EACN,SAAS;AAAA,EACT,MAAM;AAAA,EACN,KAAK;AACP;AAUA,SAAS,YAAY;AAAA,EACnB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAOG;AACD,SACE,gBAAAA,MAACC,MAAA,EAAI,eAAc,UACjB;AAAA,oBAAAF,KAACG,OAAA,EAAK,OAAM,QAAQ,mBAAQ;AAAA,IAE3B,QACC,gBAAAF,MAACC,MAAA,EAAI,eAAc,UACjB;AAAA,sBAAAF,KAACG,OAAA,EAAK,OAAM,QAAO,2BAAa;AAAA,MAC/B,aAAa,SAAS,gBAAAH,KAACG,OAAA,EAAM,kBAAO,IAAU;AAAA,MAC/C,gBAAAF,MAACC,MAAA,EACC;AAAA,wBAAAF,KAACG,OAAA,EAAK,OAAM,QAAO,kBAAI;AAAA,QACvB,gBAAAH,KAACG,OAAA,EAAK,OAAM,QAAQ,iBAAM;AAAA,QACzB,SAAS,gBAAAF,MAACE,OAAA,EAAK,OAAM,QAAO;AAAA;AAAA,UAAI;AAAA,UAAO;AAAA,WAAC,IAAU;AAAA,SACrD;AAAA,MACC,CAAC,YAAY,gBAAAH,KAACG,OAAA,EAAK,OAAM,QAAO,kCAAoB,IAAU;AAAA,OACjE,IAEA,gBAAAH,KAACG,OAAA,EAAK,OAAM,UAAS,oDAAiC;AAAA,IAGxD,gBAAAH,KAACG,OAAA,EAAK,OAAM,QAAQ,mBAAQ;AAAA,IAE5B,gBAAAH,KAACG,OAAA,EAAK,OAAM,QAAO,sBAAQ;AAAA,IAC1B,SAAS,IAAI,UACZ,gBAAAF,MAACC,MAAA,EACC;AAAA,sBAAAF,KAACG,OAAA,EAAK,OAAM,QAAQ,gBAAK;AAAA,MACzB,gBAAAH,KAACG,OAAA,EAAM,eAAK,MAAK;AAAA,MACjB,gBAAAH,KAACG,OAAA,EAAK,OAAM,QAAO,oBAAG;AAAA,MACtB,gBAAAH,KAACG,OAAA,EAAK,OAAO,UAAU,KAAK,GAAG,GAAI,eAAK,KAAI;AAAA,SAJpC,KAAK,EAKf,CACD;AAAA,IACD,gBAAAF,MAACC,MAAA,EACC;AAAA,sBAAAF,KAACG,OAAA,EAAK,OAAM,QAAO,sBAAQ;AAAA,MAC3B,gBAAAH,KAACG,OAAA,EAAK,OAAO,UAAU,IAAI,UAAU,QAAS,mBAAQ;AAAA,OACxD;AAAA,KACF;AAEJ;AAKA,SAAS,WAAW,EAAE,MAAM,OAAO,GAAuC;AACxE,QAAM,EAAE,OAAO,KAAK,aAAa,YAAY,IAAI,aAAa,KAAK,QAAQ,QAAQ,YAAY;AAC/F,QAAM,OAAO,KAAK,MAAM,OAAO,GAAG;AAClC,SACE,gBAAAF,MAACC,MAAA,EAAI,eAAc,UACjB;AAAA,oBAAAF,KAACG,OAAA,EAAK,OAAM,QAAQ,wBAAc,IAAI,UAAK,WAAW,UAAU,SAAQ;AAAA,IACvE,KAAK,WAAW,IACf,gBAAAH,KAACG,OAAA,EAAK,OAAM,QAAO,oBAAC,IAEpB,KAAK,IAAI,CAAC,MAAM,MACd,gBAAAH,KAACG,OAAA,EAAkC,OAAM,QACtC,kBADQ,GAAG,QAAQ,CAAC,IAAI,IAAI,EAE/B,CACD;AAAA,IAEF,cAAc,IAAI,gBAAAF,MAACE,OAAA,EAAK,OAAM,QAAO;AAAA;AAAA,MAAG;AAAA,MAAY;AAAA,OAAK,IAAU;AAAA,KACtE;AAEJ;AAYO,SAAS,WAAW;AAAA,EACzB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAMG;AACD,QAAM,CAAC,SAAS,UAAU,IAAIC,UAAS,KAAK;AAC5C,QAAM,CAAC,QAAQ,SAAS,IAAIA,UAAS,CAAC;AACtC,QAAM,CAAC,SAAS,UAAU,IAAIA,UAAS,CAAC;AAExC,QAAM,CAAC,WAAW,YAAY,IAAIA,UAAS,KAAK;AAIhD,QAAM,CAAC,eAAe,gBAAgB,IAAIA,UAAS,KAAK;AACxD,QAAM,CAAC,iBAAiB,kBAAkB,IAAIA,UAAwB,IAAI;AAG1E,EAAAC,WAAU,MAAM;AACd,UAAM,IAAI,YAAY,MAAM;AAC1B,iBAAW,KAAK,GAAG,2BAA2B,CAAC;AAAA,IACjD,GAAG,GAAI;AACP,WAAO,MAAM,cAAc,CAAC;AAAA,EAC9B,GAAG,CAAC,IAAI,CAAC;AAGT,QAAM,kBAAkB,MAAY;AAClC,UAAM,KAAK,cAAc;AACzB,QAAI,CAAC,GAAG,WAAW;AACjB,yBAAmB,+EAA6B;AAChD;AAAA,IACF;AACA,QAAI,GAAG,SAAS;AACd,uBAAiB;AACjB,uBAAiB,KAAK;AACtB,yBAAmB,wCAAU;AAC7B;AAAA,IACF;AACA,UAAMC,QAAO,YAAY;AACzB,QAAIA,MAAK,YAAY;AACnB,yBAAmB,8IAAyD;AAC5E;AAAA,IACF;AACA,UAAM,MAAM,eAAeA,KAAI;AAC/B,QAAI,CAAC,IAAI,IAAI;AACX,yBAAmB,qFAAmC;AACtD;AAAA,IACF;AACA,qBAAiB,IAAI;AACrB;AAAA,MACE,IAAI,SACA,uMACA,oGAA8B,IAAI,aAAa;AAAA,IACrD;AAAA,EACF;AAEA,EAAAC;AAAA,IACE,CAAC,OAAO,QAAQ;AACd,UAAI,UAAU,KAAK;AAGjB,YAAI,eAAe;AAMjB,eAAK,QAAQ;AAGb,cAAI,CAAC,aAAa,GAAG;AACnB,oBAAQ,OAAO;AAAA,cACb;AAAA,YACF;AAAA,UACF;AAAA,QACF;AACA,eAAO;AAAA,MACT,WAAW,UAAU,IAAK,MAAK,GAAG,UAAU;AAAA,eACnC,UAAU,IAAK,MAAK,GAAG,UAAU;AAAA,eACjC,UAAU,IAAK,YAAW,UAAU;AAAA,eACpC,UAAU,IAAK,YAAW,SAAS;AAAA,eACnC,UAAU,IAAK,YAAW,MAAM;AAAA,eAChC,UAAU,IAAK,iBAAgB;AAAA,eAC/B,UAAU,IAAK,cAAa,CAAAC,OAAK,CAACA,EAAC;AAAA,eACnC,UAAU,KAAK;AACtB,mBAAW,CAAAA,OAAK,CAACA,EAAC;AAClB,kBAAU,CAAC;AAAA,MACb,WAAW,WAAW,IAAI,SAAS;AACjC,cAAM,EAAE,UAAU,IAAI,aAAa,MAAM,KAAK,QAAQ,QAAQ,YAAY;AAC1E,kBAAU,OAAK,KAAK,IAAI,IAAI,GAAG,SAAS,CAAC;AAAA,MAC3C,WAAW,WAAW,IAAI,WAAW;AACnC,kBAAU,OAAK,KAAK,IAAI,IAAI,GAAG,CAAC,CAAC;AAAA,MACnC;AAAA,IACF;AAAA,IACA,EAAE,UAAU,OAAO;AAAA,EACrB;AAEA,QAAM,WAAW,MAAM,SAAS,IAAI,CAAAC,QAAM;AAAA,IACxC,IAAIA,GAAE;AAAA,IACN,MAAMA,GAAE;AAAA,IACR,KAAK,MAAM,SAASA,GAAE,EAAE,KAAK;AAAA,EAC/B,EAAE;AAEF,QAAM,SAASC,SAAQ,MAAO,MAAM,KAAK,SAAS,MAAM,GAAG,GAAG,IAAI,MAAO,CAAC,MAAM,IAAI,GAAG,CAAC;AAExF,SACE,gBAAAT,MAACC,MAAA,EAAI,eAAc,UAChB;AAAA,cACC,gBAAAF,KAAC,cAAW,MAAM,MAAM,MAAM,QAAgB,IAE9C,gBAAAA;AAAA,MAAC;AAAA;AAAA,QACC;AAAA,QACA;AAAA,QACA;AAAA,QACA,OAAO,MAAM,KAAK,UAAU,MAAM,GAAG,GAAG,IAAI;AAAA,QAC5C,QAAQ,MAAM,IAAI,QAAQ;AAAA,QAC1B;AAAA;AAAA,IACF;AAAA,IAEF,gBAAAA,KAACG,OAAA,EAAK,OAAM,QAAQ,mBAAQ;AAAA,IAC3B,kBAAkB,gBAAAH,KAACG,OAAA,EAAK,OAAM,UAAU,2BAAgB,IAAU;AAAA,IACnE,gBAAAF,MAACE,OAAA,EAAK,OAAM,QAAO;AAAA;AAAA,MACoE;AAAA,MACpF,YAAY,SAAS;AAAA,MAAO;AAAA,MAAS,UAAU,aAAa;AAAA,MAC5D,UAAU,8BAAiB;AAAA,OAC9B;AAAA,KACF;AAEJ;;;AGlQA,SAAS,OAAAQ,MAAK,QAAAC,aAAY;AAC1B,SAAgB,YAAAC,iBAAgB;AA2G1B,gBAAAC,MA+CA,QAAAC,aA/CA;AAhGN,IAAM,QAAQ,CAAC,aAAa,UAAU,aAAa,QAAQ,MAAM;AAGjE,SAAS,SAASC,SAA6B;AAC7C,SAAO;AAAA,IACL,WAAWA,QAAO,aAAa,EAAE,KAAK,IAAI;AAAA,IAC1C,QAAQ,OAAOA,QAAO,UAAU,CAAC;AAAA,IACjC,WAAW,OAAOA,QAAO,uBAAuB,CAAC;AAAA,IACjD,MAAM,OAAOA,QAAO,iBAAiB,CAAC;AAAA,IACtC,MAAM,OAAOA,QAAO,iBAAiB,CAAC;AAAA,EACxC;AACF;AAYO,SAAS,cAAc;AAAA,EAC5B,QAAAA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAKG;AACD,QAAM,CAAC,MAAM,OAAO,IAAIC,UAAS,MAAM,SAASD,OAAM,CAAC;AACvD,QAAM,CAAC,QAAQ,SAAS,IAAIC,UAAmB,CAAC,CAAC;AACjD,QAAM,CAAC,OAAO,QAAQ,IAAIA,UAAS,KAAK;AAExC,QAAM,MAAM,CAAC,UAAgC;AAC3C,YAAQ,QAAM,EAAE,GAAG,GAAG,GAAG,MAAM,EAAE;AACjC,aAAS,KAAK;AAAA,EAChB;AAEA,QAAM,OAAO,MAAM;AACjB,UAAM,OAAO,KAAK,UACf,MAAM,GAAG,EACT,IAAI,OAAK,EAAE,KAAK,CAAC,EACjB,OAAO,OAAO;AACjB,UAAM,OAAO,OAAO,SAAS,KAAK,QAAQ,EAAE;AAC5C,UAAM,YAAY,OAAO,SAAS,KAAK,WAAW,EAAE;AACpD,UAAM,OAAO,OAAO,SAAS,KAAK,MAAM,EAAE;AAC1C,UAAM,OAAO,OAAO,SAAS,KAAK,MAAM,EAAE;AAE1C,UAAM,OAAiB,CAAC;AACxB,QAAI,KAAK,WAAW,KAAK,CAAC,KAAK,MAAM,eAAe,EAAG,MAAK,KAAK,WAAW;AAC5E,QAAI,CAAC,YAAY,IAAI,EAAG,MAAK,KAAK,QAAQ;AAC1C,QAAI,CAAC,iBAAiB,SAAS,EAAG,MAAK,KAAK,WAAW;AACvD,QAAI,CAAC,iBAAiB,IAAI,EAAG,MAAK,KAAK,MAAM;AAC7C,QAAI,CAAC,iBAAiB,IAAI,EAAG,MAAK,KAAK,MAAM;AAC7C,QAAI,KAAK,SAAS,GAAG;AACnB,gBAAU,IAAI;AACd;AAAA,IACF;AAEA,UAAM,eAAe,KAAK,UAAU,IAAI,MAAM,KAAK,UAAUD,QAAO,aAAa,CAAC;AAElF,QAAI;AAGF,MAAAA,QAAO,WAAW;AAAA,QAChB,GAAI,eAAe,EAAE,YAAY,KAAK,IAAI,CAAC;AAAA,QAC3C,SAAS;AAAA,QACT,uBAAuB;AAAA,QACvB,iBAAiB;AAAA,QACjB,iBAAiB;AAAA,MACnB,CAAC;AAED,UAAI,aAAc,MAAK,GAAG,UAAU;AAAA,IACtC,QAAQ;AACN,gBAAU,CAAC,MAAM,CAAC;AAClB;AAAA,IACF;AACA,cAAU,CAAC,CAAC;AACZ,aAAS,IAAI;AAAA,EACf;AAEA,QAAM,EAAE,YAAY,QAAQ,IAAI,cAAc,OAAO;AAAA,IACnD,UAAU;AAAA,IACV,UAAU;AAAA,IACV,UAAU;AAAA,EACZ,CAAC;AAED,QAAM,UAAU,CAACE,OAAc,OAAO,SAASA,EAAC;AAEhD,SACE,gBAAAH,MAACI,MAAA,EAAI,eAAc,UACjB;AAAA,oBAAAL,KAACM,OAAA,EAAK,OAAM,QAAQ,mBAAQ;AAAA,IAC5B,gBAAAN,KAACM,OAAA,EAAK,qBAAO;AAAA,IAEb,gBAAAN;AAAA,MAAC;AAAA;AAAA,QACC,OAAM;AAAA,QACN,OAAO,KAAK;AAAA,QACZ,UAAU,CAAAO,OAAK,IAAI,EAAE,WAAWA,GAAE,CAAC;AAAA,QACnC,OAAO,eAAe;AAAA,QACtB,UAAU,MAAM,QAAQ,WAAW;AAAA,QACnC,aAAY;AAAA,QACZ,SAAS,QAAQ,WAAW;AAAA;AAAA,IAC9B;AAAA,IACA,gBAAAP;AAAA,MAAC;AAAA;AAAA,QACC,OAAM;AAAA,QACN,OAAO,KAAK;AAAA,QACZ,UAAU,CAAAO,OAAK,IAAI,EAAE,QAAQA,GAAE,QAAQ,WAAW,EAAE,EAAE,CAAC;AAAA,QACvD,OAAO,eAAe;AAAA,QACtB,UAAU,MAAM,QAAQ,QAAQ;AAAA,QAChC,SAAS,QAAQ,QAAQ;AAAA;AAAA,IAC3B;AAAA,IACA,gBAAAP;AAAA,MAAC;AAAA;AAAA,QACC,OAAM;AAAA,QACN,OAAO,KAAK;AAAA,QACZ,UAAU,CAAAO,OAAK,IAAI,EAAE,WAAWA,GAAE,QAAQ,WAAW,EAAE,EAAE,CAAC;AAAA,QAC1D,OAAO,eAAe;AAAA,QACtB,UAAU,MAAM,QAAQ,WAAW;AAAA,QACnC,SAAS,QAAQ,WAAW;AAAA;AAAA,IAC9B;AAAA,IACA,gBAAAP;AAAA,MAAC;AAAA;AAAA,QACC,OAAM;AAAA,QACN,OAAO,KAAK;AAAA,QACZ,UAAU,CAAAO,OAAK,IAAI,EAAE,MAAMA,GAAE,QAAQ,WAAW,EAAE,EAAE,CAAC;AAAA,QACrD,OAAO,eAAe;AAAA,QACtB,UAAU,MAAM,QAAQ,MAAM;AAAA,QAC9B,SAAS,QAAQ,MAAM;AAAA;AAAA,IACzB;AAAA,IACA,gBAAAP;AAAA,MAAC;AAAA;AAAA,QACC,OAAM;AAAA,QACN,OAAO,KAAK;AAAA,QACZ,UAAU,CAAAO,OAAK,IAAI,EAAE,MAAMA,GAAE,QAAQ,WAAW,EAAE,EAAE,CAAC;AAAA,QACrD,OAAO,eAAe;AAAA,QACtB,UAAU,MAAM,QAAQ,MAAM;AAAA,QAC9B,SAAS,QAAQ,MAAM;AAAA;AAAA,IACzB;AAAA,IACA,gBAAAP,KAACM,OAAA,EAAK,OAAM,QAAO,4FAAsE;AAAA,IAEzF,gBAAAN,KAACM,OAAA,EAAK,OAAM,QAAQ,mBAAQ;AAAA,IAC5B,gBAAAL,MAACK,OAAA,EAAK,OAAM,QAAO;AAAA;AAAA,MAAYJ,QAAO,aAAa,EAAE,MAAM,GAAG,CAAC;AAAA,MAAE;AAAA,OAAC;AAAA,IAClE,gBAAAD,MAACK,OAAA,EAAK,OAAM,QAAO;AAAA;AAAA,MAAYJ,QAAO,aAAa,EAAE;AAAA,OAAO;AAAA,IAE5D,gBAAAF,KAACM,OAAA,EAAK,OAAM,QAAQ,mBAAQ;AAAA,IAC3B,OAAO,SAAS,IACf,gBAAAN,KAACM,OAAA,EAAK,OAAM,OACT,iBAAO,SAAS,MAAM,IAAI,oCAA+B,iBAAiB,OAAO,KAAK,IAAI,CAAC,IAC9F,IACE;AAAA,IACH,QAAQ,gBAAAN,KAACM,OAAA,EAAK,OAAM,SAAQ,uDAAyC,IAAU;AAAA,IAChF,gBAAAN,KAACM,OAAA,EAAK,OAAM,QAAO,yDAAqC;AAAA,KAC1D;AAEJ;;;AfhGQ,gBAAAE,MACA,QAAAC,aADA;AAvDR,SAAS,SAAS,OAAuC;AACvD,QAAM,CAAC,OAAO,QAAQ,IAAIC,UAAS,MAAM,SAAS,CAAC;AACnD,EAAAC,WAAU,MAAM;AACd,UAAM,WAAW,MAAM,SAAS,EAAE,GAAG,MAAM,SAAS,EAAE,CAAC;AACvD,UAAM,GAAG,UAAU,QAAQ;AAC3B,WAAO,MAAM;AACX,YAAM,IAAI,UAAU,QAAQ;AAAA,IAC9B;AAAA,EACF,GAAG,CAAC,KAAK,CAAC;AACV,SAAO;AACT;AAGA,IAAM,SAAyD;AAAA,EAC7D,WAAW,EAAE,KAAK,UAAK,OAAO,QAAQ;AAAA,EACtC,YAAY,EAAE,KAAK,UAAK,OAAO,SAAS;AAAA,EACxC,cAAc,EAAE,KAAK,UAAK,OAAO,MAAM;AAAA,EACvC,OAAO,EAAE,KAAK,UAAK,OAAO,MAAM;AAClC;AAYO,SAAS,IAAI;AAAA,EAClB;AAAA,EACA;AAAA,EACA,QAAAC;AACF,GAIG;AACD,QAAM,QAAQ,SAAS,KAAK;AAC5B,QAAM,EAAE,KAAK,IAAI,OAAO;AACxB,QAAM,CAAC,QAAQ,SAAS,IAAIF,UAAiB,MAAM;AAGnD,EAAAC,WAAU,MAAM;AACd,QAAI,MAAM,gBAAgB,YAAa,MAAK,GAAG,UAAU;AAAA,EAC3D,GAAG,CAAC,MAAM,aAAa,IAAI,CAAC;AAE5B,QAAM,IAAI,OAAO,MAAM,WAAW,KAAK,EAAE,KAAK,QAAK,OAAO,OAAO;AACjE,QAAM,QAAQ,MAAM,aAAa,WAAW,MAAM,UAAU,MAAM;AAClE,QAAM,cAAcC,QAAO,aAAa,EAAE,KAAK,IAAI;AAEnD,SACE,gBAAAH,MAACI,MAAA,EAAI,eAAc,UAAS,aAAY,SAAQ,UAAU,GACxD;AAAA,oBAAAJ,MAACI,MAAA,EAAI,gBAAe,iBAClB;AAAA,sBAAAL,KAACM,OAAA,EAAK,MAAI,MAAC,mBAAK;AAAA,MAChB,gBAAAL,MAACK,OAAA,EAAK,OAAM,QAAO;AAAA;AAAA,QAAE,SAAS;AAAA,SAAQ;AAAA,OACxC;AAAA,IAEA,gBAAAL,MAACI,MAAA,EACC;AAAA,sBAAAL,KAACM,OAAA,EAAK,OAAM,QAAO,oBAAM;AAAA,MACzB,gBAAAL,MAACK,OAAA,EAAK,OAAO,EAAE,OACZ;AAAA,UAAE;AAAA,QAAI;AAAA,QAAE,MAAM;AAAA,QACd;AAAA,SACH;AAAA,MACC,MAAM,KAAK,gBAAAL,MAACK,OAAA,EAAK,OAAM,QAAO;AAAA;AAAA,QAAI,MAAM,GAAG;AAAA,QAAK;AAAA,SAAC,IAAU;AAAA,OAC9D;AAAA,IACA,gBAAAN,KAACM,OAAA,EAAK,OAAM,QAAQ,uBAAY;AAAA,IAE/B,WAAW,SACV,gBAAAN;AAAA,MAAC;AAAA;AAAA,QACC;AAAA,QACA;AAAA,QACA,QAAM;AAAA,QACN,YAAY;AAAA,QACZ,QAAQ;AAAA;AAAA,IACV,IACE;AAAA,IACH,WAAW,aACV,gBAAAA;AAAA,MAAC;AAAA;AAAA,QACC,UAAU,KAAK,IAAI,YAAY;AAAA,QAC/B;AAAA,QACA,QAAM;AAAA,QACN,QAAQ,MAAM,UAAU,MAAM;AAAA;AAAA,IAChC,IACE;AAAA,IACH,WAAW,YACV,gBAAAA,KAAC,iBAAc,QAAQI,SAAQ,MAAY,QAAM,MAAC,QAAQ,MAAM,UAAU,MAAM,GAAG,IACjF;AAAA,IACH,WAAW,SACV,gBAAAJ;AAAA,MAAC;AAAA;AAAA,QACC;AAAA,QACA,QAAQI;AAAA,QACR;AAAA,QACA,QAAM;AAAA,QACN,QAAQ,MAAM,UAAU,MAAM;AAAA;AAAA,IAChC,IACE;AAAA,KACN;AAEJ;;;AgBrHA,SAAS,OAAAG,OAAK,QAAAC,QAAM,UAAAC,SAAQ,YAAAC,iBAAgB;AAC5C,SAAgB,aAAAC,YAAW,WAAAC,UAAS,YAAAC,iBAAgB;;;ACDpD,OAAOC,UAAQ;AACf,OAAOC,UAAQ;AACf,OAAOC,YAAU;AAGjB,IAAM,UAAUA,OAAK,KAAKD,KAAG,QAAQ,GAAG,yBAAyB,MAAM;AAQhE,SAAS,QAAQ,WAAmB,OAAyB;AAClE,QAAM,OAAOC,OAAK,KAAK,SAAS,GAAG,SAAS,MAAM;AAClD,MAAI;AACF,UAAM,OAAOF,KAAG,SAAS,IAAI,EAAE;AAC/B,UAAM,YAAY,KAAK,IAAI,MAAM,KAAK,IAAI;AAC1C,UAAM,KAAKA,KAAG,SAAS,MAAM,GAAG;AAChC,QAAI;AACF,YAAM,MAAM,OAAO,MAAM,SAAS;AAClC,MAAAA,KAAG,SAAS,IAAI,KAAK,GAAG,WAAW,OAAO,SAAS;AACnD,aAAO,IACJ,SAAS,OAAO,EAChB,MAAM,OAAO,EACb,OAAO,OAAO,EACd,MAAM,CAAC,KAAK;AAAA,IACjB,UAAE;AACA,MAAAA,KAAG,UAAU,EAAE;AAAA,IACjB;AAAA,EACF,QAAQ;AACN,WAAO,CAAC;AAAA,EACV;AACF;;;ADwCQ,gBAAAG,OACA,QAAAC,cADA;AAtDR,IAAM,WAAW;AAYV,SAAS,WAAW,EAAE,QAAAC,QAAO,GAAoC;AACtE,QAAM,EAAE,KAAK,IAAIC,QAAO;AACxB,QAAM,CAAC,QAAQ,SAAS,IAAIC,UAAwB,MAAM,cAAc,CAAC;AACzE,QAAM,CAAC,MAAM,OAAO,IAAIA,UAAmB,MAAM,QAAQ,MAAM,QAAQ,CAAC;AACxE,QAAM,CAAC,WAAW,YAAY,IAAIA,UAAS,KAAK;AAChD,QAAM,CAAC,QAAQ,SAAS,IAAIA,UAAwB,IAAI;AAIxD,EAAAC,WAAU,MAAM;AACd,UAAM,IAAI,YAAY,MAAM;AAC1B,gBAAU,cAAc,CAAC;AACzB,cAAQ,QAAQ,MAAM,QAAQ,CAAC;AAAA,IACjC,GAAG,GAAI;AACP,WAAO,MAAM,cAAc,CAAC;AAAA,EAC9B,GAAG,CAAC,CAAC;AAGL,QAAM,UAAUC,SAAQ,MAAM,eAAeJ,QAAO,aAAa,CAAC,GAAG,CAACA,OAAM,CAAC;AAC7E,QAAM,SAASI,SAAQ,MAAO,UAAU,SAAS,OAAO,IAAI,MAAO,CAAC,OAAO,CAAC;AAE5E,QAAM,aAAaA,SAAQ,MAAMJ,QAAO,aAAa,EAAE,SAAS,GAAG,CAACA,OAAM,CAAC;AAE3E,EAAAK,UAAS,CAAC,UAAU;AAClB,QAAI,UAAU,IAAK,MAAK;AAAA,aACf,UAAU,IAAK,cAAa,CAAAC,OAAK,CAACA,EAAC;AAAA,aACnC,UAAU,KAAK;AACtB,gBAAU,eAAe,IAAI,sBAAsB,gBAAgB;AACnE,gBAAU,cAAc,CAAC;AAAA,IAC3B,WAAW,UAAU,KAAK;AACxB,gBAAU,YAAY,IAAI,yEAA4B,aAAa;AACnE,gBAAU,cAAc,CAAC;AAAA,IAC3B,WAAW,UAAU,KAAK;AACxB,uBAAiB;AACjB,gBAAU,2EAAoB;AAC9B,gBAAU,cAAc,CAAC;AAAA,IAC3B;AAAA,EACF,CAAC;AAED,SACE,gBAAAP,OAACQ,OAAA,EAAI,eAAc,UAAS,aAAY,SAAQ,UAAU,GACxD;AAAA,oBAAAR,OAACQ,OAAA,EAAI,gBAAe,iBAClB;AAAA,sBAAAT,MAACU,QAAA,EAAK,MAAI,MAAC,6CAA0B;AAAA,MACrC,gBAAAT,OAACS,QAAA,EAAK,OAAM,QAAO;AAAA;AAAA,QAAE,SAAS;AAAA,SAAQ;AAAA,OACxC;AAAA,IAEA,gBAAAT,OAACQ,OAAA,EACC;AAAA,sBAAAT,MAACU,QAAA,EAAK,OAAM,QAAO,sBAAQ;AAAA,MAC3B,gBAAAV,MAACU,QAAA,EAAK,OAAO,OAAO,SAAS,UAAU,OACpC,iBAAO,SAAS,kBAAa,mBAChC;AAAA,MACA,gBAAAV,MAACU,QAAA,EAAK,OAAM,QAAQ,iBAAO,UAAU,eAAe,eAAc;AAAA,OACpE;AAAA,IACA,gBAAAT,OAACQ,OAAA,EACC;AAAA,sBAAAT,MAACU,QAAA,EAAK,OAAM,QAAO,sBAAQ;AAAA,MAC1B,OAAO,SACN,gBAAAV,MAACU,QAAA,EAAK,OAAM,SAAQ,oDAAqC,IAEzD,gBAAAT,OAACS,QAAA,EAAK,OAAM,UAAS;AAAA;AAAA,QAAwC,OAAO;AAAA,SAAK;AAAA,OAE7E;AAAA,IAEA,gBAAAV,MAACU,QAAA,EAAK,OAAM,QAAQ,mBAAQ;AAAA,IAE3B,UACC,gBAAAT,OAACQ,OAAA,EAAI,eAAc,UACjB;AAAA,sBAAAT,MAACU,QAAA,EAAK,OAAM,QAAO,2BAAa;AAAA,MAC/B,aAAa,SAAS,gBAAAV,MAACU,QAAA,EAAM,kBAAO,IAAU;AAAA,MAC/C,gBAAAT,OAACQ,OAAA,EACC;AAAA,wBAAAT,MAACU,QAAA,EAAK,OAAM,QAAO,kBAAI;AAAA,QACvB,gBAAAV,MAACU,QAAA,EAAK,OAAM,QAAQ,oBAAU,OAAO,GAAE;AAAA,SACzC;AAAA,MACC,aACC,gBAAAV,MAACU,QAAA,EAAK,OAAM,UAAS,sEAAmD,IACtE;AAAA,MACH,CAAC,YAAY,gBAAAV,MAACU,QAAA,EAAK,OAAM,QAAO,kCAAoB,IAAU;AAAA,OACjE,IAEA,gBAAAV,MAACU,QAAA,EAAK,OAAM,UAAS,2EAAmD;AAAA,IAG1E,gBAAAV,MAACU,QAAA,EAAK,OAAM,QAAQ,mBAAQ;AAAA,IAC5B,gBAAAV,MAACU,QAAA,EAAK,OAAM,QAAO,gCAAkB;AAAA,IACpC,KAAK,WAAW,IACf,gBAAAV,MAACU,QAAA,EAAK,OAAM,QAAO,oBAAC,IAEpB,KAAK,IAAI,CAAC,MAAM,MACd,gBAAAV,MAACU,QAAA,EAA0B,OAAM,QAC9B,kBADQ,GAAG,CAAC,IAAI,IAAI,EAEvB,CACD;AAAA,IAGH,gBAAAV,MAACU,QAAA,EAAK,OAAM,QAAQ,mBAAQ;AAAA,IAC3B,SAAS,gBAAAV,MAACU,QAAA,EAAK,OAAM,SAAS,kBAAO,IAAU;AAAA,IAChD,gBAAAT,OAACS,QAAA,EAAK,OAAM,QAAO;AAAA;AAAA,MACmB,YAAY,SAAS;AAAA,MAAO;AAAA,OAClE;AAAA,KACF;AAEJ;;;A1D9EmC,gBAAAC,aAAA;AAzBnC,IAAM,iBAAiB,WAAW;AAIlC,kBAAkB,KAAK;AAEvB,IAAM,SAAS,IAAI,oBAAoB;AACvC,IAAM,OAAO,QAAQ,KAAK,MAAM,CAAC;AAEjC,IAAI,KAAK,CAAC,MAAM,WAAW;AAEzB,UAAQ,KAAK,kBAAkB,KAAK,CAAC,GAAG,MAAM,CAAC;AACjD;AAMA,IAAI,QAAQ,OAAO,OAAO;AACxB,UAAQ,OAAO,MAAM,aAAa;AAClC,UAAQ,GAAG,QAAQ,MAAM,QAAQ,OAAO,MAAM,aAAa,CAAC;AAC9D;AAEA,IAAI,gBAAgB,GAAG;AAErB,QAAM,EAAE,cAAc,IAAI,OAAO,gBAAAA,MAAC,cAAW,QAAgB,CAAE;AAI/D,UAAQ,GAAG,WAAW,MAAM,QAAQ,KAAK,CAAC,CAAC;AAC3C,OAAK,cAAc,EAAE,QAAQ,MAAM,QAAQ,KAAK,CAAC,CAAC;AACpD,OAAO;AAEL,QAAM,QAAQ,IAAI,cAAc;AAChC,QAAM,SAASC,OAAK,KAAKC,KAAG,QAAQ,GAAG,yBAAyB,MAAM;AAEtE,QAAM,cAAc,OAAO,aAAa,EAAE,KAAK,IAAI;AACnD,aAAW,KAAK,eAAgB,OAAM,IAAI,uBAAuB,CAAC,EAAE;AAEpE,QAAM,IAAI,wBAAwB,WAAW,EAAE;AAC/C,QAAM,IAAI,kCAA6B;AAEvC,QAAM,OAAO,gBAAgB;AAAA,IAC3B;AAAA,IACA;AAAA,IACA;AAAA,IACA,eAAe,SAAO,MAAM,MAAM,GAAG;AAAA,EACvC,CAAC;AAID,QAAM,MAAM,EAAE,MAAM,gBAAgB,UAAU,KAAK,IAAI,YAAY,EAAE,CAAC;AAGtE,OAAK,GAAG,UAAU;AAElB,QAAM,EAAE,cAAc,IAAI,OAAO,gBAAAF,MAAC,OAAI,OAAc,MAAY,QAAgB,CAAE;AAIlF,QAAM,SAAS,CAAC,SAAuB;AACrC,SAAK,QAAQ;AACb,YAAQ,KAAK,IAAI;AAAA,EACnB;AACA,UAAQ,GAAG,WAAW,MAAM,OAAO,CAAC,CAAC;AAErC,OAAK,cAAc,EAChB,KAAK,MAAM,OAAO,CAAC,CAAC,EACpB,MAAM,MAAM,OAAO,CAAC,CAAC;AAC1B;","names":["os","path","fs","os","path","crypto","path","$r","$g","$b","$a","channels","toCss","r","g","b","a","toPaddedHex","toRgba","toColor","color","blend","bg","fg","fgR","fgG","fgB","bgR","bgG","bgB","css","rgba","isOpaque","ensureContrastRatio","ratio","result","opaque","rgbaColor","opacity","multiplyOpacity","factor","toColorRGB","$ctx","$litmusColor","canvas","ctx","rgbaMatch","rgb","relativeLuminance","relativeLuminance2","rs","gs","bs","rr","rg","rb","bgRgba","fgRgba","bgL","fgL","contrastRatio","resultA","reduceLuminance","resultARatio","resultB","increaseLuminance","resultBRatio","cr","toChannels","value","c","s","l1","l2","DEFAULT_ANSI_COLORS","colors","v","i","constrain","low","high","escapeHTMLChar","BaseSerializeHandler","_buffer","range","excludeFinalCursorPosition","cell1","cell2","oldCell","startRow","endRow","startColumn","endColumn","row","line","startLineColumn","endLineColumn","col","cell","isLastRow","rows","equalFg","equalBg","equalFlags","StringSerializeHandler","buffer","_terminal","start","end","rowSeparator","currentLine","nextLine","thisRowLastChar","thisRowLastSecondChar","nextRowFirstChar","isNextRowFirstCharDoubleWidth","isValid","sgrSeq","fgChanged","bgChanged","flagsChanged","isEmptyCell","rowEnd","content","realCursorRow","realCursorCol","cursorMoved","moveRight","offset","curAttrData","SerializeAddon","terminal","scrollback","maxRows","correctRows","options","handler","HTMLSerializeHandler","onlySelection","selection","modes","alternativeScreenContent","_options","target","targetLength","padString","foreground","background","globalStyleDefinitions","isFg","x","fgHexColor","bgHexColor","styleDefinitions","log","log","m","log","m","os","path","log","path","b","path","os","p","os","p","fs","path","crypto","v","m","sessionMgr","cwd","b","os","path","fs","args","k","args","log","args","p","fs","path","p","config","log","DATA_DIR","path","os","fs","config","identity","delay","log","log","os","path","fs","path","path","os","fs","os","path","fs","os","path","fs","os","path","SESSION_TOKEN_FILE","path","os","fs","fs","os","path","spawnSync","fs","os","path","p","exec","args","spawnSync","fs","exec","config","exec","m","path","os","fs","m","Box","Text","useEffect","useState","os","path","os","config","path","Box","Text","useInput","useState","Box","Text","jsx","jsxs","path","Box","Text","useEffect","useState","Box","Text","jsx","jsxs","Box","Text","SelectInput","jsx","jsxs","useInput","useState","os","m","Fragment","jsx","jsxs","useState","useEffect","k","path","v","Box","Text","jsx","jsxs","m","Box","Text","useState","useInput","Box","Text","useInput","useEffect","useMemo","useState","jsx","jsxs","Box","Text","useState","useEffect","exec","useInput","v","m","useMemo","Box","Text","useState","jsx","jsxs","config","useState","k","Box","Text","v","jsx","jsxs","useState","useEffect","config","Box","Text","Box","Text","useApp","useInput","useEffect","useMemo","useState","fs","os","path","jsx","jsxs","config","useApp","useState","useEffect","useMemo","useInput","v","Box","Text","jsx","path","os"]}
|