diff --git a/.gitignore b/.gitignore index 20a823e..8ba5f96 100644 --- a/.gitignore +++ b/.gitignore @@ -15,9 +15,11 @@ GEMINI.md experiments/ logs/ nul +.chromium/ +logs/ # Session & Runtime Data -hypeai-sessions.json +.hypeai-sessions.json audit-logs/ # Node.js auto-download (for pkg builds) diff --git a/README.md b/README.md index 47f8f8f..0920162 100644 --- a/README.md +++ b/README.md @@ -45,21 +45,35 @@ Discord 메시지로 Claude Code, Gemini CLI, OpenCode를 원격으로 조작 ### 💬 인터랙티브 응답 -Claude가 물어보면 Discord 버튼으로 바로 응답 +Claude가 물어보면 Discord 버튼/드롭다운으로 바로 응답 -### 🔒 보안 설계 -화이트리스트 기반 접근 제어 + 위험 명령어 차단 +### 🔒 다층 보안 설계 +화이트리스트 + 위험 명령어 차단 + 프롬프트 인젝션 탐지 + 출력 자동 검열 ### 📦 원클릭 실행 -exe 파일 더블클릭으로 바로 시작 +exe 파일 더블클릭으로 바로 시작 (Node.js 자동 다운로드) + + + + + + +### 🔀 멀티세션 관리 +여러 AI 세션을 동시에 이름별로 생성/전환하며 운영 + + + + +### 📊 Git Diff 시각화 +git diff를 PNG 이미지로 렌더링하여 Discord에서 바로 확인 @@ -84,20 +98,54 @@ Discord에서 `!ask 코드 리뷰해줘` 입력! ## 📋 명령어 +### AI CLI + +| 명령어 | 별칭 | 설명 | +|:-------|:-----|:-----| +| `!ask [session] <메시지>` | `!a` | AI에게 메시지 전송 | + +> 💡 **멀티세션**: `!a work "코드 분석해줘"` 처럼 세션 이름을 지정하면 해당 세션으로 메시지가 전달됩니다. + +### 세션 관리 + +| 명령어 | 별칭 | 설명 | +|:-------|:-----|:-----| +| `!session create [cli]` | `!s c` | 새 세션 생성 (CLI 도구 지정 가능) | +| `!session list` | `!s ls` | 모든 세션 목록 + 상태 | +| `!session switch ` | `!s sw` | 활성 세션 전환 | +| `!session info [name]` | `!s` | 세션 상세 정보 | +| `!session new [name]` | `!s new` | 세션 대화 초기화 | +| `!session kill [name]` | `!s stop` | 진행 중인 AI 프로세스 중단 | +| `!session delete ` | `!s rm` | 세션 삭제 | +| `!session stats [name]` | `!s stat` | 토큰 사용량 통계 | +| `!session history [name] [count]` | `!s h` | 대화 기록 조회 | + +### 작업 큐 + | 명령어 | 별칭 | 설명 | |:-------|:-----|:-----| -| `!ask <메시지>` | `!a` | AI에게 메시지 전송 | -| `!session info` | `!s` | 현재 세션 상태 확인 | -| `!session new` | `!s new` | 새 대화 시작 | -| `!session kill` | `!s stop` | 진행 중인 AI 중단 | | `!task add <작업>` | `!t a` | 작업 예약 추가 | | `!task list` | `!t ls` | 예약된 작업 목록 | | `!task run` | `!t r` | 예약된 작업 순차 실행 | | `!task remove <번호>` | `!t rm` | 작업 삭제 | | `!task clear` | `!t c` | 대기 중인 작업 전체 삭제 | | `!task stop` | `!t s` | 실행 중인 작업 중단 | + +### Git 도구 + +| 명령어 | 별칭 | 설명 | +|:-------|:-----|:-----| +| `!diff` | `!d`, `!changes` | git diff를 PNG 이미지로 시각화 | +| `!diff --staged` | `!d -s` | 스테이지된 변경사항만 표시 | +| `!diff ` | | 특정 파일의 diff 표시 | +| `!diff HEAD~1` | | 특정 커밋과 비교 | + +### 시스템 + +| 명령어 | 별칭 | 설명 | +|:-------|:-----|:-----| | `!exec <명령어>` | `!run`, `!cmd` | CMD 명령어 실행 | -| `!status` | `!sysinfo` | 시스템 정보 | +| `!status` | `!sysinfo` | 시스템 정보 (CPU, 메모리, 업타임) | | `!myid` | `!id` | Discord ID 확인 | | `!help` | | 도움말 | @@ -108,10 +156,10 @@ Discord에서 `!ask 코드 리뷰해줘` 입력! | 도구 | 연동 방식 | 인터랙티브 | 세션 유지 | |:-----|:---------|:----------:|:---------:| | **Claude Code** | Agent SDK | ✅ | ✅ | -| **Gemini CLI** | subprocess | ❌ | ❌ | +| **Gemini CLI** | Stream JSON | ❌ | ❌ | | **OpenCode** | subprocess | ❌ | ❌ | -> **Claude Code**는 Agent SDK를 통해 직접 통신합니다. AI가 선택지를 물어보면 Discord 버튼으로 응답할 수 있어요! +> **Claude Code**는 Agent SDK를 통해 직접 통신합니다. AI가 선택지를 물어보면 Discord 버튼/드롭다운으로 응답할 수 있어요! (4개 이하 → 버튼, 5개 이상 → 드롭다운 메뉴) --- @@ -121,7 +169,7 @@ Discord에서 `!ask 코드 리뷰해줘` 입력! 방법 1: exe 파일 (권장) ### 사전 요구 -- **Node.js v18+** (Claude Code 사용 시 필수) +- **Node.js v18+** (없으면 자동 다운로드 시도) - Claude Code 인증 (`claude login` 또는 `ANTHROPIC_API_KEY`) ### 설치 @@ -178,8 +226,8 @@ npx tsx src/bot.ts | `ALLOWED_USER_IDS` | ✅ | — | 허용할 유저 ID (17-19자리 숫자, 쉼표 구분) | | `ANTHROPIC_API_KEY` | ❌ | — | API 키 (`claude login` 시 불필요) | | `COMMAND_PREFIX` | ❌ | `!` | 명령어 접두사 | -| `COMMAND_TIMEOUT` | ❌ | `30` | CMD 타임아웃 (초) | -| `AI_CLI_TIMEOUT` | ❌ | `300` | AI 타임아웃 (초) | +| `COMMAND_TIMEOUT` | ❌ | `30` | CMD 타임아웃 (초, 범위: 5~120) | +| `AI_CLI_TIMEOUT` | ❌ | `300` | AI 타임아웃 (초, 범위: 30~1800) | > ⚠️ **보안 주의**: Discord ID는 17-19자리 **숫자**입니다 (유저네임 ❌). ID는 외부에 노출하지 마세요! @@ -191,6 +239,26 @@ ALLOWED_USER_IDS=111111111111111111,222222222222222222 --- +## 🔒 보안 + +
+보안 기능 상세 + +| 기능 | 설명 | +|:-----|:-----| +| **화이트리스트 접근 제어** | `ALLOWED_USER_IDS`에 등록된 유저만 봇 사용 가능 | +| **위험 명령어 차단** | `format`, `shutdown`, `del /s`, `powershell` 등 위험 명령어/실행파일 차단 | +| **프롬프트 인젝션 탐지** | 역할 변경, 시스템 프롬프트 주입, 탈옥 시도 등 의심 패턴 경고 | +| **출력 자동 검열** | API 키, 토큰, 사용자 경로 등 민감 정보 자동 마스킹 | +| **민감 파일 필터링** | diff에서 `.env`, `credentials`, `secrets` 등 민감 파일 자동 제외 | +| **보안 컨텍스트 래핑** | AI에게 작업 디렉토리 제한 규칙 자동 주입 | + +자세한 보안 정책은 [SECURITY.md](SECURITY.md)를 참고하세요. + +
+ +--- + ## 🔧 Discord 봇 만들기
@@ -260,6 +328,17 @@ dist/ --- +## 📚 문서 + +| 문서 | 설명 | +|:-----|:-----| +| [GUIDE.md](GUIDE.md) | 상세 사용자/개발자 가이드 | +| [SECURITY.md](SECURITY.md) | 보안 정책 | +| [CHANGELOG.md](CHANGELOG.md) | 버전별 변경 이력 | +| [CONTRIBUTING.md](CONTRIBUTING.md) | 기여 가이드 | + +--- + ## 🤝 기여하기 기여를 환영합니다! [CONTRIBUTING.md](CONTRIBUTING.md)를 확인해주세요. diff --git a/README_EN.md b/README_EN.md index 02dc6cd..c30f45a 100644 --- a/README_EN.md +++ b/README_EN.md @@ -35,21 +35,35 @@ Control Claude Code, Gemini CLI, OpenCode remotely via Discord messages ### 💬 Interactive Responses -When Claude asks questions, respond instantly with Discord buttons +When Claude asks questions, respond instantly with Discord buttons/dropdowns -### 🔒 Security First -Whitelist-based access control + dangerous command blocking +### 🔒 Multi-Layer Security +Whitelist + command blocking + prompt injection detection + output sanitization ### 📦 One-Click Launch -Just double-click the exe file to start +Just double-click the exe file to start (auto-downloads Node.js if needed) + + + + + + +### 🔀 Multi-Session Management +Create, switch, and manage multiple named AI sessions simultaneously + + + + +### 📊 Git Diff Visualization +Render git diffs as PNG images viewable directly in Discord @@ -74,20 +88,54 @@ Type `!ask review my code` in Discord! ## 📋 Commands +### AI CLI + +| Command | Alias | Description | +|:--------|:------|:------------| +| `!ask [session] ` | `!a` | Send message to AI | + +> 💡 **Multi-session**: Use `!a work "analyze this code"` to send a message to a specific named session. + +### Session Management + +| Command | Alias | Description | +|:--------|:------|:------------| +| `!session create [cli]` | `!s c` | Create a new session (optionally specify CLI tool) | +| `!session list` | `!s ls` | List all sessions + status | +| `!session switch ` | `!s sw` | Switch active session | +| `!session info [name]` | `!s` | Show session details | +| `!session new [name]` | `!s new` | Reset session conversation | +| `!session kill [name]` | `!s stop` | Kill running AI process | +| `!session delete ` | `!s rm` | Delete a session | +| `!session stats [name]` | `!s stat` | Show token usage statistics | +| `!session history [name] [count]` | `!s h` | View conversation history | + +### Task Queue + | Command | Alias | Description | |:--------|:------|:------------| -| `!ask ` | `!a` | Send message to AI | -| `!session info` | `!s` | Check session status | -| `!session new` | `!s new` | Start new conversation | -| `!session kill` | `!s stop` | Stop running AI | | `!task add ` | `!t a` | Add a scheduled task | | `!task list` | `!t ls` | List scheduled tasks | | `!task run` | `!t r` | Run all pending tasks sequentially | | `!task remove ` | `!t rm` | Remove a task | | `!task clear` | `!t c` | Clear all pending tasks | | `!task stop` | `!t s` | Stop running tasks | + +### Git Tools + +| Command | Alias | Description | +|:--------|:------|:------------| +| `!diff` | `!d`, `!changes` | Visualize git diff as PNG image | +| `!diff --staged` | `!d -s` | Show staged changes only | +| `!diff ` | | Show diff for a specific file | +| `!diff HEAD~1` | | Compare with a specific commit | + +### System + +| Command | Alias | Description | +|:--------|:------|:------------| | `!exec ` | `!run`, `!cmd` | Execute CMD command | -| `!status` | `!sysinfo` | System info | +| `!status` | `!sysinfo` | System info (CPU, memory, uptime) | | `!myid` | `!id` | Check Discord ID | | `!help` | | Show help | @@ -98,10 +146,10 @@ Type `!ask review my code` in Discord! | Tool | Integration | Interactive | Session | |:-----|:-----------|:-----------:|:-------:| | **Claude Code** | Agent SDK | ✅ | ✅ | -| **Gemini CLI** | subprocess | ❌ | ❌ | +| **Gemini CLI** | Stream JSON | ❌ | ❌ | | **OpenCode** | subprocess | ❌ | ❌ | -> **Claude Code** communicates directly via Agent SDK. When AI asks for choices, you can respond with Discord buttons! +> **Claude Code** communicates directly via Agent SDK. When AI asks for choices, you can respond with Discord buttons/dropdowns! (4 or fewer options → buttons, 5+ → dropdown menu) --- @@ -111,7 +159,7 @@ Type `!ask review my code` in Discord! Method 1: exe File (Recommended) ### Prerequisites -- **Node.js v18+** (required for Claude Code) +- **Node.js v18+** (auto-download attempted if missing) - Claude Code auth (`claude login` or `ANTHROPIC_API_KEY`) ### Install @@ -168,8 +216,8 @@ npx tsx src/bot.ts | `ALLOWED_USER_IDS` | ✅ | — | Allowed user IDs (comma-separated) | | `ANTHROPIC_API_KEY` | ❌ | — | API key (not needed with `claude login`) | | `COMMAND_PREFIX` | ❌ | `!` | Command prefix | -| `COMMAND_TIMEOUT` | ❌ | `30` | CMD timeout (seconds) | -| `AI_CLI_TIMEOUT` | ❌ | `300` | AI timeout (seconds) | +| `COMMAND_TIMEOUT` | ❌ | `30` | CMD timeout (seconds, range: 5–120) | +| `AI_CLI_TIMEOUT` | ❌ | `300` | AI timeout (seconds, range: 30–1800) | ### Multiple Users @@ -179,6 +227,26 @@ ALLOWED_USER_IDS=111111111111111111,222222222222222222 --- +## 🔒 Security + +
+Security Features + +| Feature | Description | +|:--------|:------------| +| **Whitelist Access Control** | Only users registered in `ALLOWED_USER_IDS` can use the bot | +| **Dangerous Command Blocking** | Blocks `format`, `shutdown`, `del /s`, `powershell`, and other dangerous commands/executables | +| **Prompt Injection Detection** | Warns on suspicious patterns: role reassignment, system prompt injection, jailbreak attempts | +| **Output Sanitization** | Auto-redacts API keys, tokens, user paths, and other sensitive data | +| **Sensitive File Filtering** | Automatically excludes `.env`, `credentials`, `secrets`, etc. from diffs | +| **Security Context Wrapping** | Injects working directory restriction rules into AI tools | + +See [SECURITY.md](SECURITY.md) for detailed security policies. + +
+ +--- + ## 🔧 Creating a Discord Bot
@@ -248,6 +316,17 @@ dist/ --- +## 📚 Documentation + +| Document | Description | +|:---------|:------------| +| [GUIDE.md](GUIDE.md) | Detailed user & developer guide | +| [SECURITY.md](SECURITY.md) | Security policies | +| [CHANGELOG.md](CHANGELOG.md) | Version history | +| [CONTRIBUTING.md](CONTRIBUTING.md) | Contribution guide | + +--- + ## 🤝 Contributing Contributions are welcome! Please check [CONTRIBUTING.md](CONTRIBUTING.md). diff --git a/package-lock.json b/package-lock.json index c06d11d..55231e9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,10 +10,13 @@ "license": "MIT", "dependencies": { "@anthropic-ai/claude-agent-sdk": "^0.1.1", + "diff2html": "^3.4.56", "discord.js": "^14.16.3", - "dotenv": "^16.4.7" + "dotenv": "^16.4.7", + "puppeteer": "^24.37.2" }, "devDependencies": { + "@types/diff2html": "^0.0.5", "@types/node": "^22.10.0", "esbuild": "^0.27.3", "pkg": "^5.8.1", @@ -44,6 +47,26 @@ "zod": "^3.25.0 || ^4.0.0" } }, + "node_modules/@babel/code-frame": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/code-frame/node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, "node_modules/@babel/generator": { "version": "7.18.2", "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.18.2.tgz", @@ -73,7 +96,6 @@ "version": "7.28.5", "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", - "dev": true, "license": "MIT", "engines": { "node": ">=6.9.0" @@ -1041,6 +1063,105 @@ "node": ">= 8" } }, + "node_modules/@profoundlogic/hogan": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@profoundlogic/hogan/-/hogan-3.0.4.tgz", + "integrity": "sha512-pmNVGuooS30Mm7YbZd5T7E5zYVO6D5Ct91sn4T39mUvMUc3sCGridcnhAufL1/Bz2QzAtzEn0agNrdk3+5yWzw==", + "license": "Apache-2.0", + "dependencies": { + "nopt": "1.0.10" + }, + "bin": { + "hulk": "bin/hulk" + } + }, + "node_modules/@puppeteer/browsers": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-2.12.0.tgz", + "integrity": "sha512-Xuq42yxcQJ54ti8ZHNzF5snFvtpgXzNToJ1bXUGQRaiO8t+B6UM8sTUJfvV+AJnqtkJU/7hdy6nbKyA12aHtRw==", + "license": "Apache-2.0", + "dependencies": { + "debug": "^4.4.3", + "extract-zip": "^2.0.1", + "progress": "^2.0.3", + "proxy-agent": "^6.5.0", + "semver": "^7.7.3", + "tar-fs": "^3.1.1", + "yargs": "^17.7.2" + }, + "bin": { + "browsers": "lib/cjs/main-cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@puppeteer/browsers/node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@puppeteer/browsers/node_modules/tar-fs": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.1.1.tgz", + "integrity": "sha512-LZA0oaPOc2fVo82Txf3gw+AkEd38szODlptMYejQUhndHMLQ9M059uXR+AfS7DNo0NpINvSqDsvyaCrBVkptWg==", + "license": "MIT", + "dependencies": { + "pump": "^3.0.0", + "tar-stream": "^3.1.5" + }, + "optionalDependencies": { + "bare-fs": "^4.0.1", + "bare-path": "^3.0.0" + } + }, + "node_modules/@puppeteer/browsers/node_modules/tar-stream": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.7.tgz", + "integrity": "sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==", + "license": "MIT", + "dependencies": { + "b4a": "^1.6.4", + "fast-fifo": "^1.2.0", + "streamx": "^2.15.0" + } + }, + "node_modules/@puppeteer/browsers/node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@puppeteer/browsers/node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, "node_modules/@rollup/rollup-android-arm-eabi": { "version": "4.57.1", "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.57.1.tgz", @@ -1424,6 +1545,12 @@ "npm": ">=7.0.0" } }, + "node_modules/@tootallnate/quickjs-emscripten": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@tootallnate/quickjs-emscripten/-/quickjs-emscripten-0.23.0.tgz", + "integrity": "sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==", + "license": "MIT" + }, "node_modules/@types/chai": { "version": "5.2.3", "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", @@ -1442,6 +1569,13 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/diff2html": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/@types/diff2html/-/diff2html-0.0.5.tgz", + "integrity": "sha512-ZNANtIL5cZ/V9eDnH0P7qERxPchSSM1X6wUuOSioC+aWQE6uie/LVMO3KClOmgxLCu1fD77jy8sanYPP8wH7Yw==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/estree": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", @@ -1467,6 +1601,16 @@ "@types/node": "*" } }, + "node_modules/@types/yauzl": { + "version": "2.10.3", + "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz", + "integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==", + "license": "MIT", + "optional": true, + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@vitest/expect": { "version": "3.2.4", "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.4.tgz", @@ -1592,6 +1736,12 @@ "npm": ">=7.0.0" } }, + "node_modules/abbrev": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", + "license": "ISC" + }, "node_modules/agent-base": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", @@ -1609,7 +1759,6 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -1619,7 +1768,6 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, "license": "MIT", "dependencies": { "color-convert": "^2.0.1" @@ -1631,6 +1779,12 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "license": "Python-2.0" + }, "node_modules/array-union": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", @@ -1651,6 +1805,18 @@ "node": ">=12" } }, + "node_modules/ast-types": { + "version": "0.13.4", + "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.13.4.tgz", + "integrity": "sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.1" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/at-least-node": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz", @@ -1661,6 +1827,111 @@ "node": ">= 4.0.0" } }, + "node_modules/b4a": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.7.3.tgz", + "integrity": "sha512-5Q2mfq2WfGuFp3uS//0s6baOJLMoVduPYVeNmDYxu5OUA1/cBfvr2RIS7vi62LdNj/urk1hfmj867I3qt6uZ7Q==", + "license": "Apache-2.0", + "peerDependencies": { + "react-native-b4a": "*" + }, + "peerDependenciesMeta": { + "react-native-b4a": { + "optional": true + } + } + }, + "node_modules/bare-events": { + "version": "2.8.2", + "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.8.2.tgz", + "integrity": "sha512-riJjyv1/mHLIPX4RwiK+oW9/4c3TEUeORHKefKAKnZ5kyslbN+HXowtbaVEqt4IMUB7OXlfixcs6gsFeo/jhiQ==", + "license": "Apache-2.0", + "peerDependencies": { + "bare-abort-controller": "*" + }, + "peerDependenciesMeta": { + "bare-abort-controller": { + "optional": true + } + } + }, + "node_modules/bare-fs": { + "version": "4.5.3", + "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.5.3.tgz", + "integrity": "sha512-9+kwVx8QYvt3hPWnmb19tPnh38c6Nihz8Lx3t0g9+4GoIf3/fTgYwM4Z6NxgI+B9elLQA7mLE9PpqcWtOMRDiQ==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "bare-events": "^2.5.4", + "bare-path": "^3.0.0", + "bare-stream": "^2.6.4", + "bare-url": "^2.2.2", + "fast-fifo": "^1.3.2" + }, + "engines": { + "bare": ">=1.16.0" + }, + "peerDependencies": { + "bare-buffer": "*" + }, + "peerDependenciesMeta": { + "bare-buffer": { + "optional": true + } + } + }, + "node_modules/bare-os": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/bare-os/-/bare-os-3.6.2.tgz", + "integrity": "sha512-T+V1+1srU2qYNBmJCXZkUY5vQ0B4FSlL3QDROnKQYOqeiQR8UbjNHlPa+TIbM4cuidiN9GaTaOZgSEgsvPbh5A==", + "license": "Apache-2.0", + "optional": true, + "engines": { + "bare": ">=1.14.0" + } + }, + "node_modules/bare-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/bare-path/-/bare-path-3.0.0.tgz", + "integrity": "sha512-tyfW2cQcB5NN8Saijrhqn0Zh7AnFNsnczRcuWODH0eYAXBsJ5gVxAUuNr7tsHSC6IZ77cA0SitzT+s47kot8Mw==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "bare-os": "^3.0.1" + } + }, + "node_modules/bare-stream": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.7.0.tgz", + "integrity": "sha512-oyXQNicV1y8nc2aKffH+BUHFRXmx6VrPzlnaEvMhram0nPBrKcEdcyBg5r08D0i8VxngHFAiVyn1QKXpSG0B8A==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "streamx": "^2.21.0" + }, + "peerDependencies": { + "bare-buffer": "*", + "bare-events": "*" + }, + "peerDependenciesMeta": { + "bare-buffer": { + "optional": true + }, + "bare-events": { + "optional": true + } + } + }, + "node_modules/bare-url": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/bare-url/-/bare-url-2.3.2.tgz", + "integrity": "sha512-ZMq4gd9ngV5aTMa5p9+UfY0b3skwhHELaDkhEHetMdX0LRkW9kzaym4oo/Eh+Ghm0CCDuMTsRIGM/ytUc1ZYmw==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "bare-path": "^3.0.0" + } + }, "node_modules/base64-js": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", @@ -1682,6 +1953,15 @@ ], "license": "MIT" }, + "node_modules/basic-ftp": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.1.0.tgz", + "integrity": "sha512-RkaJzeJKDbaDWTIPiJwubyljaEPwpVWkm9Rt5h9Nd6h7tEXTJ3VB4qxdZBioV7JO5yLUaOKwz7vDOzlncUsegw==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/bl": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", @@ -1747,6 +2027,15 @@ "ieee754": "^1.1.13" } }, + "node_modules/buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", + "license": "MIT", + "engines": { + "node": "*" + } + }, "node_modules/cac": { "version": "6.7.14", "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", @@ -1757,6 +2046,15 @@ "node": ">=8" } }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/chai": { "version": "5.3.3", "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", @@ -1808,6 +2106,28 @@ "dev": true, "license": "ISC" }, + "node_modules/chromium-bidi": { + "version": "13.1.1", + "resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-13.1.1.tgz", + "integrity": "sha512-zB9MpoPd7VJwjowQqiW3FKOvQwffFMjQ8Iejp5ZW+sJaKLRhZX1sTxzl3Zt22TDB4zP0OOqs8lRoY7eAW5geyQ==", + "license": "Apache-2.0", + "dependencies": { + "mitt": "^3.0.1", + "zod": "^3.24.1" + }, + "peerDependencies": { + "devtools-protocol": "*" + } + }, + "node_modules/chromium-bidi/node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, "node_modules/cliui": { "version": "7.0.4", "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", @@ -1824,7 +2144,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, "license": "MIT", "dependencies": { "color-name": "~1.1.4" @@ -1837,7 +2156,6 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, "license": "MIT" }, "node_modules/core-util-is": { @@ -1847,11 +2165,45 @@ "dev": true, "license": "MIT" }, + "node_modules/cosmiconfig": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.0.tgz", + "integrity": "sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg==", + "license": "MIT", + "dependencies": { + "env-paths": "^2.2.1", + "import-fresh": "^3.3.0", + "js-yaml": "^4.1.0", + "parse-json": "^5.2.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/d-fischer" + }, + "peerDependencies": { + "typescript": ">=4.9.5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/data-uri-to-buffer": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-6.0.2.tgz", + "integrity": "sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw==", + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, "node_modules/debug": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "dev": true, "license": "MIT", "dependencies": { "ms": "^2.1.3" @@ -1901,6 +2253,20 @@ "node": ">=4.0.0" } }, + "node_modules/degenerator": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/degenerator/-/degenerator-5.0.1.tgz", + "integrity": "sha512-TllpMR/t0M5sqCXfj85i4XaAzxmS5tVA16dqvdkMwGmzI+dXLXnw3J+3Vdv7VKw+ThlTMboK6i9rnZ6Nntj5CQ==", + "license": "MIT", + "dependencies": { + "ast-types": "^0.13.4", + "escodegen": "^2.1.0", + "esprima": "^4.0.1" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/detect-libc": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", @@ -1911,6 +2277,37 @@ "node": ">=8" } }, + "node_modules/devtools-protocol": { + "version": "0.0.1566079", + "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1566079.tgz", + "integrity": "sha512-MJfAEA1UfVhSs7fbSQOG4czavUp1ajfg6prlAN0+cmfa2zNjaIbvq8VneP7do1WAQQIvgNJWSMeP6UyI90gIlQ==", + "license": "BSD-3-Clause" + }, + "node_modules/diff": { + "version": "8.0.3", + "resolved": "https://registry.npmjs.org/diff/-/diff-8.0.3.tgz", + "integrity": "sha512-qejHi7bcSD4hQAZE0tNAawRK1ZtafHDmMTMkrrIGgSLl7hTnQHmKCeB45xAcbfTqK2zowkM3j3bHt/4b/ARbYQ==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/diff2html": { + "version": "3.4.56", + "resolved": "https://registry.npmjs.org/diff2html/-/diff2html-3.4.56.tgz", + "integrity": "sha512-u9gfn+BlbHcyO7vItCIC4z49LJDUt31tODzOfAuJ5R1E7IdlRL6KjugcB9zOpejD+XiR+dDZbsnHSQ3g6A/u8A==", + "license": "MIT", + "dependencies": { + "@profoundlogic/hogan": "^3.0.4", + "diff": "^8.0.3" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "highlight.js": "11.11.1" + } + }, "node_modules/dir-glob": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", @@ -1976,19 +2373,35 @@ "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true, "license": "MIT" }, "node_modules/end-of-stream": { "version": "1.4.5", "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", - "dev": true, "license": "MIT", "dependencies": { "once": "^1.4.0" } }, + "node_modules/env-paths": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/error-ex": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", + "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, "node_modules/es-module-lexer": { "version": "1.7.0", "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", @@ -2042,12 +2455,54 @@ "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", - "dev": true, "license": "MIT", "engines": { "node": ">=6" } }, + "node_modules/escodegen": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.1.0.tgz", + "integrity": "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==", + "license": "BSD-2-Clause", + "dependencies": { + "esprima": "^4.0.1", + "estraverse": "^5.2.0", + "esutils": "^2.0.2" + }, + "bin": { + "escodegen": "bin/escodegen.js", + "esgenerate": "bin/esgenerate.js" + }, + "engines": { + "node": ">=6.0" + }, + "optionalDependencies": { + "source-map": "~0.6.1" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, "node_modules/estree-walker": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", @@ -2058,7 +2513,25 @@ "@types/estree": "^1.0.0" } }, - "node_modules/expand-template": { + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/events-universal": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/events-universal/-/events-universal-1.0.1.tgz", + "integrity": "sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==", + "license": "Apache-2.0", + "dependencies": { + "bare-events": "^2.7.0" + } + }, + "node_modules/expand-template": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", @@ -2078,12 +2551,38 @@ "node": ">=12.0.0" } }, + "node_modules/extract-zip": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", + "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", + "license": "BSD-2-Clause", + "dependencies": { + "debug": "^4.1.1", + "get-stream": "^5.1.0", + "yauzl": "^2.10.0" + }, + "bin": { + "extract-zip": "cli.js" + }, + "engines": { + "node": ">= 10.17.0" + }, + "optionalDependencies": { + "@types/yauzl": "^2.9.1" + } + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", "license": "MIT" }, + "node_modules/fast-fifo": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz", + "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==", + "license": "MIT" + }, "node_modules/fast-glob": { "version": "3.3.3", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", @@ -2111,6 +2610,15 @@ "reusify": "^1.0.4" } }, + "node_modules/fd-slicer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", + "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", + "license": "MIT", + "dependencies": { + "pend": "~1.2.0" + } + }, "node_modules/fdir": { "version": "6.5.0", "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", @@ -2205,12 +2713,26 @@ "version": "2.0.5", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", - "dev": true, "license": "ISC", "engines": { "node": "6.* || 8.* || >= 10.*" } }, + "node_modules/get-stream": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "license": "MIT", + "dependencies": { + "pump": "^3.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/get-tsconfig": { "version": "4.13.6", "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.6.tgz", @@ -2224,6 +2746,20 @@ "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" } }, + "node_modules/get-uri": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/get-uri/-/get-uri-6.0.5.tgz", + "integrity": "sha512-b1O07XYq8eRuVzBNgJLstU6FYc1tS6wnMtF1I1D9lE8LxZSOGZ7LhxN54yPP6mGw5f2CkXY2BQUL9Fx41qvcIg==", + "license": "MIT", + "dependencies": { + "basic-ftp": "^5.0.2", + "data-uri-to-buffer": "^6.0.2", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/github-from-package": { "version": "0.0.0", "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", @@ -2305,6 +2841,38 @@ "node": ">= 0.4" } }, + "node_modules/highlight.js": { + "version": "11.11.1", + "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-11.11.1.tgz", + "integrity": "sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w==", + "license": "BSD-3-Clause", + "optional": true, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/http-proxy-agent/node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, "node_modules/https-proxy-agent": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", @@ -2350,6 +2918,22 @@ "node": ">= 4" } }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", @@ -2381,6 +2965,21 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/ip-address": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.1.0.tgz", + "integrity": "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "license": "MIT" + }, "node_modules/is-core-module": { "version": "2.9.0", "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.9.0.tgz", @@ -2408,7 +3007,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -2451,6 +3049,18 @@ "dev": true, "license": "MIT" }, + "node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, "node_modules/jsesc": { "version": "2.5.2", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", @@ -2464,6 +3074,12 @@ "node": ">=4" } }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "license": "MIT" + }, "node_modules/jsonfile": { "version": "6.2.0", "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", @@ -2477,6 +3093,12 @@ "graceful-fs": "^4.1.6" } }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "license": "MIT" + }, "node_modules/lodash": { "version": "4.17.23", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz", @@ -2496,6 +3118,15 @@ "dev": true, "license": "MIT" }, + "node_modules/lru-cache": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, "node_modules/magic-bytes.js": { "version": "1.13.0", "resolved": "https://registry.npmjs.org/magic-bytes.js/-/magic-bytes.js-1.13.0.tgz", @@ -2572,6 +3203,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/mitt": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz", + "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==", + "license": "MIT" + }, "node_modules/mkdirp-classic": { "version": "0.5.3", "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", @@ -2583,7 +3220,6 @@ "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, "license": "MIT" }, "node_modules/multistream": { @@ -2652,6 +3288,15 @@ "dev": true, "license": "MIT" }, + "node_modules/netmask": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/netmask/-/netmask-2.0.2.tgz", + "integrity": "sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, "node_modules/node-abi": { "version": "3.87.0", "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.87.0.tgz", @@ -2686,11 +3331,25 @@ } } }, + "node_modules/nopt": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-1.0.10.tgz", + "integrity": "sha512-NWmpvLSqUrgrAC9HCuxEvb+PSloHpqVu+FqcO4eeF2h5qYRhA7ev6KvelyQAKtegUbC6RypJnlEOhd8vloNKYg==", + "license": "MIT", + "dependencies": { + "abbrev": "1" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": "*" + } + }, "node_modules/once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "dev": true, "license": "ISC", "dependencies": { "wrappy": "1" @@ -2706,6 +3365,90 @@ "node": ">=8" } }, + "node_modules/pac-proxy-agent": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/pac-proxy-agent/-/pac-proxy-agent-7.2.0.tgz", + "integrity": "sha512-TEB8ESquiLMc0lV8vcd5Ql/JAKAoyzHFXaStwjkzpOpC5Yv+pIzLfHvjTSdf3vpa2bMiUQrg9i6276yn8666aA==", + "license": "MIT", + "dependencies": { + "@tootallnate/quickjs-emscripten": "^0.23.0", + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "get-uri": "^6.0.1", + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.6", + "pac-resolver": "^7.0.1", + "socks-proxy-agent": "^8.0.5" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/pac-proxy-agent/node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/pac-proxy-agent/node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/pac-resolver": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/pac-resolver/-/pac-resolver-7.0.1.tgz", + "integrity": "sha512-5NPgf87AT2STgwa2ntRMr45jTKrYBGkVU36yT0ig/n/GMAa3oPqhZfIQ2kMEimReg0+t9kZViDVZ83qfVUlckg==", + "license": "MIT", + "dependencies": { + "degenerator": "^5.0.0", + "netmask": "^2.0.2" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/path-parse": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", @@ -2740,11 +3483,16 @@ "node": ">= 14.16" } }, + "node_modules/pend": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", + "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", + "license": "MIT" + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "dev": true, "license": "ISC" }, "node_modules/picomatch": { @@ -2881,23 +3629,107 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.4.0" } }, + "node_modules/proxy-agent": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-6.5.0.tgz", + "integrity": "sha512-TmatMXdr2KlRiA2CyDu8GqR8EjahTG3aY3nXjdzFyoZbmB8hrBsTyMezhULIXKnC0jpfjlmiZ3+EaCzoInSu/A==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "http-proxy-agent": "^7.0.1", + "https-proxy-agent": "^7.0.6", + "lru-cache": "^7.14.1", + "pac-proxy-agent": "^7.1.0", + "proxy-from-env": "^1.1.0", + "socks-proxy-agent": "^8.0.5" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/proxy-agent/node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/proxy-agent/node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, "node_modules/pump": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.3.tgz", "integrity": "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==", - "dev": true, "license": "MIT", "dependencies": { "end-of-stream": "^1.1.0", "once": "^1.3.1" } }, + "node_modules/puppeteer": { + "version": "24.37.2", + "resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-24.37.2.tgz", + "integrity": "sha512-FV1W/919ve0y0oiS/3Rp5XY4MUNUokpZOH/5M4MMDfrrvh6T9VbdKvAHrAFHBuCxvluDxhjra20W7Iz6HJUcIQ==", + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@puppeteer/browsers": "2.12.0", + "chromium-bidi": "13.1.1", + "cosmiconfig": "^9.0.0", + "devtools-protocol": "0.0.1566079", + "puppeteer-core": "24.37.2", + "typed-query-selector": "^2.12.0" + }, + "bin": { + "puppeteer": "lib/cjs/puppeteer/node/cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/puppeteer-core": { + "version": "24.37.2", + "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-24.37.2.tgz", + "integrity": "sha512-nN8qwE3TGF2vA/+xemPxbesntTuqD9vCGOiZL2uh8HES3pPzLX20MyQjB42dH2rhQ3W3TljZ4ZaKZ0yX/abQuw==", + "license": "Apache-2.0", + "dependencies": { + "@puppeteer/browsers": "2.12.0", + "chromium-bidi": "13.1.1", + "debug": "^4.4.3", + "devtools-protocol": "0.0.1566079", + "typed-query-selector": "^2.12.0", + "webdriver-bidi-protocol": "0.4.0", + "ws": "^8.19.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -2955,7 +3787,6 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -2982,6 +3813,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/resolve-pkg-maps": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", @@ -3099,7 +3939,6 @@ "version": "7.7.4", "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", - "dev": true, "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -3172,6 +4011,63 @@ "node": ">=8" } }, + "node_modules/smart-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", + "license": "MIT", + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks": { + "version": "2.8.7", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.7.tgz", + "integrity": "sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A==", + "license": "MIT", + "dependencies": { + "ip-address": "^10.0.1", + "smart-buffer": "^4.2.0" + }, + "engines": { + "node": ">= 10.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks-proxy-agent": { + "version": "8.0.5", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.5.tgz", + "integrity": "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "socks": "^2.8.3" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/socks-proxy-agent/node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "license": "BSD-3-Clause", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", @@ -3206,6 +4102,17 @@ "readable-stream": "^2.1.4" } }, + "node_modules/streamx": { + "version": "2.23.0", + "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.23.0.tgz", + "integrity": "sha512-kn+e44esVfn2Fa/O0CPFcex27fjIL6MkVae0Mm6q+E6f0hWv578YCERbv+4m02cjxvDsPKLnmxral/rR6lBMAg==", + "license": "MIT", + "dependencies": { + "events-universal": "^1.0.0", + "fast-fifo": "^1.3.2", + "text-decoder": "^1.1.0" + } + }, "node_modules/string_decoder": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", @@ -3220,7 +4127,6 @@ "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, "license": "MIT", "dependencies": { "emoji-regex": "^8.0.0", @@ -3235,7 +4141,6 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, "license": "MIT", "dependencies": { "ansi-regex": "^5.0.1" @@ -3338,6 +4243,15 @@ "node": ">= 6" } }, + "node_modules/text-decoder": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.3.tgz", + "integrity": "sha512-3/o9z3X0X0fTupwsYvR03pJ/DjWuqqrfwBgTQzdWDiQSm9KitAyz/9WqsT2JQW7KV2m+bC2ol/zqpW37NHxLaA==", + "license": "Apache-2.0", + "dependencies": { + "b4a": "^1.6.4" + } + }, "node_modules/tinybench": { "version": "2.9.0", "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", @@ -3474,11 +4388,17 @@ "node": "*" } }, + "node_modules/typed-query-selector": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/typed-query-selector/-/typed-query-selector-2.12.0.tgz", + "integrity": "sha512-SbklCd1F0EiZOyPiW192rrHZzZ5sBijB6xM+cpmrwDqObvdtunOHHIk9fCGsoK5JVIYXoyEp4iEdE3upFH3PAg==", + "license": "MIT" + }, "node_modules/typescript": { "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", - "dev": true, + "devOptional": true, "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", @@ -3691,6 +4611,12 @@ } } }, + "node_modules/webdriver-bidi-protocol": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/webdriver-bidi-protocol/-/webdriver-bidi-protocol-0.4.0.tgz", + "integrity": "sha512-U9VIlNRrq94d1xxR9JrCEAx5Gv/2W7ERSv8oWRoNe/QYbfccS0V3h/H6qeNeCRJxXGMhhnkqvwNrvPAYeuP9VA==", + "license": "Apache-2.0" + }, "node_modules/webidl-conversions": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", @@ -3730,7 +4656,6 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dev": true, "license": "MIT", "dependencies": { "ansi-styles": "^4.0.0", @@ -3748,7 +4673,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "dev": true, "license": "ISC" }, "node_modules/ws": { @@ -3776,7 +4700,6 @@ "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", - "dev": true, "license": "ISC", "engines": { "node": ">=10" @@ -3811,6 +4734,16 @@ "node": ">=10" } }, + "node_modules/yauzl": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", + "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", + "license": "MIT", + "dependencies": { + "buffer-crc32": "~0.2.3", + "fd-slicer": "~1.1.0" + } + }, "node_modules/zod": { "version": "4.3.6", "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz", diff --git a/package.json b/package.json index 88a0fae..f6777f9 100644 --- a/package.json +++ b/package.json @@ -14,11 +14,14 @@ }, "dependencies": { "@anthropic-ai/claude-agent-sdk": "^0.1.1", + "diff2html": "^3.4.56", "discord.js": "^14.16.3", - "dotenv": "^16.4.7" + "dotenv": "^16.4.7", + "puppeteer": "^24.37.2" }, "license": "MIT", "devDependencies": { + "@types/diff2html": "^0.0.5", "@types/node": "^22.10.0", "esbuild": "^0.27.3", "pkg": "^5.8.1", diff --git a/src/bot.ts b/src/bot.ts index 9236323..1a8aced 100644 --- a/src/bot.ts +++ b/src/bot.ts @@ -25,6 +25,7 @@ import { getMultiSessionManager, } from "./sessions/multiSession.js"; import { sanitizeOutput } from "./utils/sanitizeOutput.js"; +import { closeBrowser } from "./utils/diffRenderer.js"; import { initAuditLogger, getAuditLogger, audit, AuditEvent } from "./utils/auditLog.js"; import { initRateLimiter, getRateLimiter } from "./utils/rateLimiter.js"; import { isAllowedUser } from "./utils/security.js"; @@ -38,6 +39,7 @@ import statusCommand from "./commands/status.js"; import helpCommand from "./commands/help.js"; import myidCommand from "./commands/myid.js"; import taskCommand from "./commands/task.js"; +import diffCommand from "./commands/diff.js"; // ── Global error handlers (prevent silent crash in exe) ───────────── process.on("unhandledRejection", (reason) => { @@ -220,6 +222,7 @@ const allCommands: PrefixCommand[] = [ helpCommand, myidCommand, taskCommand, + diffCommand, ]; function loadCommands(client: BotClient): void { @@ -469,6 +472,7 @@ async function main(): Promise { } const logger = getAuditLogger(); if (logger) await logger.shutdown(); + await closeBrowser(); // Close puppeteer browser client.destroy(); process.exit(0); }; diff --git a/src/commands/diff.ts b/src/commands/diff.ts new file mode 100644 index 0000000..3d52251 --- /dev/null +++ b/src/commands/diff.ts @@ -0,0 +1,155 @@ +import { AttachmentBuilder, EmbedBuilder } from "discord.js"; +import type { PrefixCommand, CommandContext } from "../types.js"; +import { isAllowedUser } from "../utils/security.js"; +import { withTyping } from "../utils/typing.js"; +import { getMultiSessionManager } from "../sessions/multiSession.js"; +import { + getGitDiff, + filterSensitiveFiles, + filterSensitiveDiff, + type DiffOptions, +} from "../utils/gitDiff.js"; +import { diffToImage, createTextDiffSummary } from "../utils/diffRenderer.js"; + +/** + * Parse diff command arguments + * !diff → all changes + * !diff --staged → staged only + * !diff → specific file + * !diff HEAD~1 → compare with commit + */ +function parseDiffArgs(args: string[]): DiffOptions { + const options: DiffOptions = {}; + + for (const arg of args) { + if (arg === "--staged" || arg === "-s") { + options.staged = true; + } else if (arg.startsWith("HEAD") || arg.match(/^[a-f0-9]{7,40}$/i)) { + options.commit = arg; + } else if (!arg.startsWith("-")) { + options.file = arg; + } + } + + return options; +} + +const diffCommand: PrefixCommand = { + name: "diff", + aliases: ["d", "changes"], + description: "Show git diff as an image. Usage: !diff [--staged] [file] [commit]", + + async execute(ctx: CommandContext): Promise { + if (!isAllowedUser(ctx.message.author.id)) { + await ctx.message.reply("You are not authorized to use this bot."); + return; + } + + // Get current session's working directory + const multiSession = getMultiSessionManager(); + const activeSession = multiSession?.getActiveSession(); + const cwd = activeSession?.manager.getInfo().cwd ?? ctx.client.workingDir; + + const options = parseDiffArgs(ctx.args); + + await withTyping(ctx.message, async () => { + // Get diff data + const diffResult = await getGitDiff(cwd, options); + + if (!diffResult.isGitRepo) { + await ctx.message.reply("❌ Not a git repository."); + return; + } + + if (diffResult.files.length === 0 && !diffResult.raw) { + await ctx.message.reply("✅ No changes detected."); + return; + } + + // Filter sensitive files + const safeFiles = filterSensitiveFiles(diffResult.files); + const safeDiff = filterSensitiveDiff(diffResult.raw); + + if (!safeDiff.trim()) { + await ctx.message.reply("✅ No changes to display (sensitive files filtered)."); + return; + } + + // Create summary embed + const embed = new EmbedBuilder() + .setTitle("📊 Git Changes") + .setColor(0x238636) // GitHub green + .addFields( + { + name: "Files Changed", + value: `${safeFiles.length}`, + inline: true, + }, + { + name: "Lines Added", + value: `+${diffResult.totalAdded}`, + inline: true, + }, + { + name: "Lines Removed", + value: `-${diffResult.totalRemoved}`, + inline: true, + }, + ) + .setTimestamp(); + + // Add file list (up to 10) + if (safeFiles.length > 0) { + const fileList = safeFiles + .slice(0, 10) + .map((f) => { + const icon = f.status === "added" ? "🆕" : f.status === "deleted" ? "🗑️" : "📝"; + return `${icon} \`${f.path}\` (+${f.additions}, -${f.deletions})`; + }) + .join("\n"); + + const suffix = safeFiles.length > 10 ? `\n... and ${safeFiles.length - 10} more` : ""; + embed.addFields({ name: "Changed Files", value: fileList + suffix }); + } + + // Try to render diff as image + try { + const imageBuffer = await diffToImage(safeDiff, { + theme: "dark", + maxLines: 150, + outputFormat: "line-by-line", + }); + + const attachment = new AttachmentBuilder(imageBuffer, { + name: "diff.png", + }); + + await ctx.message.reply({ + embeds: [embed], + files: [attachment], + }); + } catch (renderError) { + // Fallback to text if image rendering fails + console.error("[Diff] Image render failed:", renderError); + + const textSummary = createTextDiffSummary( + safeFiles, + diffResult.totalAdded, + diffResult.totalRemoved, + ); + + // Attach raw diff as file + const diffFile = new AttachmentBuilder(Buffer.from(safeDiff, "utf-8"), { + name: "changes.diff", + }); + + await ctx.message.reply({ + content: textSummary, + files: [diffFile], + }); + } + }); + }, +}; + +export default diffCommand; diff --git a/src/commands/help.ts b/src/commands/help.ts index c3c0b57..d41e1b4 100644 --- a/src/commands/help.ts +++ b/src/commands/help.ts @@ -51,6 +51,15 @@ const helpCommand: PrefixCommand = { ].join("\n"), inline: false, }, + { + name: "Git Tools", + value: [ + `\`${p}diff\` — Show git diff as image (aliases: \`${p}d\`, \`${p}changes\`)`, + `\`${p}diff --staged\` — Show staged changes only`, + `\`${p}diff \` — Show diff for specific file`, + ].join("\n"), + inline: false, + }, { name: "CMD Execution", value: `\`${p}exec \` — Run a CMD command (aliases: \`${p}run\`, \`${p}cmd\`)`, diff --git a/src/utils/diffRenderer.ts b/src/utils/diffRenderer.ts new file mode 100644 index 0000000..c035f11 --- /dev/null +++ b/src/utils/diffRenderer.ts @@ -0,0 +1,360 @@ +// eslint-disable-next-line @typescript-eslint/no-require-imports +const diff2htmlModule = require("diff2html") as { html: (diff: string, config?: object) => string }; +const { html: diff2html } = diff2htmlModule; +import puppeteer, { type Browser } from "puppeteer"; +import { ensureChromium, isChromiumInstalled } from "./puppeteerSetup.js"; + +export interface RenderOptions { + theme?: "light" | "dark"; + maxLines?: number; + outputFormat?: "side-by-side" | "line-by-line"; + fontSize?: number; +} + +// Singleton browser instance for performance +let browserInstance: Browser | null = null; +let chromiumPath: string | null = null; + +/** + * Initialize Chromium (downloads if needed) + * Call this early to avoid delay on first diff render + */ +export async function initializePuppeteer(): Promise { + if (!chromiumPath) { + chromiumPath = await ensureChromium(); + } +} + +/** + * Check if Chromium is ready + */ +export function isPuppeteerReady(): boolean { + return isChromiumInstalled(); +} + +/** + * Get or create puppeteer browser instance + */ +async function getBrowser(): Promise { + if (!browserInstance || !browserInstance.connected) { + // Chromium이 없으면 다운로드 + if (!chromiumPath) { + chromiumPath = await ensureChromium(); + } + + browserInstance = await puppeteer.launch({ + headless: true, + executablePath: chromiumPath, + args: [ + "--no-sandbox", + "--disable-setuid-sandbox", + "--disable-dev-shm-usage", + "--disable-gpu", + ], + }); + } + return browserInstance; +} + +/** + * Close browser instance (call on shutdown) + */ +export async function closeBrowser(): Promise { + if (browserInstance) { + await browserInstance.close(); + browserInstance = null; + } +} + +/** + * Convert git diff to PNG image + */ +export async function diffToImage( + diffString: string, + options: RenderOptions = {}, +): Promise { + const { + theme = "dark", + outputFormat = "line-by-line", + fontSize = 12, + maxLines = 100, + } = options; + + // Truncate diff if too long + const truncatedDiff = truncateDiff(diffString, maxLines); + + // Generate HTML from diff + const diffHtml = diff2html(truncatedDiff, { + drawFileList: true, + matching: "lines", + outputFormat: outputFormat === "side-by-side" ? "side-by-side" : "line-by-line", + }); + + // Create full HTML page with styling + const fullHtml = createHtmlPage(diffHtml, theme, fontSize); + + // Render to image + const browser = await getBrowser(); + const page = await browser.newPage(); + + try { + await page.setContent(fullHtml, { waitUntil: "networkidle0" }); + + // Get content dimensions + const bodyHandle = await page.$("body"); + const boundingBox = await bodyHandle?.boundingBox(); + + if (!boundingBox) { + throw new Error("Failed to get page dimensions"); + } + + // Set viewport to content size (with max width) + const width = Math.min(Math.ceil(boundingBox.width) + 40, 1200); + const height = Math.min(Math.ceil(boundingBox.height) + 40, 4000); + + await page.setViewport({ width, height }); + + // Take screenshot + const screenshot = await page.screenshot({ + type: "png", + fullPage: false, + clip: { + x: 0, + y: 0, + width, + height, + }, + }); + + return Buffer.from(screenshot); + } finally { + await page.close(); + } +} + +/** + * Truncate diff to max lines + */ +function truncateDiff(diff: string, maxLines: number): string { + const lines = diff.split("\n"); + if (lines.length <= maxLines) { + return diff; + } + + const truncated = lines.slice(0, maxLines); + truncated.push(""); + truncated.push(`... (${lines.length - maxLines} more lines truncated)`); + return truncated.join("\n"); +} + +/** + * Create full HTML page with diff2html styling + */ +function createHtmlPage(diffHtml: string, theme: "light" | "dark", fontSize: number): string { + const isDark = theme === "dark"; + + const colors = isDark + ? { + bg: "#1e1e1e", + fg: "#d4d4d4", + headerBg: "#2d2d2d", + addBg: "#1e3a1e", + addFg: "#4ec94e", + delBg: "#3a1e1e", + delFg: "#f14c4c", + lineBg: "#252526", + lineFg: "#858585", + border: "#404040", + } + : { + bg: "#ffffff", + fg: "#24292e", + headerBg: "#f6f8fa", + addBg: "#e6ffed", + addFg: "#22863a", + delBg: "#ffeef0", + delFg: "#cb2431", + lineBg: "#fafbfc", + lineFg: "#6a737d", + border: "#e1e4e8", + }; + + return ` + + + + + + + ${diffHtml} + +`; +} + +/** + * Create a simple text-based diff summary (fallback when image fails) + */ +export function createTextDiffSummary( + files: Array<{ path: string; additions: number; deletions: number }>, + totalAdded: number, + totalRemoved: number, +): string { + const lines: string[] = [ + "📊 **Git Changes**", + "━".repeat(30), + "", + ]; + + for (const file of files.slice(0, 10)) { + const addStr = file.additions > 0 ? `+${file.additions}` : ""; + const delStr = file.deletions > 0 ? `-${file.deletions}` : ""; + const stats = [addStr, delStr].filter(Boolean).join(", "); + lines.push(`📄 \`${file.path}\` (${stats})`); + } + + if (files.length > 10) { + lines.push(`... and ${files.length - 10} more files`); + } + + lines.push(""); + lines.push(`**Total:** +${totalAdded}, -${totalRemoved} lines`); + + return lines.join("\n"); +} diff --git a/src/utils/gitDiff.ts b/src/utils/gitDiff.ts new file mode 100644 index 0000000..251810e --- /dev/null +++ b/src/utils/gitDiff.ts @@ -0,0 +1,259 @@ +import { spawn } from "node:child_process"; + +export interface DiffFile { + path: string; + status: "modified" | "added" | "deleted" | "renamed"; + additions: number; + deletions: number; +} + +export interface DiffResult { + files: DiffFile[]; + totalAdded: number; + totalRemoved: number; + raw: string; + isGitRepo: boolean; +} + +export interface DiffOptions { + staged?: boolean; + file?: string; + commit?: string; // e.g., "HEAD~1" +} + +/** + * Execute a git command and return stdout + */ +async function execGit(args: string[], cwd: string): Promise { + return new Promise((resolve, reject) => { + const proc = spawn("git", args, { + cwd, + stdio: ["ignore", "pipe", "pipe"], + windowsHide: true, + }); + + let stdout = ""; + let stderr = ""; + + proc.stdout.on("data", (data: Buffer) => { + stdout += data.toString(); + }); + + proc.stderr.on("data", (data: Buffer) => { + stderr += data.toString(); + }); + + proc.on("close", (code) => { + if (code === 0) { + resolve(stdout); + } else { + reject(new Error(stderr || `git exited with code ${code}`)); + } + }); + + proc.on("error", (err) => { + reject(err); + }); + }); +} + +/** + * Check if directory is a git repository + */ +export async function isGitRepository(cwd: string): Promise { + try { + await execGit(["rev-parse", "--is-inside-work-tree"], cwd); + return true; + } catch { + return false; + } +} + +/** + * Get git diff with statistics + */ +export async function getGitDiff(cwd: string, options: DiffOptions = {}): Promise { + const isRepo = await isGitRepository(cwd); + if (!isRepo) { + return { + files: [], + totalAdded: 0, + totalRemoved: 0, + raw: "", + isGitRepo: false, + }; + } + + // Build diff command arguments + const diffArgs = ["diff"]; + + if (options.staged) { + diffArgs.push("--cached"); + } + + if (options.commit) { + diffArgs.push(options.commit); + } + + if (options.file) { + diffArgs.push("--", options.file); + } + + // Get raw diff + let raw: string; + try { + raw = await execGit(diffArgs, cwd); + } catch { + raw = ""; + } + + // Get diff statistics + const statArgs = [...diffArgs, "--stat", "--stat-width=1000"]; + let statOutput: string; + try { + statOutput = await execGit(statArgs, cwd); + } catch { + statOutput = ""; + } + + // Parse file statistics + const files = parseStatOutput(statOutput); + + // Calculate totals + const totalAdded = files.reduce((sum, f) => sum + f.additions, 0); + const totalRemoved = files.reduce((sum, f) => sum + f.deletions, 0); + + return { + files, + totalAdded, + totalRemoved, + raw, + isGitRepo: true, + }; +} + +/** + * Get diff for unstaged changes + untracked files summary + */ +export async function getWorkingTreeStatus(cwd: string): Promise<{ + modified: string[]; + added: string[]; + deleted: string[]; + untracked: string[]; +}> { + const isRepo = await isGitRepository(cwd); + if (!isRepo) { + return { modified: [], added: [], deleted: [], untracked: [] }; + } + + try { + const status = await execGit(["status", "--porcelain"], cwd); + const lines = status.split("\n").filter(Boolean); + + const modified: string[] = []; + const added: string[] = []; + const deleted: string[] = []; + const untracked: string[] = []; + + for (const line of lines) { + const code = line.slice(0, 2); + const file = line.slice(3); + + if (code === "??") { + untracked.push(file); + } else if (code.includes("M")) { + modified.push(file); + } else if (code.includes("A")) { + added.push(file); + } else if (code.includes("D")) { + deleted.push(file); + } + } + + return { modified, added, deleted, untracked }; + } catch { + return { modified: [], added: [], deleted: [], untracked: [] }; + } +} + +/** + * Parse git diff --stat output + */ +function parseStatOutput(output: string): DiffFile[] { + const files: DiffFile[] = []; + const lines = output.split("\n"); + + for (const line of lines) { + // Match lines like: " src/file.ts | 10 ++++----" + const match = line.match(/^\s*(.+?)\s*\|\s*(\d+)\s*([+-]*)/); + if (match) { + const [, path, , changes] = match; + const additions = (changes.match(/\+/g) || []).length; + const deletions = (changes.match(/-/g) || []).length; + + let status: DiffFile["status"] = "modified"; + if (line.includes("(new)") || additions > 0 && deletions === 0) { + status = "added"; + } else if (line.includes("(gone)") || deletions > 0 && additions === 0) { + status = "deleted"; + } + + files.push({ + path: path.trim(), + status, + additions, + deletions, + }); + } + } + + return files; +} + +/** + * Filter out sensitive files from diff + */ +const SENSITIVE_PATTERNS = [ + /\.env$/, + /\.env\./, + /credentials/i, + /secret/i, + /password/i, + /\.pem$/, + /\.key$/, + /\.p12$/, + /\.pfx$/, +]; + +export function filterSensitiveFiles(files: DiffFile[]): DiffFile[] { + return files.filter((f) => + !SENSITIVE_PATTERNS.some((p) => p.test(f.path)) + ); +} + +/** + * Filter sensitive content from raw diff + */ +export function filterSensitiveDiff(raw: string): string { + const lines = raw.split("\n"); + const filtered: string[] = []; + let skipFile = false; + + for (const line of lines) { + // Check for new file header + if (line.startsWith("diff --git")) { + const filePath = line.split(" b/")[1] || ""; + skipFile = SENSITIVE_PATTERNS.some((p) => p.test(filePath)); + if (skipFile) { + filtered.push(`${line}\n[REDACTED: Sensitive file content hidden]`); + continue; + } + } + + if (!skipFile) { + filtered.push(line); + } + } + + return filtered.join("\n"); +} diff --git a/src/utils/puppeteerSetup.ts b/src/utils/puppeteerSetup.ts new file mode 100644 index 0000000..40a3779 --- /dev/null +++ b/src/utils/puppeteerSetup.ts @@ -0,0 +1,200 @@ +import path from "node:path"; +import fs from "node:fs"; +import { execSync, spawn } from "node:child_process"; + +// pkg 환경인지 확인 +declare const process: NodeJS.Process & { pkg?: unknown }; + +// Chromium 설치 경로 (exe와 같은 폴더의 .chromium) +function getChromiumPath(): string { + // pkg로 빌드된 exe인 경우 process.execPath가 exe 경로 + const baseDir = process.pkg + ? path.dirname(process.execPath) + : path.resolve(process.cwd()); + + return path.join(baseDir, ".chromium"); +} + +// 설치된 Chromium 실행 파일 경로 찾기 +function findChromiumExecutable(chromiumDir: string): string | null { + if (!fs.existsSync(chromiumDir)) { + return null; + } + + try { + // chrome-headless-shell 또는 chrome 폴더 찾기 + const dirs = fs.readdirSync(chromiumDir); + + for (const dir of dirs) { + const fullPath = path.join(chromiumDir, dir); + if (!fs.statSync(fullPath).isDirectory()) continue; + + // chrome-headless-shell-win64/chrome-headless-shell.exe + if (dir.startsWith("chrome-headless-shell")) { + const exePath = path.join(fullPath, "chrome-headless-shell.exe"); + if (fs.existsSync(exePath)) { + return exePath; + } + } + + // chrome-win64/chrome.exe 또는 chrome-win/chrome.exe + if (dir.startsWith("chrome-win")) { + const exePath = path.join(fullPath, "chrome.exe"); + if (fs.existsSync(exePath)) { + return exePath; + } + } + } + } catch { + // ignore + } + + return null; +} + +// Chromium이 설치되어 있는지 확인 +export function isChromiumInstalled(): boolean { + const chromiumDir = getChromiumPath(); + const executable = findChromiumExecutable(chromiumDir); + return executable !== null; +} + +// npx를 사용하여 Chromium 다운로드 +async function downloadWithNpx(chromiumDir: string): Promise { + return new Promise((resolve, reject) => { + console.log("[Puppeteer] Downloading Chromium using @puppeteer/browsers..."); + + const args = [ + "@puppeteer/browsers", + "install", + "chrome-headless-shell@stable", + "--path", + chromiumDir, + ]; + + const child = spawn("npx", args, { + shell: true, + stdio: "inherit", + }); + + child.on("close", (code) => { + if (code === 0) { + resolve(); + } else { + reject(new Error(`npx exited with code ${code}`)); + } + }); + + child.on("error", reject); + }); +} + +// Puppeteer 내장 브라우저 사용 시도 +function tryPuppeteerCache(): string | null { + // Puppeteer가 설치한 기본 경로 확인 + const puppeteerCachePaths = [ + path.join(process.env.USERPROFILE || "", ".cache", "puppeteer"), + path.join(process.env.LOCALAPPDATA || "", "puppeteer"), + ]; + + for (const cachePath of puppeteerCachePaths) { + if (fs.existsSync(cachePath)) { + try { + // chrome-headless-shell 또는 chrome 폴더 찾기 + const chromeDirs = fs.readdirSync(cachePath); + for (const dir of chromeDirs) { + if (dir.startsWith("chrome")) { + const versionDir = path.join(cachePath, dir); + const versions = fs.readdirSync(versionDir); + for (const version of versions) { + const winDir = path.join(versionDir, version, "chrome-headless-shell-win64"); + const exePath = path.join(winDir, "chrome-headless-shell.exe"); + if (fs.existsSync(exePath)) { + return exePath; + } + // 일반 chrome도 확인 + const chromeWinDir = path.join(versionDir, version, "chrome-win64"); + const chromeExePath = path.join(chromeWinDir, "chrome.exe"); + if (fs.existsSync(chromeExePath)) { + return chromeExePath; + } + } + } + } + } catch { + // ignore + } + } + } + return null; +} + +// Chromium 다운로드 및 설치 +export async function ensureChromium(): Promise { + const chromiumDir = getChromiumPath(); + + // 1. 이미 우리 폴더에 설치되어 있으면 사용 + let executable = findChromiumExecutable(chromiumDir); + if (executable) { + console.log(`[Puppeteer] Chromium found: ${executable}`); + return executable; + } + + // 2. Puppeteer 캐시에서 찾기 + const cachedChrome = tryPuppeteerCache(); + if (cachedChrome) { + console.log(`[Puppeteer] Using cached Chromium: ${cachedChrome}`); + return cachedChrome; + } + + // 3. 다운로드 필요 + console.log(`[Puppeteer] Chromium not found. Downloading to ${chromiumDir}...`); + console.log("[Puppeteer] This may take a few minutes on first run."); + + // chromium 폴더 생성 + if (!fs.existsSync(chromiumDir)) { + fs.mkdirSync(chromiumDir, { recursive: true }); + } + + try { + await downloadWithNpx(chromiumDir); + console.log("[Puppeteer] Download complete!"); + + // 다운로드 후 다시 찾기 + executable = findChromiumExecutable(chromiumDir); + if (executable) { + return executable; + } + + throw new Error("Chromium download completed but executable not found"); + } catch (error) { + console.error("[Puppeteer] Failed to download Chromium:", error); + console.log("[Puppeteer] Trying fallback: manual puppeteer install..."); + + // Fallback: puppeteer browsers 직접 설치 시도 + try { + execSync("npx puppeteer browsers install chrome-headless-shell", { + stdio: "inherit", + cwd: path.dirname(chromiumDir), + }); + + const fallbackExe = tryPuppeteerCache(); + if (fallbackExe) { + return fallbackExe; + } + } catch (fallbackError) { + console.error("[Puppeteer] Fallback also failed:", fallbackError); + } + + throw error; + } +} + +// 설치 경로 정보 가져오기 +export function getChromiumInfo(): { dir: string; executable: string | null } { + const dir = getChromiumPath(); + return { + dir, + executable: findChromiumExecutable(dir), + }; +} diff --git a/tests/gitDiff.test.ts b/tests/gitDiff.test.ts new file mode 100644 index 0000000..caf62f7 --- /dev/null +++ b/tests/gitDiff.test.ts @@ -0,0 +1,81 @@ +import { describe, it, expect } from "vitest"; +import { + filterSensitiveFiles, + filterSensitiveDiff, + type DiffFile, +} from "../src/utils/gitDiff.js"; + +describe("gitDiff", () => { + describe("filterSensitiveFiles", () => { + it("should filter out .env files", () => { + const files: DiffFile[] = [ + { path: "src/config.ts", status: "modified", additions: 5, deletions: 2 }, + { path: ".env", status: "modified", additions: 1, deletions: 0 }, + { path: ".env.local", status: "added", additions: 3, deletions: 0 }, + ]; + + const filtered = filterSensitiveFiles(files); + expect(filtered).toHaveLength(1); + expect(filtered[0].path).toBe("src/config.ts"); + }); + + it("should filter out credential files", () => { + const files: DiffFile[] = [ + { path: "src/app.ts", status: "modified", additions: 10, deletions: 5 }, + { path: "credentials.json", status: "added", additions: 20, deletions: 0 }, + { path: "secret.key", status: "modified", additions: 1, deletions: 1 }, + ]; + + const filtered = filterSensitiveFiles(files); + expect(filtered).toHaveLength(1); + expect(filtered[0].path).toBe("src/app.ts"); + }); + + it("should keep regular files", () => { + const files: DiffFile[] = [ + { path: "src/utils/helper.ts", status: "modified", additions: 3, deletions: 1 }, + { path: "README.md", status: "modified", additions: 10, deletions: 5 }, + { path: "package.json", status: "modified", additions: 2, deletions: 1 }, + ]; + + const filtered = filterSensitiveFiles(files); + expect(filtered).toHaveLength(3); + }); + }); + + describe("filterSensitiveDiff", () => { + it("should redact .env file content", () => { + const rawDiff = `diff --git a/src/config.ts b/src/config.ts +--- a/src/config.ts ++++ b/src/config.ts +@@ -1,3 +1,5 @@ ++// new config + export const config = {}; +diff --git a/.env b/.env +--- a/.env ++++ b/.env +@@ -1,2 +1,3 @@ ++SECRET_KEY=abc123 + TOKEN=xyz`; + + const filtered = filterSensitiveDiff(rawDiff); + expect(filtered).toContain("src/config.ts"); + expect(filtered).toContain("[REDACTED: Sensitive file content hidden]"); + expect(filtered).not.toContain("SECRET_KEY"); + expect(filtered).not.toContain("abc123"); + }); + + it("should keep non-sensitive file content", () => { + const rawDiff = `diff --git a/src/app.ts b/src/app.ts +--- a/src/app.ts ++++ b/src/app.ts +@@ -1,3 +1,5 @@ ++// new feature + console.log("hello");`; + + const filtered = filterSensitiveDiff(rawDiff); + expect(filtered).toContain("new feature"); + expect(filtered).toContain('console.log("hello")'); + }); + }); +});