diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 87a0717..2aa9a9f 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -193,12 +193,55 @@ jobs: path: jetbrains-plugin/build/distributions/*.zip if-no-files-found: error + # --------------------------------------------------------------------------- + # VSCode Extension build + # --------------------------------------------------------------------------- + build-vscode: + name: VSCode Extension + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + cache: 'npm' + cache-dependency-path: vscode-extension/package-lock.json + + - name: Install dependencies + working-directory: vscode-extension + run: npm ci + + - name: Type check + working-directory: vscode-extension + run: npx tsc --noEmit + + - name: Run tests + working-directory: vscode-extension + run: npx vitest run + + - name: Build + working-directory: vscode-extension + run: node esbuild.mjs --production + + - name: Package .vsix + working-directory: vscode-extension + run: npx @vscode/vsce package --no-dependencies -o mariadb-profiler-viewer.vsix + + - uses: actions/upload-artifact@v4 + with: + name: mariadb-profiler-viewer-vsix + path: vscode-extension/*.vsix + if-no-files-found: error + # --------------------------------------------------------------------------- # Create GitHub Release # --------------------------------------------------------------------------- release: name: Create Release - needs: [build-linux, build-windows, build-plugin] + needs: [build-linux, build-windows, build-plugin, build-vscode] runs-on: ubuntu-latest permissions: contents: write @@ -214,7 +257,7 @@ jobs: - name: Collect release assets run: | mkdir -p release - find artifacts -type f \( -name "*.so" -o -name "*.dll" -o -name "*.zip" \) -exec cp {} release/ \; + find artifacts -type f \( -name "*.so" -o -name "*.dll" -o -name "*.zip" -o -name "*.vsix" \) -exec cp {} release/ \; echo "=== Release assets ===" ls -lh release/ diff --git a/.github/workflows/vscode-build.yml b/.github/workflows/vscode-build.yml new file mode 100644 index 0000000..e89f553 --- /dev/null +++ b/.github/workflows/vscode-build.yml @@ -0,0 +1,60 @@ +name: Build VSCode Extension + +on: + push: + branches: ['**'] + paths: + - 'vscode-extension/**' + - '.github/workflows/vscode-build.yml' + pull_request: + branches: ['**'] + paths: + - 'vscode-extension/**' + - '.github/workflows/vscode-build.yml' + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + build: + if: github.event_name != 'pull_request' || !contains(github.event.pull_request.labels.*.name, 'no_run') + name: Build & Test Extension + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + cache: 'npm' + cache-dependency-path: vscode-extension/package-lock.json + + - name: Install dependencies + working-directory: vscode-extension + run: npm ci + + - name: Type check + working-directory: vscode-extension + run: npx tsc --noEmit + + - name: Run tests + working-directory: vscode-extension + run: npx vitest run + + - name: Build + working-directory: vscode-extension + run: node esbuild.mjs --production + + - name: Package .vsix + working-directory: vscode-extension + run: npm exec @vscode/vsce -- package --no-dependencies -o mariadb-profiler-viewer.vsix + + - name: Upload extension artifact + uses: actions/upload-artifact@v4 + with: + name: mariadb-profiler-viewer-vsix + path: vscode-extension/*.vsix + if-no-files-found: error diff --git a/PLAN_VSCODE_EXTENSION.md b/PLAN_VSCODE_EXTENSION.md new file mode 100644 index 0000000..9a57ec7 --- /dev/null +++ b/PLAN_VSCODE_EXTENSION.md @@ -0,0 +1,976 @@ +# VSCode Extension 実装計画: MariaDB Profiler Viewer + +## 概要 + +`php-ext-mariadb-salvage` が生成するクエリプロファイリングデータを Visual Studio Code 上でわかりやすく可視化・操作する拡張機能を作成する。 + +既存の JetBrains Plugin (`jetbrains-plugin/`) と同等の機能を VSCode で実現し、PhpStorm 以外の開発環境でもプロファイリングデータを活用可能にする。 + +**UI 方針: Webview を使用せず、VSCode ネイティブ API のみで構成する。** 軽量・高速・テーマ完全統合を優先する。 + +--- + +## JetBrains Plugin との機能対応表 + +| 機能 | JetBrains Plugin | VSCode Extension | +|------|------------------|------------------| +| クエリログビューア | Swing JTable (QueryLogPanel) | **TreeView** (クエリ一覧、展開で詳細表示) | +| クエリ詳細表示 | Swing JTextArea + HTML (QueryDetailPanel) | **Virtual Document** (`.sql` として開き、シンタックスハイライト自動適用) | +| ジョブマネージャ | Swing JList (JobListPanel) | **TreeView** (Native VSCode API) | +| バックトレースナビゲーション | OpenFileDescriptor (BacktracePanel) | TreeView 子要素 + `vscode.workspace.openTextDocument` | +| リアルタイム監視 | Timer + FileWatcher (LiveTailPanel) | **OutputChannel** (`vscode.window.createOutputChannel`) | +| 統計ダッシュボード | Graphics2D バーチャート (StatisticsPanel) | **TreeView** (Unicode バーチャート `████`) | +| 設定 | IntelliJ Configurable (ProfilerConfigurable) | `contributes.configuration` (VSCode Settings) | +| IDE アクション | AnAction (Start/Stop/Open) | `contributes.commands` + コマンドパレット | +| フレームリゾルバ | Groovy スクリプト (FrameResolverService) | JavaScript スクリプト (`vm` モジュール) | +| エラーログ | ErrorLogPanel | **OutputChannel** (`vscode.window.createOutputChannel`) | +| パスマッピング | ProfilerState テキスト設定 | VSCode Settings (JSON 形式) | +| フィルタ・検索 | テーブル上のフィルタバー | **QuickPick** (コマンドパレット) + TreeView `view/title` メニュー | + +--- + +## 技術スタック + +| 項目 | 選択 | 理由 | +|------|------|------| +| 言語 | TypeScript | VSCode Extension 標準言語 | +| ビルド | esbuild (バンドル) + tsc (型チェック) | 高速ビルド & 小バンドルサイズ | +| UI | VSCode ネイティブ API のみ | Webview 不使用、依存ゼロ、省メモリ、テーマ完全統合 | +| JSON パース | ネイティブ `JSON.parse` | 追加依存不要 | +| ファイル監視 | `fs.watchFile` (ポーリング) | Docker ボリューム対応 | +| テスト | Vitest (ユニット) + @vscode/test-electron (統合) | 高速・設定簡易 | +| パッケージ | `@vscode/vsce` | VSCode Marketplace 公式ツール | + +--- + +## ディレクトリ構成 + +``` +vscode-extension/ +├── package.json # Extension マニフェスト +├── tsconfig.json # TypeScript 設定 +├── esbuild.mjs # ビルドスクリプト +├── .vscodeignore # パッケージ除外設定 +├── README.md # Marketplace 用ドキュメント +│ +├── src/ +│ ├── extension.ts # Extension エントリポイント (activate/deactivate) +│ │ +│ ├── model/ # データモデル +│ │ ├── QueryEntry.ts # クエリログエントリ +│ │ ├── JobInfo.ts # ジョブ情報 +│ │ └── BacktraceFrame.ts # バックトレースフレーム +│ │ +│ ├── service/ # サービス層 +│ │ ├── LogParserService.ts # JSONL パーサ (オフセット対応) +│ │ ├── JobManagerService.ts # jobs.json 読み書き + CLI 連携 +│ │ ├── StatisticsService.ts # クエリ統計計算 +│ │ ├── FileWatcherService.ts # ログファイル変更検知 +│ │ └── FrameResolverService.ts # JS スクリプトによるフレーム解決 +│ │ +│ ├── provider/ # VSCode UI プロバイダ +│ │ ├── JobTreeProvider.ts # TreeView: ジョブ一覧 +│ │ ├── QueryTreeProvider.ts # TreeView: クエリ一覧 (展開で詳細) +│ │ ├── StatisticsTreeProvider.ts # TreeView: 統計ダッシュボード +│ │ └── QueryDocumentProvider.ts # Virtual Document: SQL 詳細表示 +│ │ +│ ├── command/ # VSCode コマンド +│ │ ├── startJob.ts # ジョブ開始 +│ │ ├── stopJob.ts # ジョブ停止 +│ │ ├── openLog.ts # ログファイルを開く +│ │ ├── filterQueries.ts # クエリフィルタ (QuickPick) +│ │ └── searchQueries.ts # クエリ検索 (QuickPick) +│ │ +│ └── util/ # ユーティリティ +│ ├── pathMapping.ts # Docker パスマッピング +│ └── queryUtils.ts # SQL 短縮・パラメータバインド +│ +└── test/ + ├── unit/ # ユニットテスト (Vitest) + │ ├── LogParserService.test.ts + │ ├── JobManagerService.test.ts + │ ├── StatisticsService.test.ts + │ └── QueryEntry.test.ts + │ + └── integration/ # 統合テスト (@vscode/test-electron) + └── extension.test.ts +``` + +**Webview 版との差分:** +- `webview/` ディレクトリが不要 (HTML/CSS/JS ビルドパイプラインなし) +- `ProfilerWebviewProvider.ts` → `QueryTreeProvider.ts` + `StatisticsTreeProvider.ts` + `QueryDocumentProvider.ts` に分解 +- フィルタ・検索は `command/` 配下に QuickPick ベースで実装 + +--- + +## Extension マニフェスト (package.json 設計) + +```jsonc +{ + "name": "mariadb-profiler-viewer", + "displayName": "MariaDB Profiler Viewer", + "description": "Visualize and analyze MariaDB/MySQL query profiling data from php-ext-mariadb-salvage", + "version": "0.1.0", + "publisher": "mariadb-profiler", + "engines": { "vscode": "^1.85.0" }, + "categories": ["Other", "Debuggers"], + "activationEvents": [], + + "main": "./dist/extension.js", + + "contributes": { + // Activity Bar コンテナ + "viewsContainers": { + "activitybar": [{ + "id": "mariadb-profiler", + "title": "MariaDB Profiler", + "icon": "resources/icons/profiler.svg" + }] + }, + + // 全て TreeView (Webview なし) + "views": { + "mariadb-profiler": [ + { + "id": "mariadbProfiler.jobs", + "name": "Jobs", + "type": "tree" + }, + { + "id": "mariadbProfiler.queries", + "name": "Queries", + "type": "tree" + }, + { + "id": "mariadbProfiler.statistics", + "name": "Statistics", + "type": "tree" + } + ] + }, + + // コマンド + "commands": [ + { "command": "mariadbProfiler.startJob", "title": "Start Profiling Job", "category": "MariaDB Profiler", "icon": "$(play)" }, + { "command": "mariadbProfiler.stopJob", "title": "Stop Profiling Job", "category": "MariaDB Profiler", "icon": "$(debug-stop)" }, + { "command": "mariadbProfiler.openLog", "title": "Open Profiler Log", "category": "MariaDB Profiler", "icon": "$(folder-opened)" }, + { "command": "mariadbProfiler.refresh", "title": "Refresh", "category": "MariaDB Profiler", "icon": "$(refresh)" }, + { "command": "mariadbProfiler.filterByType", "title": "Filter by Query Type", "category": "MariaDB Profiler", "icon": "$(filter)" }, + { "command": "mariadbProfiler.filterByTag", "title": "Filter by Tag", "category": "MariaDB Profiler", "icon": "$(tag)" }, + { "command": "mariadbProfiler.searchQuery", "title": "Search Queries", "category": "MariaDB Profiler", "icon": "$(search)" }, + { "command": "mariadbProfiler.clearFilter", "title": "Clear Filters", "category": "MariaDB Profiler", "icon": "$(clear-all)" }, + { "command": "mariadbProfiler.showQuerySql", "title": "Show Full SQL", "category": "MariaDB Profiler", "icon": "$(open-preview)" }, + { "command": "mariadbProfiler.startLiveTail", "title": "Start Live Tail", "category": "MariaDB Profiler", "icon": "$(eye)" }, + { "command": "mariadbProfiler.stopLiveTail", "title": "Stop Live Tail", "category": "MariaDB Profiler", "icon": "$(eye-closed)" } + ], + + // ツールバーボタン + "menus": { + "view/title": [ + { "command": "mariadbProfiler.startJob", "when": "view == mariadbProfiler.jobs", "group": "navigation" }, + { "command": "mariadbProfiler.stopJob", "when": "view == mariadbProfiler.jobs", "group": "navigation" }, + { "command": "mariadbProfiler.refresh", "when": "view == mariadbProfiler.jobs", "group": "navigation" }, + { "command": "mariadbProfiler.filterByType", "when": "view == mariadbProfiler.queries", "group": "navigation" }, + { "command": "mariadbProfiler.filterByTag", "when": "view == mariadbProfiler.queries", "group": "navigation" }, + { "command": "mariadbProfiler.searchQuery", "when": "view == mariadbProfiler.queries", "group": "navigation" }, + { "command": "mariadbProfiler.clearFilter", "when": "view == mariadbProfiler.queries", "group": "navigation" }, + { "command": "mariadbProfiler.startLiveTail","when": "view == mariadbProfiler.queries", "group": "2_liveTail" }, + { "command": "mariadbProfiler.stopLiveTail", "when": "view == mariadbProfiler.queries", "group": "2_liveTail" } + ], + "view/item/context": [ + { "command": "mariadbProfiler.showQuerySql", "when": "view == mariadbProfiler.queries && viewItem == queryEntry", "group": "inline" } + ] + }, + + // 設定 + "configuration": { + "title": "MariaDB Profiler", + "properties": { + "mariadbProfiler.logDirectory": { + "type": "string", + "default": "/tmp/mariadb_profiler", + "description": "Directory where profiler writes jobs.json and *.jsonl files" + }, + "mariadbProfiler.phpPath": { + "type": "string", + "default": "php", + "description": "Path to PHP executable for CLI operations" + }, + "mariadbProfiler.cliScriptPath": { + "type": "string", + "default": "", + "description": "Path to mariadb_profiler.php CLI tool (auto-detected from workspace if empty)" + }, + "mariadbProfiler.maxQueries": { + "type": "number", + "default": 10000, + "description": "Maximum number of queries to display" + }, + "mariadbProfiler.refreshInterval": { + "type": "number", + "default": 5, + "description": "Auto-refresh interval in seconds" + }, + "mariadbProfiler.tailBufferSize": { + "type": "number", + "default": 500, + "description": "Number of lines to keep in live tail buffer" + }, + "mariadbProfiler.pathMappings": { + "type": "object", + "default": {}, + "description": "Path mappings for Docker environments (container path -> local path)", + "additionalProperties": { "type": "string" } + }, + "mariadbProfiler.frameResolverScript": { + "type": "string", + "default": "", + "description": "JavaScript code for custom frame resolution (receives trace, tag, query variables)" + } + } + } + } +} +``` + +--- + +## 主要データフロー + +``` +[PHP Extension] + | + +-- /var/profiler/jobs.json --> JobManagerService --> JobTreeProvider (TreeView) + | | + | +--> QueryTreeProvider (TreeView) + | | + +-- /var/profiler/.jsonl --> LogParserService -+ | + | (offset read) | +--> QueryDocumentProvider + | | | (Virtual Document .sql) + | | | + | | +--> vscode.openTextDocument + | | (backtrace jump) + | | + | +--> StatisticsService + | | + | +--> StatisticsTreeProvider (TreeView) + | + +-- /var/profiler/.raw.log --> FileWatcherService --> OutputChannel (Live Tail) + +[CLI Tool] <-- startJob / stopJob commands (child_process.execFile) +``` + +--- + +## UI レイアウト + +### サイドバー全体像 + +``` ++--------------------------------------------------------------+ +| (i) MariaDB Profiler | ++--------------------------------------------------------------+ +| JOBS [>] [#] [refresh] | +| | +| * job-abc123 42 queries, 3.2s | +| * job-def456 18 queries, 1.1s | +| o job-ghi789 156 queries, 45.0s | +| o job-jkl012 73 queries, 12.3s | +| | ++--------------------------------------------------------------+ +| QUERIES [filter] [tag] [search] [clear] Filter: SELECT | +| | +| > SELECT SELECT u.* FROM us... [api] 14:23:01 | +| v INSERT INSERT INTO logs ... [api] 14:23:01 | +| +-- Tables: logs | +| +-- Tags: api | +| +-- Params: ?1 = 1 | +| +-- Backtrace: | +| > LogService.php:28 log() <- click | +| Router.php:128 dispatch() <- click | +| +-- [Show Full SQL] | +| > SELECT SELECT p.*, u.name... [web] 14:23:02 | +| > UPDATE UPDATE users SET l... [web] 14:23:02 | +| | ++--------------------------------------------------------------+ +| STATISTICS | +| | +| Total Queries: 156 | +| | +| v Query Types | +| SELECT |||||||||||||||||||||||| 78 (50%) | +| INSERT |||||||| 12 (8%) | +| UPDATE |||||| 8 (5%) | +| DELETE || 2 (1%) | +| | +| v Top Tables | +| users |||||||||||||||||||| 45 (29%) | +| posts |||||||||||||||| 30 (19%) | +| comments |||||||||| 18 (12%) | +| logs |||| 7 (4%) | +| | +| v Top Tags | +| api |||||||||||||||||||||||| 60 (38%) | +| web |||||||||||| 30 (19%) | +| cron |||| 10 (6%) | +| | ++--------------------------------------------------------------+ +``` + +### エディタ領域 (Virtual Document) + +クエリを選択して "Show Full SQL" すると、エディタタブとして SQL が開く: + +``` ++--------------------------------------------------------------+ +| [x] Query #3 - SELECT (mariadb-profiler) | ++--------------------------------------------------------------+ +| -- Job: job-abc123 | +| -- Time: 2025-01-23 14:23:02.000 | +| -- Tags: web | +| -- Status: OK | +| | +| SELECT p.*, u.name | +| FROM posts p | +| JOIN users u ON u.id = p.user_id | +| WHERE u.active = ? | +| | +| -- Bound Parameters: | +| -- ?1 = 1 | +| | +| -- Backtrace: | +| -- #0 /app/Http/Controllers/UserController.php:42 | +| -- #1 /vendor/laravel/framework/.../Router.php:128 | +| -- #2 /public/index.php:15 | ++--------------------------------------------------------------+ +``` + +- `.sql` として登録するため、VSCode の SQL シンタックスハイライトが自動適用 +- メタデータ (ジョブ、タイムスタンプ、パラメータ、バックトレース) は SQL コメント (`--`) として記述 +- 読み取り専用 (`TextDocumentContentProvider`) + +### OutputChannel (Live Tail) + +``` ++--------------------------------------------------------------+ +| OUTPUT [MariaDB Profiler Live Tail v] | ++--------------------------------------------------------------+ +| [2025-01-23 14:23:01.000] OK [api] SELECT u.* FROM users... | +| #0 /app/Http/Controllers/UserController.php:42 | +| #1 /vendor/laravel/framework/.../Router.php:128 | +| | +| [2025-01-23 14:23:01.050] OK [api] INSERT INTO logs ... | +| #0 /app/Services/LogService.php:28 | +| | +| [2025-01-23 14:23:02.100] OK [web] UPDATE users SET ... | +| #0 /app/Http/Controllers/AuthController.php:95 | ++--------------------------------------------------------------+ +``` + +- `vscode.window.createOutputChannel("MariaDB Profiler Live Tail")` で作成 +- `.show(true)` でフォーカスを奪わずに表示 +- `appendLine()` でリアルタイム追記 +- `clear()` でバッファクリア + +--- + +## 実装詳細 + +### データモデル + +#### QueryEntry.ts + +```typescript +export interface BacktraceFrame { + file: string; + line: number; + call: string; + function?: string; + class?: string; +} + +export interface QueryEntry { + query: string; + timestamp: string; + jobKey?: string; + status?: string; + tag?: string; + params?: string[]; + trace?: BacktraceFrame[]; +} + +// 算出プロパティはユーティリティ関数として提供 +export function getQueryType(entry: QueryEntry): 'SELECT' | 'INSERT' | 'UPDATE' | 'DELETE' | 'OTHER'; +export function getBoundQuery(entry: QueryEntry): string; +export function getTables(entry: QueryEntry): string[]; +export function getShortSql(entry: QueryEntry, maxLen?: number): string; +export function getSourceFile(entry: QueryEntry): string | null; +``` + +#### JobInfo.ts + +```typescript +export interface JobInfo { + key: string; + startedAt: string; + endedAt?: string; + queryCount?: number; + parent?: string; + isActive: boolean; +} + +export interface JobsFile { + active: Record; + completed: Record; +} +``` + +### サービス層 + +#### LogParserService.ts + +JetBrains 版と同等のオフセット対応 JSONL パーサ。 + +```typescript +export class LogParserService { + // ファイル全体パース + parseJsonlFile(filePath: string): QueryEntry[]; + + // オフセットからの差分パース (インクリメンタル更新用) + parseJsonlFileFromOffset(filePath: string, offset: number): { + entries: QueryEntry[]; + newOffset: number; + }; + + // Raw ログの末尾 N 行読み込み + readRawLogTail(filePath: string, lines: number): string; + + // Raw ログのオフセット読み込み (Live Tail 用) + tailRawLog(filePath: string, offset: number): { + content: string; + newOffset: number; + }; +} +``` + +**実装ポイント:** +- `fs.readFileSync` / `fs.openSync` + `fs.readSync` でオフセット読み込み +- JSON パースエラーは行単位でスキップし、OutputChannel にログ +- バッファサイズは設定値 (`tailBufferSize`) に従う + +#### JobManagerService.ts + +```typescript +export class JobManagerService { + constructor(private context: vscode.ExtensionContext); + + // jobs.json からジョブ一覧読み込み + loadJobs(): JobInfo[]; + + // アクティブ/完了済みジョブ取得 + getActiveJobs(): JobInfo[]; + getCompletedJobs(): JobInfo[]; + + // CLI 経由でジョブ開始/停止 + startJob(): Promise; // returns jobKey + stopJob(jobKey: string): Promise; + + // パスヘルパー + getJsonlPath(jobKey: string): string; + getRawLogPath(jobKey: string): string; + + // 設定値取得 + private getLogDir(): string; + private getPhpPath(): string; + private getCliScriptPath(): string; +} +``` + +**CLI 連携:** +- `child_process.execFile` で PHP CLI を呼び出し +- タイムアウト: 60 秒 +- CLI スクリプトパス: 設定値 → ワークスペースルート `cli/mariadb_profiler.php` の順にフォールバック + +#### FileWatcherService.ts + +```typescript +export class FileWatcherService implements vscode.Disposable { + // ファイル監視開始 + watchFile(filePath: string, onChange: () => void): void; + + // 監視停止 + unwatchFile(filePath: string): void; + + // 全監視停止 + dispose(): void; +} +``` + +**実装方針:** +- `fs.watchFile` (ポーリング、1000ms 間隔) を使用 + - `fs.watch` はネットワークマウントや Docker ボリュームで不安定なため +- ファイルサイズと `mtime` の変更を検知 +- `Disposable` パターンで Extension 終了時にクリーンアップ + +#### StatisticsService.ts + +```typescript +export interface QueryStats { + totalQueries: number; + byType: Record; // { SELECT: 78, INSERT: 12, ... } + byTable: Record; // { users: 45, posts: 30, ... } + byTag: Record; // { api: 60, web: 30, ... } +} + +export class StatisticsService { + computeStats(entries: QueryEntry[]): QueryStats; +} +``` + +#### FrameResolverService.ts + +JetBrains 版は Groovy スクリプトを使用しているが、VSCode 版では JavaScript に置き換える。 + +```typescript +export class FrameResolverService { + // ユーザースクリプトによるフレーム解決 + resolve(entry: QueryEntry): number; // returns frame index + + // スクリプトのキャッシュ無効化 + invalidateCache(): void; +} +``` + +**実装方針:** +- Node.js `vm` モジュールでサンドボックス実行 +- バインド変数: `trace`, `tag`, `query` (JetBrains 版と同じ) +- 戻り値: ハイライトすべきフレームのインデックス (0 始まり) +- コンパイルエラー / 実行エラーは OutputChannel にログ +- スクリプトキャッシュ: テキスト変更時のみ再コンパイル + +**デフォルトスクリプト例:** +```javascript +// trace: Array<{file, line, call, function, class_name}> +// tag: string, query: string +// Return: frame index to highlight (0-based) + +const tagDepthMap = { + 'api': 1, + 'web': 1, + 'cron': 0, +}; + +if (tag && tagDepthMap[tag] !== undefined) { + return tagDepthMap[tag]; +} +return 0; +``` + +### UI プロバイダ + +#### JobTreeProvider.ts + +VSCode ネイティブ TreeView でジョブ一覧を表示。 + +```typescript +export class JobTreeProvider implements vscode.TreeDataProvider { + private _onDidChangeTreeData = new vscode.EventEmitter(); + readonly onDidChangeTreeData = this._onDidChangeTreeData.event; + + refresh(): void; + getTreeItem(element: JobTreeItem): vscode.TreeItem; + getChildren(element?: JobTreeItem): JobTreeItem[]; +} + +export class JobTreeItem extends vscode.TreeItem { + constructor(public readonly job: JobInfo); +} +``` + +**表示:** +- アイコン: `$(circle-filled)` (アクティブ) / `$(circle-outline)` (完了) +- ラベル: `job.key` (先頭 12 文字に短縮) +- 説明 (description): `"42 queries, 3.2s"` 形式 +- コンテキスト値: `activeJob` / `completedJob` (コンテキストメニュー制御用) +- クリック時: `QueryTreeProvider` と `StatisticsTreeProvider` をそのジョブのデータで更新 + +#### QueryTreeProvider.ts + +クエリ一覧を TreeView で表示。展開すると詳細情報を表示。 + +```typescript +export class QueryTreeProvider implements vscode.TreeDataProvider { + private _onDidChangeTreeData = new vscode.EventEmitter(); + readonly onDidChangeTreeData = this._onDidChangeTreeData.event; + + // ジョブのクエリデータをロード + loadEntries(entries: QueryEntry[]): void; + + // フィルタ・検索 + setFilter(queryType: string | null): void; + setTagFilter(tag: string | null): void; + setSearchText(text: string | null): void; + clearFilters(): void; + + // TreeDataProvider + getTreeItem(element: QueryTreeItem): vscode.TreeItem; + getChildren(element?: QueryTreeItem): QueryTreeItem[]; +} + +type QueryTreeItem = QueryEntryItem | QueryMetadataItem | BacktraceFrameItem; + +// 第1階層: クエリエントリ (折りたたみ可能) +export class QueryEntryItem extends vscode.TreeItem { + contextValue = 'queryEntry'; + // ラベル: "SELECT SELECT u.* FROM us..." + // 説明: "[api] 14:23:01" + // アイコン: クエリ種別に応じた色 (ThemeIcon) + // SELECT=$(database), INSERT=$(add), UPDATE=$(edit), DELETE=$(trash) +} + +// 第2階層: メタデータ (展開時に表示) +export class QueryMetadataItem extends vscode.TreeItem { + // "Tables: users, posts" + // "Tags: api, v2" + // "Params: ?1 = 1" + // "Status: OK" +} + +// 第2階層: バックトレースフレーム (クリックでファイルジャンプ) +export class BacktraceFrameItem extends vscode.TreeItem { + // ラベル: "UserController.php:42 getUserPosts()" + // アイコン: $(arrow-right) (通常) / $(arrow-right) + 緑色 (解決済みフレーム) + // command: vscode.open (クリックでエディタにジャンプ) +} +``` + +**TreeView の description を活用したカラム風表示:** +``` + [icon] SELECT u.* FROM us... [api] 14:23:01 + ^^^^^ ^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^ + icon label description +``` + +TreeItem の `label` にクエリ SQL (短縮)、`description` にタグ+時刻を設定することで、擬似的な 2 カラム表示を実現。 + +#### StatisticsTreeProvider.ts + +統計情報を TreeView で表示。Unicode バーチャートで視覚化。 + +```typescript +export class StatisticsTreeProvider implements vscode.TreeDataProvider { + private _onDidChangeTreeData = new vscode.EventEmitter(); + readonly onDidChangeTreeData = this._onDidChangeTreeData.event; + + // 統計データ更新 + updateStats(stats: QueryStats): void; + + // TreeDataProvider + getTreeItem(element: StatTreeItem): vscode.TreeItem; + getChildren(element?: StatTreeItem): StatTreeItem[]; +} + +type StatTreeItem = StatCategoryItem | StatBarItem; + +// 第1階層: カテゴリ (折りたたみ可能) +export class StatCategoryItem extends vscode.TreeItem { + // "Total Queries: 156" + // "Query Types" (collapsible) + // "Top Tables" (collapsible) + // "Top Tags" (collapsible) +} + +// 第2階層: 個別統計 (Unicode バーチャート) +export class StatBarItem extends vscode.TreeItem { + // ラベル: "SELECT ████████████████████████" + // 説明: "78 (50%)" +} +``` + +**Unicode バーチャート生成:** +```typescript +function generateBar(value: number, max: number, barWidth: number = 24): string { + const filled = Math.round((value / max) * barWidth); + return '█'.repeat(filled) + '░'.repeat(barWidth - filled); +} + +// 例: generateBar(78, 156, 24) → "████████████░░░░░░░░░░░░" +``` + +#### QueryDocumentProvider.ts + +Virtual Document で SQL 詳細を表示。 + +```typescript +export class QueryDocumentProvider implements vscode.TextDocumentContentProvider { + // URI スキーム: "mariadb-profiler" + static readonly scheme = 'mariadb-profiler'; + + provideTextDocumentContent(uri: vscode.Uri): string; + + // クエリエントリを Virtual Document として開く + showQueryDetail(entry: QueryEntry, index: number): void; +} +``` + +**URI 設計:** +``` +mariadb-profiler:query-3.sql?job=abc123&index=3 +``` + +- `.sql` 拡張子 → VSCode が SQL 言語モードを自動適用 +- `TextDocumentContentProvider` のため読み取り専用 +- ドキュメント内容: SQL + パラメータ + バックトレース (全て SQL コメントで装飾) + +**利点:** +- SQL シンタックスハイライトが無料で得られる (VSCode 組み込み) +- ユーザーが好みの SQL Extension を入れていればそれも適用される +- `editor.wordWrap` など通常のエディタ設定が有効 + +--- + +## フィルタ・検索の UI フロー + +Webview ではフィルタバーを常時表示できるが、ネイティブ API ではそれができない。 +代わりに以下の方法で操作性を確保する: + +### 1. QuickPick によるフィルタ + +``` +[コマンドパレット or ツールバーボタン] + ↓ ++------------------------------------------+ +| Filter by Query Type | ++------------------------------------------+ +| > All (clear filter) | +| SELECT (78 queries) | +| INSERT (12 queries) | +| UPDATE (8 queries) | +| DELETE (2 queries) | ++------------------------------------------+ +``` + +### 2. QuickPick によるタグフィルタ + +``` ++------------------------------------------+ +| Filter by Tag | ++------------------------------------------+ +| > All (clear filter) | +| api (60 queries) | +| web (30 queries) | +| cron (10 queries) | ++------------------------------------------+ +``` + +### 3. InputBox による検索 + +``` ++------------------------------------------+ +| Search queries (SQL text) | ++------------------------------------------+ +| users | ++------------------------------------------+ +``` + +### 4. 現在のフィルタ状態表示 + +TreeView のタイトル (view/title) の `description` でフィルタ状態を表示: +- `"QUERIES (SELECT, tag:api, search:'users')"` のように表示 +- StatusBar アイテムでも現在のフィルタ状態を表示可能 + +--- + +## 実装ステップ + +### Phase 1: 基盤構築 + +1. **プロジェクトスキャフォールド** + - `package.json`, `tsconfig.json`, `esbuild.mjs` 作成 + - `.vscodeignore`, `.eslintrc.json` 設定 + - `npm init` + 依存パッケージインストール + +2. **Extension エントリポイント** + - `extension.ts` に `activate()` / `deactivate()` 実装 + - コマンド登録、全 TreeView 登録、OutputChannel 登録 + - `TextDocumentContentProvider` 登録 + +3. **データモデル定義** + - `QueryEntry.ts`, `JobInfo.ts`, `BacktraceFrame.ts` + - ユーティリティ関数 (queryType, boundQuery, tables, shortSql) + +4. **設定スキーマ** + - `package.json` の `contributes.configuration` 定義 + +### Phase 2: コア機能 - クエリログビューア + +5. **LogParserService** + - JSONL ファイルパーサ実装 + - オフセット対応の差分読み込み + +6. **JobManagerService** + - `jobs.json` 読み込み + - アクティブ/完了済みジョブ分類 + +7. **JobTreeProvider** + - TreeView でジョブ一覧表示 + - ジョブ選択イベント発火 + +8. **QueryTreeProvider** + - TreeView でクエリ一覧表示 + - 展開でメタデータ (テーブル、タグ、パラメータ) 表示 + - 展開でバックトレース表示 + +9. **QueryDocumentProvider** + - Virtual Document (`.sql`) でフル SQL 表示 + - SQL コメントでメタデータ・バックトレース記述 + +### Phase 3: フィルタ・ナビゲーション & Live Tail + +10. **フィルタ・検索コマンド** + - `filterByType`: QuickPick でクエリ種別フィルタ + - `filterByTag`: QuickPick でタグフィルタ + - `searchQuery`: InputBox でテキスト検索 + - `clearFilter`: 全フィルタクリア + +11. **バックトレースナビゲーション** + - `BacktraceFrameItem` クリックで `vscode.workspace.openTextDocument` + `showTextDocument` + - パスマッピング適用 + +12. **FrameResolverService** + - JavaScript (`vm` モジュール) によるフレーム解決 + - デフォルトスクリプト提供 + - 解決済みフレームに `$(arrow-right)` + 緑色アイコン + +13. **FileWatcherService** + - `fs.watchFile` によるポーリング監視 + - 変更検知コールバック + +14. **Live Tail (OutputChannel)** + - `vscode.window.createOutputChannel("MariaDB Profiler Live Tail")` + - `startLiveTail` / `stopLiveTail` コマンド + - `appendLine()` でリアルタイム追記 + - バッファサイズ超過時に `clear()` + 再出力 + +### Phase 4: 統計 & CLI 連携 + +15. **StatisticsService** + - クエリ統計計算 + +16. **StatisticsTreeProvider** + - TreeView で統計表示 + - Unicode バーチャート (`████░░░░`) で視覚化 + - カテゴリ: クエリ種別分布、テーブル別頻度、タグ別頻度 + +17. **CLI コマンド統合** + - `startJob` / `stopJob` コマンド実装 + - `child_process.execFile` で PHP CLI 呼び出し + +18. **OpenLog コマンド** + - ファイルピッカーで `.jsonl` ファイル選択 + +### Phase 5: 品質 & 仕上げ + +19. **ユニットテスト** + - LogParserService, JobManagerService, StatisticsService, QueryEntry のテスト + - Vitest で実行 + +20. **統合テスト** + - @vscode/test-electron で Extension 全体テスト + +21. **アイコン・UI 微調整** + - SVG アイコン作成 (JetBrains 版を参考) + - ThemeIcon カラー設定 + +22. **ドキュメント** + - README.md (Marketplace 用) + - CHANGELOG.md + +--- + +## Webview 版との比較 + +### メリット (ネイティブ API 方式) + +| 項目 | 詳細 | +|------|------| +| **依存ゼロ** | HTML/CSS/JS ビルドパイプライン不要。Webview フレームワーク (Preact/React) 不要 | +| **軽量** | Chromium プロセスを起動しないため省メモリ | +| **テーマ完全統合** | ネイティブ UI は VSCode テーマに自動追従。CSS 変数のマッピング不要 | +| **実装が速い** | Extension ↔ Webview 間の `postMessage` 通信プロトコル設計が不要 | +| **Remote SSH / WSL** | Webview より安定動作 | +| **ビルド簡易** | esbuild で Extension のみバンドル。Webview 用の別ビルド不要 | +| **セキュリティ** | CSP 設定、nonce 管理、`localResourceRoots` 管理が不要 | + +### デメリット・制約 + +| 項目 | 詳細 | 緩和策 | +|------|------|--------| +| **テーブル表示** | TreeView にカラム幅調整・ソート・水平スクロールがない | `label` + `description` で擬似 2 カラム表示 | +| **統計チャート** | 棒グラフは Unicode 文字 (`████`) での近似表現 | 十分視覚的に分かりやすい | +| **フィルタ UI** | 常時表示のフィルタバーが作れない | QuickPick + ツールバーボタン + ステータスバー表示 | +| **レイアウト** | スプリットペイン・複雑なレイアウト不可 | サイドバー 3 セクション構成で十分 | +| **仮想スクロール** | TreeView は VSCode が管理 (10,000 件でもパフォーマンス良好) | `maxQueries` 設定で上限制御 | + +--- + +## JetBrains Plugin との差異・VSCode 固有の考慮事項 + +### 1. UI アーキテクチャ + +| 観点 | JetBrains | VSCode | +|------|-----------|--------| +| UI 描画 | Swing (ネイティブ Java UI) | VSCode TreeView + Virtual Document + OutputChannel | +| テーブル | JTable + TableModel | TreeView (展開式) | +| チャート | Graphics2D | Unicode バーチャート (`████░░░░`) | +| スプリットペイン | JSplitPane | サイドバー 3 セクション | +| SQL 表示 | JTextArea + HTML | Virtual Document (.sql) - エディタタブ | +| Live Tail | カスタムパネル | OutputChannel | +| フィルタ | テーブル上のフィルタバー | QuickPick (コマンドパレット) | +| ファイル選択 | JFileChooser | `vscode.window.showOpenDialog` | +| 通知 | Messages.showInfoMessage | `vscode.window.showInformationMessage` | + +### 2. パフォーマンス + +- **TreeView**: VSCode が内部で仮想化しているため、大量アイテムでもパフォーマンス良好 +- **Virtual Document**: エディタの標準パスで動作するため、大きな SQL でも問題なし +- **OutputChannel**: ネイティブテキスト出力のため、高速にログ追記可能 + +### 3. フレームリゾルバ + +- JetBrains 版: **Groovy** スクリプト (JVM 上で実行) +- VSCode 版: **JavaScript** スクリプト (Node.js `vm` モジュール) +- API は同等 (`trace`, `tag`, `query` 変数を提供) + +### 4. ファイル監視 + +- JetBrains 版: IntelliJ VFS イベント + Timer ポーリング +- VSCode 版: `fs.watchFile` ポーリング (Docker ボリューム対応) +- `vscode.workspace.FileSystemWatcher` はワークスペース内のみ対象のため、外部ディレクトリには `fs.watchFile` を使用 + +### 5. 設定管理 + +- JetBrains 版: `PersistentStateComponent` + カスタム設定ダイアログ +- VSCode 版: `contributes.configuration` (VSCode 標準 Settings UI で編集) + +--- + +## リスク・課題 + +| リスク | 影響度 | 対策 | +|--------|--------|------| +| TreeView で大量クエリ表示時のパフォーマンス | 低 | VSCode の TreeView は内部で遅延読み込み。`maxQueries` で上限設定 | +| Docker ボリュームのファイル監視 | 中 | `fs.watchFile` ポーリング (1秒間隔) | +| フィルタ操作の手数 (QuickPick 呼び出し) | 中 | ツールバーボタンで 1 クリックアクセス。キーボードショートカット設定可能 | +| TreeView のカラム表示制限 | 低 | `label` + `description` で十分な情報表示可能 | +| Remote SSH / WSL 環境 | 低 | ネイティブ API のみ使用のため、Webview より安定 | + +--- + +## 将来の拡張可能性 + +1. **CodeLens 統合** - PHP ファイル上でクエリ実行元行にインラインでクエリ情報表示 +2. **Diagnostic 統合** - 重いクエリを Warning として表示 +3. **Testing Integration** - テスト実行時の自動プロファイリング +4. **TreeView Drag & Drop** - クエリをエディタにドラッグして SQL 挿入 +5. **StatusBar 統合** - アクティブジョブのクエリ数をリアルタイム表示 diff --git a/vscode-extension/.gitignore b/vscode-extension/.gitignore new file mode 100644 index 0000000..a08e1da --- /dev/null +++ b/vscode-extension/.gitignore @@ -0,0 +1,3 @@ +node_modules/ +dist/ +*.vsix diff --git a/vscode-extension/.vscodeignore b/vscode-extension/.vscodeignore new file mode 100644 index 0000000..7efcac3 --- /dev/null +++ b/vscode-extension/.vscodeignore @@ -0,0 +1,10 @@ +.vscode/** +node_modules/** +src/** +test/** +tsconfig.json +esbuild.mjs +.eslintrc.json +**/*.map +**/*.ts +!dist/** diff --git a/vscode-extension/esbuild.mjs b/vscode-extension/esbuild.mjs new file mode 100644 index 0000000..c630626 --- /dev/null +++ b/vscode-extension/esbuild.mjs @@ -0,0 +1,25 @@ +import * as esbuild from 'esbuild'; + +const production = process.argv.includes('--production'); +const watch = process.argv.includes('--watch'); + +const ctx = await esbuild.context({ + entryPoints: ['src/extension.ts'], + bundle: true, + format: 'cjs', + minify: production, + sourcemap: !production, + sourcesContent: false, + platform: 'node', + outfile: 'dist/extension.js', + external: ['vscode'], + logLevel: 'info', +}); + +if (watch) { + await ctx.watch(); + console.log('Watching for changes...'); +} else { + await ctx.rebuild(); + await ctx.dispose(); +} diff --git a/vscode-extension/package-lock.json b/vscode-extension/package-lock.json new file mode 100644 index 0000000..37c0969 --- /dev/null +++ b/vscode-extension/package-lock.json @@ -0,0 +1,3931 @@ +{ + "name": "mariadb-profiler-viewer", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "mariadb-profiler-viewer", + "version": "0.1.0", + "devDependencies": { + "@types/node": "^20.11.0", + "@types/vscode": "^1.85.0", + "@vscode/vsce": "^2.22.0", + "esbuild": "^0.27.3", + "typescript": "^5.3.0", + "vitest": "^4.0.18" + }, + "engines": { + "vscode": "^1.85.0" + } + }, + "node_modules/@azure/abort-controller": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-2.1.2.tgz", + "integrity": "sha512-nBrLsEWm4J2u5LpAPjxADTlq3trDgVZZXHNKabeXZtpq3d3AbN/KGO82R87rdDz5/lYB024rtEf10/q0urNgsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@azure/core-auth": { + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/@azure/core-auth/-/core-auth-1.10.1.tgz", + "integrity": "sha512-ykRMW8PjVAn+RS6ww5cmK9U2CyH9p4Q88YJwvUslfuMmN98w/2rdGRLPqJYObapBCdzBVeDgYWdJnFPFb7qzpg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@azure/abort-controller": "^2.1.2", + "@azure/core-util": "^1.13.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@azure/core-client": { + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/@azure/core-client/-/core-client-1.10.1.tgz", + "integrity": "sha512-Nh5PhEOeY6PrnxNPsEHRr9eimxLwgLlpmguQaHKBinFYA/RU9+kOYVOQqOrTsCL+KSxrLLl1gD8Dk5BFW/7l/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@azure/abort-controller": "^2.1.2", + "@azure/core-auth": "^1.10.0", + "@azure/core-rest-pipeline": "^1.22.0", + "@azure/core-tracing": "^1.3.0", + "@azure/core-util": "^1.13.0", + "@azure/logger": "^1.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@azure/core-rest-pipeline": { + "version": "1.22.2", + "resolved": "https://registry.npmjs.org/@azure/core-rest-pipeline/-/core-rest-pipeline-1.22.2.tgz", + "integrity": "sha512-MzHym+wOi8CLUlKCQu12de0nwcq9k9Kuv43j4Wa++CsCpJwps2eeBQwD2Bu8snkxTtDKDx4GwjuR9E8yC8LNrg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@azure/abort-controller": "^2.1.2", + "@azure/core-auth": "^1.10.0", + "@azure/core-tracing": "^1.3.0", + "@azure/core-util": "^1.13.0", + "@azure/logger": "^1.3.0", + "@typespec/ts-http-runtime": "^0.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@azure/core-tracing": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@azure/core-tracing/-/core-tracing-1.3.1.tgz", + "integrity": "sha512-9MWKevR7Hz8kNzzPLfX4EAtGM2b8mr50HPDBvio96bURP/9C+HjdH3sBlLSNNrvRAr5/k/svoH457gB5IKpmwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@azure/core-util": { + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/@azure/core-util/-/core-util-1.13.1.tgz", + "integrity": "sha512-XPArKLzsvl0Hf0CaGyKHUyVgF7oDnhKoP85Xv6M4StF/1AhfORhZudHtOyf2s+FcbuQ9dPRAjB8J2KvRRMUK2A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@azure/abort-controller": "^2.1.2", + "@typespec/ts-http-runtime": "^0.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@azure/identity": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/@azure/identity/-/identity-4.13.0.tgz", + "integrity": "sha512-uWC0fssc+hs1TGGVkkghiaFkkS7NkTxfnCH+Hdg+yTehTpMcehpok4PgUKKdyCH+9ldu6FhiHRv84Ntqj1vVcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@azure/abort-controller": "^2.0.0", + "@azure/core-auth": "^1.9.0", + "@azure/core-client": "^1.9.2", + "@azure/core-rest-pipeline": "^1.17.0", + "@azure/core-tracing": "^1.0.0", + "@azure/core-util": "^1.11.0", + "@azure/logger": "^1.0.0", + "@azure/msal-browser": "^4.2.0", + "@azure/msal-node": "^3.5.0", + "open": "^10.1.0", + "tslib": "^2.2.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@azure/logger": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@azure/logger/-/logger-1.3.0.tgz", + "integrity": "sha512-fCqPIfOcLE+CGqGPd66c8bZpwAji98tZ4JI9i/mlTNTlsIWslCfpg48s/ypyLxZTump5sypjrKn2/kY7q8oAbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typespec/ts-http-runtime": "^0.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@azure/msal-browser": { + "version": "4.28.2", + "resolved": "https://registry.npmjs.org/@azure/msal-browser/-/msal-browser-4.28.2.tgz", + "integrity": "sha512-6vYUMvs6kJxJgxaCmHn/F8VxjLHNh7i9wzfwPGf8kyBJ8Gg2yvBXx175Uev8LdrD1F5C4o7qHa2CC4IrhGE1XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@azure/msal-common": "15.14.2" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/@azure/msal-common": { + "version": "15.14.2", + "resolved": "https://registry.npmjs.org/@azure/msal-common/-/msal-common-15.14.2.tgz", + "integrity": "sha512-n8RBJEUmd5QotoqbZfd+eGBkzuFI1KX6jw2b3WcpSyGjwmzoeI/Jb99opIBPHpb8y312NB+B6+FGi2ZVSR8yfA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/@azure/msal-node": { + "version": "3.8.7", + "resolved": "https://registry.npmjs.org/@azure/msal-node/-/msal-node-3.8.7.tgz", + "integrity": "sha512-a+Xnrae+uwLnlw68bplS1X4kuJ9F/7K6afuMFyRkNIskhjgDezl5Fhrx+1pmAlDmC0VaaAxjRQMp1OmcqVwkIg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@azure/msal-common": "15.14.2", + "jsonwebtoken": "^9.0.0", + "uuid": "^8.3.0" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.3.tgz", + "integrity": "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.3.tgz", + "integrity": "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.3.tgz", + "integrity": "sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.3.tgz", + "integrity": "sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.3.tgz", + "integrity": "sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.3.tgz", + "integrity": "sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.3.tgz", + "integrity": "sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.3.tgz", + "integrity": "sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.3.tgz", + "integrity": "sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.3.tgz", + "integrity": "sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.3.tgz", + "integrity": "sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.3.tgz", + "integrity": "sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.3.tgz", + "integrity": "sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.3.tgz", + "integrity": "sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.3.tgz", + "integrity": "sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.3.tgz", + "integrity": "sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.3.tgz", + "integrity": "sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.3.tgz", + "integrity": "sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.3.tgz", + "integrity": "sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.3.tgz", + "integrity": "sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.3.tgz", + "integrity": "sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.3.tgz", + "integrity": "sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.3.tgz", + "integrity": "sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.3.tgz", + "integrity": "sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.3.tgz", + "integrity": "sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.3.tgz", + "integrity": "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "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", + "integrity": "sha512-A6ehUVSiSaaliTxai040ZpZ2zTevHYbvu/lDoeAteHI8QnaosIzm4qwtezfRg1jOYaUmnzLX1AOD6Z+UJjtifg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.57.1.tgz", + "integrity": "sha512-dQaAddCY9YgkFHZcFNS/606Exo8vcLHwArFZ7vxXq4rigo2bb494/xKMMwRRQW6ug7Js6yXmBZhSBRuBvCCQ3w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.57.1.tgz", + "integrity": "sha512-crNPrwJOrRxagUYeMn/DZwqN88SDmwaJ8Cvi/TN1HnWBU7GwknckyosC2gd0IqYRsHDEnXf328o9/HC6OkPgOg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.57.1.tgz", + "integrity": "sha512-Ji8g8ChVbKrhFtig5QBV7iMaJrGtpHelkB3lsaKzadFBe58gmjfGXAOfI5FV0lYMH8wiqsxKQ1C9B0YTRXVy4w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.57.1.tgz", + "integrity": "sha512-R+/WwhsjmwodAcz65guCGFRkMb4gKWTcIeLy60JJQbXrJ97BOXHxnkPFrP+YwFlaS0m+uWJTstrUA9o+UchFug==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.57.1.tgz", + "integrity": "sha512-IEQTCHeiTOnAUC3IDQdzRAGj3jOAYNr9kBguI7MQAAZK3caezRrg0GxAb6Hchg4lxdZEI5Oq3iov/w/hnFWY9Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.57.1.tgz", + "integrity": "sha512-F8sWbhZ7tyuEfsmOxwc2giKDQzN3+kuBLPwwZGyVkLlKGdV1nvnNwYD0fKQ8+XS6hp9nY7B+ZeK01EBUE7aHaw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.57.1.tgz", + "integrity": "sha512-rGfNUfn0GIeXtBP1wL5MnzSj98+PZe/AXaGBCRmT0ts80lU5CATYGxXukeTX39XBKsxzFpEeK+Mrp9faXOlmrw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.57.1.tgz", + "integrity": "sha512-MMtej3YHWeg/0klK2Qodf3yrNzz6CGjo2UntLvk2RSPlhzgLvYEB3frRvbEF2wRKh1Z2fDIg9KRPe1fawv7C+g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.57.1.tgz", + "integrity": "sha512-1a/qhaaOXhqXGpMFMET9VqwZakkljWHLmZOX48R0I/YLbhdxr1m4gtG1Hq7++VhVUmf+L3sTAf9op4JlhQ5u1Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.57.1.tgz", + "integrity": "sha512-QWO6RQTZ/cqYtJMtxhkRkidoNGXc7ERPbZN7dVW5SdURuLeVU7lwKMpo18XdcmpWYd0qsP1bwKPf7DNSUinhvA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.57.1.tgz", + "integrity": "sha512-xpObYIf+8gprgWaPP32xiN5RVTi/s5FCR+XMXSKmhfoJjrpRAjCuuqQXyxUa/eJTdAE6eJ+KDKaoEqjZQxh3Gw==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.57.1.tgz", + "integrity": "sha512-4BrCgrpZo4hvzMDKRqEaW1zeecScDCR+2nZ86ATLhAoJ5FQ+lbHVD3ttKe74/c7tNT9c6F2viwB3ufwp01Oh2w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.57.1.tgz", + "integrity": "sha512-NOlUuzesGauESAyEYFSe3QTUguL+lvrN1HtwEEsU2rOwdUDeTMJdO5dUYl/2hKf9jWydJrO9OL/XSSf65R5+Xw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.57.1.tgz", + "integrity": "sha512-ptA88htVp0AwUUqhVghwDIKlvJMD/fmL/wrQj99PRHFRAG6Z5nbWoWG4o81Nt9FT+IuqUQi+L31ZKAFeJ5Is+A==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.57.1.tgz", + "integrity": "sha512-S51t7aMMTNdmAMPpBg7OOsTdn4tySRQvklmL3RpDRyknk87+Sp3xaumlatU+ppQ+5raY7sSTcC2beGgvhENfuw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.57.1.tgz", + "integrity": "sha512-Bl00OFnVFkL82FHbEqy3k5CUCKH6OEJL54KCyx2oqsmZnFTR8IoNqBF+mjQVcRCT5sB6yOvK8A37LNm/kPJiZg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.57.1.tgz", + "integrity": "sha512-ABca4ceT4N+Tv/GtotnWAeXZUZuM/9AQyCyKYyKnpk4yoA7QIAuBt6Hkgpw8kActYlew2mvckXkvx0FfoInnLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.57.1.tgz", + "integrity": "sha512-HFps0JeGtuOR2convgRRkHCekD7j+gdAuXM+/i6kGzQtFhlCtQkpwtNzkNj6QhCDp7DRJ7+qC/1Vg2jt5iSOFw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.57.1.tgz", + "integrity": "sha512-H+hXEv9gdVQuDTgnqD+SQffoWoc0Of59AStSzTEj/feWTBAnSfSD3+Dql1ZruJQxmykT/JVY0dE8Ka7z0DH1hw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.57.1.tgz", + "integrity": "sha512-4wYoDpNg6o/oPximyc/NG+mYUejZrCU2q+2w6YZqrAs2UcNUChIZXjtafAiiZSUc7On8v5NyNj34Kzj/Ltk6dQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.57.1.tgz", + "integrity": "sha512-O54mtsV/6LW3P8qdTcamQmuC990HDfR71lo44oZMZlXU4tzLrbvTii87Ni9opq60ds0YzuAlEr/GNwuNluZyMQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.57.1.tgz", + "integrity": "sha512-P3dLS+IerxCT/7D2q2FYcRdWRl22dNbrbBEtxdWhXrfIMPP9lQhb5h4Du04mdl5Woq05jVCDPCMF7Ub0NAjIew==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.57.1.tgz", + "integrity": "sha512-VMBH2eOOaKGtIJYleXsi2B8CPVADrh+TyNxJ4mWPnKfLB/DBUmzW+5m1xUrcwWoMfSLagIRpjUFeW5CO5hyciQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.57.1.tgz", + "integrity": "sha512-mxRFDdHIWRxg3UfIIAwCm6NzvxG0jDX/wBN6KsQFTvKFqqg9vTrWUE68qEjHt19A5wwx5X5aUi2zuZT7YR0jrA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "20.19.33", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.33.tgz", + "integrity": "sha512-Rs1bVAIdBs5gbTIKza/tgpMuG1k3U/UMJLWecIMxNdJFDMzcM5LOiLVRYh3PilWEYDIeUDv7bpiHPLPsbydGcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/vscode": { + "version": "1.109.0", + "resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.109.0.tgz", + "integrity": "sha512-0Pf95rnwEIwDbmXGC08r0B4TQhAbsHQ5UyTIgVgoieDe4cOnf92usuR5dEczb6bTKEp7ziZH4TV1TRGPPCExtw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@typespec/ts-http-runtime": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@typespec/ts-http-runtime/-/ts-http-runtime-0.3.3.tgz", + "integrity": "sha512-91fp6CAAJSRtH5ja95T1FHSKa8aPW9/Zw6cta81jlZTUw/+Vq8jM/AfF/14h2b71wwR84JUTW/3Y8QPhDAawFA==", + "dev": true, + "license": "MIT", + "dependencies": { + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@vitest/expect": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.0.18.tgz", + "integrity": "sha512-8sCWUyckXXYvx4opfzVY03EOiYVxyNrHS5QxX3DAIi5dpJAAkyJezHCP77VMX4HKA2LDT/Jpfo8i2r5BE3GnQQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.0.0", + "@types/chai": "^5.2.2", + "@vitest/spy": "4.0.18", + "@vitest/utils": "4.0.18", + "chai": "^6.2.1", + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.0.18.tgz", + "integrity": "sha512-HhVd0MDnzzsgevnOWCBj5Otnzobjy5wLBe4EdeeFGv8luMsGcYqDuFRMcttKWZA5vVO8RFjexVovXvAM4JoJDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "4.0.18", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.21" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^6.0.0 || ^7.0.0-0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.0.18.tgz", + "integrity": "sha512-P24GK3GulZWC5tz87ux0m8OADrQIUVDPIjjj65vBXYG17ZeU3qD7r+MNZ1RNv4l8CGU2vtTRqixrOi9fYk/yKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.0.18.tgz", + "integrity": "sha512-rpk9y12PGa22Jg6g5M3UVVnTS7+zycIGk9ZNGN+m6tZHKQb7jrP7/77WfZy13Y/EUDd52NDsLRQhYKtv7XfPQw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "4.0.18", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.0.18.tgz", + "integrity": "sha512-PCiV0rcl7jKQjbgYqjtakly6T1uwv/5BQ9SwBLekVg/EaYeQFPiXcgrC2Y7vDMA8dM1SUEAEV82kgSQIlXNMvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.0.18", + "magic-string": "^0.30.21", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.0.18.tgz", + "integrity": "sha512-cbQt3PTSD7P2OARdVW3qWER5EGq7PHlvE+QfzSC0lbwO+xnt7+XH06ZzFjFRgzUX//JmpxrCu92VdwvEPlWSNw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.0.18.tgz", + "integrity": "sha512-msMRKLMVLWygpK3u2Hybgi4MNjcYJvwTb0Ru09+fOyCXIgT5raYP041DRRdiJiI3k/2U6SEbAETB3YtBrUkCFA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.0.18", + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vscode/vsce": { + "version": "2.32.0", + "resolved": "https://registry.npmjs.org/@vscode/vsce/-/vsce-2.32.0.tgz", + "integrity": "sha512-3EFJfsgrSftIqt3EtdRcAygy/OJ3hstyI1cDmIgkU9CFZW5C+3djr6mfosndCUqcVYuyjmxOK1xmFp/Bq7+NIg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@azure/identity": "^4.1.0", + "@vscode/vsce-sign": "^2.0.0", + "azure-devops-node-api": "^12.5.0", + "chalk": "^2.4.2", + "cheerio": "^1.0.0-rc.9", + "cockatiel": "^3.1.2", + "commander": "^6.2.1", + "form-data": "^4.0.0", + "glob": "^7.0.6", + "hosted-git-info": "^4.0.2", + "jsonc-parser": "^3.2.0", + "leven": "^3.1.0", + "markdown-it": "^12.3.2", + "mime": "^1.3.4", + "minimatch": "^3.0.3", + "parse-semver": "^1.1.1", + "read": "^1.0.7", + "semver": "^7.5.2", + "tmp": "^0.2.1", + "typed-rest-client": "^1.8.4", + "url-join": "^4.0.1", + "xml2js": "^0.5.0", + "yauzl": "^2.3.1", + "yazl": "^2.2.2" + }, + "bin": { + "vsce": "vsce" + }, + "engines": { + "node": ">= 16" + }, + "optionalDependencies": { + "keytar": "^7.7.0" + } + }, + "node_modules/@vscode/vsce-sign": { + "version": "2.0.9", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign/-/vsce-sign-2.0.9.tgz", + "integrity": "sha512-8IvaRvtFyzUnGGl3f5+1Cnor3LqaUWvhaUjAYO8Y39OUYlOf3cRd+dowuQYLpZcP3uwSG+mURwjEBOSq4SOJ0g==", + "dev": true, + "hasInstallScript": true, + "license": "SEE LICENSE IN LICENSE.txt", + "optionalDependencies": { + "@vscode/vsce-sign-alpine-arm64": "2.0.6", + "@vscode/vsce-sign-alpine-x64": "2.0.6", + "@vscode/vsce-sign-darwin-arm64": "2.0.6", + "@vscode/vsce-sign-darwin-x64": "2.0.6", + "@vscode/vsce-sign-linux-arm": "2.0.6", + "@vscode/vsce-sign-linux-arm64": "2.0.6", + "@vscode/vsce-sign-linux-x64": "2.0.6", + "@vscode/vsce-sign-win32-arm64": "2.0.6", + "@vscode/vsce-sign-win32-x64": "2.0.6" + } + }, + "node_modules/@vscode/vsce-sign-alpine-arm64": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-alpine-arm64/-/vsce-sign-alpine-arm64-2.0.6.tgz", + "integrity": "sha512-wKkJBsvKF+f0GfsUuGT0tSW0kZL87QggEiqNqK6/8hvqsXvpx8OsTEc3mnE1kejkh5r+qUyQ7PtF8jZYN0mo8Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "SEE LICENSE IN LICENSE.txt", + "optional": true, + "os": [ + "alpine" + ] + }, + "node_modules/@vscode/vsce-sign-alpine-x64": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-alpine-x64/-/vsce-sign-alpine-x64-2.0.6.tgz", + "integrity": "sha512-YoAGlmdK39vKi9jA18i4ufBbd95OqGJxRvF3n6ZbCyziwy3O+JgOpIUPxv5tjeO6gQfx29qBivQ8ZZTUF2Ba0w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "SEE LICENSE IN LICENSE.txt", + "optional": true, + "os": [ + "alpine" + ] + }, + "node_modules/@vscode/vsce-sign-darwin-arm64": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-darwin-arm64/-/vsce-sign-darwin-arm64-2.0.6.tgz", + "integrity": "sha512-5HMHaJRIQuozm/XQIiJiA0W9uhdblwwl2ZNDSSAeXGO9YhB9MH5C4KIHOmvyjUnKy4UCuiP43VKpIxW1VWP4tQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "SEE LICENSE IN LICENSE.txt", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@vscode/vsce-sign-darwin-x64": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-darwin-x64/-/vsce-sign-darwin-x64-2.0.6.tgz", + "integrity": "sha512-25GsUbTAiNfHSuRItoQafXOIpxlYj+IXb4/qarrXu7kmbH94jlm5sdWSCKrrREs8+GsXF1b+l3OB7VJy5jsykw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "SEE LICENSE IN LICENSE.txt", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@vscode/vsce-sign-linux-arm": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-linux-arm/-/vsce-sign-linux-arm-2.0.6.tgz", + "integrity": "sha512-UndEc2Xlq4HsuMPnwu7420uqceXjs4yb5W8E2/UkaHBB9OWCwMd3/bRe/1eLe3D8kPpxzcaeTyXiK3RdzS/1CA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "SEE LICENSE IN LICENSE.txt", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@vscode/vsce-sign-linux-arm64": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-linux-arm64/-/vsce-sign-linux-arm64-2.0.6.tgz", + "integrity": "sha512-cfb1qK7lygtMa4NUl2582nP7aliLYuDEVpAbXJMkDq1qE+olIw/es+C8j1LJwvcRq1I2yWGtSn3EkDp9Dq5FdA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "SEE LICENSE IN LICENSE.txt", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@vscode/vsce-sign-linux-x64": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-linux-x64/-/vsce-sign-linux-x64-2.0.6.tgz", + "integrity": "sha512-/olerl1A4sOqdP+hjvJ1sbQjKN07Y3DVnxO4gnbn/ahtQvFrdhUi0G1VsZXDNjfqmXw57DmPi5ASnj/8PGZhAA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "SEE LICENSE IN LICENSE.txt", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@vscode/vsce-sign-win32-arm64": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-win32-arm64/-/vsce-sign-win32-arm64-2.0.6.tgz", + "integrity": "sha512-ivM/MiGIY0PJNZBoGtlRBM/xDpwbdlCWomUWuLmIxbi1Cxe/1nooYrEQoaHD8ojVRgzdQEUzMsRbyF5cJJgYOg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "SEE LICENSE IN LICENSE.txt", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@vscode/vsce-sign-win32-x64": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-win32-x64/-/vsce-sign-win32-x64-2.0.6.tgz", + "integrity": "sha512-mgth9Kvze+u8CruYMmhHw6Zgy3GRX2S+Ed5oSokDEK5vPEwGGKnmuXua9tmFhomeAnhgJnL4DCna3TiNuGrBTQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "SEE LICENSE IN LICENSE.txt", + "optional": true, + "os": [ + "win32" + ] + }, + "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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "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==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/azure-devops-node-api": { + "version": "12.5.0", + "resolved": "https://registry.npmjs.org/azure-devops-node-api/-/azure-devops-node-api-12.5.0.tgz", + "integrity": "sha512-R5eFskGvOm3U/GzeAuxRkUsAl0hrAwGgWn6zAd2KrZmrEhWZVqLew4OOupbQlXUuojUzpGtq62SmdhJ06N88og==", + "dev": true, + "license": "MIT", + "dependencies": { + "tunnel": "0.0.6", + "typed-rest-client": "^1.8.4" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "optional": true + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", + "dev": true, + "license": "ISC" + }, + "node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "optional": true, + "dependencies": { + "base64-js": "^1.3.1", + "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==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/bundle-name": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-4.1.0.tgz", + "integrity": "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "run-applescript": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/chai": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", + "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/cheerio": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.2.0.tgz", + "integrity": "sha512-WDrybc/gKFpTYQutKIK6UvfcuxijIZfMfXaYm8NMsPQxSYvf+13fXUJ4rztGGbJcBQ/GF55gvrZ0Bc0bj/mqvg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cheerio-select": "^2.1.0", + "dom-serializer": "^2.0.0", + "domhandler": "^5.0.3", + "domutils": "^3.2.2", + "encoding-sniffer": "^0.2.1", + "htmlparser2": "^10.1.0", + "parse5": "^7.3.0", + "parse5-htmlparser2-tree-adapter": "^7.1.0", + "parse5-parser-stream": "^7.1.2", + "undici": "^7.19.0", + "whatwg-mimetype": "^4.0.0" + }, + "engines": { + "node": ">=20.18.1" + }, + "funding": { + "url": "https://github.com/cheeriojs/cheerio?sponsor=1" + } + }, + "node_modules/cheerio-select": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cheerio-select/-/cheerio-select-2.1.0.tgz", + "integrity": "sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0", + "css-select": "^5.1.0", + "css-what": "^6.1.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "dev": true, + "license": "ISC", + "optional": true + }, + "node_modules/cockatiel": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/cockatiel/-/cockatiel-3.2.1.tgz", + "integrity": "sha512-gfrHV6ZPkquExvMh9IOkKsBzNDk6sDuZ6DdBGUBkvFnTCqCxzpuq48RySgP0AnaqQkw2zynOFj9yly6T1Q2G5Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16" + } + }, + "node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true, + "license": "MIT" + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/commander": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-6.2.1.tgz", + "integrity": "sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/css-select": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.2.2.tgz", + "integrity": "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0", + "css-what": "^6.1.0", + "domhandler": "^5.0.2", + "domutils": "^3.0.1", + "nth-check": "^2.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/css-what": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.2.2.tgz", + "integrity": "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "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" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/default-browser": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-5.5.0.tgz", + "integrity": "sha512-H9LMLr5zwIbSxrmvikGuI/5KGhZ8E2zH3stkMgM5LpOWDutGM2JZaj460Udnf1a+946zc7YBgrqEWwbk7zHvGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "bundle-name": "^4.1.0", + "default-browser-id": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/default-browser-id": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-5.0.1.tgz", + "integrity": "sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/define-lazy-prop": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz", + "integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "dev": true, + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "BSD-2-Clause" + }, + "node_modules/domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "domelementtype": "^2.3.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/domutils": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz", + "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "node_modules/encoding-sniffer": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/encoding-sniffer/-/encoding-sniffer-0.2.1.tgz", + "integrity": "sha512-5gvq20T6vfpekVtqrYQsSCFZ1wEg5+wW0/QaZMWkFr6BqD3NfKs0rLCx4rrVlSWJeZb5NBJgVLswK/w2MWU+Gw==", + "dev": true, + "license": "MIT", + "dependencies": { + "iconv-lite": "^0.6.3", + "whatwg-encoding": "^3.1.1" + }, + "funding": { + "url": "https://github.com/fb55/encoding-sniffer?sponsor=1" + } + }, + "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", + "optional": true, + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "dev": true, + "license": "MIT" + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.3.tgz", + "integrity": "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.3", + "@esbuild/android-arm": "0.27.3", + "@esbuild/android-arm64": "0.27.3", + "@esbuild/android-x64": "0.27.3", + "@esbuild/darwin-arm64": "0.27.3", + "@esbuild/darwin-x64": "0.27.3", + "@esbuild/freebsd-arm64": "0.27.3", + "@esbuild/freebsd-x64": "0.27.3", + "@esbuild/linux-arm": "0.27.3", + "@esbuild/linux-arm64": "0.27.3", + "@esbuild/linux-ia32": "0.27.3", + "@esbuild/linux-loong64": "0.27.3", + "@esbuild/linux-mips64el": "0.27.3", + "@esbuild/linux-ppc64": "0.27.3", + "@esbuild/linux-riscv64": "0.27.3", + "@esbuild/linux-s390x": "0.27.3", + "@esbuild/linux-x64": "0.27.3", + "@esbuild/netbsd-arm64": "0.27.3", + "@esbuild/netbsd-x64": "0.27.3", + "@esbuild/openbsd-arm64": "0.27.3", + "@esbuild/openbsd-x64": "0.27.3", + "@esbuild/openharmony-arm64": "0.27.3", + "@esbuild/sunos-x64": "0.27.3", + "@esbuild/win32-arm64": "0.27.3", + "@esbuild/win32-ia32": "0.27.3", + "@esbuild/win32-x64": "0.27.3" + } + }, + "node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.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==", + "dev": true, + "license": "(MIT OR WTFPL)", + "optional": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/expect-type": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", + "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "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==", + "dev": true, + "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", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "dev": true, + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true, + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/github-from-package": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", + "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hosted-git-info": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-4.1.0.tgz", + "integrity": "sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==", + "dev": true, + "license": "ISC", + "dependencies": { + "lru-cache": "^6.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/htmlparser2": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-10.1.0.tgz", + "integrity": "sha512-VTZkM9GWRAtEpveh7MSF6SjjrpNVNNVJfFup7xTY3UpFtm67foy9HDVXneLtFVt4pMz5kZtgNcvCniNFb1hlEQ==", + "dev": true, + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.2.2", + "entities": "^7.0.1" + } + }, + "node_modules/htmlparser2/node_modules/entities": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz", + "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause", + "optional": true + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "dev": true, + "license": "ISC", + "optional": true + }, + "node_modules/is-docker": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz", + "integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==", + "dev": true, + "license": "MIT", + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-inside-container": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz", + "integrity": "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-docker": "^3.0.0" + }, + "bin": { + "is-inside-container": "cli.js" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-wsl": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.1.tgz", + "integrity": "sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-inside-container": "^1.0.0" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/jsonc-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.3.1.tgz", + "integrity": "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/jsonwebtoken": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz", + "integrity": "sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==", + "dev": true, + "license": "MIT", + "dependencies": { + "jws": "^4.0.1", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, + "node_modules/jwa": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", + "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz", + "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==", + "dev": true, + "license": "MIT", + "dependencies": { + "jwa": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/keytar": { + "version": "7.9.0", + "resolved": "https://registry.npmjs.org/keytar/-/keytar-7.9.0.tgz", + "integrity": "sha512-VPD8mtVtm5JNtA2AErl6Chp06JBfy7diFQ7TQQhdpWOl6MrCRB+eRbvAZUsbGQS9kiMq0coJsy0W0vHpDCkWsQ==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "dependencies": { + "node-addon-api": "^4.3.0", + "prebuild-install": "^7.0.1" + } + }, + "node_modules/leven": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", + "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/linkify-it": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-3.0.3.tgz", + "integrity": "sha512-ynTsyrFSdE5oZ/O9GEf00kPngmOfVwazR5GKDq6EYfhlpFug3J2zybX56a2PRRpc9P+FuSoGNAwjlbDs9jJBPQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "uc.micro": "^1.0.1" + } + }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", + "dev": true, + "license": "MIT" + }, + "node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/markdown-it": { + "version": "12.3.2", + "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-12.3.2.tgz", + "integrity": "sha512-TchMembfxfNVpHkbtriWltGWc+m3xszaRD0CZup7GFFhzIgQqxIfn3eGj1yZpfuflzPvfkt611B2Q/Bsk1YnGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1", + "entities": "~2.1.0", + "linkify-it": "^3.0.1", + "mdurl": "^1.0.1", + "uc.micro": "^1.0.5" + }, + "bin": { + "markdown-it": "bin/markdown-it.js" + } + }, + "node_modules/markdown-it/node_modules/entities": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-2.1.0.tgz", + "integrity": "sha512-hCx1oky9PFrJ611mf0ifBLBRW8lUUVRlFolb5gWRfIELabBlbp9xZvrqZLZAs+NxFnbfQoeGd8wDkygjg7U85w==", + "dev": true, + "license": "BSD-2-Clause", + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/mdurl": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-1.0.1.tgz", + "integrity": "sha512-/sKlQJCBYVY9Ers9hqzKou4H6V5UWc/M59TH2dvkt+84itfnq7uFOMLpOiOS4ujvHP4etln18fmIxA5R5fll0g==", + "dev": true, + "license": "MIT" + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "dev": true, + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "license": "MIT", + "optional": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/ms": { + "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/mute-stream": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz", + "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==", + "dev": true, + "license": "ISC" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/napi-build-utils": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", + "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/node-abi": { + "version": "3.87.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.87.0.tgz", + "integrity": "sha512-+CGM1L1CgmtheLcBuleyYOn7NWPVu0s0EJH2C4puxgEZb9h8QpR9G2dBfZJOAUhi7VQxuBPMd0hiISWcTyiYyQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/node-addon-api": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-4.3.0.tgz", + "integrity": "sha512-73sE9+3UaLYYFmDsFZnqCInzPyh3MqIwZO9cw58yIqAZhONrrabrYyYe3TuIqtIiOuTXVhsGau8hcrhhwSsDIQ==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0" + }, + "funding": { + "url": "https://github.com/fb55/nth-check?sponsor=1" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/obug": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.1.tgz", + "integrity": "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==", + "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT" + }, + "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" + } + }, + "node_modules/open": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/open/-/open-10.2.0.tgz", + "integrity": "sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "default-browser": "^5.2.1", + "define-lazy-prop": "^3.0.0", + "is-inside-container": "^1.0.0", + "wsl-utils": "^0.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parse-semver": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/parse-semver/-/parse-semver-1.1.1.tgz", + "integrity": "sha512-Eg1OuNntBMH0ojvEKSrvDSnwLmvVuUOSdylH/pSCPNMIspLlweJyIWXCE+k/5hm3cj/EBUYwmWkjhBALNP4LXQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^5.1.0" + } + }, + "node_modules/parse-semver/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/parse5": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", + "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5-htmlparser2-tree-adapter": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-7.1.0.tgz", + "integrity": "sha512-ruw5xyKs6lrpo9x9rCZqZZnIUntICjQAd0Wsmp396Ul9lN/h+ifgVV1x1gZHi8euej6wTfpqX8j+BFQxF0NS/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "domhandler": "^5.0.3", + "parse5": "^7.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5-parser-stream": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/parse5-parser-stream/-/parse5-parser-stream-7.1.2.tgz", + "integrity": "sha512-JyeQc9iwFLn5TbvvqACIF/VXG6abODeB3Fwmv/TGdLk2LfbWkaySGY72at4+Ty7EkPZj854u4CrICqNk2qIbow==", + "dev": true, + "license": "MIT", + "dependencies": { + "parse5": "^7.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5/node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "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==", + "dev": true, + "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": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/prebuild-install": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", + "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "detect-libc": "^2.0.0", + "expand-template": "^2.0.3", + "github-from-package": "0.0.0", + "minimist": "^1.2.3", + "mkdirp-classic": "^0.5.3", + "napi-build-utils": "^2.0.0", + "node-abi": "^3.3.0", + "pump": "^3.0.0", + "rc": "^1.2.7", + "simple-get": "^4.0.0", + "tar-fs": "^2.0.0", + "tunnel-agent": "^0.6.0" + }, + "bin": { + "prebuild-install": "bin.js" + }, + "engines": { + "node": ">=10" + } + }, + "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", + "optional": true, + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/qs": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.0.tgz", + "integrity": "sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "dev": true, + "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", + "optional": true, + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" + } + }, + "node_modules/read": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/read/-/read-1.0.7.tgz", + "integrity": "sha512-rSOKNYUmaxy0om1BNjMN4ezNT6VKK+2xF4GBhc81mkH7L60i6dp8qPYrkndNLT3QPphoII3maL9PVC9XmhHwVQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "mute-stream": "~0.0.4" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/rollup": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.57.1.tgz", + "integrity": "sha512-oQL6lgK3e2QZeQ7gcgIkS2YZPg5slw37hYufJ3edKlfQSGGm8ICoxswK15ntSzF/a8+h7ekRy7k7oWc3BQ7y8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.57.1", + "@rollup/rollup-android-arm64": "4.57.1", + "@rollup/rollup-darwin-arm64": "4.57.1", + "@rollup/rollup-darwin-x64": "4.57.1", + "@rollup/rollup-freebsd-arm64": "4.57.1", + "@rollup/rollup-freebsd-x64": "4.57.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.57.1", + "@rollup/rollup-linux-arm-musleabihf": "4.57.1", + "@rollup/rollup-linux-arm64-gnu": "4.57.1", + "@rollup/rollup-linux-arm64-musl": "4.57.1", + "@rollup/rollup-linux-loong64-gnu": "4.57.1", + "@rollup/rollup-linux-loong64-musl": "4.57.1", + "@rollup/rollup-linux-ppc64-gnu": "4.57.1", + "@rollup/rollup-linux-ppc64-musl": "4.57.1", + "@rollup/rollup-linux-riscv64-gnu": "4.57.1", + "@rollup/rollup-linux-riscv64-musl": "4.57.1", + "@rollup/rollup-linux-s390x-gnu": "4.57.1", + "@rollup/rollup-linux-x64-gnu": "4.57.1", + "@rollup/rollup-linux-x64-musl": "4.57.1", + "@rollup/rollup-openbsd-x64": "4.57.1", + "@rollup/rollup-openharmony-arm64": "4.57.1", + "@rollup/rollup-win32-arm64-msvc": "4.57.1", + "@rollup/rollup-win32-ia32-msvc": "4.57.1", + "@rollup/rollup-win32-x64-gnu": "4.57.1", + "@rollup/rollup-win32-x64-msvc": "4.57.1", + "fsevents": "~2.3.2" + } + }, + "node_modules/run-applescript": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-7.1.0.tgz", + "integrity": "sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true, + "license": "MIT" + }, + "node_modules/sax": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.4.4.tgz", + "integrity": "sha512-1n3r/tGXO6b6VXMdFT54SHzT9ytu9yr7TaELowdYpMqY/Ao7EnlQGmAQ1+RatX7Tkkdm6hONI2owqNx2aZj5Sw==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=11.0.0" + } + }, + "node_modules/semver": { + "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" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, + "node_modules/simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "optional": true + }, + "node_modules/simple-get": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "optional": true, + "dependencies": { + "decompress-response": "^6.0.0", + "once": "^1.3.1", + "simple-concat": "^1.0.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", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/std-env": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "dev": true, + "license": "MIT" + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/tar-fs": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz", + "integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + } + }, + "node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.2.tgz", + "integrity": "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyrainbow": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.0.3.tgz", + "integrity": "sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tmp": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.5.tgz", + "integrity": "sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.14" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD" + }, + "node_modules/tunnel": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.6.tgz", + "integrity": "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.6.11 <=0.7.0 || >=0.7.3" + } + }, + "node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "dev": true, + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, + "node_modules/typed-rest-client": { + "version": "1.8.11", + "resolved": "https://registry.npmjs.org/typed-rest-client/-/typed-rest-client-1.8.11.tgz", + "integrity": "sha512-5UvfMpd1oelmUPRbbaVnq+rHP7ng2cE4qoQkQeAqxRL6PklkxsM0g32/HL0yfvruK6ojQ5x8EE+HF4YV6DtuCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "qs": "^6.9.1", + "tunnel": "0.0.6", + "underscore": "^1.12.1" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/uc.micro": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-1.0.6.tgz", + "integrity": "sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA==", + "dev": true, + "license": "MIT" + }, + "node_modules/underscore": { + "version": "1.13.7", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.7.tgz", + "integrity": "sha512-GMXzWtsc57XAtguZgaQViUOzs0KTkk8ojr3/xAxXLITqf/3EMwxC0inyETfDFjH/Krbhuep0HNbbjI9i/q3F3g==", + "dev": true, + "license": "MIT" + }, + "node_modules/undici": { + "version": "7.22.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.22.0.tgz", + "integrity": "sha512-RqslV2Us5BrllB+JeiZnK4peryVTndy9Dnqq62S3yYRRTj0tFQCwEniUy2167skdGOy3vqRzEvl1Dm4sV2ReDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20.18.1" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/url-join": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/url-join/-/url-join-4.0.1.tgz", + "integrity": "sha512-jk1+QP6ZJqyOiuEI9AEWQfju/nB2Pw466kbA0LEZljHwKeMgd9WrAEgEGxjPDD2+TNbbb37rTyhEfrCXfuKXnA==", + "dev": true, + "license": "MIT" + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "dev": true, + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/vite": { + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz", + "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.27.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vitest": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.0.18.tgz", + "integrity": "sha512-hOQuK7h0FGKgBAas7v0mSAsnvrIgAvWmRFjmzpJ7SwFHH3g1k2u37JtYwOwmEKhK6ZO3v9ggDBBm0La1LCK4uQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "4.0.18", + "@vitest/mocker": "4.0.18", + "@vitest/pretty-format": "4.0.18", + "@vitest/runner": "4.0.18", + "@vitest/snapshot": "4.0.18", + "@vitest/spy": "4.0.18", + "@vitest/utils": "4.0.18", + "es-module-lexer": "^1.7.0", + "expect-type": "^1.2.2", + "magic-string": "^0.30.21", + "obug": "^2.1.1", + "pathe": "^2.0.3", + "picomatch": "^4.0.3", + "std-env": "^3.10.0", + "tinybench": "^2.9.0", + "tinyexec": "^1.0.2", + "tinyglobby": "^0.2.15", + "tinyrainbow": "^3.0.3", + "vite": "^6.0.0 || ^7.0.0", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@opentelemetry/api": "^1.9.0", + "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", + "@vitest/browser-playwright": "4.0.18", + "@vitest/browser-preview": "4.0.18", + "@vitest/browser-webdriverio": "4.0.18", + "@vitest/ui": "4.0.18", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser-playwright": { + "optional": true + }, + "@vitest/browser-preview": { + "optional": true + }, + "@vitest/browser-webdriverio": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "node_modules/whatwg-encoding": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", + "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", + "deprecated": "Use @exodus/bytes instead for a more spec-conformant and faster implementation", + "dev": true, + "license": "MIT", + "dependencies": { + "iconv-lite": "0.6.3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-mimetype": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", + "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrappy": { + "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/wsl-utils": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/wsl-utils/-/wsl-utils-0.1.0.tgz", + "integrity": "sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-wsl": "^3.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/xml2js": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.5.0.tgz", + "integrity": "sha512-drPFnkQJik/O+uPKpqSgr22mpuFHqKdbS835iAQrUC73L2F5WkboIRd63ai/2Yg6I1jzifPFKH2NTK+cfglkIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "sax": ">=0.6.0", + "xmlbuilder": "~11.0.0" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/xmlbuilder": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", + "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, + "license": "ISC" + }, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-crc32": "~0.2.3", + "fd-slicer": "~1.1.0" + } + }, + "node_modules/yazl": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/yazl/-/yazl-2.5.1.tgz", + "integrity": "sha512-phENi2PLiHnHb6QBVot+dJnaAZ0xosj7p3fWl+znIjBDlnMI2PsZCJZ306BPTFOaHf5qdDEI8x5qFrSOBN5vrw==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-crc32": "~0.2.3" + } + } + } +} diff --git a/vscode-extension/package.json b/vscode-extension/package.json new file mode 100644 index 0000000..69a1c6c --- /dev/null +++ b/vscode-extension/package.json @@ -0,0 +1,246 @@ +{ + "name": "mariadb-profiler-viewer", + "displayName": "MariaDB Profiler Viewer", + "description": "Visualize and analyze MariaDB/MySQL query profiling data from php-ext-mariadb-salvage", + "version": "0.1.0", + "publisher": "mariadb-profiler", + "engines": { + "vscode": "^1.85.0" + }, + "categories": [ + "Other", + "Debuggers" + ], + "activationEvents": [], + "main": "./dist/extension.js", + "contributes": { + "viewsContainers": { + "activitybar": [ + { + "id": "mariadb-profiler", + "title": "MariaDB Profiler", + "icon": "resources/icons/profiler.svg" + } + ] + }, + "views": { + "mariadb-profiler": [ + { + "id": "mariadbProfiler.jobs", + "name": "Jobs", + "type": "tree" + }, + { + "id": "mariadbProfiler.queries", + "name": "Queries", + "type": "tree" + }, + { + "id": "mariadbProfiler.statistics", + "name": "Statistics", + "type": "tree" + } + ] + }, + "commands": [ + { + "command": "mariadbProfiler.startJob", + "title": "Start Profiling Job", + "category": "MariaDB Profiler", + "icon": "$(play)" + }, + { + "command": "mariadbProfiler.stopJob", + "title": "Stop Profiling Job", + "category": "MariaDB Profiler", + "icon": "$(debug-stop)" + }, + { + "command": "mariadbProfiler.openLog", + "title": "Open Profiler Log", + "category": "MariaDB Profiler", + "icon": "$(folder-opened)" + }, + { + "command": "mariadbProfiler.refresh", + "title": "Refresh", + "category": "MariaDB Profiler", + "icon": "$(refresh)" + }, + { + "command": "mariadbProfiler.filterByType", + "title": "Filter by Query Type", + "category": "MariaDB Profiler", + "icon": "$(filter)" + }, + { + "command": "mariadbProfiler.filterByTag", + "title": "Filter by Tag", + "category": "MariaDB Profiler", + "icon": "$(tag)" + }, + { + "command": "mariadbProfiler.searchQuery", + "title": "Search Queries", + "category": "MariaDB Profiler", + "icon": "$(search)" + }, + { + "command": "mariadbProfiler.clearFilter", + "title": "Clear Filters", + "category": "MariaDB Profiler", + "icon": "$(clear-all)" + }, + { + "command": "mariadbProfiler.showQuerySql", + "title": "Show Full SQL", + "category": "MariaDB Profiler", + "icon": "$(open-preview)" + }, + { + "command": "mariadbProfiler.startLiveTail", + "title": "Start Live Tail", + "category": "MariaDB Profiler", + "icon": "$(eye)" + }, + { + "command": "mariadbProfiler.stopLiveTail", + "title": "Stop Live Tail", + "category": "MariaDB Profiler", + "icon": "$(eye-closed)" + } + ], + "menus": { + "view/title": [ + { + "command": "mariadbProfiler.startJob", + "when": "view == mariadbProfiler.jobs", + "group": "navigation@1" + }, + { + "command": "mariadbProfiler.stopJob", + "when": "view == mariadbProfiler.jobs", + "group": "navigation@2" + }, + { + "command": "mariadbProfiler.refresh", + "when": "view == mariadbProfiler.jobs", + "group": "navigation@3" + }, + { + "command": "mariadbProfiler.filterByType", + "when": "view == mariadbProfiler.queries", + "group": "navigation@1" + }, + { + "command": "mariadbProfiler.filterByTag", + "when": "view == mariadbProfiler.queries", + "group": "navigation@2" + }, + { + "command": "mariadbProfiler.searchQuery", + "when": "view == mariadbProfiler.queries", + "group": "navigation@3" + }, + { + "command": "mariadbProfiler.clearFilter", + "when": "view == mariadbProfiler.queries && mariadbProfiler.hasFilter", + "group": "navigation@4" + }, + { + "command": "mariadbProfiler.startLiveTail", + "when": "view == mariadbProfiler.queries && !mariadbProfiler.liveTailActive", + "group": "2_liveTail" + }, + { + "command": "mariadbProfiler.stopLiveTail", + "when": "view == mariadbProfiler.queries && mariadbProfiler.liveTailActive", + "group": "2_liveTail" + } + ], + "view/item/context": [ + { + "command": "mariadbProfiler.showQuerySql", + "when": "view == mariadbProfiler.queries && viewItem == queryEntry", + "group": "inline" + }, + { + "command": "mariadbProfiler.stopJob", + "when": "view == mariadbProfiler.jobs && viewItem == activeJob" + } + ] + }, + "configuration": { + "title": "MariaDB Profiler", + "properties": { + "mariadbProfiler.logDirectory": { + "type": "string", + "default": "/tmp/mariadb_profiler", + "description": "Directory where profiler writes jobs.json and *.jsonl files" + }, + "mariadbProfiler.phpPath": { + "type": "string", + "default": "php", + "description": "Path to PHP executable for CLI operations" + }, + "mariadbProfiler.cliScriptPath": { + "type": "string", + "default": "", + "description": "Path to mariadb_profiler.php CLI tool (auto-detected from workspace if empty)" + }, + "mariadbProfiler.maxQueries": { + "type": "number", + "default": 10000, + "description": "Maximum number of queries to display" + }, + "mariadbProfiler.refreshInterval": { + "type": "number", + "default": 5, + "description": "Auto-refresh interval in seconds" + }, + "mariadbProfiler.tailBufferSize": { + "type": "number", + "default": 500, + "description": "Number of lines to keep in live tail buffer" + }, + "mariadbProfiler.pathMappings": { + "type": "object", + "default": {}, + "description": "Path mappings for Docker environments (container path -> local path)", + "additionalProperties": { + "type": "string" + } + }, + "mariadbProfiler.frameResolverScript": { + "type": "string", + "default": "", + "description": "JavaScript code for custom frame resolution (receives trace, tag, query variables)" + } + } + }, + "capabilities": { + "untrustedWorkspaces": { + "supported": false, + "description": "This extension executes user-provided JavaScript for frame resolution and requires a trusted workspace." + } + } + }, + "scripts": { + "vscode:prepublish": "npm run build", + "build": "node esbuild.mjs --production", + "watch": "node esbuild.mjs --watch", + "typecheck": "tsc --noEmit", + "test": "vitest run", + "test:watch": "vitest", + "lint": "tsc --noEmit", + "package": "vsce package" + }, + "devDependencies": { + "@types/node": "^20.11.0", + "@types/vscode": "^1.85.0", + "@vscode/vsce": "^2.22.0", + "esbuild": "^0.27.3", + "typescript": "^5.3.0", + "vitest": "^4.0.18" + } +} diff --git a/vscode-extension/resources/icons/profiler.svg b/vscode-extension/resources/icons/profiler.svg new file mode 100644 index 0000000..54a2c3a --- /dev/null +++ b/vscode-extension/resources/icons/profiler.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/vscode-extension/src/command/filterQueries.ts b/vscode-extension/src/command/filterQueries.ts new file mode 100644 index 0000000..1ebf9ec --- /dev/null +++ b/vscode-extension/src/command/filterQueries.ts @@ -0,0 +1,99 @@ +import * as vscode from 'vscode'; +import { QueryTreeProvider } from '../provider/QueryTreeProvider'; +import { getQueryType, QueryType } from '../model/QueryEntry'; + +export function registerFilterByTypeCommand( + context: vscode.ExtensionContext, + queryTreeProvider: QueryTreeProvider, +): vscode.Disposable { + return vscode.commands.registerCommand('mariadbProfiler.filterByType', async () => { + const entries = queryTreeProvider.getEntries(); + + // Count queries by type + const typeCounts: Record = {}; + for (const entry of entries) { + const qtype = getQueryType(entry); + typeCounts[qtype] = (typeCounts[qtype] || 0) + 1; + } + + const items: vscode.QuickPickItem[] = [ + { label: 'All', description: `(${entries.length} queries)`, detail: 'Clear type filter' }, + ]; + + for (const [type, count] of Object.entries(typeCounts)) { + items.push({ label: type, description: `(${count} queries)` }); + } + + const selected = await vscode.window.showQuickPick(items, { + placeHolder: 'Filter by query type', + }); + + if (!selected) { return; } + + if (selected.label === 'All') { + queryTreeProvider.setFilter(null); + } else { + queryTreeProvider.setFilter(selected.label); + } + + updateFilterContext(queryTreeProvider); + }); +} + +export function registerFilterByTagCommand( + context: vscode.ExtensionContext, + queryTreeProvider: QueryTreeProvider, +): vscode.Disposable { + return vscode.commands.registerCommand('mariadbProfiler.filterByTag', async () => { + const entries = queryTreeProvider.getEntries(); + const tags = queryTreeProvider.getAllTags(); + + // Count by tag + const tagCounts: Record = {}; + for (const entry of entries) { + if (entry.tag) { + tagCounts[entry.tag] = (tagCounts[entry.tag] || 0) + 1; + } + } + + const items: vscode.QuickPickItem[] = [ + { label: 'All', description: `(${entries.length} queries)`, detail: 'Clear tag filter' }, + ]; + + for (const tag of tags) { + items.push({ label: tag, description: `(${tagCounts[tag] || 0} queries)` }); + } + + const selected = await vscode.window.showQuickPick(items, { + placeHolder: 'Filter by tag', + }); + + if (!selected) { return; } + + if (selected.label === 'All') { + queryTreeProvider.setTagFilter(null); + } else { + queryTreeProvider.setTagFilter(selected.label); + } + + updateFilterContext(queryTreeProvider); + }); +} + +export function registerClearFilterCommand( + context: vscode.ExtensionContext, + queryTreeProvider: QueryTreeProvider, +): vscode.Disposable { + return vscode.commands.registerCommand('mariadbProfiler.clearFilter', () => { + queryTreeProvider.clearFilters(); + updateFilterContext(queryTreeProvider); + }); +} + +export function updateFilterContext(queryTreeProvider: QueryTreeProvider): void { + vscode.commands.executeCommand( + 'setContext', + 'mariadbProfiler.hasFilter', + queryTreeProvider.hasActiveFilters(), + ); +} diff --git a/vscode-extension/src/command/liveTail.ts b/vscode-extension/src/command/liveTail.ts new file mode 100644 index 0000000..851acfd --- /dev/null +++ b/vscode-extension/src/command/liveTail.ts @@ -0,0 +1,152 @@ +import * as vscode from 'vscode'; +import { JobManagerService } from '../service/JobManagerService'; +import { LogParserService } from '../service/LogParserService'; +import { FileWatcherService } from '../service/FileWatcherService'; +import { getQueryType, getBoundQuery, formatTimestamp } from '../model/QueryEntry'; + +export class LiveTailManager implements vscode.Disposable { + private outputChannel: vscode.OutputChannel; + private fileWatcher: FileWatcherService; + private logParser: LogParserService; + private jobManager: JobManagerService; + + private isActive = false; + private currentJobKey: string | null = null; + private jsonlOffset = 0; + + constructor( + outputChannel: vscode.OutputChannel, + fileWatcher: FileWatcherService, + logParser: LogParserService, + jobManager: JobManagerService, + ) { + this.outputChannel = outputChannel; + this.fileWatcher = fileWatcher; + this.logParser = logParser; + this.jobManager = jobManager; + } + + start(jobKey: string): void { + this.stop(); + + this.currentJobKey = jobKey; + this.isActive = true; + this.jsonlOffset = 0; + + vscode.commands.executeCommand('setContext', 'mariadbProfiler.liveTailActive', true); + + this.outputChannel.clear(); + this.outputChannel.appendLine(`--- Live Tail: ${jobKey} ---`); + this.outputChannel.appendLine(''); + this.outputChannel.show(true); // Don't steal focus + + const jsonlPath = this.jobManager.getJsonlPath(jobKey); + + // Initial load + this.readNewEntries(jsonlPath); + + // Watch for changes + this.fileWatcher.watchFile(jsonlPath, () => { + if (this.isActive) { + this.readNewEntries(jsonlPath); + } + }); + } + + stop(): void { + if (this.currentJobKey) { + const jsonlPath = this.jobManager.getJsonlPath(this.currentJobKey); + this.fileWatcher.unwatchFile(jsonlPath); + } + + this.isActive = false; + this.currentJobKey = null; + this.jsonlOffset = 0; + + vscode.commands.executeCommand('setContext', 'mariadbProfiler.liveTailActive', false); + } + + getIsActive(): boolean { + return this.isActive; + } + + getCurrentJobKey(): string | null { + return this.currentJobKey; + } + + dispose(): void { + this.stop(); + } + + private readNewEntries(jsonlPath: string): void { + const result = this.logParser.parseJsonlFileFromOffset(jsonlPath, this.jsonlOffset); + this.jsonlOffset = result.newOffset; + + for (const entry of result.entries) { + const qtype = getQueryType(entry); + const boundSql = getBoundQuery(entry).replace(/\s+/g, ' ').trim(); + const shortSql = boundSql.length <= 80 ? boundSql : boundSql.substring(0, 77) + '...'; + const tag = entry.tag ? ` [${entry.tag}]` : ''; + const status = entry.status || 'ok'; + const time = formatTimestamp(entry.timestamp); + + this.outputChannel.appendLine( + `[${time}] ${qtype} ${status.toUpperCase()}${tag} ${shortSql}` + ); + + // Show backtrace frames + if (entry.trace) { + for (let i = 0; i < Math.min(entry.trace.length, 3); i++) { + const frame = entry.trace[i]; + this.outputChannel.appendLine(` #${i} ${frame.file}:${frame.line}`); + } + if (entry.trace.length > 3) { + this.outputChannel.appendLine(` ... (${entry.trace.length - 3} more frames)`); + } + this.outputChannel.appendLine(''); + } + } + } +} + +export function registerLiveTailCommands( + context: vscode.ExtensionContext, + liveTailManager: LiveTailManager, + jobManager: JobManagerService, +): vscode.Disposable[] { + const startCmd = vscode.commands.registerCommand('mariadbProfiler.startLiveTail', async () => { + const activeJobs = jobManager.getActiveJobs(); + if (activeJobs.length === 0) { + vscode.window.showInformationMessage('No active profiling jobs to tail'); + return; + } + + let jobKey: string; + + if (activeJobs.length === 1) { + jobKey = activeJobs[0].key; + } else { + const items = activeJobs.map(j => ({ + label: j.key, + description: `Started: ${new Date(j.startedAt * 1000).toLocaleString()}`, + })); + + const selected = await vscode.window.showQuickPick(items, { + placeHolder: 'Select a job to tail', + }); + + if (!selected) { return; } + jobKey = selected.label; + } + + liveTailManager.start(jobKey); + vscode.window.showInformationMessage(`Live tail started for job '${jobKey}'`); + }); + + const stopCmd = vscode.commands.registerCommand('mariadbProfiler.stopLiveTail', () => { + liveTailManager.stop(); + vscode.window.showInformationMessage('Live tail stopped'); + }); + + return [startCmd, stopCmd]; +} diff --git a/vscode-extension/src/command/openLog.ts b/vscode-extension/src/command/openLog.ts new file mode 100644 index 0000000..af6e89d --- /dev/null +++ b/vscode-extension/src/command/openLog.ts @@ -0,0 +1,46 @@ +import * as vscode from 'vscode'; +import { JobManagerService } from '../service/JobManagerService'; +import { LogParserService } from '../service/LogParserService'; +import { StatisticsService } from '../service/StatisticsService'; +import { QueryTreeProvider } from '../provider/QueryTreeProvider'; +import { StatisticsTreeProvider } from '../provider/StatisticsTreeProvider'; +import { updateFilterContext } from './filterQueries'; + +export function registerOpenLogCommand( + context: vscode.ExtensionContext, + jobManager: JobManagerService, + logParser: LogParserService, + queryTreeProvider: QueryTreeProvider, + statisticsService: StatisticsService, + statisticsTreeProvider: StatisticsTreeProvider, +): vscode.Disposable { + return vscode.commands.registerCommand('mariadbProfiler.openLog', async () => { + const uris = await vscode.window.showOpenDialog({ + canSelectFiles: true, + canSelectMany: false, + filters: { 'JSONL files': ['jsonl'] }, + defaultUri: vscode.Uri.file(jobManager.getLogDir()), + title: 'Open Profiler Log File', + }); + + if (!uris || uris.length === 0) { return; } + + const filePath = uris[0].fsPath; + + let entries; + try { + entries = logParser.parseJsonlFile(filePath); + } catch (e) { + vscode.window.showErrorMessage(`Failed to parse log file: ${e}`); + return; + } + + queryTreeProvider.loadEntries(entries); + updateFilterContext(queryTreeProvider); + + const stats = statisticsService.computeStats(entries); + statisticsTreeProvider.updateStats(stats); + + vscode.window.showInformationMessage(`Loaded ${entries.length} queries from log file`); + }); +} diff --git a/vscode-extension/src/command/searchQueries.ts b/vscode-extension/src/command/searchQueries.ts new file mode 100644 index 0000000..cdcefd9 --- /dev/null +++ b/vscode-extension/src/command/searchQueries.ts @@ -0,0 +1,27 @@ +import * as vscode from 'vscode'; +import { QueryTreeProvider } from '../provider/QueryTreeProvider'; +import { updateFilterContext } from './filterQueries'; + +export function registerSearchQueryCommand( + context: vscode.ExtensionContext, + queryTreeProvider: QueryTreeProvider, +): vscode.Disposable { + return vscode.commands.registerCommand('mariadbProfiler.searchQuery', async () => { + const text = await vscode.window.showInputBox({ + prompt: 'Search queries by SQL text', + placeHolder: 'e.g. users, SELECT, WHERE id =', + value: '', + }); + + // User cancelled + if (text === undefined) { return; } + + if (text === '') { + queryTreeProvider.setSearchText(null); + } else { + queryTreeProvider.setSearchText(text); + } + + updateFilterContext(queryTreeProvider); + }); +} diff --git a/vscode-extension/src/command/startJob.ts b/vscode-extension/src/command/startJob.ts new file mode 100644 index 0000000..b9fe36a --- /dev/null +++ b/vscode-extension/src/command/startJob.ts @@ -0,0 +1,27 @@ +import * as vscode from 'vscode'; +import { JobManagerService } from '../service/JobManagerService'; + +export function registerStartJobCommand( + context: vscode.ExtensionContext, + jobManager: JobManagerService, + onJobsChanged: () => void, +): vscode.Disposable { + return vscode.commands.registerCommand('mariadbProfiler.startJob', async () => { + const jobKey = await vscode.window.showInputBox({ + prompt: 'Enter a job key (leave empty for auto-generated key)', + placeHolder: 'my-profiling-session', + }); + + // User cancelled + if (jobKey === undefined) { return; } + + try { + const key = await jobManager.startJob(jobKey || undefined); + vscode.window.showInformationMessage(`Profiling job '${key}' started`); + onJobsChanged(); + } catch (e: unknown) { + const msg = e instanceof Error ? e.message : String(e); + vscode.window.showErrorMessage(`Failed to start job: ${msg}`); + } + }); +} diff --git a/vscode-extension/src/command/stopJob.ts b/vscode-extension/src/command/stopJob.ts new file mode 100644 index 0000000..064bb18 --- /dev/null +++ b/vscode-extension/src/command/stopJob.ts @@ -0,0 +1,45 @@ +import * as vscode from 'vscode'; +import { JobManagerService } from '../service/JobManagerService'; +import { JobInfo } from '../model/JobInfo'; + +export function registerStopJobCommand( + context: vscode.ExtensionContext, + jobManager: JobManagerService, + onJobsChanged: () => void, +): vscode.Disposable { + return vscode.commands.registerCommand('mariadbProfiler.stopJob', async (jobItem?: { job: JobInfo }) => { + let jobKey: string | undefined; + + if (jobItem?.job) { + jobKey = jobItem.job.key; + } else { + // Show picker for active jobs + const activeJobs = jobManager.getActiveJobs(); + if (activeJobs.length === 0) { + vscode.window.showInformationMessage('No active profiling jobs'); + return; + } + + const items = activeJobs.map(j => ({ + label: j.key, + description: `Started: ${new Date(j.startedAt * 1000).toLocaleString()}`, + })); + + const selected = await vscode.window.showQuickPick(items, { + placeHolder: 'Select a job to stop', + }); + + if (!selected) { return; } + jobKey = selected.label; + } + + try { + await jobManager.stopJob(jobKey); + vscode.window.showInformationMessage(`Profiling job '${jobKey}' stopped`); + onJobsChanged(); + } catch (e: unknown) { + const msg = e instanceof Error ? e.message : String(e); + vscode.window.showErrorMessage(`Failed to stop job: ${msg}`); + } + }); +} diff --git a/vscode-extension/src/extension.ts b/vscode-extension/src/extension.ts new file mode 100644 index 0000000..13f9c66 --- /dev/null +++ b/vscode-extension/src/extension.ts @@ -0,0 +1,250 @@ +import * as fs from 'fs'; +import * as vscode from 'vscode'; +import { LogParserService } from './service/LogParserService'; +import { JobManagerService } from './service/JobManagerService'; +import { StatisticsService } from './service/StatisticsService'; +import { FileWatcherService } from './service/FileWatcherService'; +import { FrameResolverService } from './service/FrameResolverService'; +import { JobTreeProvider } from './provider/JobTreeProvider'; +import { QueryTreeProvider } from './provider/QueryTreeProvider'; +import { StatisticsTreeProvider } from './provider/StatisticsTreeProvider'; +import { QueryDocumentProvider } from './provider/QueryDocumentProvider'; +import { registerStartJobCommand } from './command/startJob'; +import { registerStopJobCommand } from './command/stopJob'; +import { registerOpenLogCommand } from './command/openLog'; +import { + registerFilterByTypeCommand, + registerFilterByTagCommand, + registerClearFilterCommand, + updateFilterContext, +} from './command/filterQueries'; +import { registerSearchQueryCommand } from './command/searchQueries'; +import { LiveTailManager, registerLiveTailCommands } from './command/liveTail'; +import { JobInfo } from './model/JobInfo'; + +export function activate(context: vscode.ExtensionContext): void { + // --- Output Channels --- + const errorChannel = vscode.window.createOutputChannel('MariaDB Profiler'); + const liveTailChannel = vscode.window.createOutputChannel('MariaDB Profiler Live Tail'); + context.subscriptions.push(errorChannel, liveTailChannel); + + // --- Services --- + const logParser = new LogParserService(errorChannel); + const jobManager = new JobManagerService(errorChannel); + const statisticsService = new StatisticsService(); + const fileWatcher = new FileWatcherService(); + const frameResolver = new FrameResolverService(errorChannel); + context.subscriptions.push(fileWatcher); + + // --- UI Providers --- + const jobTreeProvider = new JobTreeProvider(); + const queryTreeProvider = new QueryTreeProvider(); + const statisticsTreeProvider = new StatisticsTreeProvider(); + const queryDocumentProvider = new QueryDocumentProvider(); + + // Register TreeViews + const jobTreeView = vscode.window.createTreeView('mariadbProfiler.jobs', { + treeDataProvider: jobTreeProvider, + showCollapseAll: false, + }); + const queryTreeView = vscode.window.createTreeView('mariadbProfiler.queries', { + treeDataProvider: queryTreeProvider, + showCollapseAll: true, + }); + const statisticsTreeView = vscode.window.createTreeView('mariadbProfiler.statistics', { + treeDataProvider: statisticsTreeProvider, + showCollapseAll: true, + }); + context.subscriptions.push(jobTreeView, queryTreeView, statisticsTreeView); + + // Register Virtual Document Provider + context.subscriptions.push( + vscode.workspace.registerTextDocumentContentProvider( + QueryDocumentProvider.scheme, + queryDocumentProvider, + ), + ); + + // --- Live Tail --- + const liveTailManager = new LiveTailManager( + liveTailChannel, fileWatcher, logParser, jobManager, + ); + context.subscriptions.push(liveTailManager); + + // --- State --- + let selectedJobKey: string | null = null; + let jsonlOffset = 0; + let refreshTimer: ReturnType | undefined; + + // --- Helper: Refresh jobs list --- + function refreshJobs(): void { + const jobs = jobManager.loadJobs(); + jobTreeProvider.refresh(jobs); + } + + // --- Helper: Load queries for a job --- + function loadJobQueries(jobKey: string): void { + const jsonlPath = jobManager.getJsonlPath(jobKey); + const entries = logParser.parseJsonlFile(jsonlPath); + + // Resolve frames + for (let i = 0; i < entries.length; i++) { + const frameIndex = frameResolver.resolve(entries[i]); + if (frameIndex > 0) { + queryTreeProvider.setResolvedFrame(i, frameIndex); + } + } + + queryTreeProvider.loadEntries(entries); + try { + jsonlOffset = entries.length > 0 ? fs.statSync(jsonlPath).size : 0; + } catch { + jsonlOffset = 0; + } + + // Update statistics + const stats = statisticsService.computeStats(entries); + statisticsTreeProvider.updateStats(stats); + + // Update filter context + updateFilterContext(queryTreeProvider); + } + + // --- Helper: Incremental update for active job --- + function updateActiveJob(): void { + if (!selectedJobKey) { return; } + + const jsonlPath = jobManager.getJsonlPath(selectedJobKey); + const result = logParser.parseJsonlFileFromOffset(jsonlPath, jsonlOffset); + + if (result.entries.length > 0) { + // Resolve frames for new entries + const startIndex = queryTreeProvider.getEntries().length; + for (let i = 0; i < result.entries.length; i++) { + const frameIndex = frameResolver.resolve(result.entries[i]); + if (frameIndex > 0) { + queryTreeProvider.setResolvedFrame(startIndex + i, frameIndex); + } + } + + queryTreeProvider.appendEntries(result.entries); + jsonlOffset = result.newOffset; + + // Recompute statistics + const allEntries = queryTreeProvider.getEntries(); + const stats = statisticsService.computeStats(allEntries); + statisticsTreeProvider.updateStats(stats); + } + + // Also refresh jobs list to pick up status changes + refreshJobs(); + } + + // --- Helper: Start/stop refresh timer --- + function startRefreshTimer(): void { + stopRefreshTimer(); + const interval = vscode.workspace.getConfiguration('mariadbProfiler') + .get('refreshInterval', 5) * 1000; + + refreshTimer = setInterval(() => { + updateActiveJob(); + }, interval); + } + + function stopRefreshTimer(): void { + if (refreshTimer) { + clearInterval(refreshTimer); + refreshTimer = undefined; + } + } + + // --- Job Selection Handler --- + const selectJobCmd = vscode.commands.registerCommand( + 'mariadbProfiler.selectJob', + (job: JobInfo) => { + selectedJobKey = job.key; + loadJobQueries(job.key); + + // Set up auto-refresh for active jobs + if (job.isActive) { + startRefreshTimer(); + } else { + stopRefreshTimer(); + } + }, + ); + context.subscriptions.push(selectJobCmd); + + // --- Show Full SQL Command --- + const showSqlCmd = vscode.commands.registerCommand( + 'mariadbProfiler.showQuerySql', + (item: { entry?: import('./model/QueryEntry').QueryEntry; entryIndex?: number }) => { + if (item?.entry) { + queryDocumentProvider.showQueryDetail(item.entry, item.entryIndex ?? 0); + } + }, + ); + context.subscriptions.push(showSqlCmd); + + // --- Register Commands --- + context.subscriptions.push( + registerStartJobCommand(context, jobManager, refreshJobs), + registerStopJobCommand(context, jobManager, refreshJobs), + registerOpenLogCommand(context, jobManager, logParser, queryTreeProvider, statisticsService, statisticsTreeProvider), + registerFilterByTypeCommand(context, queryTreeProvider), + registerFilterByTagCommand(context, queryTreeProvider), + registerClearFilterCommand(context, queryTreeProvider), + registerSearchQueryCommand(context, queryTreeProvider), + ...registerLiveTailCommands(context, liveTailManager, jobManager), + ); + + // --- Refresh Command --- + const refreshCmd = vscode.commands.registerCommand('mariadbProfiler.refresh', () => { + refreshJobs(); + if (selectedJobKey) { + loadJobQueries(selectedJobKey); + } + }); + context.subscriptions.push(refreshCmd); + + // --- Watch jobs.json for external changes --- + let jobsJsonPath = jobManager.getJobsJsonPath(); + fileWatcher.watchFile(jobsJsonPath, () => { + refreshJobs(); + }); + + // --- Configuration change handler --- + const configWatcher = vscode.workspace.onDidChangeConfiguration(e => { + if (e.affectsConfiguration('mariadbProfiler.frameResolverScript')) { + frameResolver.invalidateCache(); + } + if (e.affectsConfiguration('mariadbProfiler.refreshInterval') && selectedJobKey) { + startRefreshTimer(); + } + if (e.affectsConfiguration('mariadbProfiler.logDirectory')) { + fileWatcher.unwatchFile(jobsJsonPath); + jobsJsonPath = jobManager.getJobsJsonPath(); + fileWatcher.watchFile(jobsJsonPath, () => { + refreshJobs(); + }); + refreshJobs(); + } + }); + context.subscriptions.push(configWatcher); + + // --- Cleanup on deactivate --- + context.subscriptions.push({ + dispose: () => { + stopRefreshTimer(); + }, + }); + + // --- Initial Load --- + refreshJobs(); + + errorChannel.appendLine('[MariaDB Profiler] Extension activated'); +} + +export function deactivate(): void { + // Cleanup handled by disposables +} diff --git a/vscode-extension/src/model/JobInfo.ts b/vscode-extension/src/model/JobInfo.ts new file mode 100644 index 0000000..1571677 --- /dev/null +++ b/vscode-extension/src/model/JobInfo.ts @@ -0,0 +1,84 @@ +export interface JobData { + started_at: number; + ended_at?: number; + query_count?: number; + parent?: string | null; +} + +export interface JobsFile { + active_jobs: Record; + completed_jobs: Record; +} + +export interface JobInfo { + key: string; + startedAt: number; + endedAt?: number; + queryCount?: number; + parent?: string | null; + isActive: boolean; +} + +export function parseJobsFile(content: string): JobsFile { + const raw = JSON.parse(content); + return { + active_jobs: normalizeJobMap(raw.active_jobs), + completed_jobs: normalizeJobMap(raw.completed_jobs), + }; +} + +// PHP encodes empty associative arrays as [] instead of {} +function normalizeJobMap(value: unknown): Record { + if (Array.isArray(value) && value.length === 0) { + return {}; + } + if (typeof value === 'object' && value !== null) { + return value as Record; + } + return {}; +} + +export function jobsFileToJobInfos(jobsFile: JobsFile): JobInfo[] { + const jobs: JobInfo[] = []; + + for (const [key, data] of Object.entries(jobsFile.active_jobs)) { + jobs.push({ + key, + startedAt: data.started_at, + endedAt: data.ended_at, + queryCount: data.query_count, + parent: data.parent, + isActive: true, + }); + } + + for (const [key, data] of Object.entries(jobsFile.completed_jobs)) { + jobs.push({ + key, + startedAt: data.started_at, + endedAt: data.ended_at, + queryCount: data.query_count, + parent: data.parent, + isActive: false, + }); + } + + // Sort: active first, then by startedAt descending + jobs.sort((a, b) => { + if (a.isActive !== b.isActive) { return a.isActive ? -1 : 1; } + return b.startedAt - a.startedAt; + }); + + return jobs; +} + +export function formatDuration(startedAt: number, endedAt?: number): string { + if (endedAt === undefined) { return 'running...'; } + const seconds = endedAt - startedAt; + if (seconds < 1) { return `${Math.round(seconds * 1000)} ms`; } + if (seconds < 60) { return `${seconds.toFixed(1)}s`; } + const totalSeconds = Math.round(seconds); + const minutes = Math.floor(totalSeconds / 60); + const secs = totalSeconds % 60; + return `${minutes}m${secs}s`; +} diff --git a/vscode-extension/src/model/QueryEntry.ts b/vscode-extension/src/model/QueryEntry.ts new file mode 100644 index 0000000..087e56c --- /dev/null +++ b/vscode-extension/src/model/QueryEntry.ts @@ -0,0 +1,167 @@ +export interface BacktraceFrame { + call: string; + file: string; + line: number; + function?: string; + class_name?: string; +} + +export interface RawQueryEntry { + k: string; + q: string; + ts: number; + tag?: string; + s?: string; + params?: (string | null)[]; + trace?: BacktraceFrame[]; +} + +export interface QueryEntry { + jobKey: string; + query: string; + timestamp: number; + tag?: string; + status?: string; + params?: (string | null)[]; + trace?: BacktraceFrame[]; +} + +export type QueryType = 'SELECT' | 'INSERT' | 'UPDATE' | 'DELETE' | 'OTHER'; + +export function fromRaw(raw: RawQueryEntry): QueryEntry { + return { + jobKey: raw.k, + query: raw.q, + timestamp: raw.ts, + tag: raw.tag, + status: raw.s, + params: raw.params, + trace: raw.trace, + }; +} + +export function getQueryType(entry: QueryEntry): QueryType { + const trimmed = entry.query.trimStart().toUpperCase(); + if (trimmed.startsWith('SELECT')) { return 'SELECT'; } + if (trimmed.startsWith('INSERT')) { return 'INSERT'; } + if (trimmed.startsWith('UPDATE')) { return 'UPDATE'; } + if (trimmed.startsWith('DELETE')) { return 'DELETE'; } + return 'OTHER'; +} + +export function getBoundQuery(entry: QueryEntry): string { + if (!entry.params || entry.params.length === 0) { + return entry.query; + } + + let paramIndex = 0; + const params = entry.params; + let result = ''; + let i = 0; + const q = entry.query; + + while (i < q.length) { + // Skip string literals + if (q[i] === '\'' || q[i] === '"') { + const quote = q[i]; + result += q[i++]; + while (i < q.length) { + if (q[i] === '\\') { + result += q[i++]; + if (i < q.length) { result += q[i++]; } + } else if (q[i] === quote) { + result += q[i++]; + break; + } else { + result += q[i++]; + } + } + continue; + } + + // Skip backtick-quoted identifiers + if (q[i] === '`') { + result += q[i++]; + while (i < q.length && q[i] !== '`') { + result += q[i++]; + } + if (i < q.length) { result += q[i++]; } + continue; + } + + // Skip line comments + if (q[i] === '-' && i + 1 < q.length && q[i + 1] === '-') { + while (i < q.length && q[i] !== '\n') { + result += q[i++]; + } + continue; + } + if (q[i] === '#') { + while (i < q.length && q[i] !== '\n') { + result += q[i++]; + } + continue; + } + + // Skip block comments + if (q[i] === '/' && i + 1 < q.length && q[i + 1] === '*') { + result += q[i++]; + result += q[i++]; + while (i < q.length && !(q[i] === '*' && i + 1 < q.length && q[i + 1] === '/')) { + result += q[i++]; + } + if (i < q.length) { result += q[i++]; result += q[i++]; } + continue; + } + + // Replace placeholder + if (q[i] === '?' && paramIndex < params.length) { + const param = params[paramIndex++]; + result += param === null ? 'NULL' : `'${param.replace(/'/g, "''")}'`; + i++; + continue; + } + + result += q[i++]; + } + + return result; +} + +const TABLE_PATTERNS = [ + /\bFROM\s+(`[^`]+`\.`[^`]+`|`[^`]+`|[\w]+\.[\w]+|[\w]+)/gi, + /\bJOIN\s+(`[^`]+`\.`[^`]+`|`[^`]+`|[\w]+\.[\w]+|[\w]+)/gi, + /\bUPDATE\s+(`[^`]+`\.`[^`]+`|`[^`]+`|[\w]+\.[\w]+|[\w]+)/gi, + /\bINTO\s+(`[^`]+`\.`[^`]+`|`[^`]+`|[\w]+\.[\w]+|[\w]+)/gi, + /\bDELETE\s+FROM\s+(`[^`]+`\.`[^`]+`|`[^`]+`|[\w]+\.[\w]+|[\w]+)/gi, +]; + +export function getTables(entry: QueryEntry): string[] { + const tables = new Set(); + for (const pattern of TABLE_PATTERNS) { + pattern.lastIndex = 0; + let match; + while ((match = pattern.exec(entry.query)) !== null) { + tables.add(match[1].replace(/`/g, '').toLowerCase()); + } + } + return [...tables].sort(); +} + +export function getShortSql(entry: QueryEntry, maxLen: number = 60): string { + const sql = entry.query.replace(/\s+/g, ' ').trim(); + if (sql.length <= maxLen) { return sql; } + return sql.substring(0, maxLen - 3) + '...'; +} + +export function getSourceFile(entry: QueryEntry): string | null { + if (!entry.trace || entry.trace.length === 0) { return null; } + const frame = entry.trace[0]; + return `${frame.file}:${frame.line}`; +} + +export function formatTimestamp(ts: number): string { + const date = new Date(ts * 1000); + return date.toTimeString().split(' ')[0] + + '.' + String(date.getMilliseconds()).padStart(3, '0'); +} diff --git a/vscode-extension/src/provider/JobTreeProvider.ts b/vscode-extension/src/provider/JobTreeProvider.ts new file mode 100644 index 0000000..719efe3 --- /dev/null +++ b/vscode-extension/src/provider/JobTreeProvider.ts @@ -0,0 +1,63 @@ +import * as vscode from 'vscode'; +import { JobInfo, formatDuration } from '../model/JobInfo'; +import { shortKey } from '../util/queryUtils'; + +export class JobTreeProvider implements vscode.TreeDataProvider { + private _onDidChangeTreeData = new vscode.EventEmitter(); + readonly onDidChangeTreeData = this._onDidChangeTreeData.event; + + private jobs: JobInfo[] = []; + private _onJobSelected = new vscode.EventEmitter(); + readonly onJobSelected = this._onJobSelected.event; + + refresh(jobs: JobInfo[]): void { + this.jobs = jobs; + this._onDidChangeTreeData.fire(); + } + + getTreeItem(element: JobTreeItem): vscode.TreeItem { + return element; + } + + getChildren(): JobTreeItem[] { + return this.jobs.map(job => new JobTreeItem(job)); + } + + selectJob(job: JobInfo): void { + this._onJobSelected.fire(job); + } +} + +export class JobTreeItem extends vscode.TreeItem { + constructor(public readonly job: JobInfo) { + super(shortKey(job.key), vscode.TreeItemCollapsibleState.None); + + const queryInfo = job.queryCount !== undefined ? `${job.queryCount} queries` : 'recording...'; + const duration = formatDuration(job.startedAt, job.endedAt); + this.description = `${queryInfo}, ${duration}`; + + this.iconPath = new vscode.ThemeIcon( + job.isActive ? 'circle-filled' : 'circle-outline', + job.isActive + ? new vscode.ThemeColor('charts.green') + : new vscode.ThemeColor('disabledForeground'), + ); + + this.contextValue = job.isActive ? 'activeJob' : 'completedJob'; + + this.command = { + command: 'mariadbProfiler.selectJob', + title: 'Select Job', + arguments: [job], + }; + + this.tooltip = [ + `Job: ${job.key}`, + `Status: ${job.isActive ? 'Active' : 'Completed'}`, + `Started: ${new Date(job.startedAt * 1000).toLocaleString()}`, + job.endedAt ? `Ended: ${new Date(job.endedAt * 1000).toLocaleString()}` : '', + job.queryCount !== undefined ? `Queries: ${job.queryCount}` : '', + job.parent ? `Parent: ${job.parent}` : '', + ].filter(Boolean).join('\n'); + } +} diff --git a/vscode-extension/src/provider/QueryDocumentProvider.ts b/vscode-extension/src/provider/QueryDocumentProvider.ts new file mode 100644 index 0000000..7daa390 --- /dev/null +++ b/vscode-extension/src/provider/QueryDocumentProvider.ts @@ -0,0 +1,93 @@ +import * as vscode from 'vscode'; +import { QueryEntry, getBoundQuery, getQueryType, getTables, formatTimestamp } from '../model/QueryEntry'; + +const MAX_DOCUMENTS = 50; + +export class QueryDocumentProvider implements vscode.TextDocumentContentProvider { + static readonly scheme = 'mariadb-profiler'; + + private _onDidChange = new vscode.EventEmitter(); + readonly onDidChange = this._onDidChange.event; + + private documents = new Map(); + private insertionOrder: string[] = []; + + provideTextDocumentContent(uri: vscode.Uri): string { + const entry = this.documents.get(uri.toString()); + if (!entry) { return '-- Query not found'; } + + return this.formatQueryDocument(entry); + } + + async showQueryDetail(entry: QueryEntry, index: number): Promise { + const uri = vscode.Uri.parse( + `${QueryDocumentProvider.scheme}:query-${index}.sql?ts=${entry.timestamp}` + ); + + const key = uri.toString(); + if (this.documents.size >= MAX_DOCUMENTS && !this.documents.has(key)) { + const oldest = this.insertionOrder.shift(); + if (oldest) { this.documents.delete(oldest); } + } + this.documents.set(key, entry); + this.insertionOrder.push(key); + this._onDidChange.fire(uri); + + const doc = await vscode.workspace.openTextDocument(uri); + await vscode.window.showTextDocument(doc, { + preview: true, + viewColumn: vscode.ViewColumn.One, + }); + } + + private formatQueryDocument(entry: QueryEntry): string { + const lines: string[] = []; + + // Header metadata as SQL comments + lines.push(`-- Job: ${entry.jobKey}`); + lines.push(`-- Time: ${new Date(entry.timestamp * 1000).toISOString()}`); + lines.push(`-- Type: ${getQueryType(entry)}`); + + if (entry.tag) { + lines.push(`-- Tag: ${entry.tag}`); + } + if (entry.status) { + lines.push(`-- Status: ${entry.status}`); + } + + const tables = getTables(entry); + if (tables.length > 0) { + lines.push(`-- Tables: ${tables.join(', ')}`); + } + + lines.push(''); + + // Main SQL + lines.push(entry.query); + lines.push(''); + + // Bound parameters + if (entry.params && entry.params.length > 0) { + lines.push('-- Bound Parameters:'); + entry.params.forEach((param, i) => { + lines.push(`-- ?${i + 1} = ${param === null ? 'NULL' : param}`); + }); + lines.push(''); + + // Resolved query + lines.push('-- Resolved Query:'); + lines.push(`-- ${getBoundQuery(entry).replace(/\n/g, '\n-- ')}`); + lines.push(''); + } + + // Backtrace + if (entry.trace && entry.trace.length > 0) { + lines.push('-- Backtrace:'); + entry.trace.forEach((frame, i) => { + lines.push(`-- #${i} ${frame.file}:${frame.line} ${frame.call}`); + }); + } + + return lines.join('\n'); + } +} diff --git a/vscode-extension/src/provider/QueryTreeProvider.ts b/vscode-extension/src/provider/QueryTreeProvider.ts new file mode 100644 index 0000000..9a2b7da --- /dev/null +++ b/vscode-extension/src/provider/QueryTreeProvider.ts @@ -0,0 +1,283 @@ +import * as vscode from 'vscode'; +import { + QueryEntry, + BacktraceFrame, + getQueryType, + getShortSql, + getTables, + getBoundQuery, + formatTimestamp, + QueryType, +} from '../model/QueryEntry'; +import { applyPathMappings } from '../util/pathMapping'; + +type QueryTreeItem = QueryEntryItem | QueryMetadataItem | BacktraceHeaderItem | BacktraceFrameItem; + +export class QueryTreeProvider implements vscode.TreeDataProvider { + private _onDidChangeTreeData = new vscode.EventEmitter(); + readonly onDidChangeTreeData = this._onDidChangeTreeData.event; + + private allEntries: QueryEntry[] = []; + private filteredEntries: QueryEntry[] = []; + private typeFilter: string | null = null; + private tagFilter: string | null = null; + private searchText: string | null = null; + private resolvedFrameMap = new Map(); // entryIndex -> frameIndex + + loadEntries(entries: QueryEntry[]): void { + this.allEntries = entries; + this.applyFilters(); + } + + appendEntries(entries: QueryEntry[]): void { + this.allEntries.push(...entries); + this.applyFilters(); + } + + setFilter(queryType: string | null): void { + this.typeFilter = queryType; + this.applyFilters(); + } + + setTagFilter(tag: string | null): void { + this.tagFilter = tag; + this.applyFilters(); + } + + setSearchText(text: string | null): void { + this.searchText = text; + this.applyFilters(); + } + + clearFilters(): void { + this.typeFilter = null; + this.tagFilter = null; + this.searchText = null; + this.applyFilters(); + } + + hasActiveFilters(): boolean { + return this.typeFilter !== null || this.tagFilter !== null || this.searchText !== null; + } + + getFilterDescription(): string { + const parts: string[] = []; + if (this.typeFilter) { parts.push(this.typeFilter); } + if (this.tagFilter) { parts.push(`tag:${this.tagFilter}`); } + if (this.searchText) { parts.push(`"${this.searchText}"`); } + return parts.length > 0 ? parts.join(', ') : ''; + } + + getEntries(): QueryEntry[] { + return this.allEntries; + } + + getFilteredEntries(): QueryEntry[] { + return this.filteredEntries; + } + + setResolvedFrame(entryIndex: number, frameIndex: number): void { + this.resolvedFrameMap.set(entryIndex, frameIndex); + } + + getAllTags(): string[] { + const tags = new Set(); + for (const entry of this.allEntries) { + if (entry.tag) { tags.add(entry.tag); } + } + return [...tags].sort(); + } + + getTreeItem(element: QueryTreeItem): vscode.TreeItem { + return element; + } + + getChildren(element?: QueryTreeItem): QueryTreeItem[] { + if (!element) { + // Root level: filtered query entries + return this.filteredEntries.map((entry, index) => { + const globalIndex = this.allEntries.indexOf(entry); + return new QueryEntryItem(entry, globalIndex); + }); + } + + if (element instanceof QueryEntryItem) { + return this.getQueryChildren(element); + } + + if (element instanceof BacktraceHeaderItem) { + return this.getBacktraceChildren(element); + } + + return []; + } + + private getQueryChildren(item: QueryEntryItem): QueryTreeItem[] { + const entry = item.entry; + const children: QueryTreeItem[] = []; + + // Tables + const tables = getTables(entry); + if (tables.length > 0) { + children.push(new QueryMetadataItem(`Tables: ${tables.join(', ')}`, 'symbol-class')); + } + + // Tags + if (entry.tag) { + children.push(new QueryMetadataItem(`Tag: ${entry.tag}`, 'tag')); + } + + // Status + if (entry.status) { + const icon = entry.status === 'ok' ? 'check' : 'error'; + children.push(new QueryMetadataItem(`Status: ${entry.status}`, icon)); + } + + // Params + if (entry.params && entry.params.length > 0) { + const paramStr = entry.params.map((p, i) => `?${i + 1} = ${p === null ? 'NULL' : p}`).join(', '); + children.push(new QueryMetadataItem(`Params: ${paramStr}`, 'symbol-parameter')); + } + + // Backtrace + if (entry.trace && entry.trace.length > 0) { + children.push(new BacktraceHeaderItem(entry.trace, item.entryIndex, this.resolvedFrameMap.get(item.entryIndex))); + } + + return children; + } + + private getBacktraceChildren(item: BacktraceHeaderItem): QueryTreeItem[] { + return item.frames.map((frame, frameIndex) => { + const isResolved = item.resolvedFrameIndex === frameIndex; + return new BacktraceFrameItem(frame, isResolved); + }); + } + + private applyFilters(): void { + const maxQueries = vscode.workspace.getConfiguration('mariadbProfiler') + .get('maxQueries', 10000); + + let entries = this.allEntries; + + if (this.typeFilter) { + const filter = this.typeFilter; + entries = entries.filter(e => getQueryType(e) === filter); + } + + if (this.tagFilter) { + const tag = this.tagFilter; + entries = entries.filter(e => e.tag === tag); + } + + if (this.searchText) { + const search = this.searchText.toLowerCase(); + entries = entries.filter(e => e.query.toLowerCase().includes(search)); + } + + this.filteredEntries = entries.slice(0, maxQueries); + this._onDidChangeTreeData.fire(undefined); + } +} + +const QUERY_TYPE_ICONS: Record = { + SELECT: 'database', + INSERT: 'add', + UPDATE: 'edit', + DELETE: 'trash', + OTHER: 'question', +}; + +const QUERY_TYPE_COLORS: Record = { + SELECT: 'charts.blue', + INSERT: 'charts.green', + UPDATE: 'charts.orange', + DELETE: 'charts.red', + OTHER: 'charts.gray', +}; + +export class QueryEntryItem extends vscode.TreeItem { + readonly entry: QueryEntry; + readonly entryIndex: number; + + constructor(entry: QueryEntry, entryIndex: number) { + const qtype = getQueryType(entry); + const boundSql = getBoundQuery(entry).replace(/\s+/g, ' ').trim(); + const shortSql = boundSql.length <= 50 ? boundSql : boundSql.substring(0, 47) + '...'; + const label = `${qtype} ${shortSql}`; + + super(label, vscode.TreeItemCollapsibleState.Collapsed); + + this.entry = entry; + this.entryIndex = entryIndex; + this.contextValue = 'queryEntry'; + + // Description: [tag] HH:MM:SS.mmm + const parts: string[] = []; + if (entry.tag) { parts.push(`[${entry.tag}]`); } + parts.push(formatTimestamp(entry.timestamp)); + this.description = parts.join(' '); + + this.iconPath = new vscode.ThemeIcon( + QUERY_TYPE_ICONS[qtype], + new vscode.ThemeColor(QUERY_TYPE_COLORS[qtype]), + ); + + this.tooltip = new vscode.MarkdownString(); + this.tooltip.appendCodeblock(getBoundQuery(entry), 'sql'); + } +} + +export class QueryMetadataItem extends vscode.TreeItem { + constructor(label: string, icon: string) { + super(label, vscode.TreeItemCollapsibleState.None); + this.iconPath = new vscode.ThemeIcon(icon); + this.contextValue = 'queryMetadata'; + } +} + +export class BacktraceHeaderItem extends vscode.TreeItem { + readonly frames: BacktraceFrame[]; + readonly entryIndex: number; + readonly resolvedFrameIndex?: number; + + constructor(frames: BacktraceFrame[], entryIndex: number, resolvedFrameIndex?: number) { + super(`Backtrace (${frames.length} frames)`, vscode.TreeItemCollapsibleState.Collapsed); + this.frames = frames; + this.entryIndex = entryIndex; + this.resolvedFrameIndex = resolvedFrameIndex; + this.iconPath = new vscode.ThemeIcon('debug-stackframe'); + this.contextValue = 'backtraceHeader'; + } +} + +export class BacktraceFrameItem extends vscode.TreeItem { + constructor(frame: BacktraceFrame, isResolved: boolean) { + const fileName = frame.file.split('/').pop() || frame.file; + const label = `${fileName}:${frame.line}`; + + super(label, vscode.TreeItemCollapsibleState.None); + + this.description = frame.call; + this.contextValue = 'backtraceFrame'; + + this.iconPath = new vscode.ThemeIcon( + 'arrow-right', + isResolved ? new vscode.ThemeColor('charts.green') : undefined, + ); + + // Map path for Docker environments + const localPath = applyPathMappings(frame.file); + + this.command = { + command: 'vscode.open', + title: 'Open File', + arguments: [ + vscode.Uri.file(localPath), + { selection: new vscode.Range(frame.line - 1, 0, frame.line - 1, 0) } as vscode.TextDocumentShowOptions, + ], + }; + + this.tooltip = `${frame.file}:${frame.line}\n${frame.call}`; + } +} diff --git a/vscode-extension/src/provider/StatisticsTreeProvider.ts b/vscode-extension/src/provider/StatisticsTreeProvider.ts new file mode 100644 index 0000000..809c4ef --- /dev/null +++ b/vscode-extension/src/provider/StatisticsTreeProvider.ts @@ -0,0 +1,94 @@ +import * as vscode from 'vscode'; +import { QueryStats } from '../service/StatisticsService'; +import { generateBar, formatPercent } from '../util/queryUtils'; + +type StatTreeItem = StatHeaderItem | StatCategoryItem | StatBarItem; + +export class StatisticsTreeProvider implements vscode.TreeDataProvider { + private _onDidChangeTreeData = new vscode.EventEmitter(); + readonly onDidChangeTreeData = this._onDidChangeTreeData.event; + + private stats: QueryStats | null = null; + + updateStats(stats: QueryStats): void { + this.stats = stats; + this._onDidChangeTreeData.fire(); + } + + clear(): void { + this.stats = null; + this._onDidChangeTreeData.fire(); + } + + getTreeItem(element: StatTreeItem): vscode.TreeItem { + return element; + } + + getChildren(element?: StatTreeItem): StatTreeItem[] { + if (!this.stats) { + return [new StatHeaderItem('No job selected', 'info')]; + } + + if (!element) { + // Root level + return [ + new StatHeaderItem(`Total Queries: ${this.stats.totalQueries}`, 'pulse'), + new StatCategoryItem('Query Types', 'symbol-enum', this.stats.byType, this.stats.totalQueries), + new StatCategoryItem('Top Tables', 'symbol-class', this.stats.byTable, this.stats.totalQueries), + new StatCategoryItem('Top Tags', 'tag', this.stats.byTag, this.stats.totalQueries), + ]; + } + + if (element instanceof StatCategoryItem) { + return this.getCategoryChildren(element); + } + + return []; + } + + private getCategoryChildren(item: StatCategoryItem): StatBarItem[] { + const entries = Object.entries(item.data); + if (entries.length === 0) { + return [new StatBarItem('(none)', '', 0)]; + } + + const maxValue = entries.length > 0 ? entries[0][1] : 0; + + return entries.slice(0, 10).map(([key, value]) => { + const bar = generateBar(value, maxValue); + const pct = formatPercent(value, item.total); + return new StatBarItem(`${key} ${bar}`, `${value} (${pct})`, value); + }); + } +} + +class StatHeaderItem extends vscode.TreeItem { + constructor(label: string, icon: string) { + super(label, vscode.TreeItemCollapsibleState.None); + this.iconPath = new vscode.ThemeIcon(icon); + this.contextValue = 'statHeader'; + } +} + +class StatCategoryItem extends vscode.TreeItem { + readonly data: Record; + readonly total: number; + + constructor(label: string, icon: string, data: Record, total: number) { + const count = Object.keys(data).length; + super(label, count > 0 ? vscode.TreeItemCollapsibleState.Expanded : vscode.TreeItemCollapsibleState.None); + this.data = data; + this.total = total; + this.description = `(${count})`; + this.iconPath = new vscode.ThemeIcon(icon); + this.contextValue = 'statCategory'; + } +} + +class StatBarItem extends vscode.TreeItem { + constructor(label: string, description: string, _value: number) { + super(label, vscode.TreeItemCollapsibleState.None); + this.description = description; + this.contextValue = 'statBar'; + } +} diff --git a/vscode-extension/src/service/FileWatcherService.ts b/vscode-extension/src/service/FileWatcherService.ts new file mode 100644 index 0000000..86d3bcb --- /dev/null +++ b/vscode-extension/src/service/FileWatcherService.ts @@ -0,0 +1,71 @@ +import * as fs from 'fs'; +import * as vscode from 'vscode'; + +interface WatchEntry { + callback: () => void; + lastSize: number; + lastMtime: number; +} + +export class FileWatcherService implements vscode.Disposable { + private watchers = new Map(); + private timer: ReturnType | undefined; + private pollIntervalMs = 1000; + + constructor() { + this.timer = setInterval(() => this.poll(), this.pollIntervalMs); + } + + watchFile(filePath: string, onChange: () => void): void { + this.watchers.set(filePath, { + callback: onChange, + lastSize: this.getFileSize(filePath), + lastMtime: this.getFileMtime(filePath), + }); + } + + unwatchFile(filePath: string): void { + this.watchers.delete(filePath); + } + + dispose(): void { + if (this.timer) { + clearInterval(this.timer); + this.timer = undefined; + } + this.watchers.clear(); + } + + private poll(): void { + for (const [filePath, entry] of this.watchers) { + const size = this.getFileSize(filePath); + const mtime = this.getFileMtime(filePath); + + if (size !== entry.lastSize || mtime !== entry.lastMtime) { + entry.lastSize = size; + entry.lastMtime = mtime; + try { + entry.callback(); + } catch (e) { + // Ignore callback errors + } + } + } + } + + private getFileSize(filePath: string): number { + try { + return fs.statSync(filePath).size; + } catch { + return -1; + } + } + + private getFileMtime(filePath: string): number { + try { + return fs.statSync(filePath).mtimeMs; + } catch { + return -1; + } + } +} diff --git a/vscode-extension/src/service/FrameResolverService.ts b/vscode-extension/src/service/FrameResolverService.ts new file mode 100644 index 0000000..a9086da --- /dev/null +++ b/vscode-extension/src/service/FrameResolverService.ts @@ -0,0 +1,85 @@ +import * as vm from 'vm'; +import * as vscode from 'vscode'; +import { QueryEntry, BacktraceFrame } from '../model/QueryEntry'; + +export class FrameResolverService { + private cachedScript: vm.Script | null = null; + private cachedScriptText: string = ''; + private failedScripts = new Set(); + private errorChannel: vscode.OutputChannel; + + constructor(errorChannel: vscode.OutputChannel) { + this.errorChannel = errorChannel; + } + + resolve(entry: QueryEntry): number { + if (!entry.trace || entry.trace.length === 0) { return 0; } + + const scriptText = vscode.workspace.getConfiguration('mariadbProfiler') + .get('frameResolverScript', ''); + + if (!scriptText) { return 0; } + + if (!vscode.workspace.isTrusted) { + this.errorChannel.appendLine('[FrameResolver] Workspace is not trusted, skipping script execution'); + return 0; + } + + try { + const script = this.getScript(scriptText); + if (!script) { return 0; } + + const sandbox: Record = { + trace: entry.trace.map(f => ({ + file: f.file, + line: f.line, + call: f.call, + function: f.function || '', + class_name: f.class_name || '', + })), + tag: entry.tag || '', + query: entry.query, + }; + + const context = vm.createContext(sandbox); + const result = script.runInContext(context, { timeout: 1000 }); + + if (Number.isInteger(result) && result >= 0 && result < entry.trace.length) { + return result; + } + + return 0; + } catch (e) { + this.errorChannel.appendLine(`[FrameResolver] Error: ${e}`); + return 0; + } + } + + invalidateCache(): void { + this.cachedScript = null; + this.cachedScriptText = ''; + this.failedScripts.clear(); + } + + private getScript(scriptText: string): vm.Script | null { + if (this.cachedScriptText === scriptText && this.cachedScript) { + return this.cachedScript; + } + + if (this.failedScripts.has(scriptText)) { + return null; + } + + try { + this.cachedScript = new vm.Script(scriptText, { filename: 'frameResolver.js' }); + this.cachedScriptText = scriptText; + return this.cachedScript; + } catch (e) { + this.errorChannel.appendLine(`[FrameResolver] Compile error: ${e}`); + this.failedScripts.add(scriptText); + this.cachedScript = null; + this.cachedScriptText = ''; + return null; + } + } +} diff --git a/vscode-extension/src/service/JobManagerService.ts b/vscode-extension/src/service/JobManagerService.ts new file mode 100644 index 0000000..d09d809 --- /dev/null +++ b/vscode-extension/src/service/JobManagerService.ts @@ -0,0 +1,119 @@ +import * as fs from 'fs'; +import * as path from 'path'; +import * as vscode from 'vscode'; +import { execFile } from 'child_process'; +import { JobInfo, JobsFile, parseJobsFile, jobsFileToJobInfos } from '../model/JobInfo'; + +export class JobManagerService { + private errorChannel: vscode.OutputChannel; + + constructor(errorChannel: vscode.OutputChannel) { + this.errorChannel = errorChannel; + } + + loadJobs(): JobInfo[] { + const jobsPath = this.getJobsJsonPath(); + if (!fs.existsSync(jobsPath)) { return []; } + + try { + const content = fs.readFileSync(jobsPath, 'utf-8'); + const jobsFile = parseJobsFile(content); + return jobsFileToJobInfos(jobsFile); + } catch (e) { + this.errorChannel.appendLine(`[JobManager] Failed to load jobs.json: ${e}`); + return []; + } + } + + getActiveJobs(): JobInfo[] { + return this.loadJobs().filter(j => j.isActive); + } + + getCompletedJobs(): JobInfo[] { + return this.loadJobs().filter(j => !j.isActive); + } + + async startJob(jobKey?: string): Promise { + const args = ['job', 'start']; + if (jobKey) { args.push(jobKey); } + + const output = await this.runCli(args); + // CLI outputs: "Job '{key}' started" + const match = output.match(/Job '([^']+)' started/); + if (match) { return match[1]; } + + // Fallback: return the key if provided, or extract a safe identifier from output + if (jobKey) { return jobKey; } + const safeMatch = output.trim().match(/^[A-Za-z0-9_-]+$/); + if (safeMatch) { return safeMatch[0]; } + throw new Error('Could not determine job key from CLI output'); + } + + async stopJob(jobKey: string): Promise { + await this.runCli(['job', 'end', jobKey]); + } + + getJsonlPath(jobKey: string): string { + return path.join(this.getLogDir(), `${jobKey}.jsonl`); + } + + getRawLogPath(jobKey: string): string { + return path.join(this.getLogDir(), `${jobKey}.raw.log`); + } + + getJobsJsonPath(): string { + return path.join(this.getLogDir(), 'jobs.json'); + } + + getLogDir(): string { + return vscode.workspace.getConfiguration('mariadbProfiler') + .get('logDirectory', '/tmp/mariadb_profiler'); + } + + private getPhpPath(): string { + return vscode.workspace.getConfiguration('mariadbProfiler') + .get('phpPath', 'php'); + } + + private getCliScriptPath(): string { + const configured = vscode.workspace.getConfiguration('mariadbProfiler') + .get('cliScriptPath', ''); + + if (configured) { return configured; } + + // Auto-detect from workspace + const workspaceFolders = vscode.workspace.workspaceFolders; + if (workspaceFolders) { + for (const folder of workspaceFolders) { + const candidate = path.join(folder.uri.fsPath, 'cli', 'mariadb_profiler.php'); + if (fs.existsSync(candidate)) { return candidate; } + } + } + + return ''; + } + + private runCli(args: string[]): Promise { + return new Promise((resolve, reject) => { + const phpPath = this.getPhpPath(); + const scriptPath = this.getCliScriptPath(); + + if (!scriptPath) { + reject(new Error('CLI script path not configured and not found in workspace')); + return; + } + + const cliArgs = [scriptPath, `--log-dir=${this.getLogDir()}`, ...args]; + + execFile(phpPath, cliArgs, { timeout: 60000 }, (error, stdout, stderr) => { + if (error) { + this.errorChannel.appendLine(`[CLI] Error: ${error.message}`); + if (stderr) { this.errorChannel.appendLine(`[CLI] stderr: ${stderr}`); } + reject(error); + return; + } + resolve(stdout); + }); + }); + } +} diff --git a/vscode-extension/src/service/LogParserService.ts b/vscode-extension/src/service/LogParserService.ts new file mode 100644 index 0000000..d4ce707 --- /dev/null +++ b/vscode-extension/src/service/LogParserService.ts @@ -0,0 +1,94 @@ +import * as fs from 'fs'; +import * as vscode from 'vscode'; +import { QueryEntry, RawQueryEntry, fromRaw } from '../model/QueryEntry'; + +export class LogParserService { + private errorChannel: vscode.OutputChannel; + + constructor(errorChannel: vscode.OutputChannel) { + this.errorChannel = errorChannel; + } + + parseJsonlFile(filePath: string): QueryEntry[] { + if (!fs.existsSync(filePath)) { return []; } + + const content = fs.readFileSync(filePath, 'utf-8'); + return this.parseJsonlContent(content); + } + + parseJsonlFileFromOffset(filePath: string, offset: number): { entries: QueryEntry[]; newOffset: number } { + let fd: number; + try { + fd = fs.openSync(filePath, 'r'); + } catch { + return { entries: [], newOffset: offset }; + } + + try { + const stat = fs.fstatSync(fd); + if (stat.size <= offset) { + return { entries: [], newOffset: offset }; + } + + const bufSize = stat.size - offset; + const buffer = Buffer.alloc(bufSize); + const bytesRead = fs.readSync(fd, buffer, 0, bufSize, offset); + const content = buffer.slice(0, bytesRead).toString('utf-8'); + const entries = this.parseJsonlContent(content); + return { entries, newOffset: offset + bytesRead }; + } finally { + fs.closeSync(fd); + } + } + + readRawLogTail(filePath: string, maxLines: number = 500): string { + if (!fs.existsSync(filePath)) { return ''; } + + const content = fs.readFileSync(filePath, 'utf-8'); + const lines = content.split('\n'); + const tail = lines.slice(-maxLines); + return tail.join('\n'); + } + + tailRawLog(filePath: string, offset: number): { content: string; newOffset: number } { + let fd: number; + try { + fd = fs.openSync(filePath, 'r'); + } catch { + return { content: '', newOffset: offset }; + } + + try { + const stat = fs.fstatSync(fd); + if (stat.size <= offset) { + return { content: '', newOffset: offset }; + } + + const bufSize = stat.size - offset; + const buffer = Buffer.alloc(bufSize); + const bytesRead = fs.readSync(fd, buffer, 0, bufSize, offset); + return { content: buffer.slice(0, bytesRead).toString('utf-8'), newOffset: offset + bytesRead }; + } finally { + fs.closeSync(fd); + } + } + + private parseJsonlContent(content: string): QueryEntry[] { + const entries: QueryEntry[] = []; + const lines = content.split('\n'); + + for (const line of lines) { + const trimmed = line.trim(); + if (!trimmed) { continue; } + + try { + const raw: RawQueryEntry = JSON.parse(trimmed); + entries.push(fromRaw(raw)); + } catch (e) { + this.errorChannel.appendLine(`[LogParser] Failed to parse line: ${trimmed.substring(0, 100)}`); + } + } + + return entries; + } +} diff --git a/vscode-extension/src/service/StatisticsService.ts b/vscode-extension/src/service/StatisticsService.ts new file mode 100644 index 0000000..facc2a6 --- /dev/null +++ b/vscode-extension/src/service/StatisticsService.ts @@ -0,0 +1,48 @@ +import { QueryEntry, getQueryType, getTables } from '../model/QueryEntry'; + +export interface QueryStats { + totalQueries: number; + byType: Record; + byTable: Record; + byTag: Record; +} + +export class StatisticsService { + computeStats(entries: QueryEntry[]): QueryStats { + const byType: Record = {}; + const byTable: Record = {}; + const byTag: Record = {}; + + for (const entry of entries) { + // By type + const qtype = getQueryType(entry); + byType[qtype] = (byType[qtype] || 0) + 1; + + // By table + for (const table of getTables(entry)) { + byTable[table] = (byTable[table] || 0) + 1; + } + + // By tag + if (entry.tag) { + byTag[entry.tag] = (byTag[entry.tag] || 0) + 1; + } + } + + return { + totalQueries: entries.length, + byType: sortByValueDesc(byType), + byTable: sortByValueDesc(byTable), + byTag: sortByValueDesc(byTag), + }; + } +} + +function sortByValueDesc(record: Record): Record { + const sorted: Record = {}; + const entries = Object.entries(record).sort((a, b) => b[1] - a[1]); + for (const [key, value] of entries) { + sorted[key] = value; + } + return sorted; +} diff --git a/vscode-extension/src/util/pathMapping.ts b/vscode-extension/src/util/pathMapping.ts new file mode 100644 index 0000000..536ebc8 --- /dev/null +++ b/vscode-extension/src/util/pathMapping.ts @@ -0,0 +1,14 @@ +import * as vscode from 'vscode'; + +export function applyPathMappings(containerPath: string): string { + const config = vscode.workspace.getConfiguration('mariadbProfiler'); + const mappings = config.get>('pathMappings', {}); + + for (const [from, to] of Object.entries(mappings)) { + if (containerPath.startsWith(from)) { + return to + containerPath.slice(from.length); + } + } + + return containerPath; +} diff --git a/vscode-extension/src/util/queryUtils.ts b/vscode-extension/src/util/queryUtils.ts new file mode 100644 index 0000000..4c300a3 --- /dev/null +++ b/vscode-extension/src/util/queryUtils.ts @@ -0,0 +1,15 @@ +export function generateBar(value: number, max: number, barWidth: number = 20): string { + if (max === 0) { return '\u2591'.repeat(barWidth); } + const filled = Math.max(0, Math.min(barWidth, Math.round((value / max) * barWidth))); + return '\u2588'.repeat(filled) + '\u2591'.repeat(barWidth - filled); +} + +export function formatPercent(value: number, total: number): string { + if (total === 0) { return '0%'; } + return `${Math.round((value / total) * 100)}%`; +} + +export function shortKey(key: string, maxLen: number = 12): string { + if (key.length <= maxLen) { return key; } + return key.substring(0, maxLen); +} diff --git a/vscode-extension/test/unit/JobInfo.test.ts b/vscode-extension/test/unit/JobInfo.test.ts new file mode 100644 index 0000000..525e0ae --- /dev/null +++ b/vscode-extension/test/unit/JobInfo.test.ts @@ -0,0 +1,106 @@ +import { describe, it, expect } from 'vitest'; +import { + parseJobsFile, + jobsFileToJobInfos, + formatDuration, +} from '../../src/model/JobInfo'; + +describe('parseJobsFile', () => { + it('should parse standard jobs.json', () => { + const json = JSON.stringify({ + active_jobs: { + 'abc-123': { started_at: 1705970401.0, parent: null }, + }, + completed_jobs: { + 'def-456': { started_at: 1705970300.0, ended_at: 1705970400.0, query_count: 42 }, + }, + }); + + const result = parseJobsFile(json); + expect(Object.keys(result.active_jobs)).toEqual(['abc-123']); + expect(Object.keys(result.completed_jobs)).toEqual(['def-456']); + expect(result.active_jobs['abc-123'].started_at).toBe(1705970401.0); + expect(result.completed_jobs['def-456'].query_count).toBe(42); + }); + + it('should handle PHP empty array as empty map', () => { + const json = JSON.stringify({ + active_jobs: [], + completed_jobs: { 'def-456': { started_at: 1705970300.0 } }, + }); + + const result = parseJobsFile(json); + expect(result.active_jobs).toEqual({}); + expect(Object.keys(result.completed_jobs)).toEqual(['def-456']); + }); + + it('should handle both maps empty (PHP format)', () => { + const json = JSON.stringify({ + active_jobs: [], + completed_jobs: [], + }); + + const result = parseJobsFile(json); + expect(result.active_jobs).toEqual({}); + expect(result.completed_jobs).toEqual({}); + }); + + it('should handle parent field', () => { + const json = JSON.stringify({ + active_jobs: {}, + completed_jobs: { + 'child-job': { started_at: 100, ended_at: 200, query_count: 5, parent: 'parent-job' }, + }, + }); + + const result = parseJobsFile(json); + expect(result.completed_jobs['child-job'].parent).toBe('parent-job'); + }); +}); + +describe('jobsFileToJobInfos', () => { + it('should convert to sorted JobInfo array', () => { + const json = JSON.stringify({ + active_jobs: { + 'active-1': { started_at: 100 }, + }, + completed_jobs: { + 'completed-1': { started_at: 50, ended_at: 80, query_count: 10 }, + 'completed-2': { started_at: 90, ended_at: 95, query_count: 5 }, + }, + }); + + const jobsFile = parseJobsFile(json); + const infos = jobsFileToJobInfos(jobsFile); + + // Active jobs first + expect(infos[0].key).toBe('active-1'); + expect(infos[0].isActive).toBe(true); + + // Completed sorted by startedAt descending + expect(infos[1].key).toBe('completed-2'); + expect(infos[1].isActive).toBe(false); + expect(infos[2].key).toBe('completed-1'); + expect(infos[2].isActive).toBe(false); + expect(infos[2].queryCount).toBe(10); + }); + + it('should handle empty jobs file', () => { + const jobsFile = parseJobsFile(JSON.stringify({ active_jobs: [], completed_jobs: [] })); + expect(jobsFileToJobInfos(jobsFile)).toEqual([]); + }); +}); + +describe('formatDuration', () => { + it('should format short duration in seconds', () => { + expect(formatDuration(100, 103.2)).toBe('3.2s'); + }); + + it('should format longer duration with minutes', () => { + expect(formatDuration(100, 225)).toBe('2m5s'); + }); + + it('should handle zero duration', () => { + expect(formatDuration(100, 100)).toBe('0 ms'); + }); +}); diff --git a/vscode-extension/test/unit/LogParserService.test.ts b/vscode-extension/test/unit/LogParserService.test.ts new file mode 100644 index 0000000..55292f1 --- /dev/null +++ b/vscode-extension/test/unit/LogParserService.test.ts @@ -0,0 +1,164 @@ +import { describe, it, expect, beforeEach, afterEach } from 'vitest'; +import * as fs from 'fs'; +import * as path from 'path'; +import * as os from 'os'; + +// Mock vscode module for LogParserService +import { vi } from 'vitest'; +vi.mock('vscode', () => ({ + window: { + createOutputChannel: () => ({ + appendLine: () => {}, + show: () => {}, + clear: () => {}, + dispose: () => {}, + }), + }, + workspace: { + getConfiguration: () => ({ + get: (_key: string, defaultValue: unknown) => defaultValue, + }), + }, +})); + +import { LogParserService } from '../../src/service/LogParserService'; + +describe('LogParserService', () => { + let tmpDir: string; + let service: LogParserService; + const mockChannel = { + appendLine: () => {}, + show: () => {}, + clear: () => {}, + dispose: () => {}, + } as any; + + beforeEach(() => { + tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'logparser-test-')); + service = new LogParserService(mockChannel); + }); + + afterEach(() => { + fs.rmSync(tmpDir, { recursive: true, force: true }); + }); + + describe('parseJsonlFile', () => { + it('should parse valid JSONL file', () => { + const filePath = path.join(tmpDir, 'test.jsonl'); + const lines = [ + '{"k":"job1","q":"SELECT 1","ts":100}', + '{"k":"job1","q":"INSERT INTO t VALUES (1)","ts":101,"tag":"api"}', + ]; + fs.writeFileSync(filePath, lines.join('\n')); + + const entries = service.parseJsonlFile(filePath); + expect(entries).toHaveLength(2); + expect(entries[0].jobKey).toBe('job1'); + expect(entries[0].query).toBe('SELECT 1'); + expect(entries[1].tag).toBe('api'); + }); + + it('should skip invalid lines', () => { + const filePath = path.join(tmpDir, 'test.jsonl'); + const lines = [ + '{"k":"job1","q":"SELECT 1","ts":100}', + 'INVALID JSON', + '{"k":"job1","q":"SELECT 2","ts":102}', + ]; + fs.writeFileSync(filePath, lines.join('\n')); + + const entries = service.parseJsonlFile(filePath); + expect(entries).toHaveLength(2); + }); + + it('should return empty for non-existent file', () => { + const entries = service.parseJsonlFile('/nonexistent/file.jsonl'); + expect(entries).toEqual([]); + }); + + it('should handle empty file', () => { + const filePath = path.join(tmpDir, 'empty.jsonl'); + fs.writeFileSync(filePath, ''); + const entries = service.parseJsonlFile(filePath); + expect(entries).toEqual([]); + }); + + it('should handle trailing newline', () => { + const filePath = path.join(tmpDir, 'test.jsonl'); + fs.writeFileSync(filePath, '{"k":"j","q":"SELECT 1","ts":0}\n'); + const entries = service.parseJsonlFile(filePath); + expect(entries).toHaveLength(1); + }); + }); + + describe('parseJsonlFileFromOffset', () => { + it('should read new entries from offset', () => { + const filePath = path.join(tmpDir, 'test.jsonl'); + const line1 = '{"k":"job1","q":"SELECT 1","ts":100}\n'; + const line2 = '{"k":"job1","q":"SELECT 2","ts":101}\n'; + fs.writeFileSync(filePath, line1); + + // First read + const result1 = service.parseJsonlFileFromOffset(filePath, 0); + expect(result1.entries).toHaveLength(1); + expect(result1.newOffset).toBe(Buffer.byteLength(line1)); + + // Append new data + fs.appendFileSync(filePath, line2); + + // Second read from offset + const result2 = service.parseJsonlFileFromOffset(filePath, result1.newOffset); + expect(result2.entries).toHaveLength(1); + expect(result2.entries[0].query).toBe('SELECT 2'); + }); + + it('should return empty when no new data', () => { + const filePath = path.join(tmpDir, 'test.jsonl'); + fs.writeFileSync(filePath, '{"k":"j","q":"SELECT 1","ts":0}\n'); + + const result1 = service.parseJsonlFileFromOffset(filePath, 0); + const result2 = service.parseJsonlFileFromOffset(filePath, result1.newOffset); + expect(result2.entries).toHaveLength(0); + expect(result2.newOffset).toBe(result1.newOffset); + }); + + it('should handle non-existent file', () => { + const result = service.parseJsonlFileFromOffset('/nonexistent', 0); + expect(result.entries).toEqual([]); + expect(result.newOffset).toBe(0); + }); + }); + + describe('readRawLogTail', () => { + it('should read tail of raw log', () => { + const filePath = path.join(tmpDir, 'test.raw.log'); + const lines = Array.from({ length: 10 }, (_, i) => `line ${i}`); + fs.writeFileSync(filePath, lines.join('\n')); + + const result = service.readRawLogTail(filePath, 3); + const resultLines = result.split('\n'); + expect(resultLines).toHaveLength(3); + expect(resultLines[0]).toBe('line 7'); + expect(resultLines[2]).toBe('line 9'); + }); + + it('should return empty for non-existent file', () => { + expect(service.readRawLogTail('/nonexistent')).toBe(''); + }); + }); + + describe('tailRawLog', () => { + it('should read new content from offset', () => { + const filePath = path.join(tmpDir, 'test.raw.log'); + fs.writeFileSync(filePath, 'line 1\n'); + + const r1 = service.tailRawLog(filePath, 0); + expect(r1.content).toBe('line 1\n'); + + fs.appendFileSync(filePath, 'line 2\n'); + + const r2 = service.tailRawLog(filePath, r1.newOffset); + expect(r2.content).toBe('line 2\n'); + }); + }); +}); diff --git a/vscode-extension/test/unit/QueryEntry.test.ts b/vscode-extension/test/unit/QueryEntry.test.ts new file mode 100644 index 0000000..3fe623f --- /dev/null +++ b/vscode-extension/test/unit/QueryEntry.test.ts @@ -0,0 +1,239 @@ +import { describe, it, expect } from 'vitest'; +import { + QueryEntry, + RawQueryEntry, + fromRaw, + getQueryType, + getBoundQuery, + getTables, + getShortSql, + getSourceFile, + formatTimestamp, +} from '../../src/model/QueryEntry'; + +describe('fromRaw', () => { + it('should convert raw JSONL entry to QueryEntry', () => { + const raw: RawQueryEntry = { + k: 'job1', + q: 'SELECT * FROM users', + ts: 1705970401.123, + tag: 'api', + s: 'ok', + params: ['42'], + trace: [{ call: 'UserController->index', file: '/app/UserController.php', line: 42 }], + }; + + const entry = fromRaw(raw); + + expect(entry.jobKey).toBe('job1'); + expect(entry.query).toBe('SELECT * FROM users'); + expect(entry.timestamp).toBe(1705970401.123); + expect(entry.tag).toBe('api'); + expect(entry.status).toBe('ok'); + expect(entry.params).toEqual(['42']); + expect(entry.trace).toHaveLength(1); + expect(entry.trace![0].file).toBe('/app/UserController.php'); + }); + + it('should handle minimal entry', () => { + const raw: RawQueryEntry = { k: 'j', q: 'SELECT 1', ts: 0 }; + const entry = fromRaw(raw); + + expect(entry.jobKey).toBe('j'); + expect(entry.query).toBe('SELECT 1'); + expect(entry.tag).toBeUndefined(); + expect(entry.status).toBeUndefined(); + expect(entry.params).toBeUndefined(); + expect(entry.trace).toBeUndefined(); + }); +}); + +describe('getQueryType', () => { + it('should detect SELECT', () => { + expect(getQueryType({ jobKey: '', query: 'SELECT * FROM users', timestamp: 0 })).toBe('SELECT'); + }); + + it('should detect INSERT', () => { + expect(getQueryType({ jobKey: '', query: 'INSERT INTO logs VALUES (1)', timestamp: 0 })).toBe('INSERT'); + }); + + it('should detect UPDATE', () => { + expect(getQueryType({ jobKey: '', query: 'UPDATE users SET name = ?', timestamp: 0 })).toBe('UPDATE'); + }); + + it('should detect DELETE', () => { + expect(getQueryType({ jobKey: '', query: 'DELETE FROM logs WHERE id = 1', timestamp: 0 })).toBe('DELETE'); + }); + + it('should detect OTHER for non-standard queries', () => { + expect(getQueryType({ jobKey: '', query: 'SHOW TABLES', timestamp: 0 })).toBe('OTHER'); + }); + + it('should handle leading whitespace', () => { + expect(getQueryType({ jobKey: '', query: ' SELECT 1', timestamp: 0 })).toBe('SELECT'); + }); + + it('should be case-insensitive', () => { + expect(getQueryType({ jobKey: '', query: 'select * from t', timestamp: 0 })).toBe('SELECT'); + }); +}); + +describe('getBoundQuery', () => { + it('should return original query when no params', () => { + const entry: QueryEntry = { jobKey: '', query: 'SELECT 1', timestamp: 0 }; + expect(getBoundQuery(entry)).toBe('SELECT 1'); + }); + + it('should return original query when params is empty', () => { + const entry: QueryEntry = { jobKey: '', query: 'SELECT 1', timestamp: 0, params: [] }; + expect(getBoundQuery(entry)).toBe('SELECT 1'); + }); + + it('should bind single parameter', () => { + const entry: QueryEntry = { + jobKey: '', query: 'SELECT * FROM users WHERE id = ?', timestamp: 0, + params: ['42'], + }; + expect(getBoundQuery(entry)).toBe("SELECT * FROM users WHERE id = '42'"); + }); + + it('should bind multiple parameters', () => { + const entry: QueryEntry = { + jobKey: '', query: 'SELECT * FROM users WHERE name = ? AND id = ?', timestamp: 0, + params: ['John', '42'], + }; + expect(getBoundQuery(entry)).toBe("SELECT * FROM users WHERE name = 'John' AND id = '42'"); + }); + + it('should handle NULL parameters', () => { + const entry: QueryEntry = { + jobKey: '', query: 'INSERT INTO t (a) VALUES (?)', timestamp: 0, + params: [null], + }; + expect(getBoundQuery(entry)).toBe('INSERT INTO t (a) VALUES (NULL)'); + }); + + it('should not replace ? inside string literals', () => { + const entry: QueryEntry = { + jobKey: '', query: "SELECT * FROM t WHERE a = '?' AND b = ?", timestamp: 0, + params: ['val'], + }; + expect(getBoundQuery(entry)).toBe("SELECT * FROM t WHERE a = '?' AND b = 'val'"); + }); + + it('should not replace ? inside backtick identifiers', () => { + const entry: QueryEntry = { + jobKey: '', query: 'SELECT `?` FROM t WHERE a = ?', timestamp: 0, + params: ['val'], + }; + expect(getBoundQuery(entry)).toBe("SELECT `?` FROM t WHERE a = 'val'"); + }); + + it('should skip ? in line comments', () => { + const entry: QueryEntry = { + jobKey: '', query: 'SELECT 1 -- ? placeholder\nWHERE a = ?', timestamp: 0, + params: ['val'], + }; + expect(getBoundQuery(entry)).toBe("SELECT 1 -- ? placeholder\nWHERE a = 'val'"); + }); + + it('should skip ? in block comments', () => { + const entry: QueryEntry = { + jobKey: '', query: 'SELECT /* ? */ * FROM t WHERE a = ?', timestamp: 0, + params: ['val'], + }; + expect(getBoundQuery(entry)).toBe("SELECT /* ? */ * FROM t WHERE a = 'val'"); + }); +}); + +describe('getTables', () => { + it('should extract tables from SELECT', () => { + const entry: QueryEntry = { + jobKey: '', query: 'SELECT * FROM users u JOIN posts p ON p.user_id = u.id', timestamp: 0, + }; + expect(getTables(entry)).toEqual(['posts', 'users']); + }); + + it('should extract tables from INSERT', () => { + const entry: QueryEntry = { + jobKey: '', query: 'INSERT INTO logs (msg) VALUES (?)', timestamp: 0, + }; + expect(getTables(entry)).toEqual(['logs']); + }); + + it('should extract tables from UPDATE', () => { + const entry: QueryEntry = { + jobKey: '', query: 'UPDATE users SET name = ?', timestamp: 0, + }; + expect(getTables(entry)).toEqual(['users']); + }); + + it('should extract tables from DELETE', () => { + const entry: QueryEntry = { + jobKey: '', query: 'DELETE FROM logs WHERE id = 1', timestamp: 0, + }; + expect(getTables(entry)).toEqual(['logs']); + }); + + it('should handle backtick-quoted table names', () => { + const entry: QueryEntry = { + jobKey: '', query: 'SELECT * FROM `users`', timestamp: 0, + }; + expect(getTables(entry)).toEqual(['users']); + }); + + it('should deduplicate tables', () => { + const entry: QueryEntry = { + jobKey: '', query: 'SELECT * FROM users u WHERE u.id IN (SELECT user_id FROM users)', timestamp: 0, + }; + expect(getTables(entry)).toEqual(['users']); + }); +}); + +describe('getShortSql', () => { + it('should return short SQL as-is', () => { + const entry: QueryEntry = { jobKey: '', query: 'SELECT 1', timestamp: 0 }; + expect(getShortSql(entry)).toBe('SELECT 1'); + }); + + it('should truncate long SQL', () => { + const longQuery = 'SELECT ' + 'a'.repeat(100) + ' FROM users'; + const entry: QueryEntry = { jobKey: '', query: longQuery, timestamp: 0 }; + const result = getShortSql(entry, 20); + expect(result.length).toBe(20); + expect(result.endsWith('...')).toBe(true); + }); + + it('should normalize whitespace', () => { + const entry: QueryEntry = { jobKey: '', query: 'SELECT\n *\n FROM\n users', timestamp: 0 }; + expect(getShortSql(entry)).toBe('SELECT * FROM users'); + }); +}); + +describe('getSourceFile', () => { + it('should return null when no trace', () => { + const entry: QueryEntry = { jobKey: '', query: 'SELECT 1', timestamp: 0 }; + expect(getSourceFile(entry)).toBeNull(); + }); + + it('should return null when trace is empty', () => { + const entry: QueryEntry = { jobKey: '', query: 'SELECT 1', timestamp: 0, trace: [] }; + expect(getSourceFile(entry)).toBeNull(); + }); + + it('should return first frame as file:line', () => { + const entry: QueryEntry = { + jobKey: '', query: 'SELECT 1', timestamp: 0, + trace: [{ call: 'test()', file: '/app/Test.php', line: 42 }], + }; + expect(getSourceFile(entry)).toBe('/app/Test.php:42'); + }); +}); + +describe('formatTimestamp', () => { + it('should format unix timestamp to HH:MM:SS.mmm', () => { + const result = formatTimestamp(1705970401.123); + // Just check format, not exact value (timezone dependent) + expect(result).toMatch(/^\d{2}:\d{2}:\d{2}\.\d{3}$/); + }); +}); diff --git a/vscode-extension/test/unit/StatisticsService.test.ts b/vscode-extension/test/unit/StatisticsService.test.ts new file mode 100644 index 0000000..c36e00e --- /dev/null +++ b/vscode-extension/test/unit/StatisticsService.test.ts @@ -0,0 +1,75 @@ +import { describe, it, expect } from 'vitest'; +import { StatisticsService } from '../../src/service/StatisticsService'; +import { QueryEntry } from '../../src/model/QueryEntry'; + +describe('StatisticsService', () => { + const service = new StatisticsService(); + + it('should compute empty stats for no entries', () => { + const stats = service.computeStats([]); + expect(stats.totalQueries).toBe(0); + expect(stats.byType).toEqual({}); + expect(stats.byTable).toEqual({}); + expect(stats.byTag).toEqual({}); + }); + + it('should count queries by type', () => { + const entries: QueryEntry[] = [ + { jobKey: 'j', query: 'SELECT 1', timestamp: 0 }, + { jobKey: 'j', query: 'SELECT 2', timestamp: 0 }, + { jobKey: 'j', query: 'INSERT INTO t VALUES (1)', timestamp: 0 }, + { jobKey: 'j', query: 'UPDATE t SET a = 1', timestamp: 0 }, + { jobKey: 'j', query: 'DELETE FROM t', timestamp: 0 }, + ]; + + const stats = service.computeStats(entries); + expect(stats.totalQueries).toBe(5); + expect(stats.byType['SELECT']).toBe(2); + expect(stats.byType['INSERT']).toBe(1); + expect(stats.byType['UPDATE']).toBe(1); + expect(stats.byType['DELETE']).toBe(1); + }); + + it('should count queries by table', () => { + const entries: QueryEntry[] = [ + { jobKey: 'j', query: 'SELECT * FROM users', timestamp: 0 }, + { jobKey: 'j', query: 'SELECT * FROM users JOIN posts ON 1=1', timestamp: 0 }, + { jobKey: 'j', query: 'INSERT INTO posts VALUES (1)', timestamp: 0 }, + ]; + + const stats = service.computeStats(entries); + expect(stats.byTable['users']).toBe(2); + expect(stats.byTable['posts']).toBe(2); + }); + + it('should count queries by tag', () => { + const entries: QueryEntry[] = [ + { jobKey: 'j', query: 'SELECT 1', timestamp: 0, tag: 'api' }, + { jobKey: 'j', query: 'SELECT 2', timestamp: 0, tag: 'api' }, + { jobKey: 'j', query: 'SELECT 3', timestamp: 0, tag: 'web' }, + { jobKey: 'j', query: 'SELECT 4', timestamp: 0 }, // no tag + ]; + + const stats = service.computeStats(entries); + expect(stats.byTag['api']).toBe(2); + expect(stats.byTag['web']).toBe(1); + expect(stats.byTag['']).toBeUndefined(); // no-tag entries not counted + }); + + it('should sort by value descending', () => { + const entries: QueryEntry[] = [ + { jobKey: 'j', query: 'SELECT 1', timestamp: 0, tag: 'rare' }, + { jobKey: 'j', query: 'SELECT 2', timestamp: 0, tag: 'common' }, + { jobKey: 'j', query: 'SELECT 3', timestamp: 0, tag: 'common' }, + { jobKey: 'j', query: 'SELECT 4', timestamp: 0, tag: 'common' }, + { jobKey: 'j', query: 'SELECT 5', timestamp: 0, tag: 'medium' }, + { jobKey: 'j', query: 'SELECT 6', timestamp: 0, tag: 'medium' }, + ]; + + const stats = service.computeStats(entries); + const tagKeys = Object.keys(stats.byTag); + expect(tagKeys[0]).toBe('common'); + expect(tagKeys[1]).toBe('medium'); + expect(tagKeys[2]).toBe('rare'); + }); +}); diff --git a/vscode-extension/tsconfig.json b/vscode-extension/tsconfig.json new file mode 100644 index 0000000..8ec7dad --- /dev/null +++ b/vscode-extension/tsconfig.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "module": "commonjs", + "target": "ES2022", + "lib": ["ES2022"], + "outDir": "dist", + "rootDir": "src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "declaration": true, + "declarationMap": true, + "sourceMap": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist", "test"] +} diff --git a/vscode-extension/vitest.config.ts b/vscode-extension/vitest.config.ts new file mode 100644 index 0000000..252c33f --- /dev/null +++ b/vscode-extension/vitest.config.ts @@ -0,0 +1,8 @@ +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + test: { + include: ['test/**/*.test.ts'], + globals: true, + }, +});