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")');
+ });
+ });
+});