diff --git a/.github/workflows/publish-extension.yml b/.github/workflows/publish-extension.yml new file mode 100644 index 00000000..cbe10e20 --- /dev/null +++ b/.github/workflows/publish-extension.yml @@ -0,0 +1,198 @@ +name: Publish Nexpath VS Code Extension + +# Manual trigger (workflow_dispatch) AND automatic trigger on tag push. +# The build step always runs on every supported OS; the publish step only +# runs when triggered with publish=true OR by a tag matching vsix-v*. +# +# Required repository secrets for publishing: +# VSCE_PAT — Azure DevOps Personal Access Token (Marketplace: Manage) +# OVSX_PAT — Open VSX namespace access token +# +# Both should be created via Settings → Secrets and variables → Actions. + +on: + workflow_dispatch: + inputs: + publish: + description: "Publish to marketplaces (false = artefacts only)" + type: boolean + required: true + default: false + push: + tags: + - "vsix-v*" + +jobs: + # ────────────────────────────────────────────────────────────────────────── + # Matrix: build one .vsix per supported (os, arch) pair. + # better-sqlite3 ships per-platform prebuilds; each runner naturally has + # the right binary in node_modules after `npm install`. + # ────────────────────────────────────────────────────────────────────────── + package: + name: Package (${{ matrix.target }}) + runs-on: ${{ matrix.runner }} + strategy: + fail-fast: false + matrix: + include: + # Pin every runner to a specific version so the matrix is reproducible + # across runs; otherwise GitHub's `*-latest` labels drift to new minor + # versions without warning (and our better-sqlite3 prebuilds are + # node-abi keyed, which can desync with the runner's default node). + - target: linux-x64 + runner: ubuntu-22.04 + - target: darwin-x64 + runner: macos-13 # Intel mac for x64 prebuild + - target: darwin-arm64 + runner: macos-14 # Apple Silicon (pinned; previously macos-latest) + - target: win32-x64 + runner: windows-2022 + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Setup Node + uses: actions/setup-node@v4 + with: + node-version: "20" + cache: "npm" + cache-dependency-path: src/ext-vscode/package-lock.json + + - name: Install sub-package deps + working-directory: src/ext-vscode + run: npm ci + + - name: Typecheck + working-directory: src/ext-vscode + run: npx tsc --noEmit + + - name: Unit tests + working-directory: src/ext-vscode + run: npm test + + - name: Build esbuild bundle + working-directory: src/ext-vscode + run: npm run build + + - name: Package .vsix (with better-sqlite3 rebuilt for Cursor's Electron ABI) + working-directory: src/ext-vscode + run: | + mkdir -p dist-vsix + # package-with-electron-abi.mjs rebuilds better-sqlite3 against the + # Electron version pinned in package.json#nexpathTargets.electron, + # runs vsce package, then restores the Node-ABI binary. Skipping + # this wrapper would ship a Node-ABI binding in the .vsix and Cursor + # would fail to activate with NODE_MODULE_VERSION mismatch. + node scripts/package-with-electron-abi.mjs --target ${{ matrix.target }} --out dist-vsix/ + + - name: Upload .vsix artefact + uses: actions/upload-artifact@v4 + with: + name: vsix-${{ matrix.target }} + path: src/ext-vscode/dist-vsix/*.vsix + if-no-files-found: error + retention-days: 30 + + # Linux ARM64 builds via QEMU emulation — separate job because the + # ubuntu-latest runner is x64. Slower, but the prebuilt better-sqlite3 + # is available on npm for linux-arm64. + package-linux-arm64: + name: Package (linux-arm64) + runs-on: ubuntu-22.04 + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + with: + platforms: arm64 + + - name: Build + package inside arm64 container + run: | + docker run --rm --platform linux/arm64 \ + -v "${{ github.workspace }}":/work -w /work/src/ext-vscode \ + node:20-bullseye bash -lc " + npm ci && + npx tsc --noEmit && + npm test && + npm run build && + mkdir -p dist-vsix && + node scripts/package-with-electron-abi.mjs --target linux-arm64 --out dist-vsix/ + " + + - name: Upload .vsix artefact + uses: actions/upload-artifact@v4 + with: + name: vsix-linux-arm64 + path: src/ext-vscode/dist-vsix/*.vsix + if-no-files-found: error + retention-days: 30 + + # ────────────────────────────────────────────────────────────────────────── + # Publish — only runs on tag push or when manually triggered with publish=true. + # Pulls every matrix .vsix as an artefact, then loops them through vsce + ovsx. + # ────────────────────────────────────────────────────────────────────────── + publish: + name: Publish to marketplaces + needs: [package, package-linux-arm64] + if: ${{ github.event_name == 'push' || inputs.publish == true }} + runs-on: ubuntu-22.04 + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node + uses: actions/setup-node@v4 + with: + node-version: "20" + cache: "npm" + cache-dependency-path: src/ext-vscode/package-lock.json + + - name: Install sub-package deps (for vsce + ovsx) + working-directory: src/ext-vscode + run: npm ci + + - name: Download all .vsix artefacts + uses: actions/download-artifact@v4 + with: + pattern: vsix-* + path: dist-vsix-all + merge-multiple: true + + - name: List downloaded .vsix files + run: ls -la dist-vsix-all/ + + - name: Publish to VS Code Marketplace (vsce) + if: ${{ env.VSCE_PAT != '' }} + env: + VSCE_PAT: ${{ secrets.VSCE_PAT }} + working-directory: src/ext-vscode + run: | + for vsix in ../../dist-vsix-all/*.vsix ; do + echo "Publishing $vsix to VS Code Marketplace..." + npx vsce publish --packagePath "$vsix" + done + + - name: Publish to Open VSX (ovsx) + if: ${{ env.OVSX_PAT != '' }} + env: + OVSX_PAT: ${{ secrets.OVSX_PAT }} + working-directory: src/ext-vscode + run: | + for vsix in ../../dist-vsix-all/*.vsix ; do + echo "Publishing $vsix to Open VSX..." + npx ovsx publish "$vsix" + done + + - name: Sanity-check publication + run: | + echo "If the steps above succeeded:" + echo " - https://marketplace.visualstudio.com/items?itemName=emptyops.nexpath-vscode" + echo " - https://open-vsx.org/extension/emptyops/nexpath-vscode" + echo "" + echo "Note: marketplace ingestion may lag a few minutes after publish." diff --git a/.gitignore b/.gitignore index 4237133a..250f77cf 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,6 @@ node_modules/ *.js.map *.d.ts.map .env +src/ext-vscode/out/ +src/ext-vscode/node_modules/ +src/ext-vscode/*.vsix diff --git a/package.json b/package.json index 68b2795d..91937f68 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,7 @@ }, "scripts": { "build": "tsc", + "postbuild": "node -e \"['dist/cli/index.js','dist/server/index.js'].forEach(f => require('fs').chmodSync(f, 0o755))\"", "dev:cli": "tsx src/cli/index.ts", "dev:server": "tsx src/server/index.ts", "typecheck": "tsc --noEmit", diff --git a/scripts/dev-probe.cjs b/scripts/dev-probe.cjs new file mode 100644 index 00000000..f1cafe91 --- /dev/null +++ b/scripts/dev-probe.cjs @@ -0,0 +1,425 @@ +#!/usr/bin/env node +/** + * scripts/dev-probe.cjs — manual-testing probe tool for nexpath. + * + * .cjs extension forces CommonJS so it works regardless of the parent + * package.json having "type": "module". Designed to be a one-stop shop + * for the manual testing rounds (Round 2-4) so each test cell is one + * command, no shell-quoting issues, no relative-path gotchas. + * + * Usage from anywhere: + * node /home/emptyops/Documents/Vedanshi/NexPathMain/reviewduel/nexpath/scripts/dev-probe.cjs [args] + * + * Or from the repo root: + * node scripts/dev-probe.cjs [args] + * + * Subcommands: + * store schema Show columns of the prompts table + * store recent [N] Show N most recent prompts (default 10) + * store today Show prompts captured today + * store search Search prompt_text for a substring + * store stats Count by agent + by day + * + * cursor workspaces List all Cursor workspaces with their state.vscdb path + row count + * cursor probe [ws-id] Probe one workspace (omit ws-id to probe all) + * cursor extract [ws-id] Run our cursor extractors against live data; show decoded events + * + * trigger ping Verify nexpath CLI is reachable + Layer C is up + * trigger auto Manually invoke `nexpath auto` to bypass the watcher and test the capture path directly + * + * config show Show prompt_capture_enabled + other flags from the store's config table + * exthost-log [N] Tail Cursor's exthost.log for the last N nexpath-tagged lines (default 30) + * + * help Print this help + */ + +const path = require('path'); +const fs = require('fs'); +const os = require('os'); + +const REPO_ROOT = path.resolve(__dirname, '..'); +const STORE_DB = path.join(os.homedir(), '.nexpath', 'prompt-store.db'); +const CURSOR_WS_DIR = path.join(os.homedir(), '.config', 'Cursor', 'User', 'workspaceStorage'); + +// Load better-sqlite3 from the sub-package's node_modules (always available +// after `npm install` in src/ext-vscode/). Absolute path so we don't depend +// on cwd or relative resolution. +const BETTER_SQLITE3 = path.join(REPO_ROOT, 'src', 'ext-vscode', 'node_modules', 'better-sqlite3'); +let Database; +try { + Database = require(BETTER_SQLITE3); +} catch (e) { + console.error('Could not load better-sqlite3 from:', BETTER_SQLITE3); + console.error('Fix: cd', path.join(REPO_ROOT, 'src/ext-vscode'), '&& npm install'); + process.exit(2); +} + +// ───────────────────────── helpers ───────────────────────── + +function openStore(readonly = true) { + if (!fs.existsSync(STORE_DB)) { + console.error('Store DB not found at:', STORE_DB); + console.error('Has any prompt been captured yet? Try: node dist/cli/index.js install'); + process.exit(3); + } + return new Database(STORE_DB, { readonly }); +} + +function fmtTs(t) { + if (t == null) return '?'; + // captured_at is INTEGER ms in this schema + if (typeof t === 'number' || /^\d+$/.test(String(t))) { + return new Date(Number(t)).toISOString().replace('T', ' ').slice(0, 19) + ' UTC'; + } + return String(t); +} + +function printRow(r, maxText = 100) { + const text = String(r.prompt_text || '').slice(0, maxText); + const at = fmtTs(r.captured_at ?? r.created_at ?? r.timestamp); + const agent = r.agent || r.agent_id || '?'; + const project = r.project_root || r.project_id || r.project_path || '-'; + console.log( + ' id=' + r.id, + '| at=' + at, + '| agent=' + agent, + '| project=' + (typeof project === 'string' ? project.slice(-40) : project), + '| text=' + JSON.stringify(text), + ); +} + +function stagedRead(dbPath) { + // Copy main + WAL + SHM to /tmp, checkpoint, query. Mirrors the + // production watcher's defaultReadItemTable strategy so we read the + // same data the watcher sees. + const stagingDir = path.join(os.tmpdir(), 'dev-probe-' + Date.now() + '-' + Math.random().toString(36).slice(2, 8)); + fs.mkdirSync(stagingDir, { recursive: true }); + const stagedMain = path.join(stagingDir, path.basename(dbPath)); + fs.copyFileSync(dbPath, stagedMain); + for (const suffix of ['-wal', '-shm']) { + const sibling = dbPath + suffix; + if (fs.existsSync(sibling)) fs.copyFileSync(sibling, stagedMain + suffix); + } + const db = new Database(stagedMain, { readonly: true }); + try { + try { db.pragma('wal_checkpoint(TRUNCATE)'); } catch {} + const cleanup = () => { + try { db.close(); } catch {} + try { fs.rmSync(stagingDir, { recursive: true, force: true }); } catch {} + }; + return { db, cleanup }; + } catch (e) { + try { db.close(); } catch {} + fs.rmSync(stagingDir, { recursive: true, force: true }); + throw e; + } +} + +function fingerprint(keys) { + const map = { + 'cursor-v2024-q4': ['aiService.'], + 'cursor-v2025-q1': ['composer.composerData'], + 'cursor-v2025-q2': ['cursorAIChatService.chatHistory.'], + 'windsurf': ['cascade.'], + }; + let best = { id: null, count: 0, matchedKeys: [] }; + for (const [id, prefixes] of Object.entries(map)) { + const matched = keys.filter(k => prefixes.some(p => k.startsWith(p))); + if (matched.length > best.count) best = { id, count: matched.length, matchedKeys: matched }; + } + return best.count > 0 ? best : null; +} + +// ───────────────────────── subcommands ───────────────────────── + +function storeSchema() { + const db = openStore(); + console.log('Tables:'); + for (const t of db.prepare("SELECT name FROM sqlite_master WHERE type='table'").all()) { + console.log(' ', t.name); + } + console.log('\nColumns of "prompts":'); + for (const c of db.prepare('PRAGMA table_info(prompts)').all()) { + console.log(' ', c.name, '(' + c.type + ')'); + } + db.close(); +} + +function storeRecent(count) { + const n = parseInt(count, 10) || 10; + const db = openStore(); + const rows = db.prepare('SELECT * FROM prompts ORDER BY id DESC LIMIT ?').all(n); + console.log(`Most recent ${rows.length} prompts:`); + for (const r of rows) printRow(r); + db.close(); +} + +function storeToday() { + const db = openStore(); + // captured_at is INTEGER ms — compute "today" boundary in ms + const now = Date.now(); + const startOfDay = new Date(); + startOfDay.setHours(0, 0, 0, 0); + const rows = db.prepare( + 'SELECT * FROM prompts WHERE captured_at >= ? AND captured_at <= ? ORDER BY id DESC' + ).all(startOfDay.getTime(), now); + // Format local-time date — toISOString() returns UTC which looks wrong by 1 day in non-UTC timezones. + const localDate = `${startOfDay.getFullYear()}-${String(startOfDay.getMonth()+1).padStart(2,'0')}-${String(startOfDay.getDate()).padStart(2,'0')}`; + console.log(`Prompts captured today (${localDate}, local time): ${rows.length}`); + for (const r of rows) printRow(r); + db.close(); +} + +function storeSearch(pattern) { + if (!pattern) { console.error('Usage: store search '); process.exit(1); } + const db = openStore(); + const rows = db.prepare('SELECT * FROM prompts WHERE prompt_text LIKE ? ORDER BY id DESC LIMIT 20').all('%' + pattern + '%'); + console.log(`Matches for "${pattern}": ${rows.length}`); + for (const r of rows) printRow(r); + db.close(); +} + +function storeStats() { + const db = openStore(); + console.log('By agent:'); + for (const r of db.prepare("SELECT agent, COUNT(*) AS n FROM prompts GROUP BY agent ORDER BY n DESC").all()) { + console.log(' ', (r.agent || '(null)').padEnd(20), r.n); + } + console.log('\nBy day (last 14):'); + // captured_at is INTEGER ms — group via Date math on results + const cutoff = Date.now() - 14 * 86400 * 1000; + const rows = db.prepare('SELECT captured_at FROM prompts WHERE captured_at >= ? ORDER BY captured_at DESC').all(cutoff); + const byDay = {}; + for (const r of rows) { + const d = new Date(Number(r.captured_at)).toISOString().slice(0, 10); + byDay[d] = (byDay[d] || 0) + 1; + } + for (const day of Object.keys(byDay).sort().reverse()) { + console.log(' ', day, byDay[day]); + } + db.close(); +} + +function cursorWorkspaces() { + if (!fs.existsSync(CURSOR_WS_DIR)) { + console.error('Cursor workspaceStorage dir not found:', CURSOR_WS_DIR); + process.exit(3); + } + console.log('Cursor workspaces with state.vscdb:'); + for (const ws of fs.readdirSync(CURSOR_WS_DIR)) { + const dbPath = path.join(CURSOR_WS_DIR, ws, 'state.vscdb'); + if (!fs.existsSync(dbPath)) continue; + try { + const { db, cleanup } = stagedRead(dbPath); + try { + const has = db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='ItemTable'").all().length; + const n = has ? db.prepare('SELECT COUNT(*) AS n FROM ItemTable').get().n : 0; + console.log(' ', ws.padEnd(45), 'rows=' + n); + } finally { cleanup(); } + } catch (e) { + console.log(' ', ws.padEnd(45), 'ERR:', e.message); + } + } +} + +function cursorProbe(wsId) { + const targets = []; + if (wsId) { + const dbPath = path.join(CURSOR_WS_DIR, wsId, 'state.vscdb'); + if (!fs.existsSync(dbPath)) { console.error('Not found:', dbPath); process.exit(3); } + targets.push({ ws: wsId, dbPath }); + } else { + for (const ws of fs.readdirSync(CURSOR_WS_DIR)) { + const dbPath = path.join(CURSOR_WS_DIR, ws, 'state.vscdb'); + if (fs.existsSync(dbPath)) targets.push({ ws, dbPath }); + } + } + + for (const { ws, dbPath } of targets) { + console.log('\n=== Workspace:', ws, '==='); + try { + const { db, cleanup } = stagedRead(dbPath); + try { + const has = db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='ItemTable'").all().length; + if (!has) { console.log(' (no ItemTable)'); continue; } + const rows = db.prepare('SELECT key, value FROM ItemTable').all(); + const keys = rows.map(r => r.key); + const fp = fingerprint(keys); + console.log(' ItemTable rows:', rows.length); + console.log(' fingerprint:', fp ? `${fp.id} (matched ${fp.count} key${fp.count===1?'':'s'})` : 'UNKNOWN'); + if (fp) console.log(' matched keys:', fp.matchedKeys.slice(0, 5).join(', ')); + console.log(' first 8 keys:', keys.slice(0, 8).join(', ')); + } finally { cleanup(); } + } catch (e) { + console.log(' ERR:', e.message); + } + } +} + +function cursorExtract(wsId) { + // Use the actual production extractors from the sub-package source + // via tsx-equivalent (just require the compiled .ts via Node 22's + // experimental TS support... NO, our .ts files don't compile cleanly + // without tsc, so use the hand-rolled decoder inline below). + // + // For each row that the cursor-v2024-q4 prefix owns, parse the JSON + // value and pull out prompts. This mirrors what the extractor does but + // implemented in CJS for this probe tool. + + const targets = []; + if (wsId) { + const dbPath = path.join(CURSOR_WS_DIR, wsId, 'state.vscdb'); + if (!fs.existsSync(dbPath)) { console.error('Not found:', dbPath); process.exit(3); } + targets.push({ ws: wsId, dbPath }); + } else { + for (const ws of fs.readdirSync(CURSOR_WS_DIR)) { + const dbPath = path.join(CURSOR_WS_DIR, ws, 'state.vscdb'); + if (fs.existsSync(dbPath)) targets.push({ ws, dbPath }); + } + } + + for (const { ws, dbPath } of targets) { + console.log('\n=== Workspace:', ws, '==='); + try { + const { db, cleanup } = stagedRead(dbPath); + try { + const has = db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='ItemTable'").all().length; + if (!has) { console.log(' (no ItemTable)'); continue; } + const rows = db.prepare('SELECT key, value FROM ItemTable').all(); + let total = 0; + + // cursor-v2024-q4: aiService.prompts is a JSON-encoded array of {text, ...} + for (const r of rows) { + if (r.key !== 'aiService.prompts') continue; + try { + const arr = JSON.parse(r.value); + if (!Array.isArray(arr)) continue; + console.log(' aiService.prompts:', arr.length, 'entries'); + for (let i = 0; i < Math.min(arr.length, 5); i++) { + const e = arr[i]; + const text = e && (e.text || e.prompt || e.content); + if (text) { + console.log(' [' + i + ']', JSON.stringify(String(text).slice(0, 100))); + total++; + } + } + } catch (e) { + console.log(' aiService.prompts: parse err:', e.message); + } + } + + // cursor-v2025-q1: composer.composerData (metadata only on 3.4.20) + for (const r of rows) { + if (r.key !== 'composer.composerData') continue; + try { + const obj = JSON.parse(r.value); + console.log(' composer.composerData fields:', Object.keys(obj || {}).slice(0, 10).join(', ')); + } catch {} + } + + if (total === 0) console.log(' (no prompts decoded — workspace has no Ask-mode AI history yet)'); + } finally { cleanup(); } + } catch (e) { + console.log(' ERR:', e.message); + } + } +} + +function triggerPing() { + const { spawnSync } = require('child_process'); + console.log('PATH:', process.env.PATH); + console.log('\nChecking nexpath CLI...'); + const r1 = spawnSync('nexpath', ['--version'], { encoding: 'utf8' }); + if (r1.error) { + console.error(' nexpath not on PATH — run: cd', REPO_ROOT, '&& npm link'); + } else { + console.log(' nexpath version:', r1.stdout.trim()); + } + console.log('\nChecking Layer C status...'); + const r2 = spawnSync('node', [path.join(REPO_ROOT, 'dist', 'cli', 'index.js'), 'status'], { encoding: 'utf8' }); + if (r2.stdout) console.log(r2.stdout.split('\n').filter(l => !l.startsWith('◇')).slice(0, 12).join('\n')); +} + +function triggerAuto(promptText) { + if (!promptText) { console.error('Usage: trigger auto ""'); process.exit(1); } + const { spawnSync } = require('child_process'); + const sessionId = `dev-probe|${Date.now()}`; + const cliPath = path.join(REPO_ROOT, 'dist', 'cli', 'index.js'); + console.log('Invoking: nexpath auto with session_id=' + sessionId); + const r = spawnSync('node', [cliPath, 'auto', promptText, '--session-id', sessionId], { + encoding: 'utf8', + timeout: 15000, + cwd: REPO_ROOT, + }); + if (r.error) { console.error('Spawn err:', r.error.message); process.exit(2); } + console.log('Exit:', r.status); + if (r.stdout) console.log('stdout:', r.stdout.split('\n').filter(l => !l.startsWith('◇')).join('\n')); + if (r.stderr) console.log('stderr:', r.stderr); + // Now check the store + console.log('\nLast 3 prompts in store after this call:'); + const db = openStore(); + for (const r of db.prepare('SELECT * FROM prompts ORDER BY id DESC LIMIT 3').all()) printRow(r); + db.close(); +} + +function configShow() { + const db = openStore(); + console.log('Config table:'); + const rows = db.prepare('SELECT * FROM config').all(); + if (rows.length === 0) { console.log(' (empty)'); } + else for (const r of rows) console.log(' ', r); + db.close(); +} + +function exthostLog(nArg) { + const n = parseInt(nArg, 10) || 30; + const logsDir = path.join(os.homedir(), '.config', 'Cursor', 'logs'); + if (!fs.existsSync(logsDir)) { console.error('No Cursor logs dir:', logsDir); process.exit(3); } + // Find newest log session dir + const sessions = fs.readdirSync(logsDir).filter(d => /^\d/.test(d)).sort().reverse(); + if (!sessions.length) { console.error('No log sessions found'); process.exit(3); } + const latest = path.join(logsDir, sessions[0]); + const candidates = [ + path.join(latest, 'window1', 'exthost', 'exthost.log'), + path.join(latest, 'window1', 'renderer.log'), + ]; + for (const f of candidates) { + if (!fs.existsSync(f)) continue; + console.log('=== ' + f + ' (lines containing "nexpath", last ' + n + ') ==='); + const lines = fs.readFileSync(f, 'utf8').split('\n').filter(l => /nexpath/i.test(l)); + console.log(lines.slice(-n).join('\n') || ' (no nexpath lines)'); + console.log(''); + } +} + +// ───────────────────────── dispatch ───────────────────────── + +function usage() { + console.log(fs.readFileSync(__filename, 'utf8').split('\n').slice(1, 30).filter(l => l.startsWith(' *')).map(l => l.replace(/^ \* ?/, '')).join('\n')); +} + +const argv = process.argv.slice(2); +const cmd = argv[0]; +const sub = argv[1]; + +try { + if (!cmd || cmd === 'help' || cmd === '-h' || cmd === '--help') return usage(); + if (cmd === 'store' && sub === 'schema') return storeSchema(); + if (cmd === 'store' && sub === 'recent') return storeRecent(argv[2]); + if (cmd === 'store' && sub === 'today') return storeToday(); + if (cmd === 'store' && sub === 'search') return storeSearch(argv[2]); + if (cmd === 'store' && sub === 'stats') return storeStats(); + if (cmd === 'cursor' && sub === 'workspaces') return cursorWorkspaces(); + if (cmd === 'cursor' && sub === 'probe') return cursorProbe(argv[2]); + if (cmd === 'cursor' && sub === 'extract') return cursorExtract(argv[2]); + if (cmd === 'trigger' && sub === 'ping') return triggerPing(); + if (cmd === 'trigger' && sub === 'auto') return triggerAuto(argv.slice(2).join(' ')); + if (cmd === 'config' && sub === 'show') return configShow(); + if (cmd === 'exthost-log') return exthostLog(sub); + console.error('Unknown subcommand:', cmd, sub || ''); + usage(); + process.exit(1); +} catch (e) { + console.error('Error:', e.stack || e.message); + process.exit(1); +} diff --git a/src/agents/adapters/claude-code.test.ts b/src/agents/adapters/claude-code.test.ts new file mode 100644 index 00000000..9cf4a965 --- /dev/null +++ b/src/agents/adapters/claude-code.test.ts @@ -0,0 +1,231 @@ +import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; +import { mkdtempSync, rmSync, readFileSync, existsSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import type { InstallContext } from '../types.js'; +import { + claudeCodeAdapter, + getClaudeSettingsPath, + buildHookCommand, + buildStopHookCommand, + buildHookEntry, + writeHookEntry, + removeHookEntry, +} from './claude-code.js'; + +/** + * Unit tests for the Claude Code HookAdapter and its supporting hook helpers. + * These cover the adapter's contract (detect/install/uninstall/settingsPath/buildHooks) + * plus the building-block functions that were moved here from install.ts in M1 + * Branch 2 (v0.1.3/m1/claude-code-refactor). + * + * The install-time byte-identical guarantee is enforced separately by + * src/cli/commands/install.snapshot.test.ts. + */ +describe('claude-code adapter + helpers', () => { + let tmpHome: string; + let originalArgv1: string; + let logSpy: ReturnType; + + const makeCtx = (): InstallContext => ({ + home: tmpHome, + cwd: join(tmpHome, 'cwd'), + yes: true, + dbPath: ':memory:', + }); + + beforeEach(() => { + tmpHome = mkdtempSync(join(tmpdir(), 'nexpath-claude-adapter-')); + originalArgv1 = process.argv[1]; + process.argv[1] = '/fixture/nexpath/dist/cli/index.js'; + logSpy = vi.spyOn(console, 'log').mockImplementation(() => {}); + }); + + afterEach(() => { + process.argv[1] = originalArgv1; + logSpy.mockRestore(); + rmSync(tmpHome, { recursive: true, force: true }); + }); + + // ── helpers ───────────────────────────────────────────────────────────────── + + describe('getClaudeSettingsPath', () => { + it('joins home with .claude/settings.json', () => { + expect(getClaudeSettingsPath(tmpHome)).toBe(join(tmpHome, '.claude', 'settings.json')); + }); + }); + + describe('buildHookCommand', () => { + it('produces a node command with the resolved CLI path and the prompt-store DB path', () => { + const cmd = buildHookCommand(tmpHome); + expect(cmd).toBe(`node "/fixture/nexpath/dist/cli/index.js" auto --db "${tmpHome}/.nexpath/prompt-store.db"`); + }); + }); + + describe('buildStopHookCommand', () => { + it('produces a node command ending in `stop`', () => { + const cmd = buildStopHookCommand(tmpHome); + expect(cmd).toBe(`node "/fixture/nexpath/dist/cli/index.js" stop --db "${tmpHome}/.nexpath/prompt-store.db"`); + }); + }); + + describe('buildHookEntry', () => { + it('returns UserPromptSubmit and Stop groups each with _nexpath_hook marker and one command entry', () => { + const entry = buildHookEntry(tmpHome); + expect(Object.keys(entry).sort()).toEqual(['Stop', 'UserPromptSubmit']); + const ups = (entry.UserPromptSubmit as Array>)[0]; + expect(ups._nexpath_hook).toBe(true); + expect(ups.matcher).toBe(''); + const upsHooks = ups.hooks as Array<{ type: string; command: string }>; + expect(upsHooks).toHaveLength(1); + expect(upsHooks[0].type).toBe('command'); + expect(upsHooks[0].command).toContain('auto'); + const stop = (entry.Stop as Array>)[0]; + expect(stop._nexpath_hook).toBe(true); + const stopHooks = stop.hooks as Array<{ type: string; command: string }>; + expect(stopHooks[0].command).toContain('stop'); + }); + }); + + describe('writeHookEntry + removeHookEntry round-trip', () => { + it('writes the hook entry and produces a parseable JSON file', () => { + const settingsPath = getClaudeSettingsPath(tmpHome); + writeHookEntry(settingsPath, tmpHome); + const parsed = JSON.parse(readFileSync(settingsPath, 'utf8')); + expect(parsed.hooks.UserPromptSubmit[0]._nexpath_hook).toBe(true); + expect(parsed.hooks.Stop[0]._nexpath_hook).toBe(true); + }); + + it('is idempotent — re-writing replaces the previous nexpath hook, does not duplicate', () => { + const settingsPath = getClaudeSettingsPath(tmpHome); + writeHookEntry(settingsPath, tmpHome); + writeHookEntry(settingsPath, tmpHome); + const parsed = JSON.parse(readFileSync(settingsPath, 'utf8')); + expect(parsed.hooks.UserPromptSubmit).toHaveLength(1); + expect(parsed.hooks.Stop).toHaveLength(1); + }); + + it('preserves non-nexpath hooks already present in settings.json', () => { + const settingsPath = getClaudeSettingsPath(tmpHome); + // pre-existing hook entry from some other tool + const { mkdirSync, writeFileSync } = require('node:fs') as typeof import('node:fs'); + mkdirSync(join(tmpHome, '.claude'), { recursive: true }); + writeFileSync( + settingsPath, + JSON.stringify({ + hooks: { + UserPromptSubmit: [{ matcher: '', hooks: [{ type: 'command', command: 'other-tool' }] }], + }, + }), + 'utf8', + ); + writeHookEntry(settingsPath, tmpHome); + const parsed = JSON.parse(readFileSync(settingsPath, 'utf8')); + expect(parsed.hooks.UserPromptSubmit).toHaveLength(2); // one foreign + one nexpath + const nexpathGroup = parsed.hooks.UserPromptSubmit.find((g: { _nexpath_hook?: boolean }) => g._nexpath_hook); + const foreignGroup = parsed.hooks.UserPromptSubmit.find((g: { _nexpath_hook?: boolean }) => !g._nexpath_hook); + expect(nexpathGroup).toBeDefined(); + expect(foreignGroup.hooks[0].command).toBe('other-tool'); + }); + + it('removeHookEntry returns true and strips only the nexpath group', () => { + const settingsPath = getClaudeSettingsPath(tmpHome); + writeHookEntry(settingsPath, tmpHome); + const result = removeHookEntry(settingsPath); + expect(result).toBe(true); + const parsed = JSON.parse(readFileSync(settingsPath, 'utf8')); + // After removal, both UserPromptSubmit and Stop are gone (had only the nexpath entry) + expect(parsed.hooks.UserPromptSubmit).toBeUndefined(); + expect(parsed.hooks.Stop).toBeUndefined(); + }); + + it('removeHookEntry returns false when no nexpath hook is present', () => { + const settingsPath = getClaudeSettingsPath(tmpHome); + expect(removeHookEntry(settingsPath)).toBe(false); + }); + }); + + // ── adapter ───────────────────────────────────────────────────────────────── + + describe('claudeCodeAdapter static fields', () => { + it('has the expected id, label, and category', () => { + expect(claudeCodeAdapter.id).toBe('claude-code'); + expect(claudeCodeAdapter.label).toBe('Claude Code'); + expect(claudeCodeAdapter.category).toBe('hook'); + }); + }); + + describe('claudeCodeAdapter.detect', () => { + it('always returns true', () => { + expect(claudeCodeAdapter.detect(makeCtx())).toBe(true); + }); + }); + + describe('claudeCodeAdapter.settingsPath', () => { + it('returns ~/.claude/settings.json relative to ctx.home when no override', () => { + expect(claudeCodeAdapter.settingsPath(makeCtx())).toBe(join(tmpHome, '.claude', 'settings.json')); + }); + + it('returns ctx.settingsPath when provided (override)', () => { + const override = join(tmpHome, 'custom', 'settings.json'); + expect(claudeCodeAdapter.settingsPath({ ...makeCtx(), settingsPath: override })).toBe(override); + }); + }); + + describe('claudeCodeAdapter.buildHooks', () => { + it('returns UserPromptSubmit and Stop with command-type entries', () => { + const hooks = claudeCodeAdapter.buildHooks(makeCtx()); + expect(hooks.UserPromptSubmit).toHaveLength(1); + expect(hooks.UserPromptSubmit[0].type).toBe('command'); + expect(hooks.UserPromptSubmit[0].command).toContain('auto'); + expect(hooks.Stop[0].command).toContain('stop'); + }); + }); + + describe('claudeCodeAdapter.install', () => { + it('writes the hook entry and returns status installed', async () => { + const result = await claudeCodeAdapter.install(makeCtx()); + expect(result.status).toBe('installed'); + const settingsPath = join(tmpHome, '.claude', 'settings.json'); + expect(existsSync(settingsPath)).toBe(true); + const parsed = JSON.parse(readFileSync(settingsPath, 'utf8')); + expect(parsed.hooks.UserPromptSubmit[0]._nexpath_hook).toBe(true); + }); + + it('logs the success line containing the settings path', async () => { + await claudeCodeAdapter.install(makeCtx()); + const allLogs = logSpy.mock.calls.map((c) => c.join(' ')).join('\n'); + expect(allLogs).toContain('advisory hook written to'); + expect(allLogs).toContain(join(tmpHome, '.claude', 'settings.json')); + }); + + it('writes to ctx.settingsPath when override is provided, not to home/.claude/', async () => { + const customPath = join(tmpHome, 'custom-dir', 'override.json'); + const result = await claudeCodeAdapter.install({ ...makeCtx(), settingsPath: customPath }); + expect(result.status).toBe('installed'); + expect(existsSync(customPath)).toBe(true); + expect(existsSync(join(tmpHome, '.claude', 'settings.json'))).toBe(false); + const parsed = JSON.parse(readFileSync(customPath, 'utf8')); + expect(parsed.hooks.UserPromptSubmit[0]._nexpath_hook).toBe(true); + }); + }); + + describe('claudeCodeAdapter.uninstall', () => { + it('removes the hook entry after a prior install', async () => { + await claudeCodeAdapter.install(makeCtx()); + logSpy.mockClear(); + await claudeCodeAdapter.uninstall(makeCtx()); + const parsed = JSON.parse(readFileSync(join(tmpHome, '.claude', 'settings.json'), 'utf8')); + expect(parsed.hooks.UserPromptSubmit).toBeUndefined(); + expect(parsed.hooks.Stop).toBeUndefined(); + const allLogs = logSpy.mock.calls.map((c) => c.join(' ')).join('\n'); + expect(allLogs).toContain('advisory hook removed'); + }); + + it('logs the skip message when no nexpath hook is present', async () => { + await claudeCodeAdapter.uninstall(makeCtx()); + const allLogs = logSpy.mock.calls.map((c) => c.join(' ')).join('\n'); + expect(allLogs).toContain('hook not registered, skipped'); + }); + }); +}); diff --git a/src/agents/adapters/claude-code.ts b/src/agents/adapters/claude-code.ts new file mode 100644 index 00000000..f3372835 --- /dev/null +++ b/src/agents/adapters/claude-code.ts @@ -0,0 +1,233 @@ +import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'; +import { dirname, join, resolve } from 'node:path'; +import { registerAdapter } from '../registry.js'; +import type { HookAdapter, InstallContext, InstallResult } from '../types.js'; + +/** + * Claude Code hook helpers — relocated verbatim from src/cli/commands/install.ts + * in Milestone M1 Branch 2 (v0.1.3/m1/claude-code-refactor). Identifier names, + * function bodies, and behaviour are preserved exactly so the install snapshot + * from M1 Branch 1 remains byte-identical (zero-diff invariant; see dev plan + * §1.5 in reviewduel-submodule). + * + * src/cli/commands/install.ts re-exports these names from this module for + * backward compatibility with existing imports (install.test.ts and others). + */ + +// ── Private file helpers (duplicated from install.ts so this adapter is +// ── self-sufficient and avoids a circular import) ───────────────────────────── + +function readJsonSafe(filePath: string): Record { + try { + return JSON.parse(readFileSync(filePath, 'utf8')) as Record; + } catch { + return {}; + } +} + +function writeJson(filePath: string, data: unknown): void { + mkdirSync(dirname(filePath), { recursive: true }); + writeFileSync(filePath, JSON.stringify(data, null, 2) + '\n', 'utf8'); +} + +// ── Hook command + entry builders (moved from install.ts) ───────────────────── + +/** Path to the user-level Claude Code settings file (where hooks are configured). */ +export function getClaudeSettingsPath(home: string): string { + return join(home, '.claude', 'settings.json'); +} + +/** + * Build the shell command string for the UserPromptSubmit hook. + * + * Uses `node` explicitly with the absolute path to the running CLI so the hook + * works regardless of whether nexpath is globally installed or run from a local + * build (PATH is not required). + * + * Forward slashes are used throughout so the value is safe in PowerShell and cmd + * on Windows as well as bash/zsh on Unix. + */ +export function buildHookCommand(home: string, platform = process.platform): string { + const cliPath = resolve(process.argv[1]).replace(/\\/g, '/'); + const dbPath = join(home, '.nexpath', 'prompt-store.db').replace(/\\/g, '/'); + return `node "${cliPath}" auto --db "${dbPath}"`; +} + +/** Build the shell command string for the Stop hook. */ +export function buildStopHookCommand(home: string, platform = process.platform): string { + const cliPath = resolve(process.argv[1]).replace(/\\/g, '/'); + const dbPath = join(home, '.nexpath', 'prompt-store.db').replace(/\\/g, '/'); + return `node "${cliPath}" stop --db "${dbPath}"`; +} + +/** + * Build the UserPromptSubmit + Stop hook entry objects. + * + * The `_nexpath_hook: true` field is the reliable deduplication and removal + * marker — it survives path changes across reinstalls, unlike scanning the + * command string. + * + * No `timeout` field is set so Claude Code uses its default (600 s), which is + * required for hooks that block for UI interaction (the decision session). + */ +export function buildHookEntry(home: string, platform = process.platform): Record { + return { + UserPromptSubmit: [ + { + _nexpath_hook: true, + matcher: '', + hooks: [ + { + type: 'command', + command: buildHookCommand(home, platform), + }, + ], + }, + ], + Stop: [ + { + _nexpath_hook: true, + matcher: '', + hooks: [ + { + type: 'command', + command: buildStopHookCommand(home, platform), + }, + ], + }, + ], + }; +} + +/** + * Write the nexpath UserPromptSubmit and Stop hooks into ~/.claude/settings.json. + * + * Uses a read-filter-append pattern so existing hooks written by other tools are + * preserved. Any prior nexpath hook group (identified by `_nexpath_hook: true`) + * is removed before appending the fresh entry, making this operation idempotent. + */ +export function writeHookEntry(filePath: string, home: string, platform = process.platform): void { + const data = readJsonSafe(filePath); + const hooks = (data.hooks as Record | undefined) ?? {}; + const entry = buildHookEntry(home, platform); + + // UserPromptSubmit + const existingUPS = (hooks.UserPromptSubmit as Array> | undefined) ?? []; + hooks.UserPromptSubmit = [ + ...existingUPS.filter((g) => !g._nexpath_hook), + ...(entry.UserPromptSubmit as unknown[]), + ]; + + // Stop + const existingStop = (hooks.Stop as Array> | undefined) ?? []; + hooks.Stop = [ + ...existingStop.filter((g) => !g._nexpath_hook), + ...(entry.Stop as unknown[]), + ]; + + data.hooks = hooks; + writeJson(filePath, data); +} + +/** + * Remove the nexpath UserPromptSubmit and Stop hooks from ~/.claude/settings.json. + * + * Identifies nexpath-written hook groups by the `_nexpath_hook: true` field. + * Returns false if the file does not exist or no nexpath hooks were found. + */ +export function removeHookEntry(filePath: string): boolean { + if (!existsSync(filePath)) return false; + const data = readJsonSafe(filePath); + const hooks = data.hooks as Record | undefined; + if (!hooks) return false; + + let removed = false; + + // UserPromptSubmit + const upsGroups = hooks.UserPromptSubmit as Array> | undefined; + if (upsGroups) { + const filtered = upsGroups.filter((g) => !g._nexpath_hook); + if (filtered.length < upsGroups.length) { + removed = true; + if (filtered.length === 0) delete hooks.UserPromptSubmit; + else hooks.UserPromptSubmit = filtered; + } + } + + // Stop + const stopGroups = hooks.Stop as Array> | undefined; + if (stopGroups) { + const filtered = stopGroups.filter((g) => !g._nexpath_hook); + if (filtered.length < stopGroups.length) { + removed = true; + if (filtered.length === 0) delete hooks.Stop; + else hooks.Stop = filtered; + } + } + + if (!removed) return false; + data.hooks = hooks; + writeJson(filePath, data); + return true; +} + +// ── HookAdapter implementation ──────────────────────────────────────────────── + +/** + * Claude Code adapter. Always detected (Claude Code is treated as universally + * available, matching the existing install.ts behaviour where it is always + * added to the detected-agents list). Install writes the hook entry; uninstall + * removes it. + * + * The console.log strings, the settings file path, and the hook command bytes + * are byte-identical to the pre-refactor inline code so the install snapshot + * from M1 Branch 1 remains unchanged. + */ +export const claudeCodeAdapter: HookAdapter = { + id: 'claude-code', + label: 'Claude Code', + category: 'hook', + + detect(): boolean { + return true; + }, + + settingsPath(ctx: InstallContext): string { + return ctx.settingsPath ?? getClaudeSettingsPath(ctx.home); + }, + + buildHooks(ctx: InstallContext): Record> { + return { + UserPromptSubmit: [{ type: 'command', command: buildHookCommand(ctx.home) }], + Stop: [{ type: 'command', command: buildStopHookCommand(ctx.home) }], + }; + }, + + async install(ctx: InstallContext): Promise { + // Use ctx.settingsPath when provided (preserves pre-refactor decoupling + // between target file path and homedir-based command content). Falls back + // to deriving from ctx.home when not provided (production path). + const settingsPath = ctx.settingsPath ?? getClaudeSettingsPath(ctx.home); + try { + writeHookEntry(settingsPath, ctx.home); + console.log(`✓ ${'Claude Code'.padEnd(12)} — advisory hook written to ${settingsPath}`); + return { status: 'installed' }; + } catch (err) { + console.log(`⚠ ${'Claude Code'.padEnd(12)} — hook write failed: ${(err as Error).message}`); + return { status: 'failed', notes: (err as Error).message }; + } + }, + + async uninstall(ctx: InstallContext): Promise { + const settingsPath = ctx.settingsPath ?? getClaudeSettingsPath(ctx.home); + const removed = removeHookEntry(settingsPath); + console.log(removed + ? `✓ ${'Claude Code'.padEnd(12)} — advisory hook removed` + : `- ${'Claude Code'.padEnd(12)} — hook not registered, skipped`); + }, +}; + +// Side-effect registration on module load. The registry is in-process state; +// agents/index.ts side-effect imports this module so the adapter is registered +// before any caller invokes detectAll/getAdapter. +registerAdapter(claudeCodeAdapter); diff --git a/src/agents/adapters/cursor.test.ts b/src/agents/adapters/cursor.test.ts new file mode 100644 index 00000000..0661f074 --- /dev/null +++ b/src/agents/adapters/cursor.test.ts @@ -0,0 +1,151 @@ +import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; +import { mkdtempSync, mkdirSync, rmSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import { cursorAdapter, cursorConfigDir } from './cursor.js'; +import type { InstallContext } from '../types.js'; + +const makeCtx = (home: string): InstallContext => ({ + home, + cwd: join(home, 'cwd'), + yes: true, + dbPath: ':memory:', +}); + +describe('cursorConfigDir', () => { + it('returns the linux path under ~/.config/Cursor', () => { + expect(cursorConfigDir('/home/u', 'linux')).toBe('/home/u/.config/Cursor'); + }); + + it('returns the darwin path under Application Support', () => { + expect(cursorConfigDir('/Users/u', 'darwin')).toBe( + '/Users/u/Library/Application Support/Cursor', + ); + }); + + it('returns the win32 path under APPDATA when provided', () => { + expect( + cursorConfigDir('C:\\Users\\u', 'win32', 'C:\\Users\\u\\AppData\\Roaming'), + ).toBe('C:\\Users\\u\\AppData\\Roaming/Cursor'); + }); + + it('falls back to /AppData/Roaming on win32 when APPDATA missing', () => { + expect(cursorConfigDir('C:/U', 'win32')).toContain('Cursor'); + }); +}); + +describe('cursorAdapter — static fields', () => { + it('has the expected id, label, category', () => { + expect(cursorAdapter.id).toBe('cursor'); + expect(cursorAdapter.label).toBe('Cursor'); + expect(cursorAdapter.category).toBe('vscode-extension'); + }); + + it('declares Open VSX + VS Code Marketplace ids', () => { + expect(cursorAdapter.marketplace.openVsx).toBe('emptyops.nexpath-vscode'); + expect(cursorAdapter.marketplace.vsCode).toBe('emptyops.nexpath-vscode'); + }); +}); + +describe('cursorAdapter.detect', () => { + let tmp: string; + beforeEach(() => { + tmp = mkdtempSync(join(tmpdir(), 'cursor-detect-')); + }); + afterEach(() => { + rmSync(tmp, { recursive: true, force: true }); + }); + + it('returns false when ~/.config/Cursor/ does not exist', () => { + expect(cursorAdapter.detect(makeCtx(tmp))).toBe(false); + }); + + it('returns true when ~/.config/Cursor/ does exist (linux fixture)', () => { + mkdirSync(join(tmp, '.config', 'Cursor'), { recursive: true }); + expect(cursorAdapter.detect(makeCtx(tmp))).toBe(true); + }); +}); + +describe('cursorAdapter.chatHistoryPaths', () => { + it('returns the workspaceStorage base path under the Cursor config dir', () => { + const paths = cursorAdapter.chatHistoryPaths(makeCtx('/home/u')); + expect(paths).toHaveLength(1); + expect(paths[0]).toContain('Cursor'); + expect(paths[0]).toContain('User/workspaceStorage'); + }); +}); + +describe('cursorAdapter.extractPrompt', () => { + it('returns null (decoding happens in the extension, not the CLI adapter)', () => { + expect(cursorAdapter.extractPrompt('any.key', { anything: true })).toBeNull(); + expect(cursorAdapter.extractPrompt('aiService.prompts', '[]')).toBeNull(); + }); +}); + +describe('cursorAdapter.install', () => { + let tmp: string; + let logSpy: ReturnType; + beforeEach(() => { + tmp = mkdtempSync(join(tmpdir(), 'cursor-install-')); + logSpy = vi.spyOn(console, 'log').mockImplementation(() => {}); + }); + afterEach(() => { + rmSync(tmp, { recursive: true, force: true }); + logSpy.mockRestore(); + }); + + it('skips when Cursor is not detected and returns status=skipped', async () => { + const r = await cursorAdapter.install(makeCtx(tmp)); + expect(r.status).toBe('skipped'); + const allLogs = logSpy.mock.calls.map((c) => c.join(' ')).join('\n'); + expect(allLogs).toContain('not detected'); + }); + + it('prints deep-link instructions when Cursor IS detected', async () => { + mkdirSync(join(tmp, '.config', 'Cursor'), { recursive: true }); + const r = await cursorAdapter.install(makeCtx(tmp)); + expect(r.status).toBe('installed'); + const allLogs = logSpy.mock.calls.map((c) => c.join(' ')).join('\n'); + expect(allLogs).toContain('Open VSX'); + expect(allLogs).toContain('open-vsx.org'); + expect(allLogs).toContain('marketplace.visualstudio.com'); + expect(allLogs).toContain('cursor --install-extension'); + expect(allLogs).toContain('emptyops.nexpath-vscode'); + }); +}); + +describe('cursorAdapter.uninstall', () => { + let tmp: string; + let logSpy: ReturnType; + beforeEach(() => { + tmp = mkdtempSync(join(tmpdir(), 'cursor-uninstall-')); + logSpy = vi.spyOn(console, 'log').mockImplementation(() => {}); + }); + afterEach(() => { + rmSync(tmp, { recursive: true, force: true }); + logSpy.mockRestore(); + }); + + it('logs the skip line when Cursor is not detected', async () => { + await cursorAdapter.uninstall(makeCtx(tmp)); + const allLogs = logSpy.mock.calls.map((c) => c.join(' ')).join('\n'); + expect(allLogs).toContain('not detected'); + }); + + it('logs the uninstall instructions when Cursor IS detected', async () => { + mkdirSync(join(tmp, '.config', 'Cursor'), { recursive: true }); + await cursorAdapter.uninstall(makeCtx(tmp)); + const allLogs = logSpy.mock.calls.map((c) => c.join(' ')).join('\n'); + expect(allLogs).toContain('uninstall'); + expect(allLogs).toContain('cursor --uninstall-extension'); + }); +}); + +describe('cursorAdapter registry registration', () => { + it('is registered with the global registry under id "cursor"', async () => { + // Importing the side-effect entry registers all adapters + await import('../index.js'); + const { getAdapter } = await import('../registry.js'); + expect(getAdapter('cursor')).toBe(cursorAdapter); + }); +}); diff --git a/src/agents/adapters/cursor.ts b/src/agents/adapters/cursor.ts new file mode 100644 index 00000000..4ffedcf3 --- /dev/null +++ b/src/agents/adapters/cursor.ts @@ -0,0 +1,158 @@ +import { existsSync } from 'node:fs'; +import { join } from 'node:path'; +import { registerAdapter } from '../registry.js'; +import type { + InstallContext, + InstallResult, + VSCodeExtensionAdapter, +} from '../types.js'; + +/** + * Cursor adapter (M9 of M2 Branch 4). + * + * Cursor is a VS Code fork. Nexpath integrates via a companion VS Code + * extension that runs INSIDE Cursor (sub-package at `src/ext-vscode/`). + * This CLI-side adapter only handles install-time concerns: + * 1. Detect whether Cursor is installed on this machine. + * 2. Print deep-link instructions for the user to install the extension + * via Open VSX (Cursor's marketplace). + * 3. Self-register with the agent registry so `nexpath install` picks + * it up automatically. + * + * The actual runtime work (watching `state.vscdb`, rendering the + * decision-session webview, injecting selected options into the chat + * input) happens inside the VS Code extension at activation time — see + * `src/ext-vscode/src/extension.ts` for the wiring. + * + * The `extractPrompt` method is intentionally a stub. The architecture + * doc declares it on the `VSCodeExtensionAdapter` interface for symmetric + * API shape, but actual row decoding lives at the extension's runtime via + * `src/ext-vscode/src/extractors/` — the CLI never runs the watcher and + * therefore never decodes rows. A future refactor could relocate the + * extractors to the CLI level and have the adapter wrap them; for now + * the stub returns `null` so any caller asking the CLI adapter to decode + * a row gets the explicit "I don't know" answer. + */ + +/** Marketplace identifier used in both Open VSX and the VS Code Marketplace. */ +const MARKETPLACE_ID = 'emptyops.nexpath-vscode'; + +const OPEN_VSX_URL = `https://open-vsx.org/extension/${MARKETPLACE_ID.replace( + '.', + '/', +)}`; +const VS_CODE_MARKETPLACE_URL = `https://marketplace.visualstudio.com/items?itemName=${MARKETPLACE_ID}`; + +/** + * OS-specific Cursor configuration directory. Existence of this directory + * is the heuristic the adapter uses to decide whether Cursor is installed. + */ +export function cursorConfigDir( + home: string, + platform: NodeJS.Platform = process.platform, + appdata?: string, +): string { + switch (platform) { + case 'darwin': + return join(home, 'Library', 'Application Support', 'Cursor'); + case 'win32': + return join(appdata ?? process.env.APPDATA ?? join(home, 'AppData', 'Roaming'), 'Cursor'); + default: + return join(home, '.config', 'Cursor'); + } +} + +export const cursorAdapter: VSCodeExtensionAdapter = { + id: 'cursor', + label: 'Cursor', + category: 'vscode-extension', + marketplace: { openVsx: MARKETPLACE_ID, vsCode: MARKETPLACE_ID }, + + detect(ctx: InstallContext): boolean { + return existsSync(cursorConfigDir(ctx.home)); + }, + + chatHistoryPaths(ctx: InstallContext): string[] { + // Return the base workspaceStorage directory; per-workspace state.vscdb + // enumeration happens at extension activation time (we can't enumerate + // here because the user may open new workspaces after install runs). + return [join(cursorConfigDir(ctx.home), 'User', 'workspaceStorage')]; + }, + + /** + * Intentional stub — see "Architectural decision" below. + * + * The `VSCodeExtensionAdapter` interface declares `extractPrompt` for + * symmetric API shape (so a hypothetical future caller could ask + * "given this raw row, what's the user prompt?"). In our current + * architecture this method has **no caller**: + * + * - The `nexpath` CLI never reads `state.vscdb` rows — the watcher + * lives entirely inside the VS Code extension. + * - The extension uses the extractor modules at + * `src/ext-vscode/src/extractors/{cursor-v2024-q4, v2025-q1, + * v2025-q2, windsurf, index}.ts` directly (via + * `chat-history-watcher.ts`'s `pickExtractor` + `decodeRow` flow). + * - The CLI adapter sits on the install/configure side; it doesn't + * do row decoding. + * + * Returning `null` is the contract-compliant "I don't know" answer. + * + * ## When this should be filled in (migration path) + * + * If a future CLI tool needs to decode chat-history rows (e.g. + * `nexpath debug last-prompts `), the right move is to + * **promote the extractor modules from the sub-package to the CLI + * level** so both sides share one source of truth: + * + * 1. Move `src/ext-vscode/src/extractors/*` → `src/agents/chat-history-extractors/*`. + * 2. Move `src/ext-vscode/src/chat-history-types.ts` → + * `src/agents/chat-history-extractors/types.ts`. + * 3. Update sub-package imports in `chat-history-watcher.ts` to use + * the new CLI path. Sub-package `tsconfig.json` likely needs + * `rootDir` widened (e.g. `".."`) so imports outside `./src` are + * allowed. + * 4. Wire `cursor.ts` and `windsurf.ts` adapters' `extractPrompt` + * to call `pickExtractor` + `extractor.decodeRow` and reshape + * to the interface's `{ prompt, sessionId } | null` signature. + * 5. Leave thin re-export shims at the old sub-package paths so any + * external importer keeps working. + * + * This is a non-trivial refactor with cross-tree concerns (esbuild + * externals, tsconfig.rootDir, vitest config). Deferred from B4 + * because there is currently no caller demanding it; the unit tests + * verify only the stub-returns-null contract. + */ + extractPrompt(_rowKey: string, _rowValue: unknown) { + return null; + }, + + async install(ctx: InstallContext): Promise { + if (!this.detect(ctx)) { + console.log(`- ${'Cursor'.padEnd(12)} — not detected; skipping`); + return { status: 'skipped', notes: 'Cursor not installed on this machine' }; + } + console.log(`✓ ${'Cursor'.padEnd(12)} — install the Nexpath extension to activate guidance:`); + console.log(` Open VSX: ${OPEN_VSX_URL}`); + console.log(` VS Code Marketplace: ${VS_CODE_MARKETPLACE_URL}`); + console.log(` Or via CLI: cursor --install-extension ${MARKETPLACE_ID}`); + return { + status: 'installed', + notes: + 'Deep-link instructions printed; the user must install the VS Code extension manually before guidance activates.', + }; + }, + + async uninstall(ctx: InstallContext): Promise { + if (!this.detect(ctx)) { + console.log(`- ${'Cursor'.padEnd(12)} — not detected; skipping`); + return; + } + console.log(`- ${'Cursor'.padEnd(12)} — uninstall the Nexpath extension from the Cursor Extensions panel`); + console.log(` Or via CLI: cursor --uninstall-extension ${MARKETPLACE_ID}`); + }, +}; + +// Side-effect registration on module load — registered before any installAction +// invocation thanks to the side-effect import in src/agents/index.ts. +registerAdapter(cursorAdapter); diff --git a/src/agents/adapters/marketplace-id.test.ts b/src/agents/adapters/marketplace-id.test.ts new file mode 100644 index 00000000..58cb1f0f --- /dev/null +++ b/src/agents/adapters/marketplace-id.test.ts @@ -0,0 +1,64 @@ +import { describe, it, expect } from 'vitest'; +import { readFileSync } from 'node:fs'; +import { fileURLToPath } from 'node:url'; +import { dirname, resolve } from 'node:path'; +import { cursorAdapter } from './cursor.js'; +import { windsurfAdapter } from './windsurf.js'; + +/** + * Cross-file invariant: the marketplace ID printed by cursor + windsurf + * adapters during install() must match `.` from the + * VS Code sub-package's `package.json`. If they drift (because one file + * is updated without the other), vsce/ovsx still publishes under the + * package.json values, but our installer prints the wrong install URL — + * so users land on a 404 page instead of the real extension. + * + * This file exists separately from cursor.test.ts / windsurf.test.ts + * because the invariant is about the boundary between three files, not + * about either adapter's behaviour in isolation. + */ + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const subPackageManifestPath = resolve( + __dirname, + '..', + '..', + 'ext-vscode', + 'package.json', +); + +function readSubPackageManifestId(): string { + const raw = readFileSync(subPackageManifestPath, 'utf8'); + const pkg = JSON.parse(raw) as { publisher?: unknown; name?: unknown }; + if (typeof pkg.publisher !== 'string' || typeof pkg.name !== 'string') { + throw new Error( + `src/ext-vscode/package.json is missing publisher or name (got publisher=${String( + pkg.publisher, + )}, name=${String(pkg.name)})`, + ); + } + return `${pkg.publisher}.${pkg.name}`; +} + +describe('marketplace-id cross-file invariant', () => { + it('cursorAdapter.marketplace agrees with src/ext-vscode/package.json', () => { + const expected = readSubPackageManifestId(); + expect(cursorAdapter.marketplace.openVsx).toBe(expected); + expect(cursorAdapter.marketplace.vsCode).toBe(expected); + }); + + it('windsurfAdapter.marketplace agrees with src/ext-vscode/package.json', () => { + const expected = readSubPackageManifestId(); + expect(windsurfAdapter.marketplace.openVsx).toBe(expected); + expect(windsurfAdapter.marketplace.vsCode).toBe(expected); + }); + + it('cursor + windsurf both reference the same marketplace ID', () => { + expect(cursorAdapter.marketplace.openVsx).toBe( + windsurfAdapter.marketplace.openVsx, + ); + expect(cursorAdapter.marketplace.vsCode).toBe( + windsurfAdapter.marketplace.vsCode, + ); + }); +}); diff --git a/src/agents/adapters/windsurf.test.ts b/src/agents/adapters/windsurf.test.ts new file mode 100644 index 00000000..7c1fbc8f --- /dev/null +++ b/src/agents/adapters/windsurf.test.ts @@ -0,0 +1,154 @@ +import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; +import { mkdtempSync, mkdirSync, rmSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import { windsurfAdapter, windsurfConfigDir } from './windsurf.js'; +import type { InstallContext } from '../types.js'; + +const makeCtx = (home: string): InstallContext => ({ + home, + cwd: join(home, 'cwd'), + yes: true, + dbPath: ':memory:', +}); + +describe('windsurfConfigDir', () => { + it('returns the linux path under ~/.config/Windsurf', () => { + expect(windsurfConfigDir('/home/u', 'linux')).toBe('/home/u/.config/Windsurf'); + }); + + it('returns the darwin path under Application Support', () => { + expect(windsurfConfigDir('/Users/u', 'darwin')).toBe( + '/Users/u/Library/Application Support/Windsurf', + ); + }); + + it('returns the win32 path under APPDATA when provided', () => { + expect( + windsurfConfigDir('C:\\Users\\u', 'win32', 'C:\\Users\\u\\AppData\\Roaming'), + ).toBe('C:\\Users\\u\\AppData\\Roaming/Windsurf'); + }); +}); + +describe('windsurfAdapter — static fields', () => { + it('has the expected id, label, category', () => { + expect(windsurfAdapter.id).toBe('windsurf'); + expect(windsurfAdapter.label).toBe('Windsurf'); + expect(windsurfAdapter.category).toBe('vscode-extension'); + }); + + it('declares Open VSX + VS Code Marketplace ids', () => { + expect(windsurfAdapter.marketplace.openVsx).toBe('emptyops.nexpath-vscode'); + expect(windsurfAdapter.marketplace.vsCode).toBe('emptyops.nexpath-vscode'); + }); +}); + +describe('windsurfAdapter.detect', () => { + let tmp: string; + beforeEach(() => { + tmp = mkdtempSync(join(tmpdir(), 'windsurf-detect-')); + }); + afterEach(() => { + rmSync(tmp, { recursive: true, force: true }); + }); + + it('returns false when neither Windsurf nor codeium dir exists', () => { + expect(windsurfAdapter.detect(makeCtx(tmp))).toBe(false); + }); + + it('returns true when only the VS-Code-fork dir exists', () => { + mkdirSync(join(tmp, '.config', 'Windsurf'), { recursive: true }); + expect(windsurfAdapter.detect(makeCtx(tmp))).toBe(true); + }); + + it('returns true when only the legacy codeium dir exists', () => { + mkdirSync(join(tmp, '.codeium', 'windsurf'), { recursive: true }); + expect(windsurfAdapter.detect(makeCtx(tmp))).toBe(true); + }); + + it('returns true when both dirs exist', () => { + mkdirSync(join(tmp, '.config', 'Windsurf'), { recursive: true }); + mkdirSync(join(tmp, '.codeium', 'windsurf'), { recursive: true }); + expect(windsurfAdapter.detect(makeCtx(tmp))).toBe(true); + }); +}); + +describe('windsurfAdapter.chatHistoryPaths', () => { + it('returns both the workspaceStorage AND the codeium Cascade base path', () => { + const paths = windsurfAdapter.chatHistoryPaths(makeCtx('/home/u')); + expect(paths).toHaveLength(2); + expect(paths.some((p) => p.includes('Windsurf') && p.includes('workspaceStorage'))).toBe(true); + expect(paths.some((p) => p.endsWith('.codeium/windsurf'))).toBe(true); + }); +}); + +describe('windsurfAdapter.extractPrompt', () => { + it('returns null (decoding happens in the extension, not the CLI adapter)', () => { + expect(windsurfAdapter.extractPrompt('cascade.history', '[]')).toBeNull(); + expect(windsurfAdapter.extractPrompt('any.key', { x: 1 })).toBeNull(); + }); +}); + +describe('windsurfAdapter.install', () => { + let tmp: string; + let logSpy: ReturnType; + beforeEach(() => { + tmp = mkdtempSync(join(tmpdir(), 'windsurf-install-')); + logSpy = vi.spyOn(console, 'log').mockImplementation(() => {}); + }); + afterEach(() => { + rmSync(tmp, { recursive: true, force: true }); + logSpy.mockRestore(); + }); + + it('skips when Windsurf is not detected and returns status=skipped', async () => { + const r = await windsurfAdapter.install(makeCtx(tmp)); + expect(r.status).toBe('skipped'); + const allLogs = logSpy.mock.calls.map((c) => c.join(' ')).join('\n'); + expect(allLogs).toContain('not detected'); + }); + + it('prints deep-link instructions when Windsurf IS detected', async () => { + mkdirSync(join(tmp, '.config', 'Windsurf'), { recursive: true }); + const r = await windsurfAdapter.install(makeCtx(tmp)); + expect(r.status).toBe('installed'); + const allLogs = logSpy.mock.calls.map((c) => c.join(' ')).join('\n'); + expect(allLogs).toContain('Open VSX'); + expect(allLogs).toContain('windsurf --install-extension'); + expect(allLogs).toContain('emptyops.nexpath-vscode'); + }); +}); + +describe('windsurfAdapter.uninstall', () => { + let tmp: string; + let logSpy: ReturnType; + beforeEach(() => { + tmp = mkdtempSync(join(tmpdir(), 'windsurf-uninstall-')); + logSpy = vi.spyOn(console, 'log').mockImplementation(() => {}); + }); + afterEach(() => { + rmSync(tmp, { recursive: true, force: true }); + logSpy.mockRestore(); + }); + + it('logs the skip line when Windsurf is not detected', async () => { + await windsurfAdapter.uninstall(makeCtx(tmp)); + expect(logSpy.mock.calls.map((c) => c.join(' ')).join('\n')).toContain('not detected'); + }); + + it('logs the uninstall instructions when Windsurf IS detected', async () => { + mkdirSync(join(tmp, '.config', 'Windsurf'), { recursive: true }); + await windsurfAdapter.uninstall(makeCtx(tmp)); + const allLogs = logSpy.mock.calls.map((c) => c.join(' ')).join('\n'); + expect(allLogs).toContain('uninstall'); + expect(allLogs).toContain('windsurf --uninstall-extension'); + }); +}); + +describe('windsurfAdapter registry registration', () => { + it('is registered with the global registry under id "windsurf"', async () => { + await import('../index.js'); + const { getAdapter } = await import('../registry.js'); + expect(getAdapter('windsurf')).toBe(windsurfAdapter); + }); +}); diff --git a/src/agents/adapters/windsurf.ts b/src/agents/adapters/windsurf.ts new file mode 100644 index 00000000..8c80bbee --- /dev/null +++ b/src/agents/adapters/windsurf.ts @@ -0,0 +1,132 @@ +import { existsSync } from 'node:fs'; +import { join } from 'node:path'; +import { registerAdapter } from '../registry.js'; +import type { + InstallContext, + InstallResult, + VSCodeExtensionAdapter, +} from '../types.js'; + +/** + * Windsurf adapter (M10 of M2 Branch 4). + * + * Windsurf is Codeium's AI-native IDE (a VS Code fork, like Cursor). + * Nexpath ships the same VS Code extension to Cursor + Windsurf via + * Open VSX. This adapter mirrors `cursor.ts` but with Windsurf-specific + * config paths. + * + * Storage layout difference from Cursor: + * - Cursor uses VS Code's standard `User/workspaceStorage//state.vscdb` + * (SQLite) — covered by the cursor-v* extractors. + * - Windsurf historically uses `~/.codeium/windsurf/` (Cascade chat data + * stored as JSON files rather than SQLite). Architecture rev 2 §4.2 + * covers this; the dev plan §3 M2 §2.2 M10 calls out the difference. + * - Recent Windsurf versions ALSO populate VS Code-style storage at + * `User/workspaceStorage/`. We watch both — the Windsurf extractor + * (already shipped in B2 as a stub) handles the cascade.* keys when + * they appear; the JSON-file path watch belongs to a future + * refinement once a real Windsurf chat capture is available. + * + * The `extractPrompt` stub matches `cursor.ts` for the same reason — + * decoding lives at the extension's runtime, not in the CLI adapter. + */ + +const MARKETPLACE_ID = 'emptyops.nexpath-vscode'; + +const OPEN_VSX_URL = `https://open-vsx.org/extension/${MARKETPLACE_ID.replace( + '.', + '/', +)}`; +const VS_CODE_MARKETPLACE_URL = `https://marketplace.visualstudio.com/items?itemName=${MARKETPLACE_ID}`; + +/** + * OS-specific Windsurf configuration directory. Used both as the primary + * detection heuristic and as the base for the workspace-storage path. + */ +export function windsurfConfigDir( + home: string, + platform: NodeJS.Platform = process.platform, + appdata?: string, +): string { + switch (platform) { + case 'darwin': + return join(home, 'Library', 'Application Support', 'Windsurf'); + case 'win32': + return join(appdata ?? process.env.APPDATA ?? join(home, 'AppData', 'Roaming'), 'Windsurf'); + default: + return join(home, '.config', 'Windsurf'); + } +} + +/** + * Legacy Cascade data directory (per-user, OS-agnostic). Windsurf may + * still drop JSON chat files here in addition to the VS Code-style + * workspaceStorage path. + */ +function codeiumCascadeDir(home: string): string { + return join(home, '.codeium', 'windsurf'); +} + +export const windsurfAdapter: VSCodeExtensionAdapter = { + id: 'windsurf', + label: 'Windsurf', + category: 'vscode-extension', + marketplace: { openVsx: MARKETPLACE_ID, vsCode: MARKETPLACE_ID }, + + detect(ctx: InstallContext): boolean { + return ( + existsSync(windsurfConfigDir(ctx.home)) || + existsSync(codeiumCascadeDir(ctx.home)) + ); + }, + + chatHistoryPaths(ctx: InstallContext): string[] { + return [ + join(windsurfConfigDir(ctx.home), 'User', 'workspaceStorage'), + codeiumCascadeDir(ctx.home), + ]; + }, + + /** + * Intentional stub — same architectural decision as `cursor.ts`. See the + * comprehensive JSDoc on `cursorAdapter.extractPrompt` for the full + * rationale + migration path if a CLI caller is ever added. + * + * Short version: decoding lives in the extension runtime + * (`src/ext-vscode/src/extractors/`); the CLI adapter never decodes rows. + * Returning `null` is contract-compliant ("I don't know"). When the + * extractors are promoted to `src/agents/chat-history-extractors/`, + * this method gets wired up to delegate via `pickExtractor` + + * `extractor.decodeRow`. + */ + extractPrompt(_rowKey: string, _rowValue: unknown) { + return null; + }, + + async install(ctx: InstallContext): Promise { + if (!this.detect(ctx)) { + console.log(`- ${'Windsurf'.padEnd(12)} — not detected; skipping`); + return { status: 'skipped', notes: 'Windsurf not installed on this machine' }; + } + console.log(`✓ ${'Windsurf'.padEnd(12)} — install the Nexpath extension to activate guidance:`); + console.log(` Open VSX: ${OPEN_VSX_URL}`); + console.log(` VS Code Marketplace: ${VS_CODE_MARKETPLACE_URL}`); + console.log(` Or via CLI: windsurf --install-extension ${MARKETPLACE_ID}`); + return { + status: 'installed', + notes: + 'Deep-link instructions printed; the user must install the VS Code extension manually before guidance activates.', + }; + }, + + async uninstall(ctx: InstallContext): Promise { + if (!this.detect(ctx)) { + console.log(`- ${'Windsurf'.padEnd(12)} — not detected; skipping`); + return; + } + console.log(`- ${'Windsurf'.padEnd(12)} — uninstall the Nexpath extension from the Windsurf Extensions panel`); + console.log(` Or via CLI: windsurf --uninstall-extension ${MARKETPLACE_ID}`); + }, +}; + +registerAdapter(windsurfAdapter); diff --git a/src/agents/index.ts b/src/agents/index.ts new file mode 100644 index 00000000..0273717e --- /dev/null +++ b/src/agents/index.ts @@ -0,0 +1,8 @@ +// Adapter registrations live here as side-effect imports. +// Each adapter module calls registerAdapter() at module load. +// +// Subsequent milestones add CLI-wrap (Codex CLI + Aider) and browser +// adapters (Replit / Bolt.new / Lovable / ChatGPT). +import './adapters/claude-code.js'; +import './adapters/cursor.js'; +import './adapters/windsurf.js'; diff --git a/src/agents/registry.test.ts b/src/agents/registry.test.ts new file mode 100644 index 00000000..ef5f4d85 --- /dev/null +++ b/src/agents/registry.test.ts @@ -0,0 +1,126 @@ +import { describe, it, expect, beforeEach, vi } from 'vitest'; +import type { AgentAdapter, InstallContext } from './types.js'; + +/** + * Unit tests for the adapter registry. + * + * The registry stores adapters in a module-local mutable array, so tests would + * leak state into each other if we re-used the same module instance. Each test + * calls `vi.resetModules()` and re-imports `./registry.js` so it gets a fresh, + * empty registry. + */ +describe('agents/registry', () => { + let registerAdapter: typeof import('./registry.js')['registerAdapter']; + let detectAll: typeof import('./registry.js')['detectAll']; + let getAdapter: typeof import('./registry.js')['getAdapter']; + + const dummyCtx: InstallContext = { + home: '/fake/home', + cwd: '/fake/cwd', + yes: true, + dbPath: ':memory:', + }; + + const makeAdapter = (id: string, detectResult: boolean): AgentAdapter => ({ + id, + label: `Label-${id}`, + category: 'hook', + detect: () => detectResult, + install: async () => ({ status: 'installed' }), + uninstall: async () => {}, + }); + + beforeEach(async () => { + vi.resetModules(); + const reg = await import('./registry.js'); + registerAdapter = reg.registerAdapter; + detectAll = reg.detectAll; + getAdapter = reg.getAdapter; + }); + + describe('registerAdapter + getAdapter', () => { + it('registers an adapter and retrieves it by id', () => { + const a = makeAdapter('cursor', true); + registerAdapter(a); + expect(getAdapter('cursor')).toBe(a); + }); + + it('returns undefined for an unknown id', () => { + registerAdapter(makeAdapter('cursor', true)); + expect(getAdapter('nonexistent')).toBeUndefined(); + }); + + it('returns undefined when no adapters are registered', () => { + expect(getAdapter('any')).toBeUndefined(); + }); + + it('supports multiple registrations and finds each by its id', () => { + const a = makeAdapter('a', true); + const b = makeAdapter('b', true); + registerAdapter(a); + registerAdapter(b); + expect(getAdapter('a')).toBe(a); + expect(getAdapter('b')).toBe(b); + }); + }); + + describe('detectAll', () => { + it('returns an empty list when no adapters are registered', async () => { + const result = await detectAll(dummyCtx); + expect(result).toEqual([]); + }); + + it('returns only adapters whose detect() returns true', async () => { + const yes = makeAdapter('yes', true); + const no = makeAdapter('no', false); + registerAdapter(yes); + registerAdapter(no); + const result = await detectAll(dummyCtx); + expect(result).toEqual([yes]); + }); + + it('supports async detect() returning a Promise', async () => { + const yesAsync: AgentAdapter = { + ...makeAdapter('async-yes', true), + detect: async () => true, + }; + const noAsync: AgentAdapter = { + ...makeAdapter('async-no', false), + detect: async () => false, + }; + registerAdapter(yesAsync); + registerAdapter(noAsync); + const result = await detectAll(dummyCtx); + expect(result.map((x) => x.id)).toEqual(['async-yes']); + }); + + it('preserves insertion order across multiple detected adapters', async () => { + registerAdapter(makeAdapter('a', true)); + registerAdapter(makeAdapter('b', true)); + registerAdapter(makeAdapter('c', true)); + const result = await detectAll(dummyCtx); + expect(result.map((x) => x.id)).toEqual(['a', 'b', 'c']); + }); + + it('passes the InstallContext through to each adapter.detect()', async () => { + const detectSpy = vi.fn(() => true); + const adapter: AgentAdapter = { + ...makeAdapter('spy', true), + detect: detectSpy, + }; + registerAdapter(adapter); + await detectAll(dummyCtx); + expect(detectSpy).toHaveBeenCalledWith(dummyCtx); + expect(detectSpy).toHaveBeenCalledTimes(1); + }); + + it('skips false-detecting adapters while keeping true-detecting ones, regardless of registration order', async () => { + registerAdapter(makeAdapter('first-no', false)); + registerAdapter(makeAdapter('second-yes', true)); + registerAdapter(makeAdapter('third-no', false)); + registerAdapter(makeAdapter('fourth-yes', true)); + const result = await detectAll(dummyCtx); + expect(result.map((x) => x.id)).toEqual(['second-yes', 'fourth-yes']); + }); + }); +}); diff --git a/src/agents/registry.ts b/src/agents/registry.ts new file mode 100644 index 00000000..480d2909 --- /dev/null +++ b/src/agents/registry.ts @@ -0,0 +1,24 @@ +import type { AgentAdapter, InstallContext } from './types.js'; + +/** + * Module-local mutable list. Adapters register themselves at module import time + * via side-effect imports in ./index.ts. This file does not import any adapter + * directly — that keeps the registry decoupled from individual adapter modules. + */ +const adapters: AgentAdapter[] = []; + +export function registerAdapter(a: AgentAdapter): void { + adapters.push(a); +} + +export async function detectAll(ctx: InstallContext): Promise { + const present: AgentAdapter[] = []; + for (const a of adapters) { + if (await a.detect(ctx)) present.push(a); + } + return present; +} + +export function getAdapter(id: string): AgentAdapter | undefined { + return adapters.find((a) => a.id === id); +} diff --git a/src/agents/types.ts b/src/agents/types.ts new file mode 100644 index 00000000..bbb1f79d --- /dev/null +++ b/src/agents/types.ts @@ -0,0 +1,97 @@ +/** + * Adapter interface contract for the coding-agent hooking module (Layer B of the + * three-layer architecture). Every coding agent surface (Claude Code, Cursor, + * Windsurf, Codex CLI, Aider, Replit, Bolt.new, Lovable, ChatGPT) is wired into + * nexpath through one of these four adapter categories. + * + * See: lib/share/submodule/reviewduel-submodule/docs/architecture/coding-agent-hooks-architecture.md + */ + +export type AdapterCategory = + | 'hook' + | 'vscode-extension' + | 'cli-wrap' + | 'browser-extension'; + +export interface InstallContext { + home: string; + cwd: string; + /** Skip confirmation prompts. */ + yes: boolean; + /** Path to ~/.nexpath/prompt-store.db (or ':memory:' for tests). */ + dbPath: string; + /** + * Optional override for the target settings/config file the adapter writes to. + * If omitted, the adapter derives the path from `home`. Tests (and non-default + * install locations) pass this to decouple the file path from `home` — + * preserving the pre-refactor behaviour where `paths.claudeSettings` was + * passed independently of `homedir()`. + * + * Adapters that don't write a single settings file (e.g. CLIWrapAdapter, + * BrowserExtensionAdapter) ignore this field. + */ + settingsPath?: string; +} + +export interface InstallResult { + status: 'installed' | 'already-installed' | 'skipped' | 'failed'; + notes?: string; +} + +export interface AgentAdapter { + /** Stable identifier, e.g. 'claude-code', 'cursor', 'replit'. */ + id: string; + label: string; + category: AdapterCategory; + + /** Returns true if this agent appears to be present on the current system. */ + detect(ctx: InstallContext): Promise | boolean; + + /** Wire up the binding — write config, install extension, register shell alias, etc. */ + install(ctx: InstallContext): Promise; + + /** Reverse of install. */ + uninstall(ctx: InstallContext): Promise; +} + +/** Adapters that wire into agents with native lifecycle hooks (e.g. Claude Code). */ +export interface HookAdapter extends AgentAdapter { + category: 'hook'; + /** Path to the agent's settings file that hosts hook entries. */ + settingsPath(ctx: InstallContext): string; + /** Build the hook entries to merge into the agent's settings file. */ + buildHooks(ctx: InstallContext): Record>; +} + +/** Adapters that ship a companion VS Code-format extension (e.g. Cursor, Windsurf). */ +export interface VSCodeExtensionAdapter extends AgentAdapter { + category: 'vscode-extension'; + /** Marketplace identifiers. */ + marketplace: { openVsx: string; vsCode?: string }; + /** Per-OS chat-history paths the extension's file-watcher subscribes to. */ + chatHistoryPaths(ctx: InstallContext): string[]; + /** Schema-extractor — reads new rows out of the watched file. */ + extractPrompt(rowKey: string, rowValue: unknown): { prompt: string; sessionId: string } | null; +} + +/** Adapters that wrap a CLI binary (e.g. Codex CLI, Aider). */ +export interface CLIWrapAdapter extends AgentAdapter { + category: 'cli-wrap'; + /** The original binary name, e.g. 'codex', 'aider'. */ + realBinary: string; + /** Strategy: 'shell-alias' or 'node-proxy'. */ + strategy: 'shell-alias' | 'node-proxy'; + /** For node-proxy: heuristic that detects an agent response boundary in stdout. */ + detectResponseEnd?(stdoutChunk: string): boolean; +} + +/** Adapters that ship a browser extension content script (Replit, Bolt.new, Lovable, ChatGPT). */ +export interface BrowserExtensionAdapter extends AgentAdapter { + category: 'browser-extension'; + /** Origins this content script runs against (Manifest V3 match patterns). */ + origins: string[]; + /** Capture strategy in priority order. */ + capture: Array<'fetch' | 'websocket' | 'dom-events' | 'mutation-observer'>; + /** Inline content-script source to be bundled into the extension build. */ + contentScriptModule: string; +} diff --git a/src/cli/commands/__snapshots__/install.snapshot.test.ts.snap b/src/cli/commands/__snapshots__/install.snapshot.test.ts.snap new file mode 100644 index 00000000..3dde6cb9 --- /dev/null +++ b/src/cli/commands/__snapshots__/install.snapshot.test.ts.snap @@ -0,0 +1,47 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`installAction snapshot (M1 zero-diff safety net) > writes the same settings.json bytes and stdout as the pre-refactor baseline 1`] = ` +{ + "settingsContent": "{ + "hooks": { + "UserPromptSubmit": [ + { + "_nexpath_hook": true, + "matcher": "", + "hooks": [ + { + "type": "command", + "command": "node \\"/fixture/nexpath/dist/cli/index.js\\" auto --db \\"$HOME/.nexpath/prompt-store.db\\"" + } + ] + } + ], + "Stop": [ + { + "_nexpath_hook": true, + "matcher": "", + "hooks": [ + { + "type": "command", + "command": "node \\"/fixture/nexpath/dist/cli/index.js\\" stop --db \\"$HOME/.nexpath/prompt-store.db\\"" + } + ] + } + ] + } +} +", + "stdout": " +Before installing nexpath, a few things to know: + 1. An OpenAI API key is required (OPENAI_API_KEY) for advisory generation + 2. Prompts are stored locally at $HOME/.nexpath/prompt-store.db — nothing leaves your machine + 3. To opt out at any time: press Ctrl+X during an advisory, or run nexpath uninstall + +Detected: Claude Code +✓ Claude Code — advisory hook written to $HOME/.claude/settings.json + +Restart your agents to activate nexpath-prompt-store. +Note: advisory pipeline (nexpath auto) auto-wired for Claude Code only. +Run: nexpath status to verify connections.", +} +`; diff --git a/src/cli/commands/install.snapshot.test.ts b/src/cli/commands/install.snapshot.test.ts new file mode 100644 index 00000000..eabbc6c1 --- /dev/null +++ b/src/cli/commands/install.snapshot.test.ts @@ -0,0 +1,89 @@ +import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; +import { mkdtempSync, rmSync, readFileSync, existsSync } from 'node:fs'; +import { tmpdir, homedir } from 'node:os'; +import { join } from 'node:path'; +import { installAction, resolveAgentPaths, type AgentPaths } from './install.js'; + +/** + * Real $HOME captured at module load — DEFAULT_DB_PATH (resolved when store/db.ts + * was imported) bakes this in, so we normalise it out of the snapshot to keep + * the snapshot portable across machines. + */ +const REAL_HOME = homedir(); + +/** + * Snapshot test — captures the current behaviour of installAction before any + * refactor begins. This is the safety net for Milestone M1 Branch 2's zero-diff + * guarantee: after the refactor merges, this snapshot MUST remain byte-identical. + * + * See dev plan §1.5 and §1.6 in the reviewduel-submodule docs. + */ +describe('installAction snapshot (M1 zero-diff safety net)', () => { + let tmpHome: string; + let originalArgv1: string; + let originalPlatform: NodeJS.Platform; + let logSpy: ReturnType; + + beforeEach(() => { + tmpHome = mkdtempSync(join(tmpdir(), 'nexpath-snapshot-')); + vi.stubEnv('HOME', tmpHome); + + // Pin process.argv[1] so the hook command's resolved CLI path is deterministic. + originalArgv1 = process.argv[1]; + process.argv[1] = '/fixture/nexpath/dist/cli/index.js'; + + // Pin process.platform to 'linux' so the disclosure modifier-key ("Ctrl+X" + // vs "Cmd+X" on macOS) is stable regardless of where the test runs. + originalPlatform = process.platform; + Object.defineProperty(process, 'platform', { value: 'linux', configurable: true }); + + logSpy = vi.spyOn(console, 'log').mockImplementation(() => {}); + }); + + afterEach(() => { + process.argv[1] = originalArgv1; + Object.defineProperty(process, 'platform', { value: originalPlatform, configurable: true }); + vi.unstubAllEnvs(); + logSpy.mockRestore(); + rmSync(tmpHome, { recursive: true, force: true }); + }); + + it('writes the same settings.json bytes and stdout as the pre-refactor baseline', async () => { + // Controlled paths — no real user dirs touched, snapshot is fully portable. + const paths: AgentPaths = resolveAgentPaths( + tmpHome, + join(tmpHome, 'AppData', 'Roaming'), + join(tmpHome, 'cwd'), + 'linux', + ); + + await installAction( + { yes: true }, + { + isWin: false, + paths, + dbPath: ':memory:', + skipClipboardCheck: true, + }, + ); + + const settingsPath = join(tmpHome, '.claude', 'settings.json'); + const settingsContent = existsSync(settingsPath) + ? readFileSync(settingsPath, 'utf8') + : null; + + const stdout = logSpy.mock.calls.map((c) => c.join(' ')).join('\n'); + + // Normalise both the test-tmp home (resolved at install-time) and the real + // home (which DEFAULT_DB_PATH captured at module-import-time). + const normalise = (s: string | null): string | null => + s === null + ? null + : s.replaceAll(tmpHome, '$HOME').replaceAll(REAL_HOME, '$HOME'); + + expect({ + settingsContent: normalise(settingsContent), + stdout: normalise(stdout), + }).toMatchSnapshot(); + }); +}); diff --git a/src/cli/commands/install.test.ts b/src/cli/commands/install.test.ts index 450124ec..5f957900 100644 --- a/src/cli/commands/install.test.ts +++ b/src/cli/commands/install.test.ts @@ -784,6 +784,139 @@ describe('installAction', () => { expect(output).toContain('Claude Code only'); } finally { cleanup(); } }); + + // ── Registry-driven VSCodeExtensionAdapter installs (M2/B4) ───────────────── + // These tests stub HOME so the registry adapters (cursor / windsurf) check + // for their config dirs INSIDE the tmpDir — keeping the test hermetic and + // independent of whether the dev machine actually has Cursor / Windsurf + // installed. + + it('calls cursor adapter and prints deep-link instructions when Cursor is detected', async () => { + const { dir, cleanup } = tmpDir(); + const spy = vi.spyOn(console, 'log').mockImplementation(() => {}); + try { + // Cursor config dir present → registry adapter's detect() returns true. + mkdirSync(join(dir, '.config', 'Cursor'), { recursive: true }); + vi.stubEnv('HOME', dir); + const paths = resolveAgentPaths(dir, dir, dir); + await installAction({ yes: true }, { + paths, + isWin: false, + execFn: () => {}, + skipClipboardCheck: true, + }); + const output = spy.mock.calls.map((c) => c[0] as string).join('\n'); + expect(output).toContain('Cursor'); + expect(output).toContain('install the Nexpath extension'); + expect(output).toContain('Open VSX'); + expect(output).toContain('cursor --install-extension'); + } finally { + vi.unstubAllEnvs(); + cleanup(); + } + }); + + it('calls windsurf adapter and prints deep-link instructions when Windsurf is detected', async () => { + const { dir, cleanup } = tmpDir(); + const spy = vi.spyOn(console, 'log').mockImplementation(() => {}); + try { + mkdirSync(join(dir, '.config', 'Windsurf'), { recursive: true }); + vi.stubEnv('HOME', dir); + const paths = resolveAgentPaths(dir, dir, dir); + await installAction({ yes: true }, { + paths, + isWin: false, + execFn: () => {}, + skipClipboardCheck: true, + }); + const output = spy.mock.calls.map((c) => c[0] as string).join('\n'); + expect(output).toContain('Windsurf'); + expect(output).toContain('install the Nexpath extension'); + expect(output).toContain('windsurf --install-extension'); + } finally { + vi.unstubAllEnvs(); + cleanup(); + } + }); + + it('prints both cursor + windsurf deep-link blocks when both are detected', async () => { + const { dir, cleanup } = tmpDir(); + const spy = vi.spyOn(console, 'log').mockImplementation(() => {}); + try { + mkdirSync(join(dir, '.config', 'Cursor'), { recursive: true }); + mkdirSync(join(dir, '.config', 'Windsurf'), { recursive: true }); + vi.stubEnv('HOME', dir); + const paths = resolveAgentPaths(dir, dir, dir); + await installAction({ yes: true }, { + paths, + isWin: false, + execFn: () => {}, + skipClipboardCheck: true, + }); + const output = spy.mock.calls.map((c) => c[0] as string).join('\n'); + expect(output).toContain('cursor --install-extension'); + expect(output).toContain('windsurf --install-extension'); + } finally { + vi.unstubAllEnvs(); + cleanup(); + } + }); + + it('does NOT double-invoke the claude-code adapter from the registry loop', async () => { + const { dir, cleanup } = tmpDir(); + const spy = vi.spyOn(console, 'log').mockImplementation(() => {}); + try { + vi.stubEnv('HOME', dir); + const paths = resolveAgentPaths(dir, dir, dir); + await installAction({ yes: true }, { + paths, + isWin: false, + execFn: () => {}, + skipClipboardCheck: true, + }); + const output = spy.mock.calls.map((c) => c[0] as string).join('\n'); + // The Claude Code adapter prints exactly one "advisory hook written to" + // line per install. If the registry loop double-invoked it, we'd see two. + const matches = output.match(/advisory hook written to/g) ?? []; + expect(matches.length).toBe(1); + } finally { + vi.unstubAllEnvs(); + cleanup(); + } + }); + + it('catches adapter.install errors from the registry loop and prints failed line; loop continues', async () => { + const { cursorAdapter } = await import('../../agents/adapters/cursor.js'); + const installSpy = vi + .spyOn(cursorAdapter, 'install') + .mockRejectedValueOnce(new Error('synthetic disk failure')); + const { dir, cleanup } = tmpDir(); + const spy = vi.spyOn(console, 'log').mockImplementation(() => {}); + try { + // Need cursor detect()=true so it's in detectAll's results AND windsurf + // detect()=true so we can verify the loop continued past the throwing one. + mkdirSync(join(dir, '.config', 'Cursor'), { recursive: true }); + mkdirSync(join(dir, '.config', 'Windsurf'), { recursive: true }); + vi.stubEnv('HOME', dir); + const paths = resolveAgentPaths(dir, dir, dir); + await installAction({ yes: true }, { + paths, + isWin: false, + execFn: () => {}, + skipClipboardCheck: true, + }); + const output = spy.mock.calls.map((c) => c[0] as string).join('\n'); + expect(installSpy).toHaveBeenCalledOnce(); + // The catch block prints the ✗ line with the error message + expect(output).toMatch(/failed:.*synthetic disk failure/); + // The loop continued — windsurf's install ran too + expect(output).toContain('windsurf --install-extension'); + } finally { + installSpy.mockRestore(); + vi.unstubAllEnvs(); + cleanup(); + } + }); }); // ── uninstallAction ─────────────────────────────────────────────────────────── @@ -893,6 +1026,67 @@ describe('uninstallAction', () => { expect(output).toContain('hook not registered'); } finally { cleanup(); } }); + + // ── Registry-driven VSCodeExtensionAdapter uninstalls (M2/B4) ─────────────── + + it('calls cursor adapter uninstall and prints uninstall instructions when Cursor is detected', async () => { + const { dir, cleanup } = tmpDir(); + const spy = vi.spyOn(console, 'log').mockImplementation(() => {}); + try { + mkdirSync(join(dir, '.config', 'Cursor'), { recursive: true }); + vi.stubEnv('HOME', dir); + const paths = resolveAgentPaths(dir, dir, dir); + await uninstallAction({ paths, execFn: () => {} }); + const output = spy.mock.calls.map((c) => c[0] as string).join('\n'); + expect(output).toContain('Cursor'); + expect(output).toContain('cursor --uninstall-extension'); + } finally { + vi.unstubAllEnvs(); + cleanup(); + } + }); + + it('calls windsurf adapter uninstall and prints uninstall instructions when Windsurf is detected', async () => { + const { dir, cleanup } = tmpDir(); + const spy = vi.spyOn(console, 'log').mockImplementation(() => {}); + try { + mkdirSync(join(dir, '.config', 'Windsurf'), { recursive: true }); + vi.stubEnv('HOME', dir); + const paths = resolveAgentPaths(dir, dir, dir); + await uninstallAction({ paths, execFn: () => {} }); + const output = spy.mock.calls.map((c) => c[0] as string).join('\n'); + expect(output).toContain('Windsurf'); + expect(output).toContain('windsurf --uninstall-extension'); + } finally { + vi.unstubAllEnvs(); + cleanup(); + } + }); + + it('catches adapter.uninstall errors from the registry loop and prints failed line; loop continues', async () => { + const { cursorAdapter } = await import('../../agents/adapters/cursor.js'); + const uninstallSpy = vi + .spyOn(cursorAdapter, 'uninstall') + .mockRejectedValueOnce(new Error('synthetic uninstall failure')); + const { dir, cleanup } = tmpDir(); + const spy = vi.spyOn(console, 'log').mockImplementation(() => {}); + try { + mkdirSync(join(dir, '.config', 'Cursor'), { recursive: true }); + mkdirSync(join(dir, '.config', 'Windsurf'), { recursive: true }); + vi.stubEnv('HOME', dir); + const paths = resolveAgentPaths(dir, dir, dir); + await uninstallAction({ paths, execFn: () => {} }); + const output = spy.mock.calls.map((c) => c[0] as string).join('\n'); + expect(uninstallSpy).toHaveBeenCalledOnce(); + expect(output).toMatch(/failed:.*synthetic uninstall failure/); + // Loop continued — windsurf's uninstall ran too + expect(output).toContain('windsurf --uninstall-extension'); + } finally { + uninstallSpy.mockRestore(); + vi.unstubAllEnvs(); + cleanup(); + } + }); }); // ── getClaudeSettingsPath ───────────────────────────────────────────────────── diff --git a/src/cli/commands/install.ts b/src/cli/commands/install.ts index 02153e96..9723a4cb 100644 --- a/src/cli/commands/install.ts +++ b/src/cli/commands/install.ts @@ -6,6 +6,30 @@ import { confirm, isCancel } from '@clack/prompts'; import { openStore, closeStore, DEFAULT_DB_PATH } from '../../store/db.js'; import { isConfigSet, setConfig } from '../../store/config.js'; +// Side-effect import: registers all coding-agent adapters with the in-process +// registry. Must precede any getAdapter() / detectAll() call in installAction below. +import '../../agents/index.js'; +import { getAdapter, detectAll } from '../../agents/registry.js'; +import type { HookAdapter, InstallContext } from '../../agents/types.js'; +// Internal use + backward-compat re-export of the Claude Code hook helpers +// (moved to src/agents/adapters/claude-code.ts in M1 Branch 2 — v0.1.3/m1/claude-code-refactor). +import { + getClaudeSettingsPath, + buildHookCommand, + buildStopHookCommand, + buildHookEntry, + writeHookEntry, + removeHookEntry, +} from '../../agents/adapters/claude-code.js'; +export { + getClaudeSettingsPath, + buildHookCommand, + buildStopHookCommand, + buildHookEntry, + writeHookEntry, + removeHookEntry, +}; + export const MCP_SERVER_NAME = 'nexpath-prompt-store'; // ── Config entry builders (pure — no I/O) ───────────────────────────────────── @@ -205,145 +229,10 @@ export function removeOpenCodeEntry(filePath: string): boolean { } // ── Claude Code hook helpers ────────────────────────────────────────────────── - -/** Path to the user-level Claude Code settings file (where hooks are configured). */ -export function getClaudeSettingsPath(home: string): string { - return join(home, '.claude', 'settings.json'); -} - -/** - * Build the shell command string for the UserPromptSubmit hook. - * - * Uses `node` explicitly with the absolute path to the running CLI so the hook - * works regardless of whether nexpath is globally installed or run from a local - * build (PATH is not required). - * - * Forward slashes are used throughout so the value is safe in PowerShell and cmd - * on Windows as well as bash/zsh on Unix. - */ -export function buildHookCommand(home: string, platform = process.platform): string { - const cliPath = resolve(process.argv[1]).replace(/\\/g, '/'); - const dbPath = join(home, '.nexpath', 'prompt-store.db').replace(/\\/g, '/'); - return `node "${cliPath}" auto --db "${dbPath}"`; -} - -/** Build the shell command string for the Stop hook. */ -export function buildStopHookCommand(home: string, platform = process.platform): string { - const cliPath = resolve(process.argv[1]).replace(/\\/g, '/'); - const dbPath = join(home, '.nexpath', 'prompt-store.db').replace(/\\/g, '/'); - return `node "${cliPath}" stop --db "${dbPath}"`; -} - -/** - * Build the UserPromptSubmit + Stop hook entry objects. - * - * The `_nexpath_hook: true` field is the reliable deduplication and removal - * marker — it survives path changes across reinstalls, unlike scanning the - * command string. - * - * No `timeout` field is set so Claude Code uses its default (600 s), which is - * required for hooks that block for UI interaction (the decision session). - */ -export function buildHookEntry(home: string, platform = process.platform): Record { - return { - UserPromptSubmit: [ - { - _nexpath_hook: true, - matcher: '', - hooks: [ - { - type: 'command', - command: buildHookCommand(home, platform), - }, - ], - }, - ], - Stop: [ - { - _nexpath_hook: true, - matcher: '', - hooks: [ - { - type: 'command', - command: buildStopHookCommand(home, platform), - }, - ], - }, - ], - }; -} - -/** - * Write the nexpath UserPromptSubmit and Stop hooks into ~/.claude/settings.json. - * - * Uses a read-filter-append pattern so existing hooks written by other tools are - * preserved. Any prior nexpath hook group (identified by `_nexpath_hook: true`) - * is removed before appending the fresh entry, making this operation idempotent. - */ -export function writeHookEntry(filePath: string, home: string, platform = process.platform): void { - const data = readJsonSafe(filePath); - const hooks = (data.hooks as Record | undefined) ?? {}; - const entry = buildHookEntry(home, platform); - - // UserPromptSubmit - const existingUPS = (hooks.UserPromptSubmit as Array> | undefined) ?? []; - hooks.UserPromptSubmit = [ - ...existingUPS.filter((g) => !g._nexpath_hook), - ...(entry.UserPromptSubmit as unknown[]), - ]; - - // Stop - const existingStop = (hooks.Stop as Array> | undefined) ?? []; - hooks.Stop = [ - ...existingStop.filter((g) => !g._nexpath_hook), - ...(entry.Stop as unknown[]), - ]; - - data.hooks = hooks; - writeJson(filePath, data); -} - -/** - * Remove the nexpath UserPromptSubmit and Stop hooks from ~/.claude/settings.json. - * - * Identifies nexpath-written hook groups by the `_nexpath_hook: true` field. - * Returns false if the file does not exist or no nexpath hooks were found. - */ -export function removeHookEntry(filePath: string): boolean { - if (!existsSync(filePath)) return false; - const data = readJsonSafe(filePath); - const hooks = data.hooks as Record | undefined; - if (!hooks) return false; - - let removed = false; - - // UserPromptSubmit - const upsGroups = hooks.UserPromptSubmit as Array> | undefined; - if (upsGroups) { - const filtered = upsGroups.filter((g) => !g._nexpath_hook); - if (filtered.length < upsGroups.length) { - removed = true; - if (filtered.length === 0) delete hooks.UserPromptSubmit; - else hooks.UserPromptSubmit = filtered; - } - } - - // Stop - const stopGroups = hooks.Stop as Array> | undefined; - if (stopGroups) { - const filtered = stopGroups.filter((g) => !g._nexpath_hook); - if (filtered.length < stopGroups.length) { - removed = true; - if (filtered.length === 0) delete hooks.Stop; - else hooks.Stop = filtered; - } - } - - if (!removed) return false; - data.hooks = hooks; - writeJson(filePath, data); - return true; -} +// Function definitions moved to src/agents/adapters/claude-code.ts in M1 Branch 2 +// (v0.1.3/m1/claude-code-refactor). Bodies are byte-identical to what lived +// here before; the import + re-export at the top of this file preserves the +// existing public API so callers importing from './install.js' are unaffected. // ── Claude Code CLI helpers ─────────────────────────────────────────────────── @@ -450,11 +339,17 @@ export async function installAction( } } // Register the advisory pipeline hook (separate from MCP — different file) - try { - writeHookEntry(paths.claudeSettings, homedir(), isWin ? 'win32' : process.platform); - console.log(`\u2713 ${'Claude Code'.padEnd(12)} \u2014 advisory hook written to ${paths.claudeSettings}`); - } catch (err) { - console.log(`\u26a0 ${'Claude Code'.padEnd(12)} \u2014 hook write failed: ${(err as Error).message}`); + const claudeAdapter = getAdapter('claude-code') as HookAdapter | undefined; + if (claudeAdapter) { + await claudeAdapter.install({ + home: homedir(), + cwd: process.cwd(), + yes: !!opts.yes, + dbPath, + // Pass paths.claudeSettings so tests that inject a custom paths + // object still control the target file independently of homedir(). + settingsPath: paths.claudeSettings, + }); } } else if (REGISTER_MCP_SERVER) { if (agent.type === 'cline') { @@ -476,6 +371,35 @@ export async function installAction( } } + // \u2500\u2500 Registry-driven adapter installs (M2+) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 + // VSCodeExtensionAdapters (cursor, windsurf) self-register via + // src/agents/index.ts side-effect imports. detectAll() asks each + // registered adapter if it should run on THIS machine; only those whose + // detect() returns true end up in the list. claude-code is excluded + // here because it's already handled in the legacy for-loop above + // (agent.type === 'claude-cli' branch). + // + // Snapshot invariant: in the install-snapshot test, HOME is stubbed to a + // tmp directory where ~/.config/Cursor and ~/.config/Windsurf don't + // exist, so cursor/windsurf detect() returns false, this block prints + // nothing, and the install-snapshot bytes stay byte-identical to the + // pre-B4 baseline. CI fails red on snapshot diff. + const adapterCtx: InstallContext = { + home: homedir(), + cwd: process.cwd(), + yes: !!opts.yes, + dbPath, + }; + const detectedAdapters = await detectAll(adapterCtx); + for (const adapter of detectedAdapters) { + if (adapter.id === 'claude-code') continue; + try { + await adapter.install(adapterCtx); + } catch (err) { + console.log(`\u2717 ${adapter.label.padEnd(12)} \u2014 failed: ${(err as Error).message}`); + } + } + console.log(''); console.log('Restart your agents to activate nexpath-prompt-store.'); console.log('Note: advisory pipeline (nexpath auto) auto-wired for Claude Code only.'); @@ -620,6 +544,28 @@ export async function uninstallAction( } } + // \u2500\u2500 Registry-driven adapter uninstalls (M2+) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 + // Mirror of the installAction's registry block. Skips claude-code (already + // handled in the legacy for-loop above) and calls each detected adapter's + // uninstall() so the user can cleanly back out of every adapter the install + // touched. Errors are surfaced as a single line per adapter \u2014 they don't + // halt the rest of the uninstall. + const adapterCtx: InstallContext = { + home: homedir(), + cwd: process.cwd(), + yes: false, + dbPath: ':memory:', + }; + const detectedAdapters = await detectAll(adapterCtx); + for (const adapter of detectedAdapters) { + if (adapter.id === 'claude-code') continue; + try { + await adapter.uninstall(adapterCtx); + } catch (err) { + console.log(`\u2717 ${adapter.label.padEnd(12)} \u2014 failed: ${(err as Error).message}`); + } + } + console.log(''); console.log('MCP registration removed from all agents.'); console.log('Prompt history retained at ~/.nexpath/prompt-store.db'); diff --git a/src/ext-vscode/.vscodeignore b/src/ext-vscode/.vscodeignore new file mode 100644 index 00000000..0383c7d4 --- /dev/null +++ b/src/ext-vscode/.vscodeignore @@ -0,0 +1,133 @@ +# Files NOT to include in the packaged .vsix. +# +# `vsce package` walks the sub-package directory and includes everything +# EXCEPT what matches a pattern here (or in the default ignore list). +# What we ship in the final .vsix is the minimum runtime surface: +# - out/extension.cjs (the esbuild bundle, ~33 KB — .cjs to bypass package.json type:module) +# - media/icon.svg +# - package.json (with the marketplace metadata) +# - README.md (user-facing) +# - LICENSE (mandatory for Open VSX) +# - node_modules/better-sqlite3/ (native dep — marked external by esbuild) +# +# Everything else is dev/test/build noise. + +# Source TypeScript — bundled by esbuild into out/, never imported at runtime +src/** +**/*.ts +**/*.ts.map +**/*.js.map + +# Test fixtures + dump artefacts — never used at runtime +test-fixtures/** + +# Build / runtime configs — needed only at dev time +tsconfig.json +esbuild.config.mjs +vitest.config.ts +.vscode/** +.editorconfig +.eslintrc* +.eslintignore +.prettierrc* + +# Test infrastructure +**/*.test.ts +**/*.test.js +**/__snapshots__/** + +# Dev scripts — engineer-only, not needed at runtime +scripts/** + +# Internal docs — public README is the only doc that ships +SMOKE-TEST.md +PUBLISH.md +CROSS-OS-VERIFY.md +CHANGELOG-internal.md + +# Build outputs that should NOT ship +dist-vsix/** +*.vsix +out/**/*.map + +# Source-control / CI / OS junk +.git/ +.gitignore +.gitattributes +.github/** +.travis.yml +.circleci/** +.DS_Store +Thumbs.db + +# Lockfile — vsce defaults to ignoring this; explicit for clarity +package-lock.json +yarn.lock +pnpm-lock.yaml + +# ──────────────────────────────────────────────────────────────────────────── +# Native-module slimming — strip the parts of better-sqlite3 that aren't +# needed at runtime. The extension only loads +# node_modules/better-sqlite3/lib/index.js → build/Release/better_sqlite3.node +# Everything else (C++ source, SQLite amalgamation, node-gyp intermediates) +# is build-time only and ships ~25 MB of dead weight if not excluded. +# ──────────────────────────────────────────────────────────────────────────── + +# C++ source + SQLite amalgamation (huge, source-build only) +node_modules/better-sqlite3/src/** +node_modules/better-sqlite3/deps/** +node_modules/better-sqlite3/binding.gyp + +# node-gyp intermediate files — keep only the final .node binary +node_modules/better-sqlite3/build/config.gypi +node_modules/better-sqlite3/build/Makefile +node_modules/better-sqlite3/build/binding.Makefile +node_modules/better-sqlite3/build/better_sqlite3.target.mk +node_modules/better-sqlite3/build/sqlite3.target.mk +node_modules/better-sqlite3/build/test_extension.target.mk +node_modules/better-sqlite3/build/Release/.deps/** +node_modules/better-sqlite3/build/Release/obj/** +node_modules/better-sqlite3/build/Release/obj.target/** +node_modules/better-sqlite3/build/Release/test_extension.node + +# prebuild-install + its transitive deps — only used at install time. +# Without these the vsce package step would still pass, but the user's +# Cursor never runs `npm install` after install, so they're dead weight. +node_modules/prebuild-install/** +node_modules/detect-libc/** +node_modules/expand-template/** +node_modules/github-from-package/** +node_modules/mkdirp-classic/** +node_modules/napi-build-utils/** +node_modules/node-abi/** +node_modules/pump/** +node_modules/simple-get/** +node_modules/tar-fs/** +node_modules/tar-stream/** +node_modules/tunnel-agent/** +node_modules/decompress-response/** +node_modules/mimic-response/** +node_modules/rc/** +node_modules/deep-extend/** +node_modules/ini/** +node_modules/minimist/** +node_modules/strip-json-comments/** +node_modules/end-of-stream/** +node_modules/once/** +node_modules/wrappy/** +node_modules/chownr/** +node_modules/fs-constants/** +node_modules/buffer/** +node_modules/base64-js/** +node_modules/ieee754/** +node_modules/bl/** +node_modules/readable-stream/** +node_modules/safe-buffer/** +node_modules/inherits/** +node_modules/string_decoder/** +node_modules/util-deprecate/** +node_modules/simple-concat/** + +# bindings + file-uri-to-path: KEEP — these ARE required at runtime by +# better-sqlite3's lib/index.js to locate the .node file. +# Exception, so explicitly NOT excluded above. diff --git a/src/ext-vscode/CROSS-OS-VERIFY.md b/src/ext-vscode/CROSS-OS-VERIFY.md new file mode 100644 index 00000000..054e200c --- /dev/null +++ b/src/ext-vscode/CROSS-OS-VERIFY.md @@ -0,0 +1,145 @@ +# Cross-OS Verification — M2 Branch 6 + +> Per dev plan §3.0 Branch 6 acceptance: +> *"Published `.vsix` installs cleanly on Cursor + Windsurf on macOS and Windows; end-to-end works on both."* +> +> This is a **manual** test the engineer runs on each target OS. Linux is implicitly the primary OS (covered by B5's smoke test on the dev machine). This doc adds macOS + Windows. + +--- + +## Prerequisites + +| Item | Where | +|---|---| +| Built `.vsix` for the target OS | Either downloaded from a CI artefact run (`.github/workflows/publish-extension.yml`) or built on the target OS via `npm run package` | +| Cursor installed on the target OS | | +| Windsurf installed on the target OS | | +| `OPENAI_API_KEY` available | Either as env var or in a `.env` in the test workspace | +| `nexpath` CLI installed and on PATH | `npm install -g /nexpath` from a clone, or whichever distribution channel applies | + +--- + +## The verification matrix + +Run the procedure below for **every cell** in this matrix: + +| OS | Host | +|---|---| +| macOS (Intel) | Cursor | +| macOS (Intel) | Windsurf | +| macOS (Apple Silicon) | Cursor | +| macOS (Apple Silicon) | Windsurf | +| Windows 11 x64 | Cursor | +| Windows 11 x64 | Windsurf | + +(Linux x64 is covered by the primary smoke test in `SMOKE-TEST.md`. Linux arm64 / Windows 11 arm64 are nice-to-haves; flag if either's `.vsix` install or activation fails.) + +--- + +## Per-OS verification procedure + +### Step 0 — Capture the environment + +Run this first and paste the output into the verification table at the bottom. + +```bash +# macOS / Linux +echo "OS: $(uname -s) $(uname -r) $(uname -m)" +echo "Cursor: $(cursor --version 2>/dev/null | head -n1)" +echo "Windsurf: $(windsurf --version 2>/dev/null | head -n1)" +echo "Node: $(node -v)" +``` + +```powershell +# Windows PowerShell +echo "OS: $((Get-CimInstance Win32_OperatingSystem).Caption) $((Get-CimInstance Win32_OperatingSystem).Version) $($env:PROCESSOR_ARCHITECTURE)" +echo "Cursor: $(cursor --version | Select-Object -First 1)" +echo "Windsurf: $(windsurf --version | Select-Object -First 1)" +echo "Node: $(node -v)" +``` + +### Step 1 — Install the .vsix + +```bash +# Identify the right .vsix for THIS host: +# macOS Intel → nexpath-vscode-darwin-x64-.vsix +# macOS Apple Si → nexpath-vscode-darwin-arm64-.vsix +# Windows x64 → nexpath-vscode-win32-x64-.vsix +# Linux x64 → nexpath-vscode-linux-x64-.vsix +# Linux ARM → nexpath-vscode-linux-arm64-.vsix + +# Install into Cursor +cursor --install-extension ./nexpath-vscode--.vsix +# OR via the IDE: Ctrl+Shift+P → "Extensions: Install from VSIX..." → pick the file + +# Install into Windsurf (different CLI command on some versions) +windsurf --install-extension ./nexpath-vscode--.vsix + +# Restart both hosts so onStartupFinished fires. +``` + +### Step 2 — Verify activation + +In each host (Cursor + Windsurf), open Developer Tools (`Help → Toggle Developer Tools`) and look for: + +``` +[nexpath] extension activated +[nexpath] watcher started on N state.vscdb file(s) for host= +``` + +If the second line shows `host=vscode-generic`, the host-detector didn't recognise the appName — file an issue with `vscode.env.appName`'s exact value. + +### Step 3 — OS-specific gotchas + +| OS | Gotcha | What to do | +|---|---|---| +| **macOS** | First-launch Full Disk Access prompt | The onboarding fires a second toast on macOS pointing to System Settings → Privacy & Security → Full Disk Access. Click "Open System Settings" and grant access to Cursor / Windsurf. Until granted, the watcher can't read `~/Library/Application Support//User/workspaceStorage/*/state.vscdb`. | +| **macOS** | Gatekeeper blocks the .vsix's native binding | If `better-sqlite3.node` was downloaded from npm registry (signed), no issue. If you built locally without signing, run `xattr -d com.apple.quarantine node_modules/better-sqlite3/build/Release/better_sqlite3.node` after install. | +| **Windows** | SmartScreen / antivirus quarantines the .node binding | If the extension activates but the watcher logs "module not found: better-sqlite3", check Defender / your AV's quarantine folder. Allow the .node file. | +| **Windows** | Long path issue under `%APPDATA%\Cursor\User\workspaceStorage\\state.vscdb` | Windows path length caps at 260 chars by default. Modern Windows 10/11 supports long paths if the registry key is set. If `enumerateStateVscdbPaths` returns `[]` but the paths exist, this is likely the cause. | +| **All** | Workspace not opened | Each host stores chat history per workspace. If the user hasn't opened a folder yet, no state.vscdb exists. The extension logs `no workspace state.vscdb found` — open at least one folder and reload the window. | + +### Step 4 — Run the SMOKE-TEST flow + +Follow the same procedure documented in `SMOKE-TEST.md` steps 4-6, then paste the verification table from that doc back as evidence. + +### Step 5 — Cleanup between matrix cells + +To start fresh for the next host on the same OS: + +```bash +cursor --uninstall-extension emptyops.nexpath-vscode +windsurf --uninstall-extension emptyops.nexpath-vscode +# Optional: clear globalState so the consent toast re-prompts +rm -rf ~/.config/Cursor/User/globalStorage/emptyops.nexpath-vscode/ +# (or the equivalent path on macOS / Windows) +``` + +--- + +## Final verification table + +After running the procedure on each cell, fill in the table and attach as B6 acceptance evidence in the PR description. + +| OS / Arch | Host | Step 1 Install | Step 2 Activate | Step 3 OS gotchas resolved | Step 4 Smoke test passed | Notes | +|---|---|---|---|---|---|---| +| macOS Intel | Cursor | ✅ / ❌ | ✅ / ❌ | ✅ / ❌ (list any) | ✅ / ❌ | | +| macOS Intel | Windsurf | ✅ / ❌ | ✅ / ❌ | ✅ / ❌ | ✅ / ❌ | | +| macOS Apple Silicon | Cursor | ✅ / ❌ | ✅ / ❌ | ✅ / ❌ | ✅ / ❌ | | +| macOS Apple Silicon | Windsurf | ✅ / ❌ | ✅ / ❌ | ✅ / ❌ | ✅ / ❌ | | +| Windows 11 x64 | Cursor | ✅ / ❌ | ✅ / ❌ | ✅ / ❌ | ✅ / ❌ | | +| Windows 11 x64 | Windsurf | ✅ / ❌ | ✅ / ❌ | ✅ / ❌ | ✅ / ❌ | | + +**M2 Branch 6 is complete when every cell in the matrix shows ✅ for Steps 1, 2, and 4.** Step 3 entries should list any OS-specific gotcha you encountered (even if you resolved it) so future engineers don't have to rediscover. + +--- + +## When something doesn't work + +| Symptom | Likely cause | Fix | +|---|---|---| +| `.vsix` install fails immediately with "incompatible with current platform" | Wrong .vsix for the OS — VS Code's marketplace ID-binding rejects platform-mismatched packages | Re-download the right one (see Step 1's target table). | +| `.vsix` installs but activation crashes | better-sqlite3 native binding mismatch (e.g. arm64 binary on x64 host) | Reinstall the right .vsix; check `node_modules/better-sqlite3/build/Release/better_sqlite3.node` inside the unzipped .vsix matches the host arch. | +| Activation log shows `host=vscode-generic` instead of `cursor` / `windsurf` | `vscode.env.appName` returned something the host-detector doesn't recognise (e.g. a new fork) | File an issue with the exact appName value; the host-detector pattern can be extended. | +| Watcher starts but never fires events | Either no state.vscdb under workspaceStorage (open a folder), OR the host writes to a non-VS-Code-style location (file an issue + share a `dump-cursor-state.ts` output) | See `SMOKE-TEST.md` troubleshooting. | +| Webview never reveals after a prompt | `nexpath stop` returned null — Layer C decided no advisory needed for that prompt, OR `OPENAI_API_KEY` not set | Try a prompt known to trigger an advisory (something with `delete`, `force`, etc.). Check the prompt-store DB. | diff --git a/src/ext-vscode/LICENSE b/src/ext-vscode/LICENSE new file mode 100644 index 00000000..261eeb9e --- /dev/null +++ b/src/ext-vscode/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/src/ext-vscode/PUBLISH.md b/src/ext-vscode/PUBLISH.md new file mode 100644 index 00000000..f8cf7c62 --- /dev/null +++ b/src/ext-vscode/PUBLISH.md @@ -0,0 +1,195 @@ +# Publishing the Nexpath VS Code Extension + +> Engineer's step-by-step procedure for publishing the extension to **Open VSX** (Cursor / Windsurf default registry) and **VS Code Marketplace**. Run this each time you bump the version. + +--- + +## One-time setup (per developer / per machine) + +### 1. Marketplace publisher accounts + +The extension publishes under the publisher id `emptyops` (matches `package.json#publisher`) — the org-scoped namespace per dev plan §6 Q3 resolution (revision 15). The full marketplace ID is `emptyops.nexpath-vscode`. You need credentials for both registries: + +| Registry | Account | Token | +|---|---|---| +| **VS Code Marketplace** | Azure DevOps org with the `emptyops` publisher | Personal Access Token (PAT) with `Marketplace: Manage` scope. [Docs](https://code.visualstudio.com/api/working-with-extensions/publishing-extension#get-a-personal-access-token). | +| **Open VSX** | account in the `emptyops` namespace | Access token from the user's profile page. [Docs](https://github.com/eclipse/openvsx/wiki/Publishing-Extensions). | + +> **First-publish prerequisite — claim the publisher namespace.** Visit each marketplace and confirm that `emptyops` is either already owned by your account or available to claim. If `emptyops` is taken on either side, talk to the team lead before publishing — the choice was deliberately org-scoped to make collision unlikely, but verify before the first push. + +### 2. Export the tokens + +```bash +export VSCE_PAT="" +export OVSX_PAT="" +``` + +**Do NOT commit these.** For CI publishing, set them as GitHub Actions repository secrets (see `.github/workflows/publish-extension.yml`). + +### 3. Install the publishing tools + +`@vscode/vsce` and `ovsx` are already devDependencies. Run `npm install` in `src/ext-vscode/` to get them. + +--- + +## Version-bump checklist + +Before each publish: + +- [ ] **Bump the version** in `src/ext-vscode/package.json` following semver. +- [ ] **Update the `nexpath` repo's CHANGELOG** (in `src/ext-vscode/CHANGELOG.md` — create if missing) with the user-facing changes since the last release. +- [ ] **Tag the matching `nexpath` repo commit** with `vsix-v` after the publish succeeds. +- [ ] **Verify the README.md is up-to-date** — it's shown verbatim on both marketplaces. +- [ ] **Run the full local suite** to make sure tests pass: + ```bash + cd src/ext-vscode/ + npm test + npm run typecheck + ``` + +--- + +## Publishing — local path (single platform) + +Useful for quick iteration, hot-fix releases, or testing the package locally before committing to a full multi-platform publish. + +```bash +cd src/ext-vscode/ +npm install + +# 1. Build the extension bundle +npm run build + +# 2. Package for the CURRENT host's platform. +# Produces ./nexpath-vscode--.vsix where is +# auto-detected from process.platform + process.arch. +npm run package + +# 3. Install locally to verify +cursor --install-extension nexpath-vscode--.vsix +# Restart Cursor. Run the SMOKE-TEST.md procedure to confirm it works. + +# 4. Publish (only when local install + smoke test pass) +npm run publish:vsce # → VS Code Marketplace +npm run publish:ovsx # → Open VSX +``` + +Each `publish:*` reads its respective `*_PAT` env var. + +--- + +## Pre-merge CI dry-run (recommended before opening the B6 PR) + +Before merging Branch 6, run the matrix workflow once with publishing disabled — confirms every platform's `.vsix` builds cleanly without touching either marketplace. Fast and free. + +```bash +# 1. Push the branch to your fork so GitHub Actions can see it. +git push -u origin v0.1.3/m2/publish-and-cross-os-verify + +# 2. Trigger the workflow manually with publish=false. +gh workflow run "Publish Nexpath VS Code Extension" \ + --ref v0.1.3/m2/publish-and-cross-os-verify \ + -f publish=false + +# 3. Watch the run. +gh run watch +``` + +What to check on the run: + +| Job | Expected | +|---|---| +| `Package (linux-x64)` | green, uploads `vsix-linux-x64` artefact | +| `Package (darwin-x64)` | green, uploads `vsix-darwin-x64` artefact | +| `Package (darwin-arm64)` | green, uploads `vsix-darwin-arm64` artefact | +| `Package (win32-x64)` | green, uploads `vsix-win32-x64` artefact | +| `Package (linux-arm64)` | green (slow — QEMU emulation, expect ~10–15 min) | +| `Publish to marketplaces` | **skipped** (gated on `publish=true` or tag push) | + +Download all 5 artefacts and skim each .vsix's manifest with `vsce ls --tree --packagePath ./.vsix` to confirm the right native binding landed for each target. + +Only after this dry-run is green should the engineer: +1. Open the B6 PR with the matrix link pasted as evidence. +2. Run the real publish flow below. + +--- + +## Publishing — multi-platform path (CI-equivalent) + +For an actual release, every supported platform needs its own `.vsix` because `better-sqlite3` is a native module. Five targets: + +| Target | OS / arch | +|---|---| +| `linux-x64` | Linux x86_64 (most servers + dev boxes) | +| `linux-arm64` | Linux aarch64 (ARM servers, M1 Linux VMs) | +| `darwin-x64` | macOS Intel | +| `darwin-arm64` | macOS Apple Silicon | +| `win32-x64` | Windows x86_64 | + +### The packaging step has to run on the matching OS + +`better-sqlite3` ships per-platform prebuilds. When you run `npm install` on a macOS-arm64 box, only the `darwin-arm64` binary lands in `node_modules/`. To produce a `.vsix` for Windows, you need to install + package on a Windows machine (or set up cross-compilation, which is fiddly). + +**Recommended path:** use the CI workflow at [.github/workflows/publish-extension.yml](../../.github/workflows/publish-extension.yml). It runs a matrix on `macos-latest` / `ubuntu-latest` / `windows-latest` runners — each produces the right `.vsix` automatically. + +**Manual path (if you have access to each OS):** + +```bash +cd src/ext-vscode/ + +# On each target OS: +rm -rf node_modules dist-vsix +npm install # pulls the right better-sqlite3 prebuild +npm run package:all-platforms -- --targets # packages just one target +# → dist-vsix/nexpath-vscode--.vsix + +# Move the resulting .vsix off the machine to a single publishing host. + +# Then on the publishing host, with all platform .vsix files in ./dist-vsix/: +for f in dist-vsix/nexpath-vscode-*-${VERSION}.vsix ; do + npx vsce publish --packagePath "$f" + npx ovsx publish "$f" +done +``` + +--- + +## Post-publish verification + +After publish succeeds: + +1. **Marketplace listings live** — visit the URLs and confirm the version appears: + - + - +2. **Install from each marketplace** (using a fresh Cursor profile, e.g. `cursor --user-data-dir /tmp/fresh-profile`) and confirm: + - Extension shows up in the Extensions panel. + - Activity bar icon appears after activation. + - First-launch consent toast fires. +3. **Run the full smoke test** per `SMOKE-TEST.md` against the marketplace-installed version (not the local `.vsix`). +4. **Tag the release** in the `nexpath` repo: + ```bash + git tag -a "vsix-v$VERSION" -m "Nexpath VS Code extension v$VERSION" + git push --tags + ``` + +--- + +## Rollback + +If a published version turns out to be broken: + +1. **Open VSX:** there's no "unpublish" endpoint — bump the patch version and publish a fix. The previous version stays listed but new installs get the fix. +2. **VS Code Marketplace:** same — bump + republish. Use `vsce unpublish emptyops.nexpath-vscode@` only as a last resort (causes marketplace to flag the publisher). + +For both: keep `package.json#version` always strictly ahead of the latest published version. + +--- + +## Marketplace review queues + +| Registry | Typical SLA | +|---|---| +| Open VSX | Minutes (no human review) | +| VS Code Marketplace | Usually minutes; can spike to hours on first-publish or after extension flag | + +If you tag a release before all marketplaces show the new version, the tag is fine — just note in the release notes that "marketplace ingestion may lag by N hours." diff --git a/src/ext-vscode/README.md b/src/ext-vscode/README.md new file mode 100644 index 00000000..9380a100 --- /dev/null +++ b/src/ext-vscode/README.md @@ -0,0 +1,31 @@ +# Nexpath VS Code Extension + +Companion extension that wires Cursor and Windsurf into the Nexpath prompt-guidance pipeline. + +This sub-package lives at `nexpath/src/ext-vscode/`. It builds independently of the main CLI (via esbuild) and ships to Open VSX + the VS Code Marketplace. + +## Status + +Milestone M2 Branch 1 (`v0.1.3/m2/extension-skeleton`) — skeleton + IPC stub + first-launch onboarding + activity-bar icon. Chat-history watcher (M2/M3), webview UI (M6–M8), and the Cursor/Windsurf adapters (M9/M10) land in subsequent branches. + +## Build + +```bash +cd src/ext-vscode/ +npm install +npm run build # bundles src/extension.ts -> out/extension.js (CJS for VS Code host) +npm run watch # rebuild on save +``` + +## Test + +Unit tests live next to their source files (`*.test.ts`) and are picked up by the root repo's `npm test` (vitest). They mock the `vscode` module via `vi.mock('vscode', ...)`. + +## Module Layout + +| File | Module (per dev plan §3 M2 §2.2) | +|---|---| +| `package.json`, `tsconfig.json`, `esbuild.config.mjs`, `src/extension.ts` | M1 — skeleton | +| `src/ipc.ts` | M5 — IPC to nexpath CLI | +| `src/onboarding.ts` | M11 — first-launch consent + macOS Full-Disk-Access guidance | +| `media/icon.svg` | M12 — activity-bar icon | diff --git a/src/ext-vscode/SMOKE-TEST.md b/src/ext-vscode/SMOKE-TEST.md new file mode 100644 index 00000000..bd440c76 --- /dev/null +++ b/src/ext-vscode/SMOKE-TEST.md @@ -0,0 +1,230 @@ +# Manual Smoke Test — M2 Branch 5 + +> **Purpose:** verify the full Branch 1 → 4 wiring works against a real Cursor (or Windsurf) install. This is the acceptance gate for Milestone M2 Branch 5 per dev plan §3.0: +> +> *"End-to-end on dev machine: type real prompt in Cursor → real round-trip → decision UI appears."* + +The unit tests verify each component in isolation. This document walks through verifying they work **together** in a live host. + +--- + +## Prerequisites + +| Item | Version / state | +|---|---| +| Node.js | ≥ 18 | +| Cursor or Windsurf | Installed and launchable (config dir present under `~/.config/Cursor` on Linux, etc.) | +| `nexpath` repo | Working tree on `v0.1.3/m2/smoke-test` (or any later stacked branch) | +| OpenAI API key | Set in `OPENAI_API_KEY` env var — required by Layer C for advisory generation | +| `vsce` CLI (optional) | For packaging the extension as a `.vsix` (`npm install -g @vscode/vsce`) | + +--- + +## Step 1 — Build the extension bundle + sub-package deps + +```bash +cd ~/Documents/Vedanshi/NexPathMain/reviewduel/nexpath/src/ext-vscode +npm install +npm run build +``` + +**Expected output:** +- `node_modules/` populated (includes `better-sqlite3` with prebuilt platform binary) +- `out/extension.js` produced (~15 KB) +- No tsc errors + +--- + +## Step 2 — Install nexpath CLI hook (Claude Code path, already verified) + register the extension's host + +```bash +cd ~/Documents/Vedanshi/NexPathMain/reviewduel/nexpath +npm run build +node dist/cli/index.js install --yes +``` + +**Expected output includes:** +``` +Detected: Claude Code +✓ Claude Code — advisory hook written to /.claude/settings.json +✓ Cursor — install the Nexpath extension to activate guidance: + Open VSX: https://open-vsx.org/extension/emptyops/nexpath-vscode + VS Code Marketplace: https://marketplace.visualstudio.com/items?itemName=emptyops.nexpath-vscode + Or via CLI: cursor --install-extension emptyops.nexpath-vscode +``` + +(If Windsurf is also installed, a Windsurf block appears too.) + +--- + +## Step 3 — Install the extension into Cursor + +Two paths. + +### Path A — Extension Development Host (fastest for iteration) + +```bash +cd ~/Documents/Vedanshi/NexPathMain/reviewduel/nexpath/src/ext-vscode +code . # or `cursor .` +# Then press F5 — opens a new VS Code/Cursor window with the extension loaded. +``` + +### Path B — Packaged `.vsix` install (closer to real distribution) + +```bash +cd ~/Documents/Vedanshi/NexPathMain/reviewduel/nexpath/src/ext-vscode +vsce package +# Produces nexpath-vscode-0.1.3.vsix in this directory. + +cursor --install-extension nexpath-vscode-0.1.3.vsix +# Or in Cursor: Ctrl+Shift+P → "Extensions: Install from VSIX..." → pick the .vsix +``` + +Restart Cursor so the extension's `onStartupFinished` activation event fires. + +--- + +## Step 4 — Verify activation + onboarding + +After Cursor restarts: + +1. **Open the developer console** (`Help → Toggle Developer Tools`). +2. **Look for the activation log:** + ``` + [nexpath] extension activated + ``` +3. **First-launch toast** should appear: + > Allow Nexpath to read your AI chat history for prompt-level guidance? Data stays on your machine. + > [Allow] [Not now] + + Click **Allow**. (If you click "Not now", the watcher won't start — re-running activation requires clearing `globalState` or reopening with a fresh user profile.) + +4. **Activity bar** should show the Nexpath icon (the Y-fork). Click it. The sidebar panel should open with: + > Nexpath is active and watching for AI chat prompts. + +5. **Console should log** (one line — copy it for the verification table at the bottom): + ``` + [nexpath] watcher started on N state.vscdb file(s) for host=cursor + ``` + + If you see `[nexpath] consent not granted — watcher not started`, repeat step 3 of this document but click **Allow**. + + If you see `[nexpath] no workspace state.vscdb found under …`, open at least one workspace folder in Cursor (e.g., this nexpath repo itself) and reload the extension (`Ctrl+Shift+P` → "Developer: Reload Window"). + +--- + +## Step 5 — Trigger the round-trip + +> ### ⚠️ Use Ask mode (`Ctrl+L`) ONLY for these tests, NOT Agent/Composer mode +> +> Cursor's Agent / Composer mode can EXECUTE system commands. If you type a destructive prompt in those modes and Nexpath's advisory pipeline isn't fully wired up yet, Cursor's Agent may actually perform the destructive action before you see the warning. **Always test from Ask mode** — it's read-only and answers questions but doesn't execute commands. +> +> Equally important: every "hazard test" prompt below is written as an **information-retrieval question** that contains hazard keywords (so Layer C's classifier flags it) but does NOTHING harmful even if executed. + +In the Cursor window with the extension loaded: + +1. Open the AI chat: press **`Ctrl+L`** (Ask mode). Verify the chat panel shows the "Ask" label — NOT "Composer" or "Agent". +2. Type one of these safe hazard-trigger prompts: + > `explain why "rm -rf ~/Downloads/*" is dangerous` + + Or: + > `what are the risks of "git push --force" to main` + + Or: + > `what does "DROP TABLE users" do and how do databases prevent accidents` +3. Press Enter, wait for Cursor's normal response to finish. + +**Expected behaviour:** + +- Within ~250 ms of Cursor writing your prompt to `state.vscdb`, the watcher fires. +- `nexpath auto` is spawned with the prompt + a session ID like `|tab:`. +- `nexpath stop` is spawned immediately after; Layer C produces a `DecisionSessionPayload`. +- The Nexpath activity-bar panel **auto-reveals** with: + - Your advisory text in a styled box. + - A numbered list of "Suggested alternatives" — click one OR press the matching number key (`1`–`9`). + - A "Dismiss" link (or press `Esc` / `Ctrl+X`). + +**On click:** `handleOptionSelection` runs. Since the candidate Cursor command IDs in `chat-input-injector.ts` are unverified, you should expect the **clipboard fallback** path: + +> Nexpath: pasted to clipboard — paste into the chat input to use it. + +Paste into Cursor's chat input and re-send — that's the manual workaround until Step 6 verifies the direct-injection IDs. + +--- + +## Step 6 — Verify (or correct) the chat-input command IDs + +The `chat-input-injector.ts` file ships with a list of HEURISTIC GUESSES for Cursor / Windsurf chat-input commands. Until B5 verifies them, the injection always falls through to the clipboard. To find the real command IDs: + +1. In the running Cursor / Windsurf, open the developer console (`Help → Toggle Developer Tools`). +2. In the console, run: + ```js + await vscode.commands.getCommands(true) + // Filter for chat-related candidates: + // .filter(c => /chat|aichat|compose|cursor.*input|prompt/i.test(c)) + ``` + + **Note:** the `vscode` global is only available inside extension code, not in the dev tools' window. Use the extension's debug console (`Ctrl+Shift+Y` in the extension-host launch) or add a temporary `console.log` in `extension.ts.activate()`: + ```ts + const allCommands = await vscode.commands.getCommands(true); + console.log(allCommands.filter(c => /chat|compose|aichat/i.test(c))); + ``` +3. Identify the command(s) that write text to the chat input. Common candidates to investigate first: + - `composer.newChat` + - `aichat.insertWithSelection` + - `cursor.composer.focus` + - `cursor.aichat.insertSelection` + - Anything matching `*ChatInput*` or `*PromptInput*` +4. Test one in the dev console: + ```js + await vscode.commands.executeCommand('', 'test text') + ``` + If the text lands in the chat input → that's the right command. +5. **Update** `src/ext-vscode/src/chat-input-injector.ts`'s `CURSOR_CANDIDATE_COMMANDS` / `WINDSURF_CANDIDATE_COMMANDS` list — put the verified IDs at the top of the array. +6. Re-build the extension (`npm run build`), reload Cursor, re-test step 5. + +When this works, clicking an option in the webview should write the option text **directly into Cursor's chat input** instead of just the clipboard. That's the final acceptance gate for B5. + +--- + +## Step 7 — Verification table (fill in + share) + +After completing steps 1-5, paste the following table back with your observed values. This is the B5 acceptance evidence. + +| Step | Observation | +|---|---| +| 4.2 | `[nexpath] extension activated` log line: YES / NO | +| 4.3 | Consent toast appeared and Allow was clicked: YES / NO | +| 4.4 | Nexpath icon visible in activity bar: YES / NO | +| 4.5 | Watcher start log line + which host: paste here | +| 5 (auto fired) | `nexpath auto` invoked (check Cursor's process tree or strace `node`): YES / NO | +| 5 (stop fired) | `nexpath stop` invoked: YES / NO | +| 5 (panel revealed) | Decision-session UI auto-revealed in activity bar panel: YES / NO | +| 5 (advisory rendered) | Advisory text + numbered options visible: YES / NO | +| 5 (click option) | Click on an option triggers the clipboard fallback toast: YES / NO | +| 6 (command IDs) | Discovered command IDs for direct chat-input injection: list here | +| 6 (direct inject works) | After updating the candidate list, click writes directly to chat input: YES / NO | + +If any **NO** appears, log it as an issue with reproduction steps. **Step 6's direct-inject result is the final B5 gate** — when it's YES, M2 Branch 5 is done. + +--- + +## Troubleshooting + +| Symptom | Likely cause + fix | +|---|---| +| `[nexpath] consent not granted` | Click "Allow" on the first-launch toast. If the toast doesn't reappear, the user explicitly denied — clear globalState via `Ctrl+Shift+P` → `Developer: Reload Window With Extensions Disabled`, then re-open. | +| `[nexpath] no workspace state.vscdb found` | Open at least one folder in Cursor (`File → Open Folder`) and reload the window. | +| Watcher fires but nothing is rendered | `nexpath stop` returned `null` — Layer C decided no advisory is needed for this prompt. Try a prompt that's known to trigger an advisory (something with `delete`, `force`, etc.). Or, if there's no `.env` with `OPENAI_API_KEY` in the workspace, Stage 2 may silently no-op. | +| `Error: Cannot find module 'better-sqlite3'` | Run `npm install` inside `src/ext-vscode/`. If the prebuilt binary fails on your platform, `npm install --build-from-source` (needs `node-gyp`). | +| Schema-unknown toast appears | Cursor schema isn't recognised by any of the extractors. Capture a dump via `npx tsx scripts/dump-cursor-state.ts --name cursor-debug --redact` and share — see dev plan §2.5. | +| Extension activates but webview never reveals | Check the developer console for errors. Specifically: spawn-failures (nexpath CLI not on PATH), or `viewProvider undefined`. | +| `nexpath auto` runs but no advisory appears later | `OPENAI_API_KEY` not set in the workspace `.env`, or the prompt didn't trigger Stage 2. Check `~/.nexpath/prompt-store.db` for the captured prompt: `sqlite3 ~/.nexpath/prompt-store.db "SELECT prompt_text FROM prompts ORDER BY id DESC LIMIT 3;"` | + +--- + +## What this test does NOT cover + +- **Cross-OS behaviour:** macOS + Windows verification is **Branch 6** scope. B5 is "primary OS only" per the dev plan. +- **Multi-workspace concurrency:** opening two Cursor windows on different workspaces simultaneously. The session-id composer prefixes with workspace path, so they should be independent, but B5 doesn't formally test this. +- **Pre-prompt blocking:** the current capture is post-send (Cursor has already submitted the prompt by the time the watcher sees it). Pre-send blocking is a deferred open question in architecture doc §11 / dev plan §7. diff --git a/src/ext-vscode/esbuild.config.mjs b/src/ext-vscode/esbuild.config.mjs new file mode 100644 index 00000000..a0253d66 --- /dev/null +++ b/src/ext-vscode/esbuild.config.mjs @@ -0,0 +1,36 @@ +import { build, context } from 'esbuild'; + +const watch = process.argv.includes('--watch'); + +const config = { + entryPoints: ['src/extension.ts'], + bundle: true, + // `.cjs` extension is load-bearing: package.json#type is "module" (so the + // source-side .ts imports use ESM resolution), but esbuild bundles the + // extension entry as CommonJS — which is what VS Code's extension host + // requires (it `require()`s the main entry). Naming the output .cjs tells + // Node to use the CJS loader regardless of the package's type field; + // naming it .js would trigger ERR_REQUIRE_ESM and silently break activation. + // Locked by src/package-main-format.test.ts. + outfile: 'out/extension.cjs', + platform: 'node', + target: 'node18', + format: 'cjs', + // `vscode` is provided by the host. `better-sqlite3` is a native module + // (.node bindings) that esbuild cannot bundle; node_modules/better-sqlite3 + // ships in the .vsix and is loaded via dynamic import at runtime so the + // native load only hits on first chat-history read. + external: ['vscode', 'better-sqlite3'], + sourcemap: true, + minify: false, + logLevel: 'info', +}; + +if (watch) { + const ctx = await context(config); + await ctx.watch(); + console.log('[esbuild] watching src/extension.ts...'); +} else { + await build(config); + console.log('[esbuild] built out/extension.cjs'); +} diff --git a/src/ext-vscode/media/icon-marketplace.svg b/src/ext-vscode/media/icon-marketplace.svg new file mode 100644 index 00000000..8a47639b --- /dev/null +++ b/src/ext-vscode/media/icon-marketplace.svg @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/src/ext-vscode/media/icon.png b/src/ext-vscode/media/icon.png new file mode 100644 index 00000000..c048f97d Binary files /dev/null and b/src/ext-vscode/media/icon.png differ diff --git a/src/ext-vscode/media/icon.svg b/src/ext-vscode/media/icon.svg new file mode 100644 index 00000000..89bf2f70 --- /dev/null +++ b/src/ext-vscode/media/icon.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/ext-vscode/package-lock.json b/src/ext-vscode/package-lock.json new file mode 100644 index 00000000..c2fefe60 --- /dev/null +++ b/src/ext-vscode/package-lock.json @@ -0,0 +1,8030 @@ +{ + "name": "nexpath-vscode", + "version": "0.1.3", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "nexpath-vscode", + "version": "0.1.3", + "license": "Apache-2.0", + "dependencies": { + "better-sqlite3": "^12" + }, + "devDependencies": { + "@electron/rebuild": "^3.7.2", + "@types/better-sqlite3": "^7", + "@types/node": "^20", + "@types/vscode": "^1.80.0", + "@vscode/vsce": "^3", + "esbuild": "^0.21", + "ovsx": "^0.10", + "tsx": "^4", + "typescript": "^5", + "vitest": "^2" + }, + "engines": { + "node": ">=18", + "vscode": "^1.80.0" + } + }, + "node_modules/@azu/format-text": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@azu/format-text/-/format-text-1.0.2.tgz", + "integrity": "sha512-Swi4N7Edy1Eqq82GxgEECXSSLyn6GOb5htRFPzBDdUkECGXtlf12ynO5oJSpWKPwCaUssOu7NfhDcCWpIC6Ywg==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@azu/style-format": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@azu/style-format/-/style-format-1.0.1.tgz", + "integrity": "sha512-AHcTojlNBdD/3/KxIKlg8sxIWHfOtQszLvOpagLTO+bjC3u7SAszu1lf//u7JJC50aUSH+BVWDD/KvaA6Gfn5g==", + "dev": true, + "license": "WTFPL", + "dependencies": { + "@azu/format-text": "^1.0.1" + } + }, + "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.23.0", + "resolved": "https://registry.npmjs.org/@azure/core-rest-pipeline/-/core-rest-pipeline-1.23.0.tgz", + "integrity": "sha512-Evs1INHo+jUjwHi1T6SG6Ua/LHOQBCLuKEEE6efIpt4ZOoNonaT1kP32GoOcdNDbfqsD2445CPri3MubBy5DEQ==", + "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.4", + "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.1", + "resolved": "https://registry.npmjs.org/@azure/identity/-/identity-4.13.1.tgz", + "integrity": "sha512-5C/2WD5Vb1lHnZS16dNQRPMjN6oV/Upba+C9nBIs15PmOi6A3ZGs4Lr2u60zw4S04gi+u3cEXiqTVP7M4Pz3kw==", + "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": "^5.5.0", + "@azure/msal-node": "^5.1.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": "5.10.1", + "resolved": "https://registry.npmjs.org/@azure/msal-browser/-/msal-browser-5.10.1.tgz", + "integrity": "sha512-hTbvOi9Ko2Jvn+G/fSmjzHf9WbNcf/o3epMtbeGx/pMwMrVAbi6OgCJVeCfsAb8IybSRpaCSc4EDRlYAhgngUQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@azure/msal-common": "16.6.1" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/@azure/msal-common": { + "version": "16.6.1", + "resolved": "https://registry.npmjs.org/@azure/msal-common/-/msal-common-16.6.1.tgz", + "integrity": "sha512-VxKdEtUwDuLD0F1hOQP7kye0YadZxFJfv37Em440geEf/w9uggKnHpRrqwZJOdxmPUOdhZ9kyRtKuAJW8wUcRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/@azure/msal-node": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/@azure/msal-node/-/msal-node-5.2.1.tgz", + "integrity": "sha512-tmQiQ2HvtzaeLqYGy3BemiPOSGPY4wCy1IW5zDWITKSs/s35WEd7Zij/hCxvUdAOzj6U3qnyaGbYXY91ortFEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@azure/msal-common": "16.6.1", + "jsonwebtoken": "^9.0.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@electron/node-gyp": { + "version": "10.2.0-electron.1", + "resolved": "git+ssh://git@github.com/electron/node-gyp.git#06b29aafb7708acef8b3669835c8a7857ebc92d2", + "integrity": "sha512-CrYo6TntjpoMO1SHjl5Pa/JoUsECNqNdB7Kx49WLQpWzPw53eEITJ2Hs9fh/ryUYDn4pxZz11StaBYBrLFJdqg==", + "dev": true, + "license": "MIT", + "dependencies": { + "env-paths": "^2.2.0", + "exponential-backoff": "^3.1.1", + "glob": "^8.1.0", + "graceful-fs": "^4.2.6", + "make-fetch-happen": "^10.2.1", + "nopt": "^6.0.0", + "proc-log": "^2.0.1", + "semver": "^7.3.5", + "tar": "^6.2.1", + "which": "^2.0.2" + }, + "bin": { + "node-gyp": "bin/node-gyp.js" + }, + "engines": { + "node": ">=12.13.0" + } + }, + "node_modules/@electron/node-gyp/node_modules/brace-expansion": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.0.tgz", + "integrity": "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@electron/node-gyp/node_modules/glob": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", + "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", + "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": "^5.0.1", + "once": "^1.3.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@electron/node-gyp/node_modules/minimatch": { + "version": "5.1.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.9.tgz", + "integrity": "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@electron/rebuild": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/@electron/rebuild/-/rebuild-3.7.2.tgz", + "integrity": "sha512-19/KbIR/DAxbsCkiaGMXIdPnMCJLkcf8AvGnduJtWBs/CBwiAjY1apCqOLVxrXg+rtXFCngbXhBanWjxLUt1Mg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@electron/node-gyp": "git+https://github.com/electron/node-gyp.git#06b29aafb7708acef8b3669835c8a7857ebc92d2", + "@malept/cross-spawn-promise": "^2.0.0", + "chalk": "^4.0.0", + "debug": "^4.1.1", + "detect-libc": "^2.0.1", + "fs-extra": "^10.0.0", + "got": "^11.7.0", + "node-abi": "^3.45.0", + "node-api-version": "^0.2.0", + "ora": "^5.1.0", + "read-binary-file-arch": "^1.0.6", + "semver": "^7.3.5", + "tar": "^6.0.5", + "yargs": "^17.0.1" + }, + "bin": { + "electron-rebuild": "lib/cli.js" + }, + "engines": { + "node": ">=12.13.0" + } + }, + "node_modules/@electron/rebuild/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@emnapi/core": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz", + "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.1", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", + "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", + "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.7.tgz", + "integrity": "sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.7.tgz", + "integrity": "sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.7.tgz", + "integrity": "sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@gar/promisify": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@gar/promisify/-/promisify-1.1.3.tgz", + "integrity": "sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@isaacs/cliui": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-9.0.0.tgz", + "integrity": "sha512-AokJm4tuBHillT+FpMtxQ60n8ObyXBatq7jD2/JA9dxbDDokKQm8KMht5ibGzLVU9IJDIKK4TPKgMHEYMn3lMg==", + "dev": true, + "license": "BlueOak-1.0.0", + "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/@malept/cross-spawn-promise": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@malept/cross-spawn-promise/-/cross-spawn-promise-2.0.0.tgz", + "integrity": "sha512-1DpKU0Z5ThltBwjNySMC14g0CkbyhCaz9FkhxqNsZI6uAPJXFS8cMXlBKo26FJ8ZuW6S9GCMcR9IO5k2X5/9Fg==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/malept" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/subscription/pkg/npm-.malept-cross-spawn-promise?utm_medium=referral&utm_source=npm_fund" + } + ], + "license": "Apache-2.0", + "dependencies": { + "cross-spawn": "^7.0.1" + }, + "engines": { + "node": ">= 12.13.0" + } + }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "0.2.12", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.12.tgz", + "integrity": "sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.4.3", + "@emnapi/runtime": "^1.4.3", + "@tybys/wasm-util": "^0.10.0" + } + }, + "node_modules/@node-rs/crc32": { + "version": "1.10.6", + "resolved": "https://registry.npmjs.org/@node-rs/crc32/-/crc32-1.10.6.tgz", + "integrity": "sha512-+llXfqt+UzgoDzT9of5vPQPGqTAVCohU74I9zIBkNo5TH6s2P31DFJOGsJQKN207f0GHnYv5pV3wh3BCY/un/A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "optionalDependencies": { + "@node-rs/crc32-android-arm-eabi": "1.10.6", + "@node-rs/crc32-android-arm64": "1.10.6", + "@node-rs/crc32-darwin-arm64": "1.10.6", + "@node-rs/crc32-darwin-x64": "1.10.6", + "@node-rs/crc32-freebsd-x64": "1.10.6", + "@node-rs/crc32-linux-arm-gnueabihf": "1.10.6", + "@node-rs/crc32-linux-arm64-gnu": "1.10.6", + "@node-rs/crc32-linux-arm64-musl": "1.10.6", + "@node-rs/crc32-linux-x64-gnu": "1.10.6", + "@node-rs/crc32-linux-x64-musl": "1.10.6", + "@node-rs/crc32-wasm32-wasi": "1.10.6", + "@node-rs/crc32-win32-arm64-msvc": "1.10.6", + "@node-rs/crc32-win32-ia32-msvc": "1.10.6", + "@node-rs/crc32-win32-x64-msvc": "1.10.6" + } + }, + "node_modules/@node-rs/crc32-android-arm-eabi": { + "version": "1.10.6", + "resolved": "https://registry.npmjs.org/@node-rs/crc32-android-arm-eabi/-/crc32-android-arm-eabi-1.10.6.tgz", + "integrity": "sha512-vZAMuJXm3TpWPOkkhxdrofWDv+Q+I2oO7ucLRbXyAPmXFNDhHtBxbO1rk9Qzz+M3eep8ieS4/+jCL1Q0zacNMQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@node-rs/crc32-android-arm64": { + "version": "1.10.6", + "resolved": "https://registry.npmjs.org/@node-rs/crc32-android-arm64/-/crc32-android-arm64-1.10.6.tgz", + "integrity": "sha512-Vl/JbjCinCw/H9gEpZveWCMjxjcEChDcDBM8S4hKay5yyoRCUHJPuKr4sjVDBeOm+1nwU3oOm6Ca8dyblwp4/w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@node-rs/crc32-darwin-arm64": { + "version": "1.10.6", + "resolved": "https://registry.npmjs.org/@node-rs/crc32-darwin-arm64/-/crc32-darwin-arm64-1.10.6.tgz", + "integrity": "sha512-kARYANp5GnmsQiViA5Qu74weYQ3phOHSYQf0G+U5wB3NB5JmBHnZcOc46Ig21tTypWtdv7u63TaltJQE41noyg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@node-rs/crc32-darwin-x64": { + "version": "1.10.6", + "resolved": "https://registry.npmjs.org/@node-rs/crc32-darwin-x64/-/crc32-darwin-x64-1.10.6.tgz", + "integrity": "sha512-Q99bevJVMfLTISpkpKBlXgtPUItrvTWKFyiqoKH5IvscZmLV++NH4V13Pa17GTBmv9n18OwzgQY4/SRq6PQNVA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@node-rs/crc32-freebsd-x64": { + "version": "1.10.6", + "resolved": "https://registry.npmjs.org/@node-rs/crc32-freebsd-x64/-/crc32-freebsd-x64-1.10.6.tgz", + "integrity": "sha512-66hpawbNjrgnS9EDMErta/lpaqOMrL6a6ee+nlI2viduVOmRZWm9Rg9XdGTK/+c4bQLdtC6jOd+Kp4EyGRYkAg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@node-rs/crc32-linux-arm-gnueabihf": { + "version": "1.10.6", + "resolved": "https://registry.npmjs.org/@node-rs/crc32-linux-arm-gnueabihf/-/crc32-linux-arm-gnueabihf-1.10.6.tgz", + "integrity": "sha512-E8Z0WChH7X6ankbVm8J/Yym19Cq3otx6l4NFPS6JW/cWdjv7iw+Sps2huSug+TBprjbcEA+s4TvEwfDI1KScjg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@node-rs/crc32-linux-arm64-gnu": { + "version": "1.10.6", + "resolved": "https://registry.npmjs.org/@node-rs/crc32-linux-arm64-gnu/-/crc32-linux-arm64-gnu-1.10.6.tgz", + "integrity": "sha512-LmWcfDbqAvypX0bQjQVPmQGazh4dLiVklkgHxpV4P0TcQ1DT86H/SWpMBMs/ncF8DGuCQ05cNyMv1iddUDugoQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@node-rs/crc32-linux-arm64-musl": { + "version": "1.10.6", + "resolved": "https://registry.npmjs.org/@node-rs/crc32-linux-arm64-musl/-/crc32-linux-arm64-musl-1.10.6.tgz", + "integrity": "sha512-k8ra/bmg0hwRrIEE8JL1p32WfaN9gDlUUpQRWsbxd1WhjqvXea7kKO6K4DwVxyxlPhBS9Gkb5Urq7Y4mXANzaw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@node-rs/crc32-linux-x64-gnu": { + "version": "1.10.6", + "resolved": "https://registry.npmjs.org/@node-rs/crc32-linux-x64-gnu/-/crc32-linux-x64-gnu-1.10.6.tgz", + "integrity": "sha512-IfjtqcuFK7JrSZ9mlAFhb83xgium30PguvRjIMI45C3FJwu18bnLk1oR619IYb/zetQT82MObgmqfKOtgemEKw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@node-rs/crc32-linux-x64-musl": { + "version": "1.10.6", + "resolved": "https://registry.npmjs.org/@node-rs/crc32-linux-x64-musl/-/crc32-linux-x64-musl-1.10.6.tgz", + "integrity": "sha512-LbFYsA5M9pNunOweSt6uhxenYQF94v3bHDAQRPTQ3rnjn+mK6IC7YTAYoBjvoJP8lVzcvk9hRj8wp4Jyh6Y80g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@node-rs/crc32-wasm32-wasi": { + "version": "1.10.6", + "resolved": "https://registry.npmjs.org/@node-rs/crc32-wasm32-wasi/-/crc32-wasm32-wasi-1.10.6.tgz", + "integrity": "sha512-KaejdLgHMPsRaxnM+OG9L9XdWL2TabNx80HLdsCOoX9BVhEkfh39OeahBo8lBmidylKbLGMQoGfIKDjq0YMStw==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@napi-rs/wasm-runtime": "^0.2.5" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@node-rs/crc32-win32-arm64-msvc": { + "version": "1.10.6", + "resolved": "https://registry.npmjs.org/@node-rs/crc32-win32-arm64-msvc/-/crc32-win32-arm64-msvc-1.10.6.tgz", + "integrity": "sha512-x50AXiSxn5Ccn+dCjLf1T7ZpdBiV1Sp5aC+H2ijhJO4alwznvXgWbopPRVhbp2nj0i+Gb6kkDUEyU+508KAdGQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@node-rs/crc32-win32-ia32-msvc": { + "version": "1.10.6", + "resolved": "https://registry.npmjs.org/@node-rs/crc32-win32-ia32-msvc/-/crc32-win32-ia32-msvc-1.10.6.tgz", + "integrity": "sha512-DpDxQLaErJF9l36aghe1Mx+cOnYLKYo6qVPqPL9ukJ5rAGLtCdU0C+Zoi3gs9ySm8zmbFgazq/LvmsZYU42aBw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@node-rs/crc32-win32-x64-msvc": { + "version": "1.10.6", + "resolved": "https://registry.npmjs.org/@node-rs/crc32-win32-x64-msvc/-/crc32-win32-x64-msvc-1.10.6.tgz", + "integrity": "sha512-5B1vXosIIBw1m2Rcnw62IIfH7W9s9f7H7Ma0rRuhT8HR4Xh8QCgw6NJSI2S2MCngsGktYnAhyUvs81b7efTyQw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@npmcli/fs": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-2.1.2.tgz", + "integrity": "sha512-yOJKRvohFOaLqipNtwYB9WugyZKhC/DZC4VYPmpaCzDBrA8YpK3qHZ8/HGscMnE4GqbkLNuVcCnxkeQEdGt6LQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "@gar/promisify": "^1.1.3", + "semver": "^7.3.5" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/@npmcli/move-file": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@npmcli/move-file/-/move-file-2.0.1.tgz", + "integrity": "sha512-mJd2Z5TjYWq/ttPLLGqArdtnC74J6bOzg4rMDnN+p1xTacZ2yPRCk2y0oSWQtygLR9YVQXgOcONrwtnk3JupxQ==", + "deprecated": "This functionality has been moved to @npmcli/fs", + "dev": true, + "license": "MIT", + "dependencies": { + "mkdirp": "^1.0.4", + "rimraf": "^3.0.2" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.3.tgz", + "integrity": "sha512-x35CNW/ANXG3hE/EZpRU8MXX1JDN86hBb2wMGAtltkz7pc6cxgjpy1OMMfDosOQ+2hWqIkag/fGok1Yady9nGw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.3.tgz", + "integrity": "sha512-xw3xtkDApIOGayehp2+Rz4zimfkaX65r4t47iy+ymQB2G4iJCBBfj0ogVg5jpvjpn8UWn/+q9tprxleYeNp3Hw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.3.tgz", + "integrity": "sha512-vo6Y5Qfpx7/5EaamIwi0WqW2+zfiusVihKatLvtN1VFVy3D13uERk/6gZLU1UiHRL6fDXqj/ELIeVRGnvcTE1g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.3.tgz", + "integrity": "sha512-D+0QGcZhBzTN82weOnsSlY7V7+RMmPuF1CkbxyMAGE8+ZHeUjyb76ZiWmBlCu//AQQONvxcqRbwZTajZKqjuOw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.3.tgz", + "integrity": "sha512-6HnvHCT7fDyj6R0Ph7A6x8dQS/S38MClRWeDLqc0MdfWkxjiu1HSDYrdPhqSILzjTIC/pnXbbJbo+ft+gy/9hQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.3.tgz", + "integrity": "sha512-KHLgC3WKlUYW3ShFKnnosZDOJ0xjg9zp7au3sIm2bs/tGBeC2ipmvRh/N7JKi0t9Ue20C0dpEshi8WUubg+cnA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.3.tgz", + "integrity": "sha512-DV6fJoxEYWJOvaZIsok7KrYl0tPvga5OZ2yvKHNNYyk/2roMLqQAbGhr78EQ5YhHpnhLKJD3S1WFusAkmUuV5g==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.3.tgz", + "integrity": "sha512-mQKoJAzvuOs6F+TZybQO4GOTSMUu7v0WdxEk24krQ/uUxXoPTtHjuaUuPmFhtBcM4K0ons8nrE3JyhTuCFtT/w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.3.tgz", + "integrity": "sha512-Whjj2qoiJ6+OOJMGptTYazaJvjOJm+iKHpXQM1P3LzGjt7Ff++Tp7nH4N8J/BUA7R9IHfDyx4DJIflifwnbmIA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.3.tgz", + "integrity": "sha512-4YTNHKqGng5+yiZt3mg77nmyuCfmNfX4fPmyUapBcIk+BdwSwmCWGXOUxhXbBEkFHtoN5boLj/5NON+u5QC9tg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.3.tgz", + "integrity": "sha512-SU3kNlhkpI4UqlUc2VXPGK9o886ZsSeGfMAX2ba2b8DKmMXq4AL7KUrkSWVbb7koVqx41Yczx6dx5PNargIrEA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.3.tgz", + "integrity": "sha512-6lDLl5h4TXpB1mTf2rQWnAk/LcXrx9vBfu/DT5TIPhvMhRWaZ5MxkIc8u4lJAmBo6klTe1ywXIUHFjylW505sg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.3.tgz", + "integrity": "sha512-BMo8bOw8evlup/8G+cj5xWtPyp93xPdyoSN16Zy90Q2QZ0ZYRhCt6ZJSwbrRzG9HApFabjwj2p25TUPDWrhzqQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.3.tgz", + "integrity": "sha512-E0L8X1dZN1/Rph+5VPF6Xj2G7JJvMACVXtamTJIDrVI44Y3K+G8gQaMEAavbqCGTa16InptiVrX6eM6pmJ+7qA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.3.tgz", + "integrity": "sha512-oZJ/WHaVfHUiRAtmTAeo3DcevNsVvH8mbvodjZy7D5QKvCefO371SiKRpxoDcCxB3PTRTLayWBkvmDQKTcX/sw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.3.tgz", + "integrity": "sha512-Dhbyh7j9FybM3YaTgaHmVALwA8AkUwTPccyCQ79TG9AJUsMQqgN1DDEZNr4+QUfwiWvLDumW5vdwzoeUF+TNxQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.3.tgz", + "integrity": "sha512-cJd1X5XhHHlltkaypz1UcWLA8AcoIi1aWhsvaWDskD1oz2eKCypnqvTQ8ykMNI0RSmm7NkTdSqSSD7zM0xa6Ig==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.3.tgz", + "integrity": "sha512-DAZDBHQfG2oQuhY7mc6I3/qB4LU2fQCjRvxbDwd/Jdvb9fypP4IJ4qmtu6lNjes6B531AI8cg1aKC2di97bUxA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.3.tgz", + "integrity": "sha512-cRxsE8c13mZOh3vP+wLDxpQBRrOHDIGOWyDL93Sy0Ga8y515fBcC2pjUfFwUe5T7tqvTvWbCpg1URM/AXdWIXA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.3.tgz", + "integrity": "sha512-QaWcIgRxqEdQdhJqW4DJctsH6HCmo5vHxY0krHSX4jMtOqfzC+dqDGuHM87bu4H8JBeibWx7jFz+h6/4C8wA5Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.3.tgz", + "integrity": "sha512-AaXwSvUi3QIPtroAUw1t5yHGIyqKEXwH54WUocFolZhpGDruJcs8c+xPNDRn4XiQsS7MEwnYsHW2l0MBLDMkWg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.3.tgz", + "integrity": "sha512-65LAKM/bAWDqKNEelHlcHvm2V+Vfb8C6INFxQXRHCvaVN1rJfwr4NvdP4FyzUaLqWfaCGaadf6UbTm8xJeYfEg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.3.tgz", + "integrity": "sha512-EEM2gyhBF5MFnI6vMKdX1LAosE627RGBzIoGMdLloPZkXrUN0Ckqgr2Qi8+J3zip/8NVVro3/FjB+tjhZUgUHA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.3.tgz", + "integrity": "sha512-E5Eb5H/DpxaoXH++Qkv28RcUJboMopmdDUALBczvHMf7hNIxaDZqwY5lK12UK1BHacSmvupoEWGu+n993Z0y1A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.3.tgz", + "integrity": "sha512-hPt/bgL5cE+Qp+/TPHBqptcAgPzgj46mPcg/16zNUmbQk0j+mOEQV/+Lqu8QRtDV3Ek95Q6FeFITpuhl6OTsAA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@secretlint/config-creator": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/@secretlint/config-creator/-/config-creator-10.2.2.tgz", + "integrity": "sha512-BynOBe7Hn3LJjb3CqCHZjeNB09s/vgf0baBaHVw67w7gHF0d25c3ZsZ5+vv8TgwSchRdUCRrbbcq5i2B1fJ2QQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@secretlint/types": "^10.2.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@secretlint/config-loader": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/@secretlint/config-loader/-/config-loader-10.2.2.tgz", + "integrity": "sha512-ndjjQNgLg4DIcMJp4iaRD6xb9ijWQZVbd9694Ol2IszBIbGPPkwZHzJYKICbTBmh6AH/pLr0CiCaWdGJU7RbpQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@secretlint/profiler": "^10.2.2", + "@secretlint/resolver": "^10.2.2", + "@secretlint/types": "^10.2.2", + "ajv": "^8.17.1", + "debug": "^4.4.1", + "rc-config-loader": "^4.1.3" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@secretlint/core": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/@secretlint/core/-/core-10.2.2.tgz", + "integrity": "sha512-6rdwBwLP9+TO3rRjMVW1tX+lQeo5gBbxl1I5F8nh8bgGtKwdlCMhMKsBWzWg1ostxx/tIG7OjZI0/BxsP8bUgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@secretlint/profiler": "^10.2.2", + "@secretlint/types": "^10.2.2", + "debug": "^4.4.1", + "structured-source": "^4.0.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@secretlint/formatter": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/@secretlint/formatter/-/formatter-10.2.2.tgz", + "integrity": "sha512-10f/eKV+8YdGKNQmoDUD1QnYL7TzhI2kzyx95vsJKbEa8akzLAR5ZrWIZ3LbcMmBLzxlSQMMccRmi05yDQ5YDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@secretlint/resolver": "^10.2.2", + "@secretlint/types": "^10.2.2", + "@textlint/linter-formatter": "^15.2.0", + "@textlint/module-interop": "^15.2.0", + "@textlint/types": "^15.2.0", + "chalk": "^5.4.1", + "debug": "^4.4.1", + "pluralize": "^8.0.0", + "strip-ansi": "^7.1.0", + "table": "^6.9.0", + "terminal-link": "^4.0.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@secretlint/formatter/node_modules/chalk": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/@secretlint/node": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/@secretlint/node/-/node-10.2.2.tgz", + "integrity": "sha512-eZGJQgcg/3WRBwX1bRnss7RmHHK/YlP/l7zOQsrjexYt6l+JJa5YhUmHbuGXS94yW0++3YkEJp0kQGYhiw1DMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@secretlint/config-loader": "^10.2.2", + "@secretlint/core": "^10.2.2", + "@secretlint/formatter": "^10.2.2", + "@secretlint/profiler": "^10.2.2", + "@secretlint/source-creator": "^10.2.2", + "@secretlint/types": "^10.2.2", + "debug": "^4.4.1", + "p-map": "^7.0.3" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@secretlint/profiler": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/@secretlint/profiler/-/profiler-10.2.2.tgz", + "integrity": "sha512-qm9rWfkh/o8OvzMIfY8a5bCmgIniSpltbVlUVl983zDG1bUuQNd1/5lUEeWx5o/WJ99bXxS7yNI4/KIXfHexig==", + "dev": true, + "license": "MIT" + }, + "node_modules/@secretlint/resolver": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/@secretlint/resolver/-/resolver-10.2.2.tgz", + "integrity": "sha512-3md0cp12e+Ae5V+crPQYGd6aaO7ahw95s28OlULGyclyyUtf861UoRGS2prnUrKh7MZb23kdDOyGCYb9br5e4w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@secretlint/secretlint-formatter-sarif": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/@secretlint/secretlint-formatter-sarif/-/secretlint-formatter-sarif-10.2.2.tgz", + "integrity": "sha512-ojiF9TGRKJJw308DnYBucHxkpNovDNu1XvPh7IfUp0A12gzTtxuWDqdpuVezL7/IP8Ua7mp5/VkDMN9OLp1doQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "node-sarif-builder": "^3.2.0" + } + }, + "node_modules/@secretlint/secretlint-rule-no-dotenv": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/@secretlint/secretlint-rule-no-dotenv/-/secretlint-rule-no-dotenv-10.2.2.tgz", + "integrity": "sha512-KJRbIShA9DVc5Va3yArtJ6QDzGjg3PRa1uYp9As4RsyKtKSSZjI64jVca57FZ8gbuk4em0/0Jq+uy6485wxIdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@secretlint/types": "^10.2.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@secretlint/secretlint-rule-preset-recommend": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/@secretlint/secretlint-rule-preset-recommend/-/secretlint-rule-preset-recommend-10.2.2.tgz", + "integrity": "sha512-K3jPqjva8bQndDKJqctnGfwuAxU2n9XNCPtbXVI5JvC7FnQiNg/yWlQPbMUlBXtBoBGFYp08A94m6fvtc9v+zA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@secretlint/source-creator": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/@secretlint/source-creator/-/source-creator-10.2.2.tgz", + "integrity": "sha512-h6I87xJfwfUTgQ7irWq7UTdq/Bm1RuQ/fYhA3dtTIAop5BwSFmZyrchph4WcoEvbN460BWKmk4RYSvPElIIvxw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@secretlint/types": "^10.2.2", + "istextorbinary": "^9.5.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@secretlint/types": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/@secretlint/types/-/types-10.2.2.tgz", + "integrity": "sha512-Nqc90v4lWCXyakD6xNyNACBJNJ0tNCwj2WNk/7ivyacYHxiITVgmLUFXTBOeCdy79iz6HtN9Y31uw/jbLrdOAg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@sindresorhus/is": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz", + "integrity": "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/is?sponsor=1" + } + }, + "node_modules/@sindresorhus/merge-streams": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/merge-streams/-/merge-streams-2.3.0.tgz", + "integrity": "sha512-LtoMMhxAlorcGhmFYI+LhPgbPZCkgP6ra1YL604EeF6U98pLlQ3iWIGMdWSC+vWmPBWBNgmDBAhnAobLROJmwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@szmarczak/http-timer": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-4.0.6.tgz", + "integrity": "sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==", + "dev": true, + "license": "MIT", + "dependencies": { + "defer-to-connect": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@textlint/ast-node-types": { + "version": "15.7.0", + "resolved": "https://registry.npmjs.org/@textlint/ast-node-types/-/ast-node-types-15.7.0.tgz", + "integrity": "sha512-wZILFXsRf2gGK9k0Fr69GUdfuZV9CrtByga8Qkw0CLyKBBfZXdNQlJF2XdZ2Ju9ggrTbAWehGo0RjCsAHSBWtA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@textlint/linter-formatter": { + "version": "15.7.0", + "resolved": "https://registry.npmjs.org/@textlint/linter-formatter/-/linter-formatter-15.7.0.tgz", + "integrity": "sha512-wrpDID1bfBt5XIUXtgw8NmGIuRFansWAtX1FLHv/zLRt3sgxCFnyon88SbhPOPITEgIlfFcsESElCSLzWySubQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@azu/format-text": "^1.0.2", + "@azu/style-format": "^1.0.1", + "@textlint/module-interop": "15.7.0", + "@textlint/resolver": "15.7.0", + "@textlint/types": "15.7.0", + "chalk": "^4.1.2", + "debug": "^4.4.3", + "js-yaml": "^4.1.1", + "lodash": "^4.18.1", + "pluralize": "^2.0.0", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1", + "table": "^6.9.0", + "text-table": "^0.2.0" + } + }, + "node_modules/@textlint/linter-formatter/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@textlint/linter-formatter/node_modules/pluralize": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-2.0.0.tgz", + "integrity": "sha512-TqNZzQCD4S42De9IfnnBvILN7HAW7riLqsCyp8lgjXeysyPlX5HhqKAcJHHHb9XskE4/a+7VGC9zzx8Ls0jOAw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@textlint/linter-formatter/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@textlint/module-interop": { + "version": "15.7.0", + "resolved": "https://registry.npmjs.org/@textlint/module-interop/-/module-interop-15.7.0.tgz", + "integrity": "sha512-+VdoeGFH66OasijhoO7D3OSrqTfJNAIH2OoQHEc5StlO0dqDO2JfZStCbuBPP/ZbpJvuYdoERBP+nKqTAT24vA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@textlint/resolver": { + "version": "15.7.0", + "resolved": "https://registry.npmjs.org/@textlint/resolver/-/resolver-15.7.0.tgz", + "integrity": "sha512-f94t/8ZR97uhOu2KvBujMGGtfdoJQZLjDNN7+7PNLaTjCtGn+XrqKjSP9lzXgBhUbKSygRN8AlrCMx9S70FHKw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@textlint/types": { + "version": "15.7.0", + "resolved": "https://registry.npmjs.org/@textlint/types/-/types-15.7.0.tgz", + "integrity": "sha512-soItNoFZ8Ua4WCgWwOaTEEyTkX7bjArwVxhN1F2UySeqPsP8QeRiKNUjAIfWQFHddTY6UYR1sHaEh+O0sQPv0Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@textlint/ast-node-types": "15.7.0" + } + }, + "node_modules/@tootallnate/once": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.1.tgz", + "integrity": "sha512-HqmEUIGRJ5fSXchkVgR5F7qn48bDBzv0kWj/Kfu5e6uci4UlEeng4331LnBkWffb++Ei3FOVLxo8JJWMFBDMeQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.2", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.2.tgz", + "integrity": "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@types/better-sqlite3": { + "version": "7.6.13", + "resolved": "https://registry.npmjs.org/@types/better-sqlite3/-/better-sqlite3-7.6.13.tgz", + "integrity": "sha512-NMv9ASNARoKksWtsq/SHakpYAYnhBrQgGD8zkLYk/jaK8jUGn08CfEdTRgYhMypUQAfzSP8W6gNLe0q19/t4VA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/cacheable-request": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.3.tgz", + "integrity": "sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/http-cache-semantics": "*", + "@types/keyv": "^3.1.4", + "@types/node": "*", + "@types/responselike": "^1.0.0" + } + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/http-cache-semantics": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", + "integrity": "sha512-L3LgimLHXtGkWikKnsPg0/VFx9OGZaC+eN1u4r+OB1XRqH3meBIAVC2zr1WdMH+RHmnRkqliQAOHNJ/E0j/e0Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/keyv": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/@types/keyv/-/keyv-3.1.4.tgz", + "integrity": "sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/node": { + "version": "20.19.41", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.41.tgz", + "integrity": "sha512-ECymXOukMnOoVkC2bb1Vc/w/836DXncOg5m8Xj1RH7xSHZJWNYY6Zh7EH477vcnD5egKNNfy2RpNOmuChhFPgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/normalize-package-data": { + "version": "2.4.4", + "resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.4.tgz", + "integrity": "sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/responselike": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.3.tgz", + "integrity": "sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/sarif": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@types/sarif/-/sarif-2.1.7.tgz", + "integrity": "sha512-kRz0VEkJqWLf1LLVN4pT1cg1Z9wAuvI6L97V3m2f5B76Tg8d413ddvLBPTEHAZJlnn4XSvu0FkZtViCQGVyrXQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/vscode": { + "version": "1.120.0", + "resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.120.0.tgz", + "integrity": "sha512-feaT4Rst+FkTch5zz/ZbNCxoIvo55YU80Be2kiL7OJcod4+CUYf2lUBPdIJzozNnSEMq1VRTGrWEcCGFB3fBmA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@typespec/ts-http-runtime": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@typespec/ts-http-runtime/-/ts-http-runtime-0.3.5.tgz", + "integrity": "sha512-yURCknZhvywvQItHMMmFSo+fq5arCUIyz/CVk7jD89MSai7dkaX8ufjCWp3NttLojoTVbcE72ri+be/TnEbMHw==", + "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": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-2.1.9.tgz", + "integrity": "sha512-UJCIkTBenHeKT1TTlKMJWy1laZewsRIzYighyYiJKZreqtdxSos/S1t+ktRMQWu2CKqaarrkeszJx1cgC5tGZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "2.1.9", + "@vitest/utils": "2.1.9", + "chai": "^5.1.2", + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-2.1.9.tgz", + "integrity": "sha512-tVL6uJgoUdi6icpxmdrn5YNo3g3Dxv+IHJBr0GXHaEdTcw3F+cPKnsXFhli6nO+f/6SDKPHEK1UN+k+TQv0Ehg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "2.1.9", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.12" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^5.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-2.1.9.tgz", + "integrity": "sha512-KhRIdGV2U9HOUzxfiHmY8IFHTdqtOhIzCpd8WRdJiE7D/HUcZVD0EgQCVjm+Q9gkUXWgBvMmTtZgIG48wq7sOQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-2.1.9.tgz", + "integrity": "sha512-ZXSSqTFIrzduD63btIfEyOmNcBmQvgOVsPNPe0jYtESiXkhd8u2erDLnMxmGrDCwHCCHE7hxwRDCT3pt0esT4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "2.1.9", + "pathe": "^1.1.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-2.1.9.tgz", + "integrity": "sha512-oBO82rEjsxLNJincVhLhaxxZdEtV0EFHMK5Kmx5sJ6H9L183dHECjiefOAdnqpIgT5eZwT04PoggUnW88vOBNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "2.1.9", + "magic-string": "^0.30.12", + "pathe": "^1.1.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-2.1.9.tgz", + "integrity": "sha512-E1B35FwzXXTs9FHNK6bDszs7mtydNi5MIfUWpceJ8Xbfb1gBMscAnwLbEu+B44ed6W3XjL9/ehLPHR1fkf1KLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyspy": "^3.0.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-2.1.9.tgz", + "integrity": "sha512-v0psaMSkNJ3A2NMrUEHFRzJtDPFn+/VWZ5WxImB21T9fjucJRmS7xCS3ppEnARb9y11OAzaD+P2Ps+b+BGX5iQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "2.1.9", + "loupe": "^3.1.2", + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vscode/vsce": { + "version": "3.9.1", + "resolved": "https://registry.npmjs.org/@vscode/vsce/-/vsce-3.9.1.tgz", + "integrity": "sha512-MPn5p+DoudI+3GfJSpAZZraE1lgLv0LcwbH3+xy7RgEhty3UIkmUMUA+5jPTDaxXae00AnX5u77FxGM8FhfKKA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@azure/identity": "^4.1.0", + "@secretlint/node": "^10.1.2", + "@secretlint/secretlint-formatter-sarif": "^10.1.2", + "@secretlint/secretlint-rule-no-dotenv": "^10.1.2", + "@secretlint/secretlint-rule-preset-recommend": "^10.1.2", + "@vscode/vsce-sign": "^2.0.0", + "azure-devops-node-api": "^12.5.0", + "chalk": "^4.1.2", + "cheerio": "^1.0.0-rc.9", + "cockatiel": "^3.1.2", + "commander": "^12.1.0", + "form-data": "^4.0.0", + "glob": "^11.0.0", + "hosted-git-info": "^4.0.2", + "jsonc-parser": "^3.2.0", + "leven": "^3.1.0", + "markdown-it": "^14.1.0", + "mime": "^1.3.4", + "minimatch": "^3.0.3", + "parse-semver": "^1.1.1", + "read": "^1.0.7", + "secretlint": "^10.1.2", + "semver": "^7.5.2", + "tmp": "^0.2.3", + "typed-rest-client": "^1.8.4", + "url-join": "^4.0.1", + "xml2js": "^0.5.0", + "yauzl": "^3.2.1", + "yazl": "^2.2.2" + }, + "bin": { + "vsce": "vsce" + }, + "engines": { + "node": ">= 20" + }, + "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/abbrev": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", + "dev": true, + "license": "ISC" + }, + "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/agentkeepalive": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.6.0.tgz", + "integrity": "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "humanize-ms": "^1.2.1" + }, + "engines": { + "node": ">= 8.0.0" + } + }, + "node_modules/aggregate-error": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", + "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "clean-stack": "^2.0.0", + "indent-string": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ajv": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-escapes": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.3.0.tgz", + "integrity": "sha512-BvU8nYgGQBxcmMuEeUEmNTvrMVjJNSH7RgW24vXexN4Ven6qCvy4TntnvlnwnMLTVlcRQQdbRY8NKnaIoeWDNg==", + "dev": true, + "license": "MIT", + "dependencies": { + "environment": "^1.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "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/astral-regex": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", + "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "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==", + "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/better-sqlite3": { + "version": "12.10.0", + "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-12.10.0.tgz", + "integrity": "sha512-CyzaZRQKyHkB2ZInfTTl2nvT33EbDpjkLEbE8/Zck3Ll6O0qqvuGdrJ45HgtH+HykRg88ITY3AdreBGN70aBSQ==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "bindings": "^1.5.0", + "prebuild-install": "^7.1.1" + }, + "engines": { + "node": "20.x || 22.x || 23.x || 24.x || 25.x || 26.x" + } + }, + "node_modules/binaryextensions": { + "version": "6.11.0", + "resolved": "https://registry.npmjs.org/binaryextensions/-/binaryextensions-6.11.0.tgz", + "integrity": "sha512-sXnYK/Ij80TO3lcqZVV2YgfKN5QjUWIRk/XSm2J/4bd/lPko3lvk0O4ZppH6m+6hB2/GTu+ptNwVFe1xh+QLQw==", + "dev": true, + "license": "Artistic-2.0", + "dependencies": { + "editions": "^6.21.0" + }, + "engines": { + "node": ">=4" + }, + "funding": { + "url": "https://bevry.me/fund" + } + }, + "node_modules/bindings": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "license": "MIT", + "dependencies": { + "file-uri-to-path": "1.0.0" + } + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "license": "MIT", + "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/boundary": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/boundary/-/boundary-2.0.0.tgz", + "integrity": "sha512-rJKn5ooC9u8q13IMCrW0RSp31pxBCHE3y9V/tp3TdWSLf8Em3p6Di4NBpfzbJge9YjjFEsD0RtFEjtvHL5VyEA==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/brace-expansion": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz", + "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "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", + "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/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cacache": { + "version": "16.1.3", + "resolved": "https://registry.npmjs.org/cacache/-/cacache-16.1.3.tgz", + "integrity": "sha512-/+Emcj9DAXxX4cwlLmRI9c166RuL3w30zp4R7Joiv2cQTtTtA+jeuCAjH3ZlGnYS3tKENSrKhAzVVP9GVyzeYQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "@npmcli/fs": "^2.1.0", + "@npmcli/move-file": "^2.0.0", + "chownr": "^2.0.0", + "fs-minipass": "^2.1.0", + "glob": "^8.0.1", + "infer-owner": "^1.0.4", + "lru-cache": "^7.7.1", + "minipass": "^3.1.6", + "minipass-collect": "^1.0.2", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "mkdirp": "^1.0.4", + "p-map": "^4.0.0", + "promise-inflight": "^1.0.1", + "rimraf": "^3.0.2", + "ssri": "^9.0.0", + "tar": "^6.1.11", + "unique-filename": "^2.0.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/cacache/node_modules/brace-expansion": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.0.tgz", + "integrity": "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/cacache/node_modules/chownr": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/cacache/node_modules/glob": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", + "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", + "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": "^5.0.1", + "once": "^1.3.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/cacache/node_modules/lru-cache": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/cacache/node_modules/minimatch": { + "version": "5.1.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.9.tgz", + "integrity": "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/cacache/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cacache/node_modules/p-map": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", + "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "aggregate-error": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cacheable-lookup": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-5.0.4.tgz", + "integrity": "sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.6.0" + } + }, + "node_modules/cacheable-request": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-7.0.4.tgz", + "integrity": "sha512-v+p6ongsrp0yTGbJXjgxPow2+DL93DASP4kXCDKb8/bwRtt9OEF3whggkkDkGNzgcWy2XaF4a8nZglC7uElscg==", + "dev": true, + "license": "MIT", + "dependencies": { + "clone-response": "^1.0.2", + "get-stream": "^5.1.0", + "http-cache-semantics": "^4.0.0", + "keyv": "^4.0.0", + "lowercase-keys": "^2.0.0", + "normalize-url": "^6.0.1", + "responselike": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "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": "5.3.3", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", + "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/check-error": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz", + "integrity": "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16" + } + }, + "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==", + "license": "ISC" + }, + "node_modules/ci-info": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-2.0.0.tgz", + "integrity": "sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/clean-stack": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", + "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/cli-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", + "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "restore-cursor": "^3.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cli-spinners": { + "version": "2.9.2", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz", + "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/cliui/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/clone": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", + "integrity": "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/clone-response": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.3.tgz", + "integrity": "sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-response": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/clone-response/node_modules/mimic-response": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz", + "integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "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": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/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": "12.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz", + "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "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/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "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==", + "license": "MIT", + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/deep-eql": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "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==", + "license": "MIT", + "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/defaults": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.4.tgz", + "integrity": "sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "clone": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/defer-to-connect": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.1.tgz", + "integrity": "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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/define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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==", + "license": "Apache-2.0", + "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/editions": { + "version": "6.22.0", + "resolved": "https://registry.npmjs.org/editions/-/editions-6.22.0.tgz", + "integrity": "sha512-UgGlf8IW75je7HZjNDpJdCv4cGJWIi6yumFdZ0R7A8/CIhQiWUjyGLCxdHpd8bmyD1gnkfUNK0oeOXqUS2cpfQ==", + "dev": true, + "license": "Artistic-2.0", + "dependencies": { + "version-range": "^4.15.0" + }, + "engines": { + "ecmascript": ">= es5", + "node": ">=4" + }, + "funding": { + "url": "https://bevry.me/fund" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/encoding": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz", + "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "iconv-lite": "^0.6.2" + } + }, + "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==", + "license": "MIT", + "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/env-paths": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/environment": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/environment/-/environment-1.1.0.tgz", + "integrity": "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/err-code": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz", + "integrity": "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==", + "dev": true, + "license": "MIT" + }, + "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.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/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==", + "license": "(MIT OR WTFPL)", + "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/exponential-backoff": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/exponential-backoff/-/exponential-backoff-3.1.3.tgz", + "integrity": "sha512-ZgEeZXj30q+I0EN+CbSSpIyPaJ5HVQD18Z1m+u1FXbAeT94mr1zw50q4q6jiiC447Nl/YTcIYSAftiGqetwXCA==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.2.tgz", + "integrity": "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/fastq": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", + "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/file-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", + "license": "MIT" + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/follow-redirects": { + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz", + "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "dev": true, + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "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==", + "license": "MIT" + }, + "node_modules/fs-extra": { + "version": "11.3.5", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.5.tgz", + "integrity": "sha512-eKpRKAovdpZtR1WopLHxlBWvAgPny3c4gX1G5Jhwmmw4XJj0ifSD5qB5TOo8hmA0wlRKDAOAhEE1yVPgs6Fgcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/fs-minipass": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", + "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/fs-minipass/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "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-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-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/get-stream": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pump": "^3.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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==", + "license": "MIT" + }, + "node_modules/glob": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-11.1.0.tgz", + "integrity": "sha512-vuNwKSaKiqm7g0THUBu2x7ckSs3XJLXE+2ssL7/MfTGPLLcrJQ/4Uq1CjPTtO5cCIiRxqvN6Twy1qOwhL0Xjcw==", + "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": "BlueOak-1.0.0", + "dependencies": { + "foreground-child": "^3.3.1", + "jackspeak": "^4.1.1", + "minimatch": "^10.1.1", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^2.0.0" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/glob/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/glob/node_modules/brace-expansion": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", + "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/glob/node_modules/minimatch": { + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.5" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/globalthis": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", + "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-properties": "^1.2.1", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/globby": { + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-14.1.0.tgz", + "integrity": "sha512-0Ia46fDOaT7k4og1PDW4YbodWWr3scS2vAr2lTbsplOt2WkKp0vQbkI9wKis/T5LV/dqPjO3bpS/z6GTJB82LA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sindresorhus/merge-streams": "^2.1.0", + "fast-glob": "^3.3.3", + "ignore": "^7.0.3", + "path-type": "^6.0.0", + "slash": "^5.1.0", + "unicorn-magic": "^0.3.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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/got": { + "version": "11.8.6", + "resolved": "https://registry.npmjs.org/got/-/got-11.8.6.tgz", + "integrity": "sha512-6tfZ91bOr7bOXnK7PRDCGBLa1H4U080YHNaAQ2KsMGlLEzRbk44nsZF2E1IeRc3vtJHPVbKCYgdFbaGO2ljd8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sindresorhus/is": "^4.0.0", + "@szmarczak/http-timer": "^4.0.5", + "@types/cacheable-request": "^6.0.1", + "@types/responselike": "^1.0.0", + "cacheable-lookup": "^5.0.3", + "cacheable-request": "^7.0.2", + "decompress-response": "^6.0.0", + "http2-wrapper": "^1.0.0-beta.5.2", + "lowercase-keys": "^2.0.0", + "p-cancelable": "^2.0.0", + "responselike": "^2.0.0" + }, + "engines": { + "node": ">=10.19.0" + }, + "funding": { + "url": "https://github.com/sindresorhus/got?sponsor=1" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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.3", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.3.tgz", + "integrity": "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==", + "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-cache-semantics": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", + "integrity": "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==", + "dev": true, + "license": "BSD-2-Clause" + }, + "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/http2-wrapper": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-1.0.3.tgz", + "integrity": "sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "quick-lru": "^5.1.1", + "resolve-alpn": "^1.0.0" + }, + "engines": { + "node": ">=10.19.0" + } + }, + "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/humanize-ms": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz", + "integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.0.0" + } + }, + "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==", + "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" + }, + "node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/index-to-position": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/index-to-position/-/index-to-position-1.2.0.tgz", + "integrity": "sha512-Yg7+ztRkqslMAS2iFaU+Oa4KTSidr63OsFGlOrJoW981kIYO3CGCS3wA95P1mUi/IVSJkn0D479KTJpVpvFNuw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/infer-owner": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/infer-owner/-/infer-owner-1.0.4.tgz", + "integrity": "sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A==", + "dev": true, + "license": "ISC" + }, + "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==", + "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==", + "license": "ISC" + }, + "node_modules/ip-address": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.2.0.tgz", + "integrity": "sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/is-ci": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-2.0.0.tgz", + "integrity": "sha512-YfJT7rkpQB0updsdHLGWrvhBJfcfzNNawYDNIyQXJz0IViGf75O8EBPKSdvw2rF+LGCsX4FZ8tcr3b19LcZq4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ci-info": "^2.0.0" + }, + "bin": { + "is-ci": "bin.js" + } + }, + "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-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "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-interactive": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-1.0.0.tgz", + "integrity": "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-it-type": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/is-it-type/-/is-it-type-5.1.3.tgz", + "integrity": "sha512-AX2uU0HW+TxagTgQXOJY7+2fbFHemC7YFBwN1XqD8qQMKdtfbOC8OC3fUb4s5NU59a3662Dzwto8tWDdZYRXxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "globalthis": "^1.0.2" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/is-lambda": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-lambda/-/is-lambda-1.0.1.tgz", + "integrity": "sha512-z7CMFGNrENq5iFB9Bqo64Xk6Y9sg+epq1myIcdHaGnbMTYOxvzsEtdYqQUylB7LxfkvgrrjP32T6Ywciio9UIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-unicode-supported": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", + "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "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/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/istextorbinary": { + "version": "9.5.0", + "resolved": "https://registry.npmjs.org/istextorbinary/-/istextorbinary-9.5.0.tgz", + "integrity": "sha512-5mbUj3SiZXCuRf9fT3ibzbSSEWiy63gFfksmGfdOzujPjW3k+z8WvIBxcJHBoQNlaZaiyB25deviif2+osLmLw==", + "dev": true, + "license": "Artistic-2.0", + "dependencies": { + "binaryextensions": "^6.11.0", + "editions": "^6.21.0", + "textextensions": "^6.11.0" + }, + "engines": { + "node": ">=4" + }, + "funding": { + "url": "https://bevry.me/fund" + } + }, + "node_modules/jackspeak": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.2.3.tgz", + "integrity": "sha512-ykkVRwrYvFm1nb2AJfKKYPr0emF6IiXDYUaFx4Zn9ZuIH7MrzEZ3sD5RlqGXNRpHtvUHJyOnCEFxOlNDtGo7wg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^9.0.0" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "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/jsonfile": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", + "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "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/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.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": "5.0.0", + "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.0.tgz", + "integrity": "sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "uc.micro": "^2.0.0" + } + }, + "node_modules/lodash": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", + "dev": true, + "license": "MIT" + }, + "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/lodash.truncate": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/lodash.truncate/-/lodash.truncate-4.4.2.tgz", + "integrity": "sha512-jttmRe7bRse52OsWIMDLaXxWqRAmtIUccAQ3garviCqJjafXOfNMO0yMfNpdD6zbGaTU0P5Nz7e7gAT6cKmJRw==", + "dev": true, + "license": "MIT" + }, + "node_modules/log-symbols": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", + "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/loupe": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", + "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/lowercase-keys": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz", + "integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "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/make-fetch-happen": { + "version": "10.2.1", + "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-10.2.1.tgz", + "integrity": "sha512-NgOPbRiaQM10DYXvN3/hhGVI2M5MtITFryzBGxHM5p4wnFxsVCbxkrBrDsk+EZ5OB4jEOT7AjDxtdF+KVEFT7w==", + "dev": true, + "license": "ISC", + "dependencies": { + "agentkeepalive": "^4.2.1", + "cacache": "^16.1.0", + "http-cache-semantics": "^4.1.0", + "http-proxy-agent": "^5.0.0", + "https-proxy-agent": "^5.0.0", + "is-lambda": "^1.0.1", + "lru-cache": "^7.7.1", + "minipass": "^3.1.6", + "minipass-collect": "^1.0.2", + "minipass-fetch": "^2.0.3", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "negotiator": "^0.6.3", + "promise-retry": "^2.0.1", + "socks-proxy-agent": "^7.0.0", + "ssri": "^9.0.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/make-fetch-happen/node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/make-fetch-happen/node_modules/http-proxy-agent": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz", + "integrity": "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@tootallnate/once": "2", + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/make-fetch-happen/node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/make-fetch-happen/node_modules/lru-cache": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/make-fetch-happen/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/markdown-it": { + "version": "14.1.1", + "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.1.1.tgz", + "integrity": "sha512-BuU2qnTti9YKgK5N+IeMubp14ZUKUUw7yeJbkjtosvHiP0AZ5c8IAgEMk79D0eC8F23r4Ac/q8cAIFdm2FtyoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1", + "entities": "^4.4.0", + "linkify-it": "^5.0.0", + "mdurl": "^2.0.0", + "punycode.js": "^2.3.1", + "uc.micro": "^2.1.0" + }, + "bin": { + "markdown-it": "bin/markdown-it.mjs" + } + }, + "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": "2.0.0", + "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.0.0.tgz", + "integrity": "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==", + "dev": true, + "license": "MIT" + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "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-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=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==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "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==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/minipass-collect": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/minipass-collect/-/minipass-collect-1.0.2.tgz", + "integrity": "sha512-6T6lH0H8OG9kITm/Jm6tdooIbogG9e0tLgpY6mphXSm/A9u8Nq1ryBG+Qspiub9LjWlBPsPS3tWQ/Botq4FdxA==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minipass-collect/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-fetch": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-2.1.2.tgz", + "integrity": "sha512-LT49Zi2/WMROHYoqGgdlQIZh8mLPZmOrN2NdJjMXxYe4nkN6FUyuPuOAOedNJDrx0IRGg9+4guZewtp8hE6TxA==", + "dev": true, + "license": "MIT", + "dependencies": { + "minipass": "^3.1.6", + "minipass-sized": "^1.0.3", + "minizlib": "^2.1.2" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + }, + "optionalDependencies": { + "encoding": "^0.1.13" + } + }, + "node_modules/minipass-fetch/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-flush": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/minipass-flush/-/minipass-flush-1.0.7.tgz", + "integrity": "sha512-TbqTz9cUwWyHS2Dy89P3ocAGUGxKjjLuR9z8w4WUTGAVgEj17/4nhgo2Du56i0Fm3Pm30g4iA8Lcqctc76jCzA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minipass-flush/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-pipeline": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz", + "integrity": "sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-pipeline/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-sized": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/minipass-sized/-/minipass-sized-1.0.3.tgz", + "integrity": "sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-sized/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minizlib": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", + "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minizlib/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "dev": true, + "license": "MIT", + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "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==", + "license": "MIT" + }, + "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.12", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", + "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", + "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==", + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.4.tgz", + "integrity": "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/node-abi": { + "version": "3.92.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.92.0.tgz", + "integrity": "sha512-KdHvFWZjEKDf0cakgFjebl371GPsISX2oZHcuyKqM7DtogIsHrqKeLTo8wBHxaXRAQlY2PsPlZmfo+9ZCxEREQ==", + "license": "MIT", + "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/node-api-version": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/node-api-version/-/node-api-version-0.2.1.tgz", + "integrity": "sha512-2xP/IGGMmmSQpI1+O/k72jF/ykvZ89JeuKX3TLJAYPDVLUalrshrLHkeVcCCZqG/eEa635cr8IBYzgnDvM2O8Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.3.5" + } + }, + "node_modules/node-sarif-builder": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/node-sarif-builder/-/node-sarif-builder-3.4.0.tgz", + "integrity": "sha512-tGnJW6OKRii9u/b2WiUViTJS+h7Apxx17qsMUjsUeNDiMMX5ZFf8F8Fcz7PAQ6omvOxHZtvDTmOYKJQwmfpjeg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/sarif": "^2.1.7", + "fs-extra": "^11.1.1" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/nopt": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-6.0.0.tgz", + "integrity": "sha512-ZwLpbTgdhuZUnZzjd7nb1ZV+4DoiC6/sfiVKok72ym/4Tlf+DFdlHYmT2JPmcNNWV6Pi3SDf1kT+A4r9RTuT9g==", + "dev": true, + "license": "ISC", + "dependencies": { + "abbrev": "^1.0.0" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/normalize-package-data": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-6.0.2.tgz", + "integrity": "sha512-V6gygoYb/5EmNI+MEGrWkC+e6+Rr7mTmfHrxDbLzxQogBkgzo76rkok0Am6thgSF7Mv2nLOajAJj5vDJZEFn7g==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "hosted-git-info": "^7.0.0", + "semver": "^7.3.5", + "validate-npm-package-license": "^3.0.4" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/normalize-package-data/node_modules/hosted-git-info": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-7.0.2.tgz", + "integrity": "sha512-puUZAUKT5m8Zzvs72XWy3HtvVbTWljRE66cP60bxJzAqf2DgICo7lYTY2IHUmLnNpjYvw5bvmoHvPc0QO2a62w==", + "dev": true, + "license": "ISC", + "dependencies": { + "lru-cache": "^10.0.1" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/normalize-package-data/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/normalize-url": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz", + "integrity": "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "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==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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/ora": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/ora/-/ora-5.4.1.tgz", + "integrity": "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "bl": "^4.1.0", + "chalk": "^4.1.0", + "cli-cursor": "^3.1.0", + "cli-spinners": "^2.5.0", + "is-interactive": "^1.0.0", + "is-unicode-supported": "^0.1.0", + "log-symbols": "^4.1.0", + "strip-ansi": "^6.0.0", + "wcwidth": "^1.0.1" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ora/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ora/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ovsx": { + "version": "0.10.12", + "resolved": "https://registry.npmjs.org/ovsx/-/ovsx-0.10.12.tgz", + "integrity": "sha512-WwMj1iQDvCk02029oxPnkFXsPrHZ+WzmoNW5pJ8JGepHtL30i2JE4s3C3wqzQqj6a35vx2hp0gV3TdfefGmvMg==", + "dev": true, + "license": "EPL-2.0", + "dependencies": { + "@vscode/vsce": "^3.7.1", + "commander": "^6.2.1", + "follow-redirects": "^1.16.0", + "is-ci": "^2.0.0", + "leven": "^3.1.0", + "semver": "^7.6.0", + "tmp": "^0.2.3", + "yauzl-promise": "^4.0.0" + }, + "bin": { + "ovsx": "bin/ovsx" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/ovsx/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/p-cancelable": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-2.1.1.tgz", + "integrity": "sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/p-map": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-7.0.4.tgz", + "integrity": "sha512-tkAQEw8ysMzmkhgw8k+1U/iPhWNhykKnSk4Rd5zLoPJCuJaGRPo6YposrZgaxHKzDHdDWWZvE/Sk7hsL2X/CpQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true, + "license": "BlueOak-1.0.0" + }, + "node_modules/parse-json": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-8.3.0.tgz", + "integrity": "sha512-ybiGyvspI+fAoRQbIPRddCcSTV9/LsJbf0e/S85VLowVGzRmokfneg2kwVW/KU5rOXrPSbF1qAKPMgNTqqROQQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.26.2", + "index-to-position": "^1.1.0", + "type-fest": "^4.39.1" + }, + "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/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-scurry": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz", + "integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "11.3.6", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.3.6.tgz", + "integrity": "sha512-Gf/KoL3C/MlI7Bt0PGI9I+TeTC/I6r/csU58N4BSNc4lppLBeKsOdFYkK+dX0ABDUMJNfCHTyPpzwwO21Awd3A==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/path-type": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-6.0.0.tgz", + "integrity": "sha512-Vj7sf++t5pBD637NSfkxpHSMfWaeig5+DKWLhcqIYx6mWQz5hdJTGDVMQiJcw1ZYkhs7AazKDGpRVji1LJCZUQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pathe": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", + "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/pathval": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz", + "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.16" + } + }, + "node_modules/pend": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", + "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", + "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": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pluralize": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-8.0.0.tgz", + "integrity": "sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss": { + "version": "8.5.14", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.14.tgz", + "integrity": "sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg==", + "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==", + "deprecated": "No longer maintained. Please contact the author of the relevant native addon; alternatives are available.", + "license": "MIT", + "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/proc-log": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-2.0.1.tgz", + "integrity": "sha512-Kcmo2FhfDTXdcbfDH76N7uBYHINxc/8GW7UAVuVP9I+Va3uHSerrnKV6dLooga/gh7GlgzuCCr/eoldnL1muGw==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/promise-inflight": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz", + "integrity": "sha512-6zWPyEOFaQBJYcGMHBKTKJ3u6TBsnMFOIZSa6ce1e/ZrrsOlnHRHbabMjLiBYKp+n44X9eUI6VUPaukCXHuG4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/promise-retry": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/promise-retry/-/promise-retry-2.0.1.tgz", + "integrity": "sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "err-code": "^2.0.2", + "retry": "^0.12.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/pump": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", + "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/punycode.js": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode.js/-/punycode.js-2.3.1.tgz", + "integrity": "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/qs": { + "version": "6.15.1", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.1.tgz", + "integrity": "sha512-6YHEFRL9mfgcAvql/XhwTvf5jKcOiiupt2FiJxHkiX1z4j7WL8J/jRHYLluORvc1XxB5rV20KoeK00gVJamspg==", + "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/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "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/quick-lru": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", + "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", + "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/rc-config-loader": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/rc-config-loader/-/rc-config-loader-4.1.4.tgz", + "integrity": "sha512-3GiwEzklkbXTDp52UR5nT8iXgYAx1V9ZG/kDZT7p60u2GCv2XTwQq4NzinMoMpNtXhmt3WkhYXcj6HH8HdwCEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.4.3", + "js-yaml": "^4.1.1", + "json5": "^2.2.3", + "require-from-string": "^2.0.2" + } + }, + "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/read-binary-file-arch": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/read-binary-file-arch/-/read-binary-file-arch-1.0.6.tgz", + "integrity": "sha512-BNg9EN3DD3GsDXX7Aa8O4p92sryjkmzYYgmgTAc6CA4uGLEDzFfxOxugu21akOxpcXHiEgsYkC6nPsQvLLLmEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.3.4" + }, + "bin": { + "read-binary-file-arch": "cli.js" + } + }, + "node_modules/read-pkg": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-9.0.1.tgz", + "integrity": "sha512-9viLL4/n1BJUCT1NXVTdS1jtm80yDEgR5T4yCelII49Mbj0v1rZdKqj7zCiYdbB0CuCgdrvHcNogAKTFPBocFA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/normalize-package-data": "^2.4.3", + "normalize-package-data": "^6.0.0", + "parse-json": "^8.0.0", + "type-fest": "^4.6.0", + "unicorn-magic": "^0.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/read-pkg/node_modules/unicorn-magic": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.1.0.tgz", + "integrity": "sha512-lRfVq8fE8gz6QMBuDM6a+LO3IAzTi05H6gCVaUpir2E1Rwpo4ZUog45KpNXKC/Mn3Yb9UDuHumeFTo9iV/D9FQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve-alpn": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.2.1.tgz", + "integrity": "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==", + "dev": true, + "license": "MIT" + }, + "node_modules/responselike": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/responselike/-/responselike-2.0.1.tgz", + "integrity": "sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "lowercase-keys": "^2.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/restore-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", + "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "onetime": "^5.1.0", + "signal-exit": "^3.0.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/restore-cursor/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rimraf/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/rollup": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.3.tgz", + "integrity": "sha512-pAQK9HalE84QSm4Po3EmWIZPd3FnjkShVkiMlz1iligWYkWQ7wHYd1PF/T7QZ5TVSD6uSTon5gBVMSM4JfBV+A==", + "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.60.3", + "@rollup/rollup-android-arm64": "4.60.3", + "@rollup/rollup-darwin-arm64": "4.60.3", + "@rollup/rollup-darwin-x64": "4.60.3", + "@rollup/rollup-freebsd-arm64": "4.60.3", + "@rollup/rollup-freebsd-x64": "4.60.3", + "@rollup/rollup-linux-arm-gnueabihf": "4.60.3", + "@rollup/rollup-linux-arm-musleabihf": "4.60.3", + "@rollup/rollup-linux-arm64-gnu": "4.60.3", + "@rollup/rollup-linux-arm64-musl": "4.60.3", + "@rollup/rollup-linux-loong64-gnu": "4.60.3", + "@rollup/rollup-linux-loong64-musl": "4.60.3", + "@rollup/rollup-linux-ppc64-gnu": "4.60.3", + "@rollup/rollup-linux-ppc64-musl": "4.60.3", + "@rollup/rollup-linux-riscv64-gnu": "4.60.3", + "@rollup/rollup-linux-riscv64-musl": "4.60.3", + "@rollup/rollup-linux-s390x-gnu": "4.60.3", + "@rollup/rollup-linux-x64-gnu": "4.60.3", + "@rollup/rollup-linux-x64-musl": "4.60.3", + "@rollup/rollup-openbsd-x64": "4.60.3", + "@rollup/rollup-openharmony-arm64": "4.60.3", + "@rollup/rollup-win32-arm64-msvc": "4.60.3", + "@rollup/rollup-win32-ia32-msvc": "4.60.3", + "@rollup/rollup-win32-x64-gnu": "4.60.3", + "@rollup/rollup-win32-x64-msvc": "4.60.3", + "fsevents": "~2.3.2" + } + }, + "node_modules/rollup/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/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/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "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", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "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==", + "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.6.0", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.6.0.tgz", + "integrity": "sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=11.0.0" + } + }, + "node_modules/secretlint": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/secretlint/-/secretlint-10.2.2.tgz", + "integrity": "sha512-xVpkeHV/aoWe4vP4TansF622nBEImzCY73y/0042DuJ29iKIaqgoJ8fGxre3rVSHHbxar4FdJobmTnLp9AU0eg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@secretlint/config-creator": "^10.2.2", + "@secretlint/formatter": "^10.2.2", + "@secretlint/node": "^10.2.2", + "@secretlint/profiler": "^10.2.2", + "debug": "^4.4.1", + "globby": "^14.1.0", + "read-pkg": "^9.0.1" + }, + "bin": { + "secretlint": "bin/secretlint.js" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/semver": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.0.tgz", + "integrity": "sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "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.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4" + }, + "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/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "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==", + "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/simple-get": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", + "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", + "dependencies": { + "decompress-response": "^6.0.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, + "node_modules/simple-invariant": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/simple-invariant/-/simple-invariant-2.0.1.tgz", + "integrity": "sha512-1sbhsxqI+I2tqlmjbz99GXNmZtr6tKIyEgGGnJw/MKGblalqk/XoOYYFJlBzTKZCxx8kLaD3FD5s9BEEjx5Pyg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/slash": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-5.1.0.tgz", + "integrity": "sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/slice-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-4.0.0.tgz", + "integrity": "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "astral-regex": "^2.0.0", + "is-fullwidth-code-point": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/smart-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks": { + "version": "2.8.9", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.9.tgz", + "integrity": "sha512-LJhUYUvItdQ0LkJTmPeaEObWXAqFyfmP85x0tch/ez9cahmhlBBLbIqDFnvBnUJGagb0JbIQrkBs1wJ+yRYpEw==", + "dev": true, + "license": "MIT", + "dependencies": { + "ip-address": "^10.1.1", + "smart-buffer": "^4.2.0" + }, + "engines": { + "node": ">= 10.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks-proxy-agent": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-7.0.0.tgz", + "integrity": "sha512-Fgl0YPZ902wEsAyiQ+idGd1A7rSFx/ayC1CQVMw5P+EQx2V0SgpGtf6OKFhVjPflPUl9YMmEOnmfjCdMUsygww==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^6.0.2", + "debug": "^4.3.3", + "socks": "^2.6.2" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/socks-proxy-agent/node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.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/spdx-correct": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz", + "integrity": "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "spdx-expression-parse": "^3.0.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-exceptions": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.5.0.tgz", + "integrity": "sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==", + "dev": true, + "license": "CC-BY-3.0" + }, + "node_modules/spdx-expression-parse": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", + "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-license-ids": { + "version": "3.0.23", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.23.tgz", + "integrity": "sha512-CWLcCCH7VLu13TgOH+r8p1O/Znwhqv/dbb6lqWy67G+pT1kHmeD/+V36AVb/vq8QMIQwVShJ6Ssl5FPh0fuSdw==", + "dev": true, + "license": "CC0-1.0" + }, + "node_modules/ssri": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/ssri/-/ssri-9.0.1.tgz", + "integrity": "sha512-o57Wcn66jMQvfHG1FlYbWeZWW/dHZhJXjpIcTfXldXEk5nz5lStPo3mK0OJQfGR3RbZUlbISexbljkJzuEj/8Q==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.1.1" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/ssri/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "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==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "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==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/structured-source": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/structured-source/-/structured-source-4.0.0.tgz", + "integrity": "sha512-qGzRFNJDjFieQkl/sVOI2dUjHKRyL9dAJi2gCPGJLbJHBIkyOHxjuocpIEfbLioX+qSJpvbYdT49/YCdMznKxA==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "boundary": "^2.0.0" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-hyperlinks": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/supports-hyperlinks/-/supports-hyperlinks-3.2.0.tgz", + "integrity": "sha512-zFObLMyZeEwzAoKCyu1B91U79K2t7ApXuQfo8OuxwXLDgcKxuwM+YvcbIhm6QWqz7mHUH1TVytR1PwVVjEuMig==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0", + "supports-color": "^7.0.0" + }, + "engines": { + "node": ">=14.18" + }, + "funding": { + "url": "https://github.com/chalk/supports-hyperlinks?sponsor=1" + } + }, + "node_modules/table": { + "version": "6.9.0", + "resolved": "https://registry.npmjs.org/table/-/table-6.9.0.tgz", + "integrity": "sha512-9kY+CygyYM6j02t5YFHbNz2FN5QmYGv9zAjVp4lCDjlCw7amdckXlEt/bjMhUIfj4ThGRE4gCUH5+yGnNuPo5A==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "ajv": "^8.0.1", + "lodash.truncate": "^4.4.2", + "slice-ansi": "^4.0.0", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/table/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/table/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/tar": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", + "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", + "deprecated": "Old versions of tar 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": { + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^5.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "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==", + "license": "MIT", + "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==", + "license": "MIT", + "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/tar/node_modules/chownr": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/tar/node_modules/minipass": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", + "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=8" + } + }, + "node_modules/terminal-link": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/terminal-link/-/terminal-link-4.0.0.tgz", + "integrity": "sha512-lk+vH+MccxNqgVqSnkMVKx4VLJfnLjDBGzH16JVZjKE2DoxP57s6/vt6JmXV5I3jBcfGrxNrYtC+mPtU7WJztA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-escapes": "^7.0.0", + "supports-hyperlinks": "^3.2.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", + "dev": true, + "license": "MIT" + }, + "node_modules/textextensions": { + "version": "6.11.0", + "resolved": "https://registry.npmjs.org/textextensions/-/textextensions-6.11.0.tgz", + "integrity": "sha512-tXJwSr9355kFJI3lbCkPpUH5cP8/M0GGy2xLO34aZCjMXBaK3SoPnZwr/oWmo1FdCnELcs4npdCIOFtq9W3ruQ==", + "dev": true, + "license": "Artistic-2.0", + "dependencies": { + "editions": "^6.21.0" + }, + "engines": { + "node": ">=4" + }, + "funding": { + "url": "https://bevry.me/fund" + } + }, + "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": "0.3.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinypool": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", + "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + } + }, + "node_modules/tinyrainbow": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-1.2.0.tgz", + "integrity": "sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-3.0.2.tgz", + "integrity": "sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==", + "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/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "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/tsx": { + "version": "4.21.1", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.1.tgz", + "integrity": "sha512-5QE2Q04cN1u0993w0LT5rPw3faZqZU1fFn1mGE0pV53N1Dn7c+QFFxQu1mBeSgeOXwFyTicZw02wVgp3Tb5cAQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.27.0" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "node_modules/tsx/node_modules/@esbuild/aix-ppc64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.7.tgz", + "integrity": "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/android-arm": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.7.tgz", + "integrity": "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/android-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.7.tgz", + "integrity": "sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/android-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.7.tgz", + "integrity": "sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/darwin-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.7.tgz", + "integrity": "sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/darwin-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.7.tgz", + "integrity": "sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.7.tgz", + "integrity": "sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/freebsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.7.tgz", + "integrity": "sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-arm": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.7.tgz", + "integrity": "sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.7.tgz", + "integrity": "sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-ia32": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.7.tgz", + "integrity": "sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-loong64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.7.tgz", + "integrity": "sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-mips64el": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.7.tgz", + "integrity": "sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-ppc64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.7.tgz", + "integrity": "sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-riscv64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.7.tgz", + "integrity": "sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-s390x": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.7.tgz", + "integrity": "sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.7.tgz", + "integrity": "sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/netbsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.7.tgz", + "integrity": "sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/openbsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.7.tgz", + "integrity": "sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/sunos-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.7.tgz", + "integrity": "sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/win32-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.7.tgz", + "integrity": "sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/win32-ia32": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.7.tgz", + "integrity": "sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/win32-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.7.tgz", + "integrity": "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/esbuild": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.7.tgz", + "integrity": "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.7", + "@esbuild/android-arm": "0.27.7", + "@esbuild/android-arm64": "0.27.7", + "@esbuild/android-x64": "0.27.7", + "@esbuild/darwin-arm64": "0.27.7", + "@esbuild/darwin-x64": "0.27.7", + "@esbuild/freebsd-arm64": "0.27.7", + "@esbuild/freebsd-x64": "0.27.7", + "@esbuild/linux-arm": "0.27.7", + "@esbuild/linux-arm64": "0.27.7", + "@esbuild/linux-ia32": "0.27.7", + "@esbuild/linux-loong64": "0.27.7", + "@esbuild/linux-mips64el": "0.27.7", + "@esbuild/linux-ppc64": "0.27.7", + "@esbuild/linux-riscv64": "0.27.7", + "@esbuild/linux-s390x": "0.27.7", + "@esbuild/linux-x64": "0.27.7", + "@esbuild/netbsd-arm64": "0.27.7", + "@esbuild/netbsd-x64": "0.27.7", + "@esbuild/openbsd-arm64": "0.27.7", + "@esbuild/openbsd-x64": "0.27.7", + "@esbuild/openharmony-arm64": "0.27.7", + "@esbuild/sunos-x64": "0.27.7", + "@esbuild/win32-arm64": "0.27.7", + "@esbuild/win32-ia32": "0.27.7", + "@esbuild/win32-x64": "0.27.7" + } + }, + "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==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, + "node_modules/type-fest": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", + "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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": "2.1.0", + "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz", + "integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==", + "dev": true, + "license": "MIT" + }, + "node_modules/underscore": { + "version": "1.13.8", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.8.tgz", + "integrity": "sha512-DXtD3ZtEQzc7M8m4cXotyHR+FAS18C64asBYY5vqZexfYryNNnDc02W4hKg3rdQuqOYas1jkseX0+nZXjTXnvQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/undici": { + "version": "7.25.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.25.0.tgz", + "integrity": "sha512-xXnp4kTyor2Zq+J1FfPI6Eq3ew5h6Vl0F/8d9XU5zZQf1tX9s2Su1/3PiMmUANFULpmksxkClamIZcaUqryHsQ==", + "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/unicorn-magic": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.3.0.tgz", + "integrity": "sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/unique-filename": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-2.0.1.tgz", + "integrity": "sha512-ODWHtkkdx3IAR+veKxFV+VBkUMcN+FaqzUUd7IZzt+0zhDZFPFxhlqwPF3YQvMHx1TD0tdgYl+kuPnJ8E6ql7A==", + "dev": true, + "license": "ISC", + "dependencies": { + "unique-slug": "^3.0.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/unique-slug": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-3.0.0.tgz", + "integrity": "sha512-8EyMynh679x/0gqE9fT9oilG+qEt+ibFyqjuVTsZn1+CMxH+XLlpvr2UZx4nVcCwTpx81nICr2JQFkM+HPLq4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "imurmurhash": "^0.1.4" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "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==", + "license": "MIT" + }, + "node_modules/validate-npm-package-license": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", + "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "spdx-correct": "^3.0.0", + "spdx-expression-parse": "^3.0.0" + } + }, + "node_modules/version-range": { + "version": "4.15.0", + "resolved": "https://registry.npmjs.org/version-range/-/version-range-4.15.0.tgz", + "integrity": "sha512-Ck0EJbAGxHwprkzFO966t4/5QkRuzh+/I1RxhLgUKKwEn+Cd8NwM60mE3AqBZg5gYODoXW0EFsQvbZjRlvdqbg==", + "dev": true, + "license": "Artistic-2.0", + "engines": { + "node": ">=4" + }, + "funding": { + "url": "https://bevry.me/fund" + } + }, + "node_modules/vite": { + "version": "5.4.21", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/vite-node": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-2.1.9.tgz", + "integrity": "sha512-AM9aQ/IPrW/6ENLQg3AGY4K1N2TGZdR5e4gu/MmmR2xR3Ll1+dib+nook92g4TV3PXVyeyxdWwtaCAiUL0hMxA==", + "dev": true, + "license": "MIT", + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.3.7", + "es-module-lexer": "^1.5.4", + "pathe": "^1.1.2", + "vite": "^5.0.0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vitest": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-2.1.9.tgz", + "integrity": "sha512-MSmPM9REYqDGBI8439mA4mWhV5sKmDlBKWIYbA3lRb2PTHACE0mgKwA8yQ2xq9vxDTuk4iPrECBAEW2aoFXY0Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "2.1.9", + "@vitest/mocker": "2.1.9", + "@vitest/pretty-format": "^2.1.9", + "@vitest/runner": "2.1.9", + "@vitest/snapshot": "2.1.9", + "@vitest/spy": "2.1.9", + "@vitest/utils": "2.1.9", + "chai": "^5.1.2", + "debug": "^4.3.7", + "expect-type": "^1.1.0", + "magic-string": "^0.30.12", + "pathe": "^1.1.2", + "std-env": "^3.8.0", + "tinybench": "^2.9.0", + "tinyexec": "^0.3.1", + "tinypool": "^1.0.1", + "tinyrainbow": "^1.2.0", + "vite": "^5.0.0", + "vite-node": "2.1.9", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/node": "^18.0.0 || >=20.0.0", + "@vitest/browser": "2.1.9", + "@vitest/ui": "2.1.9", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "node_modules/wcwidth": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", + "integrity": "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==", + "dev": true, + "license": "MIT", + "dependencies": { + "defaults": "^1.0.3" + } + }, + "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/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "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/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "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==", + "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/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "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/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yauzl": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-3.3.0.tgz", + "integrity": "sha512-PtGEvEP30p7sbIBJKUBjUnqgTVOyMURc4dLo9iNyAJnNIEz9pm88cCXF21w94Kg3k6RXkeZh5DHOGS0qEONvNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-crc32": "~0.2.3", + "pend": "~1.2.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yauzl-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yauzl-promise/-/yauzl-promise-4.0.0.tgz", + "integrity": "sha512-/HCXpyHXJQQHvFq9noqrjfa/WpQC2XYs3vI7tBiAi4QiIU1knvYhZGaO1QPjwIVMdqflxbmwgMXtYeaRiAE0CA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@node-rs/crc32": "^1.7.0", + "is-it-type": "^5.1.2", + "simple-invariant": "^2.0.1" + }, + "engines": { + "node": ">=16" + } + }, + "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/src/ext-vscode/package.json b/src/ext-vscode/package.json new file mode 100644 index 00000000..5f530349 --- /dev/null +++ b/src/ext-vscode/package.json @@ -0,0 +1,107 @@ +{ + "name": "nexpath-vscode", + "displayName": "Nexpath", + "description": "Coding-agent prompt guidance for Cursor and Windsurf — between-prompt advisory that helps you stop, reconsider, and reframe before sending.", + "version": "0.1.3", + "publisher": "emptyops", + "license": "Apache-2.0", + "private": true, + "type": "module", + "engines": { + "vscode": "^1.80.0", + "node": ">=18" + }, + "categories": [ + "Other", + "Machine Learning", + "Programming Languages" + ], + "keywords": [ + "cursor", + "windsurf", + "ai", + "prompt", + "guidance", + "advisory", + "nexpath" + ], + "homepage": "https://github.com/hi0001234d/nexpath", + "repository": { + "type": "git", + "url": "https://github.com/hi0001234d/nexpath.git", + "directory": "src/ext-vscode" + }, + "bugs": { + "url": "https://github.com/hi0001234d/nexpath/issues" + }, + "activationEvents": [ + "onStartupFinished" + ], + "main": "./out/extension.cjs", + "icon": "media/icon.png", + "galleryBanner": { + "color": "#1e1e2e", + "theme": "dark" + }, + "contributes": { + "viewsContainers": { + "activitybar": [ + { + "id": "nexpath", + "title": "Nexpath", + "icon": "media/icon.svg" + } + ] + }, + "views": { + "nexpath": [ + { + "id": "nexpath.status", + "name": "Nexpath", + "type": "webview" + } + ] + }, + "commands": [ + { + "command": "nexpath.showAdvisory", + "title": "Show pending advisory", + "category": "Nexpath" + } + ] + }, + "scripts": { + "vscode:prepublish": "npm run build", + "build": "node esbuild.config.mjs", + "watch": "node esbuild.config.mjs --watch", + "typecheck": "tsc --noEmit", + "test": "vitest run", + "dump-cursor-state": "tsx scripts/dump-cursor-state.ts", + "rebuild:electron": "electron-rebuild --version $npm_package_nexpathTargets_electron --force --only better-sqlite3 --module-dir .", + "rebuild:node": "npm rebuild better-sqlite3", + "package": "node scripts/package-with-electron-abi.mjs", + "package:all-platforms": "node scripts/package-per-platform.mjs", + "publish:ovsx": "ovsx publish", + "publish:vsce": "vsce publish" + }, + "nexpathTargets": { + "comment": "Target Electron version for the native better-sqlite3 binding inside .vsix. Cursor 3.4.20 + recent VS Code use Electron 39 (NODE_MODULE_VERSION 140). Bump this when Cursor / VS Code upgrade Electron; locked by src/native-binding-abi.test.ts.", + "electron": "39.0.0", + "electronAbi": 140 + }, + "dependencies": { + "better-sqlite3": "^12" + }, + "devDependencies": { + "@electron/rebuild": "^3.7.2", + "@types/better-sqlite3": "^7", + "@types/node": "^20", + "@types/vscode": "^1.80.0", + "@vscode/vsce": "^3", + "esbuild": "^0.21", + "ovsx": "^0.10", + "tsx": "^4", + "typescript": "^5", + "vitest": "^2" + } +} diff --git a/src/ext-vscode/scripts/dump-cursor-state.ts b/src/ext-vscode/scripts/dump-cursor-state.ts new file mode 100644 index 00000000..a6cdc26c --- /dev/null +++ b/src/ext-vscode/scripts/dump-cursor-state.ts @@ -0,0 +1,213 @@ +#!/usr/bin/env node +/** + * scripts/dump-cursor-state.ts — capture verified `state.vscdb` fixtures + * from a real machine for extractor regression testing. + * + * Pure helpers (filtering, redaction, arg parsing, path resolution, + * discovery) live in `src/cursor-state-dump-helpers.ts` so they're + * typechecked and unit-tested. This file owns only the I/O orchestration: + * copy-to-staging-dir, SQLite read via better-sqlite3, and writing + * fixture JSON files. + * + * Run after Cursor has been used (especially after real prompts have been + * sent) so the fixtures contain the actual chat data. See the JSDoc in + * each `src/extractors/*.ts` file for current schema-finding status. + * + * Usage: + * npx tsx scripts/dump-cursor-state.ts --name [--redact] [--src ] + */ + +import { writeFile, mkdir, copyFile } from 'node:fs/promises'; +import { existsSync } from 'node:fs'; +import { platform as osPlatform, tmpdir } from 'node:os'; +import { join, dirname, resolve, basename } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { + cursorConfigRoot, + discoverAllStateVscdb, + parseArgs, + redactValue, + shouldKeepItemTable, + type DiscoveredDb, +} from '../src/cursor-state-dump-helpers.js'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); + +interface DumpedRow { + table: 'ItemTable' | 'cursorDiskKV'; + key: string; + value: string; +} + +interface DumpedDb { + capturedAt: string; + sourcePath: string; + platform: string; + redacted: boolean; + /** Names of all tables present (useful for schema-fingerprint diagnostics). */ + tables: string[]; + /** Rows kept after filtering. */ + rows: DumpedRow[]; +} + +function printUsage(): void { + console.log(`Usage: + npx tsx scripts/dump-cursor-state.ts --name [--src ] [--redact] + +Options: + --name Fixture name prefix (no extension). REQUIRED. + One output file per discovered state.vscdb, suffixed with + the source kind (global / workspace-). + --src Path to a single state.vscdb. If omitted, all state.vscdb + files under the OS's Cursor config tree are dumped. + --redact Replace string values longer than 8 chars with same-length + placeholders. Use this if dumps may contain sensitive content. + +Output: + src/ext-vscode/test-fixtures/state-vscdb-samples/-.json +`); +} + +/** + * better-sqlite3 is WAL-aware: it reads the main DB and the sibling -wal/-shm + * transparently. We copy all three siblings to a tmp dir so we never touch + * the file Cursor is actively writing to. + */ +async function readDbAsSnapshot(path: string): Promise { + const stagingDir = join( + tmpdir(), + `nexpath-dump-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`, + ); + await mkdir(stagingDir, { recursive: true }); + const stagedMain = join(stagingDir, basename(path)); + await copyFile(path, stagedMain); + for (const suffix of ['-wal', '-shm'] as const) { + const sibling = path + suffix; + if (existsSync(sibling)) { + await copyFile(sibling, stagedMain + suffix); + } + } + + const mod = (await import('better-sqlite3')) as unknown as { + default: new (path: string, options?: { readonly?: boolean }) => { + prepare(sql: string): { + all(...params: unknown[]): Array>; + }; + pragma(pragma: string): unknown; + close(): void; + }; + }; + const Database = mod.default; + const db = new Database(stagedMain, { readonly: true }); + try { + db.pragma('wal_checkpoint(TRUNCATE)'); + } catch { + // not WAL — fine + } + + const dump: DumpedDb = { + capturedAt: new Date().toISOString(), + sourcePath: path, + platform: osPlatform(), + redacted: false, + tables: [], + rows: [], + }; + + const tables = ( + db + .prepare( + "SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'", + ) + .all() as Array<{ name: string }> + ).map((r) => r.name); + dump.tables = tables; + + if (tables.includes('ItemTable')) { + const rows = db + .prepare('SELECT key, value FROM ItemTable') + .all() as Array<{ key: string; value: string }>; + for (const r of rows) { + if (!shouldKeepItemTable(r.key)) continue; + dump.rows.push({ table: 'ItemTable', key: r.key, value: r.value }); + } + } + if (tables.includes('cursorDiskKV')) { + const rows = db + .prepare('SELECT key, value FROM cursorDiskKV') + .all() as Array<{ key: string; value: string }>; + for (const r of rows) { + dump.rows.push({ table: 'cursorDiskKV', key: r.key, value: r.value }); + } + } + + db.close(); + return dump; +} + +async function main(): Promise { + const parsed = parseArgs(process.argv.slice(2)); + if (!parsed.ok) { + if (parsed.help) { + printUsage(); + process.exit(0); + } + console.error(parsed.error); + printUsage(); + process.exit(1); + } + const args = parsed.args; + + const targets: DiscoveredDb[] = args.src + ? [{ path: args.src, label: 'src' }] + : discoverAllStateVscdb(cursorConfigRoot()); + + if (targets.length === 0) { + console.error(`No state.vscdb files found under: ${cursorConfigRoot()}`); + console.error('Pass --src for a single file.'); + process.exit(2); + } + + console.log(`Discovered ${targets.length} state.vscdb file(s):`); + for (const t of targets) console.log(` - [${t.label}] ${t.path}`); + console.log(''); + + const subPackageRoot = resolve(__dirname, '..'); + const outDir = join(subPackageRoot, 'test-fixtures', 'state-vscdb-samples'); + await mkdir(outDir, { recursive: true }); + + for (const t of targets) { + try { + const dump = await readDbAsSnapshot(t.path); + if (args.redact) { + dump.redacted = true; + for (const row of dump.rows) { + row.value = redactValue(row.value); + } + } + const outPath = join(outDir, `${args.name}-${t.label}.json`); + await writeFile(outPath, JSON.stringify(dump, null, 2) + '\n', 'utf8'); + console.log( + `[${t.label}] ${dump.rows.length} rows kept (${dump.tables.join( + ', ', + )}) → ${outPath}`, + ); + } catch (err) { + const e = err instanceof Error ? err : new Error(String(err)); + console.error(`[${t.label}] ERROR: ${e.message}`); + } + } + + console.log(''); + console.log('Next steps:'); + console.log(' 1. Review the JSON files for sensitive content before committing.'); + console.log(' 2. Re-run with --redact if needed.'); + console.log(' 3. Reference these fixtures from extractor regression tests.'); +} + +main().catch((err: unknown) => { + const e = err instanceof Error ? err : new Error(String(err)); + console.error(`Error: ${e.message}`); + process.exit(1); +}); diff --git a/src/ext-vscode/scripts/package-per-platform.mjs b/src/ext-vscode/scripts/package-per-platform.mjs new file mode 100644 index 00000000..eb7708ff --- /dev/null +++ b/src/ext-vscode/scripts/package-per-platform.mjs @@ -0,0 +1,117 @@ +#!/usr/bin/env node +/** + * scripts/package-per-platform.mjs — produces one .vsix per VS Code + * platform target so the marketplace can serve the right prebuilt + * better-sqlite3 binary to each user. + * + * Background: + * `better-sqlite3` is a native module (.node binding). VS Code + * extensions can't bundle native modules across platforms in a single + * .vsix — the marketplace's "platform-specific extensions" feature + * solves this. We publish one .vsix per (os, arch) pair, each + * tagged via `vsce package --target `, and the marketplace + * serves the right one based on the user's OS. + * + * Usage: + * node scripts/package-per-platform.mjs # all 5 targets → ./dist-vsix/ + * node scripts/package-per-platform.mjs --targets darwin-arm64,linux-x64 + * node scripts/package-per-platform.mjs --out /tmp/vsix + * + * Pre-flight (each platform's .vsix must be built ON that platform OR + * the better-sqlite3 prebuild must be downloaded for the target): + * - For local dev: this script runs `vsce package --target ` on + * the current machine. The resulting .vsix includes the + * better-sqlite3 build that's currently in node_modules — which + * matches THIS machine's OS. To produce a .vsix for a different OS, + * run this script on that OS (or use the GitHub Actions workflow + * at .github/workflows/publish-extension.yml). + * + * - For CI: the matrix runner naturally handles this — each OS job + * produces its own platform's .vsix. + */ + +import { spawnSync } from 'node:child_process'; +import { mkdir } from 'node:fs/promises'; +import { readFileSync } from 'node:fs'; +import { join, dirname, resolve } from 'node:path'; +import { fileURLToPath } from 'node:url'; +// The helpers live under src/ (so they're typechecked by the sub-package's +// tsconfig + auto-discovered by vitest). esbuild compiles them into +// out/package-per-platform-helpers.js when `npm run build` runs. +import { + parsePackagerArgs, + buildVsixFilename, +} from '../out/package-per-platform-helpers.js'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); +const subPackageRoot = resolve(__dirname, '..'); + +function readPackageJson() { + const raw = readFileSync(join(subPackageRoot, 'package.json'), 'utf8'); + const pkg = JSON.parse(raw); + if (typeof pkg.name !== 'string' || typeof pkg.version !== 'string') { + throw new Error('package.json is missing name or version'); + } + return { name: pkg.name, version: pkg.version }; +} + +async function main() { + let args; + try { + args = parsePackagerArgs(process.argv.slice(2)); + } catch (err) { + console.error(err instanceof Error ? err.message : String(err)); + console.error(''); + console.error('Usage:'); + console.error(' node scripts/package-per-platform.mjs [--targets a,b,c] [--out ]'); + process.exit(1); + } + + const { name, version } = readPackageJson(); + const outDir = resolve(subPackageRoot, args.outDir); + await mkdir(outDir, { recursive: true }); + + console.log(`Building ${args.targets.length} .vsix file(s) for ${name}@${version}`); + console.log(`Output dir: ${outDir}`); + console.log(''); + + // Each per-platform package call goes through package-with-electron-abi.mjs + // so better-sqlite3 is rebuilt against the target Electron ABI before vsce + // package runs, and restored to the system-Node ABI afterwards. Skipping + // that wrapper would package a Node-ABI binary inside the .vsix and Cursor + // would fail to activate with NODE_MODULE_VERSION 127 vs 140 (see the + // wrapper script's JSDoc for the full story). + const wrapperScript = join(subPackageRoot, 'scripts', 'package-with-electron-abi.mjs'); + const results = []; + for (const target of args.targets) { + const filename = buildVsixFilename(name, target, version); + const outPath = join(outDir, filename); + console.log(`▸ ${target} → ${filename}`); + const r = spawnSync( + 'node', + [wrapperScript, '--target', target, '--out', outPath], + { stdio: 'inherit', cwd: subPackageRoot }, + ); + results.push({ target, ok: r.status === 0, exitCode: r.status }); + if (r.status !== 0) { + console.error(` ✗ vsce package (with electron rebuild) failed for ${target} (exit ${r.status})`); + } + } + + console.log(''); + console.log('Summary:'); + for (const { target, ok } of results) { + console.log(` ${ok ? '✓' : '✗'} ${target}`); + } + const failed = results.filter((r) => !r.ok); + if (failed.length > 0) { + console.error(`${failed.length}/${results.length} target(s) failed.`); + process.exit(2); + } +} + +main().catch((err) => { + console.error(err instanceof Error ? err.stack : String(err)); + process.exit(1); +}); diff --git a/src/ext-vscode/scripts/package-with-electron-abi.mjs b/src/ext-vscode/scripts/package-with-electron-abi.mjs new file mode 100644 index 00000000..14a8ee84 --- /dev/null +++ b/src/ext-vscode/scripts/package-with-electron-abi.mjs @@ -0,0 +1,112 @@ +#!/usr/bin/env node +/** + * scripts/package-with-electron-abi.mjs + * + * Wraps `vsce package` with the Electron-ABI dance for native modules: + * + * 1. Rebuild `better-sqlite3` against the target Electron version's + * headers — the binary inside the .vsix MUST match the host's + * Electron ABI (NODE_MODULE_VERSION 140 for Electron 39 / Cursor 3.4.20) + * or activation fails with the ABI-mismatch error users have seen. + * + * 2. Run `vsce package` (forwards any extra argv after `--`, e.g. + * `--target linux-x64 --out dist-vsix/`). + * + * 3. ALWAYS restore the system-Node binary via `npm rebuild better-sqlite3`, + * whether step 2 succeeded or failed. Without this, the next + * `npm test` / `npm run dump-cursor-state` invocation crashes with + * "NODE_MODULE_VERSION 140 requires 127" because tests run under + * system Node ABI, not Electron's. + * + * Exit code is forwarded from `vsce package` so CI build steps surface + * packaging failures correctly. Cleanup runs in a finally-style block. + * + * Target Electron version is read from `package.json#nexpathTargets.electron`; + * locked there + cross-checked by `src/native-binding-abi.test.ts`. + */ + +import { spawnSync } from 'node:child_process'; +import { readFileSync } from 'node:fs'; +import { fileURLToPath } from 'node:url'; +import { dirname, resolve } from 'node:path'; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const subPkgRoot = resolve(__dirname, '..'); + +function readTargetElectron() { + const pkg = JSON.parse( + readFileSync(resolve(subPkgRoot, 'package.json'), 'utf8'), + ); + const v = pkg?.nexpathTargets?.electron; + if (typeof v !== 'string' || !/^\d+\.\d+\.\d+/.test(v)) { + throw new Error( + `package.json#nexpathTargets.electron must be a semver string (got ${JSON.stringify(v)})`, + ); + } + return v; +} + +function run(label, cmd, args, opts = {}) { + console.log(`▸ ${label}`); + console.log(` $ ${cmd} ${args.join(' ')}`); + // On Windows, `npx` / `npm` / `vsce` are `.cmd` shims. Node's spawn (esp. on + // recent versions, post CVE-2024-27980) refuses to execute `.cmd`/`.bat` + // without a shell, so `spawnSync('npx', …)` fails with exit 1 BEFORE the tool + // even runs — which aborted packaging and left no .vsix (then + // `code --install-extension` failed with ENOENT). Running through the shell + // lets Windows resolve the `.cmd` shim. POSIX is unaffected (shell stays false). + const useShell = process.platform === 'win32'; + const r = spawnSync(cmd, args, { stdio: 'inherit', cwd: subPkgRoot, shell: useShell, ...opts }); + if (r.error) { + // Surface the underlying spawn error (was swallowed → opaque "exit 1"). + console.error(` ✗ spawn failed: ${r.error.message}`); + } + return r.status ?? 1; +} + +const electronVersion = readTargetElectron(); +const extraArgs = process.argv.slice(2); + +console.log(`Packaging nexpath-vscode with better-sqlite3 rebuilt against Electron ${electronVersion}`); +console.log(''); + +// `--force` is required. Without it, @electron/rebuild detects "already +// built for this Electron version" via a marker it left from a prior run, +// AND skips rebuilding — even when the binary currently on disk is the +// Node-ABI prebuilt restored by the prior orchestrator run's cleanup step. +// Net effect of skipping the rebuild + then npm rebuild restoring Node-ABI: +// the .vsix ships the Node-ABI binary, Cursor fails to activate with the +// NODE_MODULE_VERSION mismatch error. This is exactly the bug that the +// "Round 1 install" surfaced. +const rebuildElectronCode = run( + `Rebuild better-sqlite3 → Electron ${electronVersion} (ABI for Cursor)`, + 'npx', + ['electron-rebuild', '--version', electronVersion, '--force', '--only', 'better-sqlite3', '--module-dir', '.'], +); +if (rebuildElectronCode !== 0) { + console.error(`✗ electron-rebuild failed with exit ${rebuildElectronCode} — aborting before vsce package.`); + // Best-effort restore so the dev tree isn't left half-built. + run('Cleanup: restore Node-ABI binary', 'npm', ['rebuild', 'better-sqlite3']); + process.exit(rebuildElectronCode); +} + +let packageCode = 0; +try { + packageCode = run(`vsce package ${extraArgs.join(' ')}`.trim(), 'npx', ['vsce', 'package', ...extraArgs]); +} finally { + const restoreCode = run('Restore Node-ABI binary (so tests + dev work again)', 'npm', ['rebuild', 'better-sqlite3']); + if (restoreCode !== 0) { + console.error( + `✗ npm rebuild better-sqlite3 failed (exit ${restoreCode}) — your local node_modules has the Electron-ABI binary still. Run "npm rebuild better-sqlite3" manually before running tests.`, + ); + } +} + +if (packageCode !== 0) { + console.error(`✗ vsce package failed with exit ${packageCode}.`); + process.exit(packageCode); +} + +console.log(''); +console.log(`✓ Packaged. The .vsix inside contains better-sqlite3 built for Electron ${electronVersion} (ABI for Cursor / Windsurf).`); +console.log(' Local node_modules/better-sqlite3 has been restored to the system-Node ABI so npm test continues to work.'); diff --git a/src/ext-vscode/src/advisory-fallback.test.ts b/src/ext-vscode/src/advisory-fallback.test.ts new file mode 100644 index 00000000..6f6e5c34 --- /dev/null +++ b/src/ext-vscode/src/advisory-fallback.test.ts @@ -0,0 +1,135 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { + createAdvisoryFallback, + toPayload, + STATUS_BAR_TEXT, + type StatusBarHandle, +} from './advisory-fallback.js'; +import type { StoredAdvisory } from './advisory-store-reader.js'; + +const advisory = (overrides: Partial = {}): StoredAdvisory => ({ + projectRoot: '/proj', + stage: 'implementation', + flagType: 'stage_transition', + pinchLabel: 'Quick check.', + createdAt: 1000, + status: 'shown', + l1: ['Full: run the suite'], + l2: ['Lighter: run changed tests'], + l3: ['Min: smoke test'], + ...overrides, +}); + +describe('toPayload', () => { + it('maps L1 (full-depth) options to a webview payload', () => { + expect(toPayload(advisory())).toEqual({ + advisory: 'Quick check.', + options: [{ id: '0', label: 'Full: run the suite' }], + }); + }); + it('falls back to L2 then L3 when earlier levels are empty', () => { + expect(toPayload(advisory({ l1: [], l2: ['l2a', 'l2b'], l3: [] })).options).toEqual([ + { id: '0', label: 'l2a' }, + { id: '1', label: 'l2b' }, + ]); + expect(toPayload(advisory({ l1: [], l2: [], l3: ['l3a'] })).options).toEqual([ + { id: '0', label: 'l3a' }, + ]); + }); + it('uses a generic headline when pinchLabel is empty', () => { + expect(toPayload(advisory({ pinchLabel: '' })).advisory).toBe('Nexpath advisory'); + }); +}); + +describe('createAdvisoryFallback', () => { + let statusBar: StatusBarHandle & { show: ReturnType; hide: ReturnType }; + let publishPayload: ReturnType; + let showInfoOnce: ReturnType; + let read: ReturnType; + + beforeEach(() => { + statusBar = { show: vi.fn(), hide: vi.fn() }; + publishPayload = vi.fn(); + showInfoOnce = vi.fn(); + read = vi.fn(); + }); + + it('lights the status bar + first-time hint when a fresh advisory exists', async () => { + read.mockResolvedValue(advisory({ createdAt: 5000 })); + const fb = createAdvisoryFallback({ + publishPayload, statusBar, showInfoOnce, read, + now: () => 6000, recentWindowMs: 60_000, + }); + await fb.armIfPending('/proj'); + expect(statusBar.show).toHaveBeenCalledWith(STATUS_BAR_TEXT, expect.stringContaining('Quick check.')); + expect(showInfoOnce).toHaveBeenCalledOnce(); + }); + + it('does NOT surface when there is no advisory', async () => { + read.mockResolvedValue(null); + const fb = createAdvisoryFallback({ publishPayload, statusBar, showInfoOnce, read }); + await fb.armIfPending('/proj'); + expect(statusBar.show).not.toHaveBeenCalled(); + expect(showInfoOnce).not.toHaveBeenCalled(); + }); + + it('does NOT surface a stale advisory (older than the recency window)', async () => { + read.mockResolvedValue(advisory({ createdAt: 1000 })); + const fb = createAdvisoryFallback({ + publishPayload, statusBar, showInfoOnce, read, + now: () => 1_000_000, recentWindowMs: 60_000, + }); + await fb.armIfPending('/proj'); + expect(statusBar.show).not.toHaveBeenCalled(); + }); + + it('shows the first-time hint only once', async () => { + read.mockResolvedValue(advisory({ createdAt: 5000 })); + const fb = createAdvisoryFallback({ + publishPayload, statusBar, showInfoOnce, read, now: () => 6000, + }); + await fb.armIfPending('/proj'); + await fb.armIfPending('/proj'); + expect(showInfoOnce).toHaveBeenCalledOnce(); + expect(statusBar.show).toHaveBeenCalledTimes(2); + }); + + it('never throws when the reader rejects', async () => { + read.mockRejectedValue(new Error('db gone')); + const fb = createAdvisoryFallback({ publishPayload, statusBar, read }); + await expect(fb.armIfPending('/proj')).resolves.toBeUndefined(); + expect(statusBar.show).not.toHaveBeenCalled(); + }); + + it('clear() hides the status bar', () => { + const fb = createAdvisoryFallback({ publishPayload, statusBar, read }); + fb.clear(); + expect(statusBar.hide).toHaveBeenCalledOnce(); + }); + + it('showAdvisory() publishes the waiting advisory to the webview and hides the status bar', async () => { + read.mockResolvedValue(advisory({ createdAt: 5000 })); + const fb = createAdvisoryFallback({ publishPayload, statusBar, read, now: () => 6000 }); + await fb.armIfPending('/proj'); // arms pendingProject + await fb.showAdvisory(); + expect(publishPayload).toHaveBeenCalledWith(toPayload(advisory({ createdAt: 5000 }))); + expect(statusBar.hide).toHaveBeenCalled(); + }); + + it('showAdvisory() is a no-op when nothing is pending', async () => { + const fb = createAdvisoryFallback({ publishPayload, statusBar, read }); + await fb.showAdvisory(); + expect(read).not.toHaveBeenCalled(); + expect(publishPayload).not.toHaveBeenCalled(); + }); + + it('showAdvisory() hides the status bar if the advisory vanished from the store', async () => { + read.mockResolvedValueOnce(advisory({ createdAt: 5000 })); // arm + read.mockResolvedValueOnce(null); // gone by the time we open it + const fb = createAdvisoryFallback({ publishPayload, statusBar, read, now: () => 6000 }); + await fb.armIfPending('/proj'); + await fb.showAdvisory(); + expect(publishPayload).not.toHaveBeenCalled(); + expect(statusBar.hide).toHaveBeenCalled(); + }); +}); diff --git a/src/ext-vscode/src/advisory-fallback.ts b/src/ext-vscode/src/advisory-fallback.ts new file mode 100644 index 00000000..55c82f95 --- /dev/null +++ b/src/ext-vscode/src/advisory-fallback.ts @@ -0,0 +1,139 @@ +import type { DecisionSessionPayload } from './ipc.js'; +import { + readLatestAdvisory, + type StoredAdvisory, + type AdvisoryStoreReaderDeps, +} from './advisory-store-reader.js'; + +/** + * In-editor advisory fallback (Layer B only). + * + * The terminal popup Layer C spawns on `nexpath stop` is the primary advisory + * surface, but it is environment-fragile: on Linux it needs a GUI terminal + * emulator + `$DISPLAY`/`$WAYLAND_DISPLAY` + a session bus, and even then can + * land behind the editor. When it doesn't appear, the advisory Layer C already + * generated would be lost. + * + * This module is the safety net: when a prompt cycle finishes WITHOUT the user + * selecting in the terminal popup, it checks nexpath's store (read-only) for a + * fresh advisory and, if one exists, lights a status-bar item. Clicking it (the + * `nexpath.showAdvisory` command) reveals that advisory in the extension's own + * webview — a surface that always works, on every OS, with no terminal at all. + * Selecting an option there injects it into the chat input via the same path the + * terminal popup uses. Layer C is untouched: we only READ the row it wrote. + * + * Pure + dependency-injected (no `vscode` import) so it unit-tests without a + * host; `extension.ts` supplies the real status-bar / webview / toast bindings. + */ + +/** Minimal status-bar surface the fallback drives (real impl backed by `vscode.StatusBarItem`). */ +export interface StatusBarHandle { + show(text: string, tooltip: string): void; + hide(): void; +} + +export interface AdvisoryFallbackDeps { + /** Push a payload to the webview (`view-provider.publishPayload`). */ + publishPayload: (payload: DecisionSessionPayload) => void; + /** The status-bar surface to light when an advisory is waiting. */ + statusBar: StatusBarHandle; + /** Read the latest stored advisory for a project (defaults to the real store reader). */ + read?: (projectRoot: string) => Promise; + /** Reader options (db path etc.) passed through to the default reader. */ + readerDeps?: AdvisoryStoreReaderDeps; + /** Show a one-time discoverability toast (caller gates "once-ever" via globalState). */ + showInfoOnce?: (message: string) => void; + /** Injectable clock (tests). */ + now?: () => number; + /** + * How recent (ms) an advisory must be to count as "from this cycle". Guards + * against resurfacing a stale advisory from earlier in the session on every + * subsequent prompt. Defaults to 60s. + */ + recentWindowMs?: number; +} + +export const STATUS_BAR_TEXT = '$(lightbulb) Nexpath advisory'; +export const FIRST_TIME_HINT = + 'Nexpath has an advisory ready. If the terminal popup didn’t appear, ' + + 'click “Nexpath advisory” in the status bar to view it here.'; + +export interface AdvisoryFallback { + /** + * Call right after `auto` parks an advisory (before the terminal popup). + * Surfaces the status bar if a fresh advisory exists for `projectRoot` so the + * user has an in-editor path regardless of whether the popup opens; no-op + * otherwise. + */ + armIfPending(projectRoot: string): Promise; + /** Call when the user DID select in the terminal popup — clears the pending fallback. */ + clear(): void; + /** Command handler (`nexpath.showAdvisory`): reveal the waiting advisory in the webview. */ + showAdvisory(): Promise; +} + +/** Map a stored advisory → the webview payload. Prefers L1 (full-depth) options. */ +export function toPayload(a: StoredAdvisory): DecisionSessionPayload { + const options = a.l1.length ? a.l1 : a.l2.length ? a.l2 : a.l3; + return { + advisory: a.pinchLabel || 'Nexpath advisory', + options: options.map((label, i) => ({ id: String(i), label })), + }; +} + +export function createAdvisoryFallback(deps: AdvisoryFallbackDeps): AdvisoryFallback { + const read = + deps.read ?? ((projectRoot: string) => readLatestAdvisory(projectRoot, deps.readerDeps)); + const now = deps.now ?? (() => Date.now()); + const recentWindowMs = deps.recentWindowMs ?? 60_000; + + let pendingProject: string | null = null; + let hintShown = false; + + return { + async armIfPending(projectRoot: string): Promise { + let advisory: StoredAdvisory | null; + try { + advisory = await read(projectRoot); + } catch { + return; + } + if (!advisory) return; + // Only surface advisories generated during (roughly) this cycle. + if (now() - advisory.createdAt > recentWindowMs) return; + + pendingProject = projectRoot; + deps.statusBar.show( + STATUS_BAR_TEXT, + `${advisory.pinchLabel || 'Nexpath'} — click to view nexpath’s advisory`, + ); + if (!hintShown && deps.showInfoOnce) { + hintShown = true; + deps.showInfoOnce(FIRST_TIME_HINT); + } + }, + + clear(): void { + pendingProject = null; + deps.statusBar.hide(); + }, + + async showAdvisory(): Promise { + if (pendingProject === null) return; + let advisory: StoredAdvisory | null; + try { + advisory = await read(pendingProject); + } catch { + return; + } + if (!advisory) { + deps.statusBar.hide(); + pendingProject = null; + return; + } + deps.publishPayload(toPayload(advisory)); + deps.statusBar.hide(); + pendingProject = null; + }, + }; +} diff --git a/src/ext-vscode/src/advisory-store-reader.test.ts b/src/ext-vscode/src/advisory-store-reader.test.ts new file mode 100644 index 00000000..fbe427e4 --- /dev/null +++ b/src/ext-vscode/src/advisory-store-reader.test.ts @@ -0,0 +1,165 @@ +import { describe, it, expect, vi } from 'vitest'; +import { + readLatestAdvisory, + readInjectedPrompt, + parseJsonStringArray, + defaultStorePath, + type ReadAdvisoryRowFn, + type ReadSessionStateFn, +} from './advisory-store-reader.js'; + +describe('parseJsonStringArray', () => { + it('parses a JSON string-array', () => { + expect(parseJsonStringArray('["a","b"]')).toEqual(['a', 'b']); + }); + it('drops non-string members', () => { + expect(parseJsonStringArray('["a",1,null,"b"]')).toEqual(['a', 'b']); + }); + it('returns [] for null / undefined / empty / non-string', () => { + expect(parseJsonStringArray(null)).toEqual([]); + expect(parseJsonStringArray(undefined)).toEqual([]); + expect(parseJsonStringArray('')).toEqual([]); + expect(parseJsonStringArray(42)).toEqual([]); + }); + it('returns [] for malformed JSON', () => { + expect(parseJsonStringArray('[not json')).toEqual([]); + }); + it('returns [] for non-array JSON', () => { + expect(parseJsonStringArray('{"a":1}')).toEqual([]); + }); +}); + +describe('defaultStorePath', () => { + it('points at ~/.nexpath/prompt-store.db (mirrors Layer C DEFAULT_DB_PATH)', () => { + expect(defaultStorePath('/home/u')).toBe('/home/u/.nexpath/prompt-store.db'); + }); +}); + +describe('readLatestAdvisory', () => { + const baseRow = { + project_root: '/proj', + stage: 'implementation', + flag_type: 'stage_transition', + pinch_label: 'Quick check.', + generated_l1: '["Run the full test suite"]', + generated_l2: '["Run the changed tests"]', + generated_l3: '["Smoke-test the happy path"]', + created_at: 1700000000000, + status: 'shown', + }; + + it('normalises a row into a StoredAdvisory', async () => { + const readRow: ReadAdvisoryRowFn = vi.fn().mockResolvedValue(baseRow); + const out = await readLatestAdvisory('/proj', { readRow, dbPath: '/db' }); + expect(out).toEqual({ + projectRoot: '/proj', + stage: 'implementation', + flagType: 'stage_transition', + pinchLabel: 'Quick check.', + createdAt: 1700000000000, + status: 'shown', + l1: ['Run the full test suite'], + l2: ['Run the changed tests'], + l3: ['Smoke-test the happy path'], + }); + expect(readRow).toHaveBeenCalledWith('/db', '/proj'); + }); + + it('returns null when no row exists', async () => { + const readRow: ReadAdvisoryRowFn = vi.fn().mockResolvedValue(null); + expect(await readLatestAdvisory('/proj', { readRow })).toBeNull(); + }); + + it('returns null when the row has no generated options at any level', async () => { + const readRow: ReadAdvisoryRowFn = vi.fn().mockResolvedValue({ + ...baseRow, + generated_l1: null, + generated_l2: null, + generated_l3: null, + }); + expect(await readLatestAdvisory('/proj', { readRow })).toBeNull(); + }); + + it('keeps the advisory when only L2/L3 options exist', async () => { + const readRow: ReadAdvisoryRowFn = vi.fn().mockResolvedValue({ + ...baseRow, + generated_l1: null, + generated_l2: '["lighter"]', + generated_l3: null, + }); + const out = await readLatestAdvisory('/proj', { readRow }); + expect(out?.l1).toEqual([]); + expect(out?.l2).toEqual(['lighter']); + }); + + it('coerces a string created_at to a number', async () => { + const readRow: ReadAdvisoryRowFn = vi.fn().mockResolvedValue({ + ...baseRow, + created_at: '1700000000001', + }); + const out = await readLatestAdvisory('/proj', { readRow }); + expect(out?.createdAt).toBe(1700000000001); + }); + + it('never throws when the reader rejects — returns null', async () => { + const readRow: ReadAdvisoryRowFn = vi.fn().mockRejectedValue(new Error('db locked')); + await expect(readLatestAdvisory('/proj', { readRow })).resolves.toBeNull(); + }); + + it('tolerates missing/odd field types defensively', async () => { + const readRow: ReadAdvisoryRowFn = vi.fn().mockResolvedValue({ + generated_l1: '["x"]', + // everything else missing + }); + const out = await readLatestAdvisory('/proj', { readRow }); + expect(out).toMatchObject({ + projectRoot: '/proj', // falls back to the requested project root + stage: '', + flagType: '', + pinchLabel: '', + status: '', + createdAt: 0, + l1: ['x'], + }); + }); +}); + +describe('readInjectedPrompt', () => { + it('returns the persisted lastInjectedPrompt from session_states', async () => { + const readState: ReadSessionStateFn = vi.fn().mockResolvedValue({ + state_json: JSON.stringify({ lastInjectedPrompt: 'Run the full test suite.', promptCount: 7 }), + }); + const out = await readInjectedPrompt('/proj', { readState, dbPath: '/db' }); + expect(out).toBe('Run the full test suite.'); + expect(readState).toHaveBeenCalledWith('/db', '/proj'); + }); + + it('returns null when lastInjectedPrompt is absent / empty / null', async () => { + for (const v of [undefined, '', null, 42]) { + const readState: ReadSessionStateFn = vi.fn().mockResolvedValue({ + state_json: JSON.stringify({ lastInjectedPrompt: v }), + }); + expect(await readInjectedPrompt('/proj', { readState })).toBeNull(); + } + }); + + it('returns null when there is no session_states row', async () => { + const readState: ReadSessionStateFn = vi.fn().mockResolvedValue(null); + expect(await readInjectedPrompt('/proj', { readState })).toBeNull(); + }); + + it('returns null when state_json is missing or not a string', async () => { + const readState: ReadSessionStateFn = vi.fn().mockResolvedValue({ state_json: 123 }); + expect(await readInjectedPrompt('/proj', { readState })).toBeNull(); + }); + + it('returns null on malformed state_json', async () => { + const readState: ReadSessionStateFn = vi.fn().mockResolvedValue({ state_json: '{not json' }); + expect(await readInjectedPrompt('/proj', { readState })).toBeNull(); + }); + + it('never throws when the reader rejects', async () => { + const readState: ReadSessionStateFn = vi.fn().mockRejectedValue(new Error('locked')); + await expect(readInjectedPrompt('/proj', { readState })).resolves.toBeNull(); + }); +}); diff --git a/src/ext-vscode/src/advisory-store-reader.ts b/src/ext-vscode/src/advisory-store-reader.ts new file mode 100644 index 00000000..d9561a1d --- /dev/null +++ b/src/ext-vscode/src/advisory-store-reader.ts @@ -0,0 +1,237 @@ +import { homedir } from 'node:os'; +import { join } from 'node:path'; + +/** + * Read-only reader for the advisory nexpath's pipeline parks in its store. + * + * Layer C's `nexpath auto` writes the generated decision-session content to the + * `pending_advisories` table in `~/.nexpath/prompt-store.db` BEFORE `nexpath + * stop` renders it in the terminal popup. When that popup can't open (headless + * Linux, no terminal emulator, Wayland without an X11 foreground tool, a missing + * session bus, …) the advisory is generated but never shown. This module lets + * the extension surface it in-editor as a fallback — reading the SAME row Layer + * C wrote, with NO write access and NO Layer C code change. + * + * The store is owned exclusively by the CLI (sql.js); we only read it. Reads use + * the same staging-copy + dynamic-import-better-sqlite3 pattern the chat-history + * watcher uses (ABI-safe, never blocks a concurrent CLI write). Every failure + * path returns `null` rather than throwing — a fallback must never break the host. + */ + +/** A pending advisory row from nexpath's `pending_advisories` table. */ +export interface StoredAdvisory { + projectRoot: string; + stage: string; + flagType: string; + pinchLabel: string; + /** unix ms — when `nexpath auto` parked this advisory. */ + createdAt: number; + /** 'pending' | 'shown'. */ + status: string; + /** Generated option text per cascade level (full → simpler → minimal viable). */ + l1: string[]; + l2: string[]; + l3: string[]; +} + +/** + * Default store path — mirrors Layer C's `DEFAULT_DB_PATH` + * (`~/.nexpath/prompt-store.db`). The extension never passes `--db` to the CLI, + * so the CLI uses this same default; reading it here keeps the two in lockstep. + */ +export function defaultStorePath(home: string = homedir()): string { + return join(home, '.nexpath', 'prompt-store.db'); +} + +/** Reads the latest raw `pending_advisories` row for a project (injectable for tests). */ +export type ReadAdvisoryRowFn = ( + dbPath: string, + projectRoot: string, +) => Promise | null>; + +/** + * Stage-copy the store and run a single-row read-only query. Mirrors the + * watcher's `defaultReadItemTable` so the native module stays out of the eager + * require graph and a concurrent CLI write can never corrupt the read. Returns + * the first row, or null on any failure (missing file / missing table / drift). + */ +async function stagedGetRow( + dbPath: string, + sql: string, + param: string, +): Promise | null> { + const { copyFile, mkdir, rm } = await import('node:fs/promises'); + const { existsSync } = await import('node:fs'); + const { tmpdir } = await import('node:os'); + const { basename, join: pjoin } = await import('node:path'); + + if (!existsSync(dbPath)) return null; + + const stagingDir = pjoin( + tmpdir(), + `nexpath-store-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`, + ); + await mkdir(stagingDir, { recursive: true }); + const staged = pjoin(stagingDir, basename(dbPath)); + try { + await copyFile(dbPath, staged); + } catch (err) { + void rm(stagingDir, { recursive: true, force: true }).catch(() => {}); + if ((err as NodeJS.ErrnoException).code === 'ENOENT') return null; + throw err; + } + // sql.js rewrites the whole file (no WAL), but copy any siblings defensively. + for (const suffix of ['-wal', '-shm'] as const) { + const sib = dbPath + suffix; + if (existsSync(sib)) { + try { await copyFile(sib, staged + suffix); } catch { /* best-effort */ } + } + } + + const mod = (await import('better-sqlite3')) as unknown as { + default: new (path: string, options?: { readonly?: boolean }) => { + prepare(sql: string): { get(...params: unknown[]): Record | undefined }; + close(): void; + }; + }; + const Database = mod.default; + const db = new Database(staged, { readonly: true }); + try { + return db.prepare(sql).get(param) ?? null; + } catch { + return null; + } finally { + db.close(); + void rm(stagingDir, { recursive: true, force: true }).catch(() => {}); + } +} + +/** Default reader: newest `pending_advisories` row for `projectRoot`. */ +export const defaultReadAdvisoryRow: ReadAdvisoryRowFn = (dbPath, projectRoot) => + stagedGetRow( + dbPath, + `SELECT project_root, stage, flag_type, pinch_label, + generated_l1, generated_l2, generated_l3, created_at, status + FROM pending_advisories + WHERE project_root = ? + ORDER BY created_at DESC + LIMIT 1`, + projectRoot, + ); + +/** Parse a JSON string-array column (Layer C stores generated_l1/l2/l3 as JSON). */ +export function parseJsonStringArray(raw: unknown): string[] { + if (typeof raw !== 'string' || raw.length === 0) return []; + try { + const v: unknown = JSON.parse(raw); + return Array.isArray(v) ? v.filter((x): x is string => typeof x === 'string') : []; + } catch { + return []; + } +} + +export interface AdvisoryStoreReaderDeps { + /** Override the store path (defaults to `~/.nexpath/prompt-store.db`). */ + dbPath?: string; + /** Override the raw-row reader (tests). */ + readRow?: ReadAdvisoryRowFn; +} + +/** + * Read the latest advisory nexpath generated for `projectRoot`, normalised into + * a {@link StoredAdvisory}. Returns `null` when the store / row / generated + * options are absent or malformed (nothing actionable to surface). Never throws. + */ +export async function readLatestAdvisory( + projectRoot: string, + deps: AdvisoryStoreReaderDeps = {}, +): Promise { + const dbPath = deps.dbPath ?? defaultStorePath(); + const readRow = deps.readRow ?? defaultReadAdvisoryRow; + + let row: Record | null; + try { + row = await readRow(dbPath, projectRoot); + } catch { + return null; + } + if (!row) return null; + + const l1 = parseJsonStringArray(row.generated_l1); + const l2 = parseJsonStringArray(row.generated_l2); + const l3 = parseJsonStringArray(row.generated_l3); + // No generated options ⇒ nothing to show the user; skip. + if (l1.length === 0 && l2.length === 0 && l3.length === 0) return null; + + const createdAtRaw = row.created_at; + const createdAt = + typeof createdAtRaw === 'number' + ? createdAtRaw + : Number.isFinite(Number(createdAtRaw)) + ? Number(createdAtRaw) + : 0; + + return { + projectRoot: typeof row.project_root === 'string' ? row.project_root : projectRoot, + stage: typeof row.stage === 'string' ? row.stage : '', + flagType: typeof row.flag_type === 'string' ? row.flag_type : '', + pinchLabel: typeof row.pinch_label === 'string' ? row.pinch_label : '', + createdAt, + status: typeof row.status === 'string' ? row.status : '', + l1, l2, l3, + }; +} + +// ── Selected-prompt recovery (Windows crash-on-exit safety net) ────────────── + +/** Reads the `session_states.state_json` blob for a project (injectable for tests). */ +export type ReadSessionStateFn = ( + dbPath: string, + projectRoot: string, +) => Promise | null>; + +/** Default: read the project's session_states row, staged + read-only. */ +export const defaultReadSessionState: ReadSessionStateFn = (dbPath, projectRoot) => + stagedGetRow( + dbPath, + 'SELECT state_json FROM session_states WHERE project_root = ?', + projectRoot, + ); + +export interface InjectedPromptReaderDeps { + dbPath?: string; + readState?: ReadSessionStateFn; +} + +/** + * Recover the prompt the user just selected in the terminal popup. + * + * Layer C's `nexpath stop` persists the chosen option to + * `session_states.lastInjectedPrompt` BEFORE it writes stdout and force-exits. + * On Windows that force-exit trips a libuv assertion (the process dies with a + * non-zero code and stdout can be lost), so the extension can't rely on stdout + * to learn the selection. Reading this persisted value recovers it so the + * injection still happens. Read-only; returns null when absent/malformed. + */ +export async function readInjectedPrompt( + projectRoot: string, + deps: InjectedPromptReaderDeps = {}, +): Promise { + const dbPath = deps.dbPath ?? defaultStorePath(); + const readState = deps.readState ?? defaultReadSessionState; + + let row: Record | null; + try { + row = await readState(dbPath, projectRoot); + } catch { + return null; + } + if (!row || typeof row.state_json !== 'string') return null; + try { + const state = JSON.parse(row.state_json) as { lastInjectedPrompt?: unknown }; + const v = state?.lastInjectedPrompt; + return typeof v === 'string' && v.length > 0 ? v : null; + } catch { + return null; + } +} diff --git a/src/ext-vscode/src/chat-history-types.ts b/src/ext-vscode/src/chat-history-types.ts new file mode 100644 index 00000000..ffe956cd --- /dev/null +++ b/src/ext-vscode/src/chat-history-types.ts @@ -0,0 +1,90 @@ +/** + * Shared types for the chat-history capture layer (M2 Branch 2). The watcher, + * extractors, and fingerprint logic all speak this vocabulary. + * + * VS Code (and its forks Cursor / Windsurf) stores extension state in a SQLite + * database with a single `ItemTable(key TEXT, value TEXT)` table. Per-version + * extractors decode rows from this table into normalised ChatHistoryEvents. + */ + +/** A single row from VS Code/Cursor's standard ItemTable. */ +export interface ItemTableRow { + key: string; + /** JSON-encoded string in practice; extractors are responsible for parsing. */ + value: string; +} + +/** + * One user prompt captured from the agent's chat history. Emitted by the + * watcher; consumed by the extension layer which composes a full session_id + * (workspaceId + rawSessionId) before forwarding to the nexpath IPC layer. + */ +export interface ChatHistoryEvent { + /** The prompt text the user submitted. */ + prompt: string; + /** + * Per-row session piece (tab id, conversation id, prompts-index). The + * extension layer combines this with `vscode.workspace.workspaceFile` / + * workspace id to derive the full nexpath session_id. + */ + rawSessionId: string; + /** Capture timestamp (extension-local clock). */ + capturedAt: Date; + /** Storage file/dir this row was read from. */ + sourcePath: string; + /** Identifier of the extractor that decoded this row, e.g. `cursor-v2025-q2`. */ + extractorId: string; +} + +/** Contract every per-version extractor implements (M3). */ +export interface ChatHistoryExtractor { + /** Stable identifier, e.g. `cursor-v2024-q4`, `windsurf`. */ + id: string; + /** Human-readable label for diagnostics + telemetry. */ + label: string; + /** + * Key prefixes that uniquely fingerprint this version. The fingerprint + * matcher treats each entry as a prefix; exact keys are prefixes of + * themselves. The extractor with the most observed-key prefix matches + * wins (see `pickExtractor` in `extractors/index.ts`). + */ + fingerprintKeys: readonly string[]; + /** Whether this extractor decodes rows with the given ItemTable key. */ + ownsKey(key: string): boolean; + /** + * Decode a single ItemTable row into zero or more chat events. + * Returns [] if the row contains no new user prompts (assistant messages, + * malformed JSON, foreign rows). + */ + decodeRow(row: ItemTableRow, sourcePath: string): ChatHistoryEvent[]; +} + +/** Result from `pickExtractor` (M4). */ +export type FingerprintResult = + | { + kind: 'known'; + extractor: ChatHistoryExtractor; + /** Observed keys that triggered the match — useful for logs. */ + matchedKeys: readonly string[]; + } + | { + kind: 'unknown'; + observedKeyCount: number; + /** First few observed keys, for the "schema unknown" toast / log. */ + observedSampleKeys: readonly string[]; + }; + +/** Storage layout the watcher needs to handle. */ +export type StorageKind = 'cursor-sqlite' | 'windsurf-dir'; + +/** A path the watcher is monitoring. */ +export interface WatchTarget { + /** Absolute path to a SQLite file (Cursor) or a directory (Windsurf). */ + path: string; + kind: StorageKind; + /** + * Extractor for this target. For Cursor, may be left undefined — the + * watcher fingerprints the ItemTable on first read and caches the result. + */ + extractor?: ChatHistoryExtractor; +} diff --git a/src/ext-vscode/src/chat-history-watcher.test.ts b/src/ext-vscode/src/chat-history-watcher.test.ts new file mode 100644 index 00000000..8084ae32 --- /dev/null +++ b/src/ext-vscode/src/chat-history-watcher.test.ts @@ -0,0 +1,946 @@ +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; +import { EventEmitter } from 'node:events'; +import { mkdtempSync, rmSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import { + createChatHistoryWatcher, + defaultReadItemTable, + defaultReadWindsurfJsonFiles, + type ReadItemTableFn, + type ReadWindsurfJsonFilesFn, +} from './chat-history-watcher.js'; +import { mkdirSync, writeFileSync } from 'node:fs'; +import type { + ChatHistoryEvent, + ChatHistoryExtractor, + ItemTableRow, + WatchTarget, +} from './chat-history-types.js'; + +/** Fake fs.watch — returns a minimal EventEmitter compatible with FSWatcher. */ +interface FakeFSWatcher extends EventEmitter { + close: () => void; +} + +function makeFakeWatcher(): FakeFSWatcher { + const w = new EventEmitter() as FakeFSWatcher; + w.close = vi.fn(); + return w; +} + +function makeExtractor( + id: string, + events: ChatHistoryEvent[], + ownsKey: (k: string) => boolean = () => true, +): ChatHistoryExtractor { + return { + id, + label: id, + fingerprintKeys: [id], + ownsKey, + decodeRow: () => events, + }; +} + +function ev( + prompt: string, + rawSessionId: string, + sourcePath: string, +): ChatHistoryEvent { + return { + prompt, + rawSessionId, + capturedAt: new Date(0), + sourcePath, + extractorId: 'test', + }; +} + +const cursorTarget = (path: string, extractor?: ChatHistoryExtractor): WatchTarget => ({ + path, + kind: 'cursor-sqlite', + extractor, +}); + +describe('createChatHistoryWatcher', () => { + let watchFn: ReturnType; + let createdWatchers: FakeFSWatcher[]; + let readItemTableFn: ReturnType; + let onEvent: ReturnType; + let onError: ReturnType; + let onSchemaUnknown: ReturnType; + + beforeEach(() => { + createdWatchers = []; + watchFn = vi.fn((_path: string, listener?: (event: string, filename: string) => void) => { + const w = makeFakeWatcher(); + // Wire the listener so .emit('change', ...) actually triggers it. + // Mirrors node:fs watch() behaviour where the listener arg is + // registered as a 'change' / 'rename' event handler on the FSWatcher. + if (listener) w.on('change', listener); + createdWatchers.push(w); + return w; + }); + readItemTableFn = vi.fn(async () => []); + onEvent = vi.fn(); + onError = vi.fn(); + onSchemaUnknown = vi.fn(); + }); + + it('start() registers a watcher for every target (plus WAL+SHM siblings for cursor-sqlite)', () => { + const w = createChatHistoryWatcher({ + targets: [cursorTarget('/a/state.vscdb'), cursorTarget('/b/state.vscdb')], + onEvent, + watchFn: watchFn as never, + readItemTableFn, + }); + w.start(); + // Per target: main file + -wal + -shm = 3 watchers. Two targets = 6. + // The -wal / -shm watches are load-bearing because live Cursor uses + // SQLite WAL mode and writes go to the sibling, not the main file. + expect(watchFn).toHaveBeenCalledTimes(6); + expect(watchFn).toHaveBeenCalledWith('/a/state.vscdb', expect.any(Function)); + expect(watchFn).toHaveBeenCalledWith('/a/state.vscdb-wal', expect.any(Function)); + expect(watchFn).toHaveBeenCalledWith('/a/state.vscdb-shm', expect.any(Function)); + expect(watchFn).toHaveBeenCalledWith('/b/state.vscdb', expect.any(Function)); + expect(watchFn).toHaveBeenCalledWith('/b/state.vscdb-wal', expect.any(Function)); + expect(watchFn).toHaveBeenCalledWith('/b/state.vscdb-shm', expect.any(Function)); + }); + + it('pollMs backstop captures new prompts even when fs.watch NEVER fires (Windows)', async () => { + // Simulates the Windows failure: fs.watch goes silent after start, so the + // ONLY way new prompts surface is the poll. No watcher .emit() is called. + readItemTableFn.mockResolvedValue([]); + const extractor = makeExtractor('test', [ev('polled-prompt', 's-poll', '/p/state.vscdb')]); + const w = createChatHistoryWatcher({ + targets: [cursorTarget('/p/state.vscdb', extractor)], + onEvent, + watchFn: watchFn as never, + readItemTableFn, + debounceMs: 5, + pollMs: 10, + }); + w.start(); + await new Promise((r) => setTimeout(r, 20)); // prime pass reads [] → no emit + expect(onEvent).toHaveBeenCalledTimes(0); + + // A new prompt lands; fs.watch is dead → only the poll can catch it. + readItemTableFn.mockResolvedValue([{ key: 'k', value: 'v' }]); + await new Promise((r) => setTimeout(r, 40)); // poll (10ms) re-reads → emit + expect(onEvent).toHaveBeenCalledTimes(1); + expect(onEvent.mock.calls[0]![0].prompt).toBe('polled-prompt'); + + // stop() must cancel the poll — no further emits after a new row appears. + w.stop(); + onEvent.mockClear(); + readItemTableFn.mockResolvedValue([{ key: 'k2', value: 'v2' }]); + await new Promise((r) => setTimeout(r, 40)); + expect(onEvent).toHaveBeenCalledTimes(0); + }); + + it('WAL sibling change triggers a re-read of the main target (WAL-mode liveness)', async () => { + // Start with no rows so the initial pass primes nothing; then a NEW row + // appears via a WAL-sibling change and that should produce one emit. + readItemTableFn.mockResolvedValue([]); + const extractor = makeExtractor('test', [ev('prompt-after-wal-fire', 's-2', '/p/state.vscdb')]); + const w = createChatHistoryWatcher({ + targets: [cursorTarget('/p/state.vscdb', extractor)], + onEvent, + watchFn: watchFn as never, + readItemTableFn, + debounceMs: 5, + }); + w.start(); + await new Promise((r) => setTimeout(r, 20)); + // After prime: no events emitted yet. + expect(onEvent).toHaveBeenCalledTimes(0); + + // Now a new row appears. WAL-sibling change should drive a re-read. + readItemTableFn.mockResolvedValue([{ key: 'k2', value: 'v2' }]); + // The 2nd watcher in createdWatchers is the -wal sibling (index 1). + expect(createdWatchers.length).toBeGreaterThanOrEqual(2); + createdWatchers[1]!.emit('change', 'change', '/p/state.vscdb-wal'); + await new Promise((r) => setTimeout(r, 30)); + + expect(onEvent).toHaveBeenCalledTimes(1); + expect(onEvent.mock.calls[0]![0].prompt).toBe('prompt-after-wal-fire'); + }); + + it('cursor-sqlite: runs ALL extractors that own a row, not just the fingerprint-winner', async () => { + // Regression for the fingerprint-tie bug found in M2 manual testing R2.1: + // workspaces with BOTH aiService.prompts (cursor-v2024-q4) AND + // composer.composerData (cursor-v2025-q1) caused pickExtractor to tie at + // 1 match each, registry order picked cursor-v2025-q1, which doesn't own + // aiService.prompts → all Ask-mode prompts were silently discarded. + // Fix: per-row, run every extractor whose ownsKey returns true. + // Empty first read primes nothing; then the multi-key rows appear. + readItemTableFn.mockResolvedValue([]); + + const w = createChatHistoryWatcher({ + targets: [{ path: '/p/state.vscdb', kind: 'cursor-sqlite' }], + onEvent, + watchFn: watchFn as never, + readItemTableFn, + debounceMs: 1, + }); + w.start(); + await new Promise((r) => setTimeout(r, 20)); + + readItemTableFn.mockResolvedValue([ + { key: 'composer.composerData', value: JSON.stringify({ selectedComposerIds: [] }) }, + { key: 'aiService.prompts', value: JSON.stringify([{ text: 'real prompt' }]) }, + ]); + createdWatchers[0]!.emit('change', 'change', '/p/state.vscdb'); + await new Promise((r) => setTimeout(r, 20)); + + // Expect: cursor-v2024-q4 decodes aiService.prompts → 1 event with our prompt + // (cursor-v2025-q1 also runs but ownsKey returns false for aiService.prompts + // and the composer.composerData value is metadata only → 0 events from it) + expect(onEvent).toHaveBeenCalled(); + const events = onEvent.mock.calls.map((c) => c[0]); + const prompts = events.map((e) => e.prompt); + expect(prompts).toContain('real prompt'); + }); + + it('windsurf-dir targets do NOT get WAL siblings watched (only cursor-sqlite uses WAL)', () => { + const w = createChatHistoryWatcher({ + targets: [{ path: '/ws/codeium', kind: 'windsurf-dir' }], + onEvent, + watchFn: watchFn as never, + readItemTableFn, + }); + w.start(); + // Just the dir itself, no WAL siblings (Windsurf uses JSON files not SQLite) + expect(watchFn).toHaveBeenCalledTimes(1); + expect(watchFn).toHaveBeenCalledWith('/ws/codeium', expect.any(Function)); + }); + + it('FIFO-shift regression: a single new prompt that pushes the oldest out emits exactly once (real cursor-v2024-q4 wiring)', async () => { + // The exact M2 R3 live-test scenario (2026-05-20): Cursor's + // aiService.prompts is a rolling FIFO of ~10 prompts. When a new + // prompt is submitted, the oldest is dropped and ALL indexes shift. + // Before the cursor-v2024-q4 rawSessionId fix, the dedup signature + // shifted along with the indexes → every restart re-emitted all 10 + // prompts. After the fix (rawSessionId = 'ask-mode' constant), the + // signature is sourcePath+text-driven and survives FIFO shifts. + // This test wires the real cursor-v2024-q4 extractor — via the + // unified extractor cache — to prove the integration works. + const { cursorV2024Q4 } = await import('./extractors/cursor-v2024-q4.js'); + const initialPrompts = Array.from({ length: 10 }, (_, i) => ({ text: `prompt-${i}` })); + readItemTableFn.mockResolvedValueOnce([ + { key: 'aiService.prompts', value: JSON.stringify(initialPrompts) }, + ]); + const w = createChatHistoryWatcher({ + targets: [cursorTarget('/p/state.vscdb', cursorV2024Q4)], + onEvent, + watchFn: watchFn as never, + readItemTableFn, + debounceMs: 1, + }); + w.start(); + await new Promise((r) => setTimeout(r, 20)); + expect(onEvent).toHaveBeenCalledTimes(0); // initial pass primed only + + // User submits a new prompt — FIFO drops prompt-0, appends prompt-10. + const shifted = [ + ...Array.from({ length: 9 }, (_, i) => ({ text: `prompt-${i + 1}` })), + { text: 'prompt-10' }, + ]; + readItemTableFn.mockResolvedValue([ + { key: 'aiService.prompts', value: JSON.stringify(shifted) }, + ]); + createdWatchers[0]!.emit('change', 'change', '/p/state.vscdb'); + await new Promise((r) => setTimeout(r, 20)); + + expect(onEvent).toHaveBeenCalledTimes(1); + expect(onEvent.mock.calls[0]![0].prompt).toBe('prompt-10'); + }); + + it('prime-then-new-prompt: existing rows are primed silently, NEW row after start() emits once', async () => { + // End-to-end of the M2 R2/R3 fix: extension activation must not replay + // historical state.vscdb rows to Layer C; only prompts that appear + // AFTER the watcher has primed should fire onEvent. Mirrors what + // happens when the user (a) opens Cursor with prior Ask-mode history + // already in state.vscdb, then (b) submits a brand-new prompt. + const oldRow = { key: 'aiService.prompts', value: '[{"text":"old"}]' }; + const newRow = { key: 'aiService.prompts', value: '[{"text":"old"},{"text":"new"}]' }; + // Initial read: only the "old" row is present. + readItemTableFn.mockResolvedValueOnce([oldRow]); + const extractor = makeExtractor('test', []); + extractor.ownsKey = (k: string) => k === 'aiService.prompts'; + extractor.decodeRow = (row) => { + const arr = JSON.parse(row.value) as Array<{ text: string }>; + return arr.map((p, i) => ev(p.text, `prompts-index:${i}`, '/p/state.vscdb')); + }; + const w = createChatHistoryWatcher({ + targets: [cursorTarget('/p/state.vscdb', extractor)], + onEvent, + watchFn: watchFn as never, + readItemTableFn, + debounceMs: 1, + }); + w.start(); + await new Promise((r) => setTimeout(r, 20)); + // Prime registered both signatures internally; no events emitted. + expect(onEvent).toHaveBeenCalledTimes(0); + + // Now a new prompt arrives — state.vscdb grows by one entry. + readItemTableFn.mockResolvedValue([newRow]); + createdWatchers[0]!.emit('change', 'change', '/p/state.vscdb'); + await new Promise((r) => setTimeout(r, 20)); + + // Exactly ONE event for the NEW prompt; the "old" one is deduped. + expect(onEvent).toHaveBeenCalledTimes(1); + expect(onEvent.mock.calls[0]![0].prompt).toBe('new'); + }); + + it('initial-pass after start() reads existing rows but does NOT emit them (prime-only dedup)', async () => { + // Prime-only initial-pass behaviour. Historical prompts that already exist + // in state.vscdb when the extension activates must NOT be replayed to + // Layer C — Layer C is built around Claude Code's hook semantics, which + // fire only on NEW prompts. Replaying historical prompts on every Cursor + // restart bypassed Layer C's 3-prompt warmup + 5-prompt cooldown gates + // and caused advisory storms (root-caused during M2 R2/R3 manual testing). + const fakeRows: ItemTableRow[] = [{ key: 'k1', value: 'v1' }]; + readItemTableFn.mockResolvedValue(fakeRows); + const extractor = makeExtractor('test', [ev('hi', 's-1', '/p')]); + const w = createChatHistoryWatcher({ + targets: [cursorTarget('/p', extractor)], + onEvent, + watchFn: watchFn as never, + readItemTableFn, + debounceMs: 1, + }); + w.start(); + await new Promise((r) => setTimeout(r, 20)); + // Read happened (so dedup is primed) but no event was emitted. + expect(readItemTableFn).toHaveBeenCalledTimes(1); + expect(onEvent).toHaveBeenCalledTimes(0); + }); + + it('deduplicates events across multiple reads (signature dedup)', async () => { + // Initial pass primes dedup with the existing row (no emit). Then we + // simulate a NEW row appearing — that's the first emit. A subsequent + // change with the same row hits dedup and does not re-emit. + const extractor = makeExtractor('test', [ev('same', 's-1', '/p')]); + readItemTableFn.mockResolvedValue([]); // first read = empty (prime with no rows) + const w = createChatHistoryWatcher({ + targets: [cursorTarget('/p', extractor)], + onEvent, + watchFn: watchFn as never, + readItemTableFn, + debounceMs: 1, + }); + w.start(); + await new Promise((r) => setTimeout(r, 20)); + + // Now a real new prompt arrives on disk. Subsequent reads return the row. + readItemTableFn.mockResolvedValue([{ key: 'k', value: 'v' }]); + createdWatchers[0]!.emit('change', 'change', '/p'); + await new Promise((r) => setTimeout(r, 20)); + createdWatchers[0]!.emit('change', 'change', '/p'); + await new Promise((r) => setTimeout(r, 20)); + + // First post-prime read emitted once; subsequent reads dedup the same row. + expect(onEvent).toHaveBeenCalledTimes(1); + }); + + // ── Cross-extractor dedup (Cursor 3.x Composer↔Ask mirror) ────────────── + // Cursor 3.x writes Composer prompts to BOTH globalStorage cursorDiskKV + // (decoded as bubbleId rawSessionId) AND workspaceStorage aiService.prompts + // (decoded as 'ask-mode' rawSessionId). Different sourcePath|rawSessionId|prompt + // signatures bypass the primary dedup. The cross-extractor map dedups by + // prompt text alone within a 60s window. + + it('cross-extractor dedup: same prompt text from two extractors within 60s emits once', async () => { + const t0 = new Date('2026-05-27T12:00:00Z'); + // Same prompt text, but the two events arrive with different rawSessionIds + // and sourcePaths — mimicking Composer (bubbleId) + Ask (ask-mode) mirror. + const extractor = makeExtractor('mixed', [ + ev('write a test', 'bubbleId-abc', '/global/state.vscdb'), + ev('write a test', 'ask-mode', '/workspace/state.vscdb'), + ]); + readItemTableFn.mockResolvedValue([]); // prime empty + const w = createChatHistoryWatcher({ + targets: [cursorTarget('/global/state.vscdb', extractor)], + onEvent, + watchFn: watchFn as never, + readItemTableFn, + nowFn: () => t0, + debounceMs: 1, + }); + w.start(); + await new Promise((r) => setTimeout(r, 20)); + + // Now both mirror events arrive in one read. + readItemTableFn.mockResolvedValue([{ key: 'k', value: 'v' }]); + createdWatchers[0]!.emit('change', 'change', '/global/state.vscdb'); + await new Promise((r) => setTimeout(r, 20)); + + // Only ONE emit — the second event was suppressed by cross-extractor dedup. + expect(onEvent).toHaveBeenCalledTimes(1); + }); + + it('cross-extractor dedup: same prompt text re-submitted after 60s passes through', async () => { + let clock = new Date('2026-05-27T12:00:00Z').getTime(); + readItemTableFn.mockResolvedValue([]); + const extractor = makeExtractor('mixed', [ + ev('write a test', 'session-a', '/p/state.vscdb'), + ]); + const w = createChatHistoryWatcher({ + targets: [cursorTarget('/p/state.vscdb', extractor)], + onEvent, + watchFn: watchFn as never, + readItemTableFn, + nowFn: () => new Date(clock), + debounceMs: 1, + }); + w.start(); + await new Promise((r) => setTimeout(r, 20)); + + // First emission of the prompt. + readItemTableFn.mockResolvedValue([{ key: 'k', value: 'v' }]); + createdWatchers[0]!.emit('change', 'change', '/p/state.vscdb'); + await new Promise((r) => setTimeout(r, 20)); + expect(onEvent).toHaveBeenCalledTimes(1); + + // Advance clock past 60s window + swap signature so primary dedup doesn't catch it. + clock += 65_000; + const reEmit = makeExtractor('mixed', [ + ev('write a test', 'session-b', '/p/state.vscdb'), + ]); + // Replace the watcher's extractor lookup by mocking decodeRow via the + // single-extractor cache — easiest path is a second target with a fresh + // extractor pointing at the same prompt text. + readItemTableFn.mockImplementation(async () => [{ key: 'k', value: 'v' }]); + // Force the extractor decode to return the new rawSessionId by replacing + // the underlying mock; cursor target's per-row extractor is fed by + // extractorsToTry which uses target.extractor. + Object.assign(extractor, { decodeRow: reEmit.decodeRow }); + + createdWatchers[0]!.emit('change', 'change', '/p/state.vscdb'); + await new Promise((r) => setTimeout(r, 20)); + + // Window expired → second emission passes through. + expect(onEvent).toHaveBeenCalledTimes(2); + }); + + it('cross-extractor dedup: initial-pass priming does NOT block fresh emissions of same text', async () => { + // globalStorage state.vscdb is workspace-agnostic — initial-pass primes + // dedup with bubble rows from PRIOR workspaces. A fresh user prompt with + // matching text must still emit. Regression guard for the priming bug + // discovered during S01 manual run 2026-05-27: priming with NOW timestamp + // dropped legit P1 captures. Fix primes with timestamp 0 (far past). + const t0 = new Date('2026-05-27T12:00:00Z'); + // Initial pass returns one historical row decoding to "write a test". + readItemTableFn.mockResolvedValue([{ key: 'historical', value: 'v' }]); + const extractor = makeExtractor('mixed', [ + ev('write a test', 'historical-session', '/global/state.vscdb'), + ]); + const w = createChatHistoryWatcher({ + targets: [cursorTarget('/global/state.vscdb', extractor)], + onEvent, + watchFn: watchFn as never, + readItemTableFn, + nowFn: () => t0, + debounceMs: 1, + }); + w.start(); + await new Promise((r) => setTimeout(r, 20)); + + // Initial pass primed the historical row silently — no emit yet. + expect(onEvent).toHaveBeenCalledTimes(0); + + // Now the user submits a NEW prompt with identical text in the live session. + // Different rawSessionId so primary signature dedup does NOT match. + const liveExtractor = makeExtractor('mixed', [ + ev('write a test', 'live-session', '/global/state.vscdb'), + ]); + Object.assign(extractor, { decodeRow: liveExtractor.decodeRow }); + readItemTableFn.mockResolvedValue([{ key: 'live', value: 'v' }]); + createdWatchers[0]!.emit('change', 'change', '/global/state.vscdb'); + await new Promise((r) => setTimeout(r, 20)); + + // MUST emit. If priming had used NOW timestamp, this would have been + // deduped inside the 60s window and the user's P1 prompt would vanish. + expect(onEvent).toHaveBeenCalledTimes(1); + }); + + it('debounces a burst of change events into a single read', async () => { + readItemTableFn.mockResolvedValue([]); + const w = createChatHistoryWatcher({ + targets: [cursorTarget('/p', makeExtractor('test', []))], + onEvent, + watchFn: watchFn as never, + readItemTableFn, + debounceMs: 50, + }); + w.start(); + // initial-pass schedule fires after 50ms; before it does, fire 5 more changes + for (let i = 0; i < 5; i++) { + createdWatchers[0]!.emit('change', 'change', '/p'); + } + await new Promise((r) => setTimeout(r, 80)); + // Only one read should have occurred (the bursts coalesced) + expect(readItemTableFn).toHaveBeenCalledTimes(1); + }); + + it('emits onSchemaUnknown when the ItemTable has no recognised fingerprint', async () => { + readItemTableFn.mockResolvedValue([ + { key: 'unrelated.thing.1', value: 'x' }, + { key: 'unrelated.thing.2', value: 'x' }, + ]); + const w = createChatHistoryWatcher({ + targets: [cursorTarget('/p')], // no explicit extractor — must fingerprint + onEvent, + onSchemaUnknown, + watchFn: watchFn as never, + readItemTableFn, + debounceMs: 1, + }); + w.start(); + await new Promise((r) => setTimeout(r, 20)); + expect(onSchemaUnknown).toHaveBeenCalledTimes(1); + const arg = onSchemaUnknown.mock.calls[0]![0]; + expect(arg.path).toBe('/p'); + expect(arg.observedSampleKeys).toEqual([ + 'unrelated.thing.1', + 'unrelated.thing.2', + ]); + expect(onEvent).not.toHaveBeenCalled(); + }); + + it('emits onSchemaUnknown only once per path across repeated unknown reads', async () => { + // Windsurf's workspaceStorage state.vscdb never holds chat, so it stays + // unknown on every fs.watch fire. The watcher must keep re-checking (a + // fresh Cursor workspace gains chat keys later) but notify the user only + // once — otherwise the log + info toast re-fire endlessly. + readItemTableFn.mockResolvedValue([ + { key: 'windsurf.cascadeViewContainerId.state', value: 'x' }, + ]); + const w = createChatHistoryWatcher({ + targets: [cursorTarget('/p')], // no extractor — fingerprinted every read + onEvent, + onSchemaUnknown, + watchFn: watchFn as never, + readItemTableFn, + debounceMs: 1, + }); + w.start(); + await new Promise((r) => setTimeout(r, 15)); // initial read: unknown -> notify once + // Three more spaced fs.watch fires (beyond the debounce so they don't coalesce). + for (let i = 0; i < 3; i++) { + createdWatchers[0]!.emit('change', 'change', '/p'); + await new Promise((r) => setTimeout(r, 15)); + } + // Re-checked on every read (still unknown each time) ... + expect(readItemTableFn.mock.calls.length).toBeGreaterThanOrEqual(4); + // ... but surfaced to the user exactly once. + expect(onSchemaUnknown).toHaveBeenCalledTimes(1); + expect(onEvent).not.toHaveBeenCalled(); + }); + + it('forwards readItemTableFn errors to onError without crashing', async () => { + readItemTableFn.mockRejectedValueOnce(new Error('sqlite parse error')); + const w = createChatHistoryWatcher({ + targets: [cursorTarget('/p', makeExtractor('test', []))], + onEvent, + onError, + watchFn: watchFn as never, + readItemTableFn, + debounceMs: 1, + }); + w.start(); + await new Promise((r) => setTimeout(r, 20)); + expect(onError).toHaveBeenCalledTimes(1); + expect(onError.mock.calls[0]![0].message).toContain('sqlite parse error'); + }); + + it('forwards fs.watch error events to onError', async () => { + const w = createChatHistoryWatcher({ + targets: [cursorTarget('/p', makeExtractor('test', []))], + onEvent, + onError, + watchFn: watchFn as never, + readItemTableFn, + debounceMs: 1, + }); + w.start(); + createdWatchers[0]!.emit('error', new Error('watch boom')); + expect(onError).toHaveBeenCalledTimes(1); + expect(onError.mock.calls[0]![0].message).toContain('watch boom'); + }); + + it('stop() closes all watchers and cancels pending debouncers', async () => { + const w = createChatHistoryWatcher({ + targets: [cursorTarget('/a'), cursorTarget('/b')], + onEvent, + watchFn: watchFn as never, + readItemTableFn, + debounceMs: 50, + }); + w.start(); + // before the debounced read fires, stop + w.stop(); + await new Promise((r) => setTimeout(r, 80)); + for (const fw of createdWatchers) { + expect(fw.close).toHaveBeenCalled(); + } + // no reads should have happened — debouncers were cancelled + expect(readItemTableFn).not.toHaveBeenCalled(); + }); + + it('stamps capturedAt with nowFn (clock injection)', async () => { + const fixedDate = new Date('2026-05-14T17:00:00Z'); + // Empty first read so the initial pass primes nothing — then the real + // emit happens after a simulated change. + readItemTableFn.mockResolvedValue([]); + const extractor = makeExtractor('test', [ev('hi', 's', '/p')]); + const w = createChatHistoryWatcher({ + targets: [cursorTarget('/p', extractor)], + onEvent, + watchFn: watchFn as never, + readItemTableFn, + nowFn: () => fixedDate, + debounceMs: 1, + }); + w.start(); + await new Promise((r) => setTimeout(r, 20)); + readItemTableFn.mockResolvedValue([{ key: 'k', value: 'v' }]); + createdWatchers[0]!.emit('change', 'change', '/p'); + await new Promise((r) => setTimeout(r, 20)); + expect(onEvent.mock.calls[0]![0].capturedAt).toEqual(fixedDate); + }); + + it('windsurf-dir targets are accepted without crashing (default stub decoder = no events)', () => { + const w = createChatHistoryWatcher({ + targets: [{ path: '/ws', kind: 'windsurf-dir' }], + onEvent, + watchFn: watchFn as never, + readItemTableFn, + // Default readWindsurfJsonFilesFn handles missing dir (returns []) + // so this passes without disk IO. + debounceMs: 1, + }); + expect(() => w.start()).not.toThrow(); + expect(() => w.stop()).not.toThrow(); + }); + + // ── windsurf-dir code path (Drift A fix) ──────────────────────────────── + // These tests cover the new processWindsurfTarget flow: readWindsurf → + // decodeWindsurf → dedup → onEvent. The default decoder is a stub, so + // we inject `decodeWindsurfFn` to simulate what the engineer's real + // decoder will eventually do. + + it('windsurf-dir: pipes readWindsurfJsonFilesFn output through decodeWindsurfFn → onEvent', async () => { + // Initial pass primes (empty), then files appear → emits. + const fakeFiles = [ + { path: '/ws/a.json', parsed: { user_prompt: 'first' } }, + { path: '/ws/b.json', parsed: { user_prompt: 'second' } }, + ]; + const readWindsurfJsonFilesFn = vi.fn(async () => []); + // Test stand-in for the real decoder: emit one event per file with the + // user_prompt field. Mirrors what the engineer's real decoder will do. + const decodeWindsurfFn = vi.fn( + (parsed: unknown, sourcePath: string): ChatHistoryEvent[] => { + if (typeof parsed === 'object' && parsed !== null && 'user_prompt' in parsed) { + return [ + ev( + (parsed as { user_prompt: string }).user_prompt, + `windsurf-session:${sourcePath}`, + sourcePath, + ), + ]; + } + return []; + }, + ); + + const w = createChatHistoryWatcher({ + targets: [{ path: '/ws', kind: 'windsurf-dir' }], + onEvent, + watchFn: watchFn as never, + readItemTableFn, + readWindsurfJsonFilesFn, + decodeWindsurfFn, + debounceMs: 1, + }); + w.start(); + await new Promise((r) => setTimeout(r, 20)); + + readWindsurfJsonFilesFn.mockResolvedValue(fakeFiles); + createdWatchers[0]!.emit('change', 'change', '/ws'); + await new Promise((r) => setTimeout(r, 20)); + + expect(readWindsurfJsonFilesFn).toHaveBeenCalledWith('/ws'); + expect(decodeWindsurfFn).toHaveBeenCalledTimes(2); + expect(decodeWindsurfFn).toHaveBeenCalledWith(fakeFiles[0]!.parsed, '/ws/a.json'); + expect(onEvent).toHaveBeenCalledTimes(2); + expect(onEvent.mock.calls[0]![0].prompt).toBe('first'); + expect(onEvent.mock.calls[1]![0].prompt).toBe('second'); + }); + + it('windsurf-dir: deduplicates emitted events across multiple files (signature dedup)', async () => { + // Realistic case: same conversation referenced in two JSON files (e.g. + // Windsurf renamed a session file, leaving an old copy + a new one). + // Both decode to the same prompt+sessionId+sourcePath* — dedup should + // emit only the first. + // (*signature includes sourcePath; we use the same path here to force + // a true dedup hit. The realistic engineer's decoder will derive + // `rawSessionId` from session metadata inside the JSON, not the file + // path, making dedup work even when the same conversation is split + // across files.) + const fakeFiles = [ + { path: '/ws/a.json', parsed: { p: 'hi' } }, + { path: '/ws/a.json', parsed: { p: 'hi' } }, // same path → same signature + ]; + const readWindsurfJsonFilesFn = vi.fn(async () => []); + const decodeWindsurfFn = vi.fn( + (_parsed: unknown, sourcePath: string): ChatHistoryEvent[] => [ + ev('hi', 'sess-1', sourcePath), + ], + ); + + const w = createChatHistoryWatcher({ + targets: [{ path: '/ws', kind: 'windsurf-dir' }], + onEvent, + watchFn: watchFn as never, + readItemTableFn, + readWindsurfJsonFilesFn, + decodeWindsurfFn, + debounceMs: 1, + }); + w.start(); + await new Promise((r) => setTimeout(r, 20)); + + // Initial pass primes (empty). Now the duplicate-file scenario appears. + readWindsurfJsonFilesFn.mockResolvedValue(fakeFiles); + createdWatchers[0]!.emit('change', 'change', '/ws'); + await new Promise((r) => setTimeout(r, 20)); + + // Decoder ran for both files (one event each), but signature dedup + // collapses them into a single onEvent emission. + expect(decodeWindsurfFn).toHaveBeenCalledTimes(2); + expect(onEvent).toHaveBeenCalledTimes(1); + }); + + it('windsurf-dir: forwards readWindsurfJsonFilesFn errors to onError', async () => { + const readWindsurfJsonFilesFn = vi.fn(async () => { + throw new Error('windsurf read boom'); + }); + const w = createChatHistoryWatcher({ + targets: [{ path: '/ws', kind: 'windsurf-dir' }], + onEvent, + onError, + watchFn: watchFn as never, + readItemTableFn, + readWindsurfJsonFilesFn, + debounceMs: 1, + }); + w.start(); + await new Promise((r) => setTimeout(r, 20)); + expect(onError).toHaveBeenCalledTimes(1); + expect(onError.mock.calls[0]![0].message).toContain('windsurf read boom'); + expect(onEvent).not.toHaveBeenCalled(); + }); +}); + +// ── defaultReadWindsurfJsonFiles — real fs scan against a tmp dir ───────── +// +// Tests the production reader's directory-scan + JSON-parse logic. Uses +// real fs (no mocks) because the implementation is fs-IO-bound and the +// test cost is trivial (a tmp dir + a few small files). + +describe('defaultReadWindsurfJsonFiles', () => { + let tmpDirPath: string; + + beforeEach(() => { + tmpDirPath = mkdtempSync(join(tmpdir(), 'nexpath-windsurf-test-')); + }); + afterEach(() => { + rmSync(tmpDirPath, { recursive: true, force: true }); + }); + + it('returns [] when the directory does not exist (no crash)', async () => { + const out = await defaultReadWindsurfJsonFiles( + join(tmpDirPath, 'does-not-exist'), + ); + expect(out).toEqual([]); + }); + + it('returns [] for an empty directory', async () => { + const out = await defaultReadWindsurfJsonFiles(tmpDirPath); + expect(out).toEqual([]); + }); + + it('reads + parses every .json file in the directory', async () => { + writeFileSync(join(tmpDirPath, 'a.json'), JSON.stringify({ x: 1 })); + writeFileSync(join(tmpDirPath, 'b.json'), JSON.stringify({ y: 2 })); + const out = await defaultReadWindsurfJsonFiles(tmpDirPath); + expect(out).toHaveLength(2); + const byPath = new Map(out.map((r) => [r.path, r.parsed])); + expect(byPath.get(join(tmpDirPath, 'a.json'))).toEqual({ x: 1 }); + expect(byPath.get(join(tmpDirPath, 'b.json'))).toEqual({ y: 2 }); + }); + + it('skips files that are not .json (case-insensitive on the suffix)', async () => { + writeFileSync(join(tmpDirPath, 'data.json'), JSON.stringify({ ok: true })); + writeFileSync(join(tmpDirPath, 'README.md'), 'not json'); + writeFileSync(join(tmpDirPath, 'binary.dat'), 'not json'); + const out = await defaultReadWindsurfJsonFiles(tmpDirPath); + expect(out).toHaveLength(1); + expect(out[0]!.path.endsWith('data.json')).toBe(true); + }); + + it('silently drops malformed JSON files (preserves valid siblings)', async () => { + writeFileSync(join(tmpDirPath, 'good.json'), JSON.stringify({ valid: true })); + writeFileSync(join(tmpDirPath, 'bad.json'), '{ not valid json,'); + const out = await defaultReadWindsurfJsonFiles(tmpDirPath); + expect(out).toHaveLength(1); + expect(out[0]!.path.endsWith('good.json')).toBe(true); + expect(out[0]!.parsed).toEqual({ valid: true }); + }); + + it('honours nested directories by NOT recursing (shallow scan only)', async () => { + writeFileSync(join(tmpDirPath, 'top.json'), JSON.stringify({ depth: 0 })); + mkdirSync(join(tmpDirPath, 'nested')); + writeFileSync(join(tmpDirPath, 'nested', 'deep.json'), JSON.stringify({ depth: 1 })); + const out = await defaultReadWindsurfJsonFiles(tmpDirPath); + // Only top.json; nested/deep.json is not reached + expect(out).toHaveLength(1); + expect(out[0]!.parsed).toEqual({ depth: 0 }); + }); +}); + +// ── defaultReadItemTable — the production better-sqlite3 + WAL reader ───────── +// +// These tests exercise the REAL reader against synthetic SQLite files (built +// with better-sqlite3 in the test setup). They cover the WAL-mode branch +// from dev plan §2.5 — the whole reason we swapped sql.js → better-sqlite3 +// in M2/B4. + +describe('defaultReadItemTable', () => { + let tmpDirPath: string; + + beforeEach(() => { + tmpDirPath = mkdtempSync(join(tmpdir(), 'nexpath-readtable-test-')); + }); + afterEach(() => { + rmSync(tmpDirPath, { recursive: true, force: true }); + }); + + /** Build a real .vscdb file with an ItemTable populated from the given rows. */ + async function createTestVscdb( + fileName: string, + rows: Array<{ key: string; value: string }>, + options: { walMode?: boolean } = {}, + ): Promise { + const mod = (await import('better-sqlite3')) as unknown as { + default: new (path: string) => { + exec(sql: string): unknown; + prepare(sql: string): { run(...args: unknown[]): unknown }; + pragma(pragma: string): unknown; + close(): void; + }; + }; + const Database = mod.default; + const path = join(tmpDirPath, fileName); + const db = new Database(path); + if (options.walMode) db.pragma('journal_mode = WAL'); + db.exec('CREATE TABLE ItemTable (key TEXT NOT NULL, value TEXT NOT NULL)'); + const insert = db.prepare('INSERT INTO ItemTable (key, value) VALUES (?, ?)'); + for (const r of rows) insert.run(r.key, r.value); + db.close(); + return path; + } + + /** Build a real .vscdb file with NO ItemTable (different schema). */ + async function createEmptyVscdb(fileName: string): Promise { + const mod = (await import('better-sqlite3')) as unknown as { + default: new (path: string) => { exec(sql: string): unknown; close(): void }; + }; + const Database = mod.default; + const path = join(tmpDirPath, fileName); + const db = new Database(path); + db.exec('CREATE TABLE OtherTable (x INTEGER)'); + db.close(); + return path; + } + + it('reads happy-path ItemTable rows from a real .vscdb file', async () => { + const dbPath = await createTestVscdb('happy.vscdb', [ + { key: 'aiService.prompts', value: '["hello","world"]' }, + { key: 'composer.composerData', value: '{"selectedComposerIds":[]}' }, + { key: 'workbench.editor.recent', value: '[]' }, + ]); + const rows = await defaultReadItemTable(dbPath); + expect(rows).toHaveLength(3); + expect(rows.find((r) => r.key === 'aiService.prompts')?.value).toBe('["hello","world"]'); + expect(rows.find((r) => r.key === 'composer.composerData')?.value).toBe('{"selectedComposerIds":[]}'); + }); + + it('returns [] when the .vscdb has no ItemTable (defensive)', async () => { + const dbPath = await createEmptyVscdb('no-itemtable.vscdb'); + const rows = await defaultReadItemTable(dbPath); + expect(rows).toEqual([]); + }); + + it('reads rows from a .vscdb opened in WAL mode (the Cursor scenario)', async () => { + // Build a WAL-mode .vscdb. The journal-mode pragma + the inserts create + // the .vscdb-wal sibling. better-sqlite3 reads from both transparently + // when opening the staged copy. + const dbPath = await createTestVscdb( + 'wal.vscdb', + [ + { key: 'aiService.prompts', value: '[]' }, + { key: 'cursor/agentLayout.sidebarWidth', value: '320' }, + ], + { walMode: true }, + ); + const rows = await defaultReadItemTable(dbPath); + expect(rows.length).toBeGreaterThanOrEqual(2); + expect(rows.find((r) => r.key === 'aiService.prompts')?.value).toBe('[]'); + expect(rows.find((r) => r.key === 'cursor/agentLayout.sidebarWidth')?.value).toBe('320'); + }); + + it('does not modify the source .vscdb file (operates on a tmp staging copy)', async () => { + const dbPath = await createTestVscdb('source-untouched.vscdb', [ + { key: 'aiService.prompts', value: '["x"]' }, + ]); + const { statSync } = await import('node:fs'); + const sizeBefore = statSync(dbPath).size; + await defaultReadItemTable(dbPath); + await defaultReadItemTable(dbPath); + await defaultReadItemTable(dbPath); + const sizeAfter = statSync(dbPath).size; + expect(sizeAfter).toBe(sizeBefore); + }); + + it('returns [] when the .vscdb path does not exist (host cleaned up workspace between activate and first read)', async () => { + // Reproduces the live 2026-05-20 R3 finding: Cursor enumerated 4 + // workspaceStorage dirs at activate-time, then cleaned up two of them + // before the debounced first read fired. The watcher previously threw + // ENOENT through onError on every fire; now defaultReadItemTable treats + // a missing main file as "no rows" so it never escalates. + const missingPath = join(tmpDirPath, 'never-existed.vscdb'); + const rows = await defaultReadItemTable(missingPath); + expect(rows).toEqual([]); + }); + + it('returns [] when the .vscdb is deleted between existsSync and copyFile (race)', async () => { + // Simulates the existsSync→copyFile race window. We delete the file + // through a wrapped readItemTable that runs deletion just before the + // copy. Here we use a more straightforward approach: create the file, + // then move it aside on the same tick the read starts. The real race + // happens at the OS level when Cursor's cleanup overlaps with our read. + const dbPath = await createTestVscdb('race.vscdb', [ + { key: 'aiService.prompts', value: '[]' }, + ]); + // Read once to verify happy path. + expect(await defaultReadItemTable(dbPath)).toHaveLength(1); + // Delete and read again — second read should return [] cleanly. + rmSync(dbPath, { force: true }); + expect(await defaultReadItemTable(dbPath)).toEqual([]); + }); +}); diff --git a/src/ext-vscode/src/chat-history-watcher.ts b/src/ext-vscode/src/chat-history-watcher.ts new file mode 100644 index 00000000..87aaea41 --- /dev/null +++ b/src/ext-vscode/src/chat-history-watcher.ts @@ -0,0 +1,553 @@ +import { watch, type FSWatcher, type WatchListener } from 'node:fs'; +import type { + ChatHistoryEvent, + ChatHistoryExtractor, + ItemTableRow, + WatchTarget, +} from './chat-history-types.js'; +import { ALL_EXTRACTORS, pickExtractor } from './extractors/index.js'; +import { decodeWindsurfJsonFile } from './extractors/windsurf.js'; + +/** + * Chat-history watcher (M2 Branch 2). + * + * Watches the agent's persisted chat-history storage and emits a + * `ChatHistoryEvent` for every NEW user prompt that appears. + * + * Per-agent storage model: + * - Cursor: a single SQLite file (`state.vscdb`) under + * `User/globalStorage/`. Treated as kind `cursor-sqlite`. + * - Windsurf: a directory under `~/.codeium/windsurf/` containing JSON + * conversation files. Treated as kind `windsurf-dir`. Each fs.watch + * fire rescans the directory, parses every .json file, and passes the + * parsed value to `decodeWindsurfJsonFile` (in `extractors/windsurf.ts`). + * The decoder is currently a no-op pending live-install schema + * inspection (see its TODO); the FS plumbing is in place so the only + * missing piece is the field extraction. + * + * Pipeline (Cursor / sqlite): + * fs.watch fires -> debounce (default 250 ms) -> read file bytes + * -> parse ItemTable -> pickExtractor(observed keys) -> for each row + * the extractor owns, decode -> dedupe new events by signature -> + * emit via `onEvent`. + * + * The actual SQLite parsing is injected via `readItemTableFn` so tests + * can run without better-sqlite3. In production a better-sqlite3-backed + * reader is used — chosen over sql.js because the live Cursor `.vscdb` + * file is in **WAL mode** (the main file is ~4 KB; all writes go to the + * sibling `.vscdb-wal`). sql.js operates on a buffer and cannot read the + * WAL siblings, so it never sees the live data (dev plan §2.5). The + * better-sqlite3 reader copies main + wal + shm to a tmp staging dir, + * checkpoints the WAL into the staged main file, then reads — never + * touching the live file Cursor is actively writing to. + */ + +/** Pure function that turns a state.vscdb FILE PATH into ItemTable rows. */ +export type ReadItemTableFn = (dbPath: string) => Promise; + +/** + * Pure function that scans a Windsurf chat-data directory and returns its + * `.json` files as `{path, parsed}` pairs. Malformed or unreadable files + * are skipped silently (drop one bad file, don't blow up the whole scan); + * a missing directory returns `[]` so activate-time enumeration doesn't + * have to pre-check existence. + */ +export type ReadWindsurfJsonFilesFn = ( + dirPath: string, +) => Promise>; + +/** + * Default Windsurf JSON reader: lists `/*.json`, reads each file, + * `JSON.parse`s the contents, skips files that fail either step. Used by + * the watcher when the host is Windsurf and a codeium cascade target was + * enumerated at activate time. + */ +export const defaultReadWindsurfJsonFiles: ReadWindsurfJsonFilesFn = async ( + dirPath, +) => { + const { readdir, readFile } = await import('node:fs/promises'); + const { existsSync } = await import('node:fs'); + const { join } = await import('node:path'); + + if (!existsSync(dirPath)) return []; + let entries: string[]; + try { + entries = await readdir(dirPath); + } catch { + return []; + } + + const out: Array<{ path: string; parsed: unknown }> = []; + for (const name of entries) { + if (!name.toLowerCase().endsWith('.json')) continue; + const fullPath = join(dirPath, name); + let content: string; + try { + content = await readFile(fullPath, 'utf8'); + } catch { + continue; + } + let parsed: unknown; + try { + parsed = JSON.parse(content); + } catch { + continue; + } + out.push({ path: fullPath, parsed }); + } + return out; +}; + +/** + * Default WAL-aware reader using better-sqlite3. + * + * Strategy: + * 1. Stage: copy main + .vscdb-wal + .vscdb-shm to a tmp dir. + * 2. Open the staged copy read-only with better-sqlite3 (native module — + * handles WAL transparently because the staged copy retains the WAL + * file alongside). + * 3. Run `PRAGMA wal_checkpoint(TRUNCATE)` on the staged copy to fold + * the WAL pages into the main file (belt-and-braces; better-sqlite3 + * reads them either way). + * 4. Query `ItemTable`. + * 5. Close + clean up the staging dir. + * + * Dynamic import keeps the native module out of the eager require graph + * (cheaper extension startup; only loaded on first chat-history read). + */ +export const defaultReadItemTable: ReadItemTableFn = async (dbPath) => { + const { copyFile, mkdir, rm } = await import('node:fs/promises'); + const { existsSync } = await import('node:fs'); + const { tmpdir } = await import('node:os'); + const { basename, join } = await import('node:path'); + + // Race-safe read: a workspace storage dir enumerated at activate-time can + // be deleted by the host (e.g. Cursor cleaning up stale workspaces) before + // the first debounced read fires. Treat a missing main file as "no rows" + // rather than throwing a noisy ENOENT through onError. fs.watch on the + // missing path will continue to fire harmlessly if the file ever returns. + if (!existsSync(dbPath)) return []; + + const stagingDir = join( + tmpdir(), + `nexpath-watcher-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`, + ); + await mkdir(stagingDir, { recursive: true }); + const stagedMain = join(stagingDir, basename(dbPath)); + try { + await copyFile(dbPath, stagedMain); + } catch (err) { + // Lost the race between the existsSync above and the copyFile here, or + // the file became unreadable for another reason. Clean up the (empty) + // staging dir and treat as no rows. + void rm(stagingDir, { recursive: true, force: true }).catch(() => {}); + if ((err as NodeJS.ErrnoException).code === 'ENOENT') return []; + throw err; + } + for (const suffix of ['-wal', '-shm'] as const) { + const sibling = dbPath + suffix; + if (existsSync(sibling)) { + try { + await copyFile(sibling, stagedMain + suffix); + } catch { + // WAL/SHM races are common; better-sqlite3 reads main regardless. + } + } + } + + const mod = (await import('better-sqlite3')) as unknown as { + default: new (path: string, options?: { readonly?: boolean }) => { + prepare(sql: string): { + all(...params: unknown[]): Array>; + }; + pragma(pragma: string): unknown; + close(): void; + }; + }; + const Database = mod.default; + const db = new Database(stagedMain, { readonly: true }); + try { + try { + db.pragma('wal_checkpoint(TRUNCATE)'); + } catch { + // Not in WAL mode — fine, continue. + } + // Discover which of the known chat-data tables exist in this DB. + // - `ItemTable` is the standard VS Code state table — holds Ask-mode + // `aiService.prompts` (cursor-v2024-q4) and the Composer-metadata + // `composer.composerData` row (cursor-v2025-q1). + // - `cursorDiskKV` is Cursor's modern Composer / Agent chat storage — + // `composerData:` (metadata) + `bubbleId::` + // (individual user / assistant messages). Lives in `globalStorage/ + // state.vscdb` and is the default storage location for Cursor's + // Agent mode (the right-side chat panel in 3.4.20+). + // Either table may be absent on freshly-created state.vscdb files; + // treat as empty rather than throwing. + const tableNames = ( + db + .prepare( + "SELECT name FROM sqlite_master WHERE type='table' AND name IN ('ItemTable','cursorDiskKV')", + ) + .all() as Array<{ name: string }> + ).map((r) => r.name); + const out: Array<{ key: string; value: string }> = []; + if (tableNames.includes('ItemTable')) { + const rows = db + .prepare('SELECT key, value FROM ItemTable') + .all() as Array<{ key: string; value: string }>; + for (const r of rows) { + out.push({ key: String(r.key), value: String(r.value) }); + } + } + if (tableNames.includes('cursorDiskKV')) { + // cursorDiskKV keys are prefixed with `cursorDiskKV/` in the row + // stream so existing ItemTable extractors (which match keys like + // `aiService.prompts` exactly) can NEVER accidentally consume them, + // and the new composer-bubble extractor uses the prefix as a + // load-bearing fingerprint signal. The extractor strips the prefix + // before parsing the row's value. + const rows = db + .prepare('SELECT key, value FROM cursorDiskKV') + .all() as Array<{ key: string; value: string }>; + for (const r of rows) { + out.push({ + key: `cursorDiskKV/${String(r.key)}`, + value: String(r.value), + }); + } + } + return out; + } finally { + db.close(); + // Best-effort cleanup — staging dir is in /tmp, OS will clean up eventually. + void rm(stagingDir, { recursive: true, force: true }).catch(() => {}); + } +}; + +export interface ChatHistoryWatcherOptions { + /** Paths the watcher should monitor. */ + targets: WatchTarget[]; + /** Emitted for every NEW user prompt detected. */ + onEvent: (e: ChatHistoryEvent) => void; + /** Emitted on any non-fatal error (read failure, watch error, etc.). */ + onError?: (err: Error) => void; + /** + * Emitted when a Cursor target's ItemTable doesn't fingerprint to any + * known extractor — extension layer surfaces this as a "schema unknown, + * please update nexpath extension" toast. + */ + onSchemaUnknown?: (info: { + path: string; + observedSampleKeys: readonly string[]; + }) => void; + + // ── Dependency injection (tests substitute these) ───────────────────────── + /** Debounce window for coalescing fs.watch events. Default 250 ms. */ + debounceMs?: number; + /** + * Polling backstop interval (ms). When > 0, every target is re-read on this + * interval IN ADDITION to fs.watch. Default 0 (off — preserves test behaviour; + * production wiring sets it). This exists because `fs.watch` is unreliable on + * Windows for SQLite files written by another process: WAL checkpoints + * delete+recreate the `-wal`/`-shm` siblings, which orphans the Windows watch + * handle so it fires once (or not at all) and then goes silent — observed as + * "only the first 1–2 prompts captured, then nothing". Polling re-reads + * regardless; the dedup set means already-seen prompts are never re-emitted, + * so a poll only surfaces genuinely new prompts. + */ + pollMs?: number; + /** Inject `fs.watch` (defaults to node:fs `watch`). */ + watchFn?: typeof watch; + /** Inject the ItemTable reader (defaults to better-sqlite3 WAL-aware reader). */ + readItemTableFn?: ReadItemTableFn; + /** Inject the Windsurf JSON-dir reader (defaults to fs/promises-backed scan). */ + readWindsurfJsonFilesFn?: ReadWindsurfJsonFilesFn; + /** + * Inject the Windsurf JSON-file decoder. Defaults to + * `decodeWindsurfJsonFile` from `./extractors/windsurf.js`. Exposed for + * tests; once a real decoder is filled in upstream, production callers + * normally leave this unset and pick up the new behaviour automatically. + */ + decodeWindsurfFn?: (parsed: unknown, sourcePath: string) => ChatHistoryEvent[]; + /** Inject the clock (defaults to `() => new Date()`). */ + nowFn?: () => Date; +} + +export interface ChatHistoryWatcher { + /** Start fs watchers + fire an initial read so existing rows are diffed. */ + start(): void; + /** Stop all watchers + cancel pending debouncers. */ + stop(): void; +} + +export function createChatHistoryWatcher( + opts: ChatHistoryWatcherOptions, +): ChatHistoryWatcher { + const debounceMs = opts.debounceMs ?? 250; + const pollMs = opts.pollMs ?? 0; + const watchFn = opts.watchFn ?? watch; + const readItemTableFn = opts.readItemTableFn ?? defaultReadItemTable; + const readWindsurfJsonFilesFn = + opts.readWindsurfJsonFilesFn ?? defaultReadWindsurfJsonFiles; + const decodeWindsurfFn = opts.decodeWindsurfFn ?? decodeWindsurfJsonFile; + const nowFn = opts.nowFn ?? (() => new Date()); + + const fsWatchers: FSWatcher[] = []; + let pollTimer: ReturnType | undefined; + const debouncers = new Map>(); + /** Deduplication key per emitted event so we never emit the same prompt twice. */ + const seenSignatures = new Set(); + /** Per-target extractor cache — first read decides; subsequent reads reuse. */ + const extractorCache = new Map(); + /** + * Targets whose initial read has completed. On the first read for a target, + * existing rows are registered in `seenSignatures` WITHOUT calling `onEvent` + * — this matches the Claude-Code-hook contract that downstream Layer C is + * built around (only NEW user prompts feed the pipeline; historical prompts + * accumulated in state.vscdb are NOT replayed every activation). Without + * this prime-only behaviour, every Cursor restart re-emitted the entire + * prompt backlog, flooding Layer C's session-state machine and producing + * advisory storms that bypass the 3-prompt warmup + 5-prompt cooldown gates. + */ + const primedTargets = new Set(); + + /** + * Paths already surfaced via `onSchemaUnknown`. The unknown-schema branch + * does NOT cache an extractor (it must keep re-checking, because a fresh + * Cursor workspace's state.vscdb starts with no chat keys and only gains + * them once the user chats). But re-notifying on every fs.watch fire floods + * the log and re-pops the info toast — acutely visible on Windsurf, whose + * workspaceStorage state.vscdb never holds chat and so stays unknown forever. + * Notify once per path; keep re-checking silently thereafter. + */ + const reportedUnknownPaths = new Set(); + + /** + * Cross-extractor dedup. Cursor 3.x mirrors Composer prompts into BOTH + * globalStorage cursorDiskKV (decoded by cursor-composer-bubble) AND + * workspaceStorage ItemTable.aiService.prompts (decoded by cursor-v2024-q4 + * as "ask-mode"). Without this map every Composer prompt is captured twice + * → Layer C's prompt_count grows at 2× rate → classifier fires at wrong + * positions. Discovered during S01 manual testing 2026-05-27. + * + * Keyed by trimmed prompt text → last-seen epoch ms. A prompt re-emitted + * within DEDUP_WINDOW_MS of its first sighting is dropped. + */ + const recentPromptTimestamps = new Map(); + const DEDUP_WINDOW_MS = 60_000; + + function signatureOf(e: ChatHistoryEvent): string { + return `${e.sourcePath}|${e.rawSessionId}|${e.prompt}`; + } + + /** Returns true if this prompt text was already emitted in the dedup window. */ + function isCrossExtractorDuplicate(promptText: string, now: number): boolean { + const key = promptText.trim(); + const last = recentPromptTimestamps.get(key); + if (last !== undefined && now - last < DEDUP_WINDOW_MS) { + return true; + } + recentPromptTimestamps.set(key, now); + // Lazy GC: drop entries older than the window so the map doesn't grow forever. + if (recentPromptTimestamps.size > 200) { + for (const [k, t] of recentPromptTimestamps) { + if (now - t >= DEDUP_WINDOW_MS) recentPromptTimestamps.delete(k); + } + } + return false; + } + + function reportError(err: unknown, path: string): void { + const e = err instanceof Error ? err : new Error(String(err)); + opts.onError?.(new Error(`[chat-history-watcher] ${path}: ${e.message}`)); + } + + async function processSqliteTarget(target: WatchTarget): Promise { + try { + const rows = await readItemTableFn(target.path); + const isInitialPass = !primedTargets.has(target.path); + primedTargets.add(target.path); + + // Per-row, run ALL extractors that own the row's key — not just the + // single "fingerprint winner". The old logic picked one extractor via + // `pickExtractor` and used it for every row, which silently dropped + // prompts when the workspace had keys from multiple Cursor eras: e.g. + // both `aiService.prompts` (cursor-v2024-q4) AND `composer.composerData` + // (cursor-v2025-q1) exist in modern Cursor workspaces. The fingerprint + // count tied at 1 each, registry order picked cursor-v2025-q1, which + // doesn't own aiService.prompts → all Ask-mode prompts were silently + // discarded. Discovered during M2 manual testing Round 2 after the WAL + // fix (which got fs.watch firing correctly) revealed that events + // STILL weren't emitting. + // + // `pickExtractor` is still used to drive the schema-unknown toast + // (matching ANY extractor = schema known, matching NONE = unknown), + // but the per-row decoding now uses every extractor whose `ownsKey` + // returns true for that row. + const extractorsToTry = target.extractor + ? [target.extractor] + : (extractorCache.get(target.path) ?? ALL_EXTRACTORS); + + if (!target.extractor && !extractorCache.has(target.path)) { + const fp = pickExtractor(rows.map((r) => r.key)); + if (fp.kind === 'known') { + // Cache ALL_EXTRACTORS once we've confirmed at least one matches — + // subsequent reads of this target skip the unknown-schema check. + extractorCache.set(target.path, ALL_EXTRACTORS); + } else { + if (!reportedUnknownPaths.has(target.path)) { + reportedUnknownPaths.add(target.path); + opts.onSchemaUnknown?.({ + path: target.path, + observedSampleKeys: fp.observedSampleKeys, + }); + } + return; + } + } + + for (const row of rows) { + for (const extractor of extractorsToTry) { + if (!extractor.ownsKey(row.key)) continue; + const decoded = extractor.decodeRow(row, target.path); + for (const ev of decoded) { + const sig = signatureOf(ev); + if (seenSignatures.has(sig)) continue; + seenSignatures.add(sig); + // Initial pass: prime dedup only. Subsequent fs.watch fires emit + // only truly-new prompts. See primedTargets docstring. + if (isInitialPass) { + // Cross-extractor dedup is meant to catch Composer→Ask MIRROR + // emissions seconds apart, NOT historical prompts from prior + // sessions that happen to share text with a fresh user prompt. + // globalStorage state.vscdb is workspace-agnostic so initial + // pass sees bubbles from ALL prior workspaces. Priming with + // timestamp 0 ensures `now - 0 >> DEDUP_WINDOW_MS`, so any + // genuinely new emission of the same text passes through. + recentPromptTimestamps.set(ev.prompt.trim(), 0); + continue; + } + const now = nowFn(); + if (isCrossExtractorDuplicate(ev.prompt, now.getTime())) continue; + opts.onEvent({ ...ev, capturedAt: now }); + } + } + } + } catch (err) { + reportError(err, target.path); + } + } + + async function processWindsurfTarget(target: WatchTarget): Promise { + try { + const files = await readWindsurfJsonFilesFn(target.path); + const isInitialPass = !primedTargets.has(target.path); + primedTargets.add(target.path); + for (const { path: filePath, parsed } of files) { + const decoded = decodeWindsurfFn(parsed, filePath); + for (const ev of decoded) { + const sig = signatureOf(ev); + if (seenSignatures.has(sig)) continue; + seenSignatures.add(sig); + if (isInitialPass) { + // See sqlite-target's comment on why timestamp=0 here. + recentPromptTimestamps.set(ev.prompt.trim(), 0); + continue; + } + const now = nowFn(); + if (isCrossExtractorDuplicate(ev.prompt, now.getTime())) continue; + opts.onEvent({ ...ev, capturedAt: now }); + } + } + } catch (err) { + reportError(err, target.path); + } + } + + function schedule(target: WatchTarget): void { + const existing = debouncers.get(target.path); + if (existing) clearTimeout(existing); + const t = setTimeout(() => { + debouncers.delete(target.path); + if (target.kind === 'cursor-sqlite') { + void processSqliteTarget(target); + } else { + void processWindsurfTarget(target); + } + }, debounceMs); + debouncers.set(target.path, t); + } + + return { + start(): void { + for (const target of opts.targets) { + try { + const listener: WatchListener = () => schedule(target); + // Watch the primary path (the SQLite main file for cursor-sqlite, + // or the codeium dir for windsurf-dir). + const w = watchFn(target.path, listener); + w.on('error', (err: Error) => reportError(err, target.path)); + fsWatchers.push(w); + + // ── WAL-mode liveness fix ──────────────────────────────────────── + // For cursor-sqlite targets, Cursor uses SQLite WAL mode — all + // writes go to `-wal`, NOT the main file. fs.watch on the + // main file alone would never fire for new prompts (the main file + // only changes at checkpoint time, which can be minutes or hours + // later). Also watch the WAL sibling so we re-read whenever Cursor + // writes a new prompt. The read path (defaultReadItemTable) already + // copies main + wal + shm to a tmp staging dir and checkpoints + // before reading, so it always sees the latest data regardless of + // which file triggered the watch. Discovered during M2 manual + // testing Round 2 when live Cursor prompts didn't reach the store. + if (target.kind === 'cursor-sqlite') { + for (const suffix of ['-wal', '-shm'] as const) { + const siblingPath = target.path + suffix; + try { + const sw = watchFn(siblingPath, listener); + sw.on('error', (err: Error) => reportError(err, siblingPath)); + fsWatchers.push(sw); + } catch { + // Sibling doesn't exist yet (WAL hasn't been initialised) — + // that's fine; Cursor creates it on first chat write, and + // the main-file watch will fire when SQLite eventually + // checkpoints, prompting a re-read that sees the new rows. + } + } + } + + // Initial pass so any existing rows are diffed immediately + schedule(target); + } catch (err) { + reportError(err, target.path); + } + } + + // Polling backstop — re-read every target on an interval so capture does + // not depend on fs.watch firing (which is unreliable on Windows for the + // SQLite WAL recreate pattern; see pollMs doc). Dedup makes this safe: a + // poll only emits prompts not already seen. + if (pollMs > 0) { + pollTimer = setInterval(() => { + for (const target of opts.targets) schedule(target); + }, pollMs); + if (typeof (pollTimer as { unref?: () => void }).unref === 'function') { + (pollTimer as { unref: () => void }).unref(); + } + } + }, + stop(): void { + if (pollTimer !== undefined) { clearInterval(pollTimer); pollTimer = undefined; } + for (const t of debouncers.values()) clearTimeout(t); + debouncers.clear(); + for (const w of fsWatchers) { + try { + w.close(); + } catch { + // ignore + } + } + fsWatchers.length = 0; + }, + }; +} diff --git a/src/ext-vscode/src/chat-input-injector.test.ts b/src/ext-vscode/src/chat-input-injector.test.ts new file mode 100644 index 00000000..b3ac8aa7 --- /dev/null +++ b/src/ext-vscode/src/chat-input-injector.test.ts @@ -0,0 +1,132 @@ +import { describe, it, expect, vi } from 'vitest'; + +vi.mock('vscode', () => ({ + commands: { + executeCommand: vi.fn(), + getCommands: vi.fn(), + }, +})); + +import { + chatInputInject, + CANDIDATE_COMMANDS, +} from './chat-input-injector.js'; + +describe('chatInputInject', () => { + it('returns false immediately for vscode-generic (no AI chat input)', async () => { + const exec = vi.fn(); + const list = vi.fn(); + const result = await chatInputInject('hello', { + host: 'vscode-generic', + executeCommand: exec, + getCommands: list, + }); + expect(result).toBe(false); + expect(exec).not.toHaveBeenCalled(); + expect(list).not.toHaveBeenCalled(); + }); + + it('tries Cursor candidates when host = cursor', async () => { + const exec = vi.fn().mockResolvedValue(undefined); + const list = vi + .fn() + .mockResolvedValue([CANDIDATE_COMMANDS.cursor[0]]); // first candidate available + const result = await chatInputInject('hello', { + host: 'cursor', + executeCommand: exec, + getCommands: list, + }); + expect(result).toBe(true); + expect(list).toHaveBeenCalledWith(true); + expect(exec).toHaveBeenCalledWith(CANDIDATE_COMMANDS.cursor[0], 'hello'); + }); + + it('tries each cursor candidate in order until one succeeds', async () => { + const exec = vi.fn(); + // First two candidates throw, third succeeds + exec + .mockRejectedValueOnce(new Error('first fail')) + .mockRejectedValueOnce(new Error('second fail')) + .mockResolvedValueOnce(undefined); + const list = vi.fn().mockResolvedValue([...CANDIDATE_COMMANDS.cursor]); + const result = await chatInputInject('hi', { + host: 'cursor', + executeCommand: exec, + getCommands: list, + }); + expect(result).toBe(true); + expect(exec).toHaveBeenCalledTimes(3); + expect(exec).toHaveBeenLastCalledWith(CANDIDATE_COMMANDS.cursor[2], 'hi'); + }); + + it('skips candidates that are not in vscode.commands list', async () => { + const exec = vi.fn().mockResolvedValue(undefined); + // Only the third candidate is available + const list = vi.fn().mockResolvedValue([CANDIDATE_COMMANDS.cursor[2]]); + const result = await chatInputInject('hi', { + host: 'cursor', + executeCommand: exec, + getCommands: list, + }); + expect(result).toBe(true); + expect(exec).toHaveBeenCalledTimes(1); + expect(exec).toHaveBeenCalledWith(CANDIDATE_COMMANDS.cursor[2], 'hi'); + }); + + it('returns false when no cursor candidate is available', async () => { + const exec = vi.fn(); + const list = vi.fn().mockResolvedValue(['unrelated.command']); + const result = await chatInputInject('hi', { + host: 'cursor', + executeCommand: exec, + getCommands: list, + }); + expect(result).toBe(false); + expect(exec).not.toHaveBeenCalled(); + }); + + it('returns false when all available cursor candidates throw', async () => { + const exec = vi.fn().mockRejectedValue(new Error('always fails')); + const list = vi.fn().mockResolvedValue([...CANDIDATE_COMMANDS.cursor]); + const result = await chatInputInject('hi', { + host: 'cursor', + executeCommand: exec, + getCommands: list, + }); + expect(result).toBe(false); + expect(exec).toHaveBeenCalledTimes(CANDIDATE_COMMANDS.cursor.length); + }); + + it('returns false when getCommands itself throws', async () => { + const exec = vi.fn(); + const list = vi.fn().mockRejectedValue(new Error('command list broken')); + const result = await chatInputInject('hi', { + host: 'cursor', + executeCommand: exec, + getCommands: list, + }); + expect(result).toBe(false); + expect(exec).not.toHaveBeenCalled(); + }); + + it('tries Windsurf candidates when host = windsurf', async () => { + const exec = vi.fn().mockResolvedValue(undefined); + const list = vi.fn().mockResolvedValue([CANDIDATE_COMMANDS.windsurf[0]]); + const result = await chatInputInject('hi', { + host: 'windsurf', + executeCommand: exec, + getCommands: list, + }); + expect(result).toBe(true); + expect(exec).toHaveBeenCalledWith(CANDIDATE_COMMANDS.windsurf[0], 'hi'); + }); +}); + +describe('CANDIDATE_COMMANDS', () => { + it('exposes both cursor and windsurf candidate arrays', () => { + expect(Array.isArray(CANDIDATE_COMMANDS.cursor)).toBe(true); + expect(Array.isArray(CANDIDATE_COMMANDS.windsurf)).toBe(true); + expect(CANDIDATE_COMMANDS.cursor.length).toBeGreaterThan(0); + expect(CANDIDATE_COMMANDS.windsurf.length).toBeGreaterThan(0); + }); +}); diff --git a/src/ext-vscode/src/chat-input-injector.ts b/src/ext-vscode/src/chat-input-injector.ts new file mode 100644 index 00000000..181a8625 --- /dev/null +++ b/src/ext-vscode/src/chat-input-injector.ts @@ -0,0 +1,103 @@ +import * as vscode from 'vscode'; +import type { Host } from './host-detector.js'; + +/** + * Chat-input injector (fills the B4 contract from + * `project_b4_prompt_injection_contract` memory). + * + * Per-host candidate `vscode.commands.*` IDs that MIGHT write text to the + * AI chat input. These are heuristic guesses based on community + * documentation / reverse-engineering — none are official APIs. The + * function tries each in order; the first one that doesn't throw + accepts + * the text argument is treated as success. + * + * **None of these IDs are verified yet against a real running Cursor / + * Windsurf.** Branch 5 (smoke-test) is where the engineer hand-verifies + * the actual command IDs and prunes / re-orders this list based on + * observed reality. The `dryRun` injection from B5 should also try + * `vscode.commands.getCommands(true)` against a live Cursor and log the + * filtered candidate set. + * + * Until B5 verifies, this function's net effect on Cursor 3.4.20 will + * almost certainly be "all candidates fail, return false, clipboard + * fallback takes over". That's safe — `handleOptionSelection` falls + * through cleanly to the clipboard path documented in dev plan §2.4. + */ + +const CURSOR_CANDIDATE_COMMANDS: ReadonlyArray = [ + // These are educated guesses. Verify against a live Cursor by running + // `vscode.commands.getCommands(true)` from a development host and filtering. + 'cursor.aichat.insertWithSelection', + 'composer.newChat', + 'cursor.composer.focus', + 'aichat.insertSelection', + 'workbench.action.chat.open', +]; + +const WINDSURF_CANDIDATE_COMMANDS: ReadonlyArray = [ + // Same caveat — verify in B5 against a real Windsurf. + 'windsurf.cascade.newConversation', + 'cascade.openChat', + 'codeium.openCascade', +]; + +/** Re-export for tests that want to assert against the list. */ +export const CANDIDATE_COMMANDS = { + cursor: CURSOR_CANDIDATE_COMMANDS, + windsurf: WINDSURF_CANDIDATE_COMMANDS, +}; + +export interface ChatInputInjectorDeps { + /** Inject the host-resolver. Tests pass a fixed host. */ + host?: Host; + /** Inject the command executor. Tests provide a mock. */ + executeCommand?: (id: string, ...args: unknown[]) => Thenable; + /** Inject the command lister. Tests provide a mock. */ + getCommands?: (filterInternal?: boolean) => Thenable; +} + +/** + * Try to inject `text` into the host's AI chat input. Returns `true` if a + * candidate command executed without throwing. Returns `false` (so + * `handleOptionSelection` falls back to clipboard) if no candidate + * succeeded — including when the host is plain VS Code (no AI chat input + * to inject into). + */ +export async function chatInputInject( + text: string, + deps: ChatInputInjectorDeps = {}, +): Promise { + const host = deps.host ?? 'vscode-generic'; + if (host === 'vscode-generic') return false; + + const exec = + deps.executeCommand ?? + ((id: string, ...args: unknown[]) => + vscode.commands.executeCommand(id, ...args)); + const list = + deps.getCommands ?? + ((filter?: boolean) => vscode.commands.getCommands(filter)); + + const candidates = + host === 'cursor' ? CURSOR_CANDIDATE_COMMANDS : WINDSURF_CANDIDATE_COMMANDS; + + // Only try commands that actually exist on the current host — avoids + // wasted "no such command" rejections in the developer console. + let available: Set; + try { + available = new Set(await list(true)); + } catch { + return false; + } + + for (const id of candidates) { + if (!available.has(id)) continue; + try { + await exec(id, text); + return true; + } catch { + // try next + } + } + return false; +} diff --git a/src/ext-vscode/src/chat-pipeline.test.ts b/src/ext-vscode/src/chat-pipeline.test.ts new file mode 100644 index 00000000..1e7a9437 --- /dev/null +++ b/src/ext-vscode/src/chat-pipeline.test.ts @@ -0,0 +1,192 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { createChatEventHandler } from './chat-pipeline.js'; +import type { ChatHistoryEvent } from './chat-history-types.js'; +import type { StopSelection } from './ipc.js'; + +const makeEvent = (overrides: Partial = {}): ChatHistoryEvent => ({ + prompt: 'hello world', + rawSessionId: 'tab:abc-123', + capturedAt: new Date(0), + sourcePath: '/fake/state.vscdb', + extractorId: 'cursor-v2025-q2', + ...overrides, +}); + +const fakeSelection: StopSelection = { selectedPrompt: 'Refine the request' }; + +describe('createChatEventHandler', () => { + let spawnAuto: ReturnType; + let spawnStop: ReturnType; + let injectSelection: ReturnType; + let onAfterCapture: ReturnType; + let errorLog: ReturnType; + + beforeEach(() => { + spawnAuto = vi.fn().mockResolvedValue(undefined); + spawnStop = vi.fn().mockResolvedValue(null); + injectSelection = vi.fn().mockResolvedValue(undefined); + onAfterCapture = vi.fn().mockResolvedValue(undefined); + errorLog = vi.fn(); + }); + + it('arms the fallback after auto (before stop), then injects on a selection', async () => { + spawnStop.mockResolvedValueOnce(fakeSelection); + const handler = createChatEventHandler({ + spawnAuto, + spawnStop, + injectSelection, + onAfterCapture, + logger: { error: errorLog }, + }); + const event = makeEvent(); + await handler(event); + expect(spawnAuto).toHaveBeenCalledWith('hello world', 'tab:abc-123', event); + expect(onAfterCapture).toHaveBeenCalledWith(event); + expect(spawnStop).toHaveBeenCalledWith('tab:abc-123', event); + expect(injectSelection).toHaveBeenCalledWith('Refine the request', event); + // onAfterCapture runs BEFORE stop + expect(onAfterCapture.mock.invocationCallOrder[0]).toBeLessThan( + spawnStop.mock.invocationCallOrder[0], + ); + expect(errorLog).not.toHaveBeenCalled(); + }); + + it('arms the fallback even when there is no selection (does not inject)', async () => { + spawnStop.mockResolvedValueOnce(null); + const handler = createChatEventHandler({ + spawnAuto, + spawnStop, + injectSelection, + onAfterCapture, + logger: { error: errorLog }, + }); + const event = makeEvent(); + await handler(event); + expect(onAfterCapture).toHaveBeenCalledWith(event); + expect(injectSelection).not.toHaveBeenCalled(); + }); + + it('tolerates a missing onAfterCapture callback', async () => { + spawnStop.mockResolvedValueOnce(null); + const handler = createChatEventHandler({ + spawnAuto, + spawnStop, + injectSelection, + logger: { error: errorLog }, + }); + await expect(handler(makeEvent())).resolves.toBeUndefined(); + expect(errorLog).not.toHaveBeenCalled(); + }); + + it('uses composeSessionId to derive the session id (workspace-prefixed)', async () => { + spawnStop.mockResolvedValueOnce(fakeSelection); + const compose = vi.fn((e: ChatHistoryEvent) => `ws-X|${e.rawSessionId}`); + const handler = createChatEventHandler({ + spawnAuto, + spawnStop, + injectSelection, + composeSessionId: compose, + logger: { error: errorLog }, + }); + const event = makeEvent(); + await handler(event); + expect(compose).toHaveBeenCalledOnce(); + expect(spawnAuto).toHaveBeenCalledWith('hello world', 'ws-X|tab:abc-123', event); + expect(spawnStop).toHaveBeenCalledWith('ws-X|tab:abc-123', event); + }); + + it('forwards the originating ChatHistoryEvent so callers can derive per-event cwd', async () => { + spawnStop.mockResolvedValueOnce(fakeSelection); + const handler = createChatEventHandler({ + spawnAuto, + spawnStop, + injectSelection, + onAfterCapture, + logger: { error: errorLog }, + }); + const event = makeEvent({ + sourcePath: '/home/u/.config/Cursor/User/workspaceStorage/abc/state.vscdb', + }); + await handler(event); + expect((spawnAuto.mock.calls[0][2] as ChatHistoryEvent).sourcePath).toBe(event.sourcePath); + expect((onAfterCapture.mock.calls[0][0] as ChatHistoryEvent).sourcePath).toBe(event.sourcePath); + expect((spawnStop.mock.calls[0][1] as ChatHistoryEvent).sourcePath).toBe(event.sourcePath); + }); + + it('returns early when spawnAuto rejects (no arm, no stop, no inject)', async () => { + spawnAuto.mockRejectedValueOnce(new Error('auto blew up')); + const handler = createChatEventHandler({ + spawnAuto, + spawnStop, + injectSelection, + onAfterCapture, + logger: { error: errorLog }, + }); + await handler(makeEvent()); + expect(spawnAuto).toHaveBeenCalledOnce(); + expect(onAfterCapture).not.toHaveBeenCalled(); + expect(spawnStop).not.toHaveBeenCalled(); + expect(injectSelection).not.toHaveBeenCalled(); + expect(errorLog).toHaveBeenCalledWith('[nexpath] spawnAuto failed:', expect.any(Error)); + }); + + it('continues past an onAfterCapture error (fallback failure is non-fatal)', async () => { + onAfterCapture.mockRejectedValueOnce(new Error('arm blew up')); + spawnStop.mockResolvedValueOnce(fakeSelection); + const handler = createChatEventHandler({ + spawnAuto, + spawnStop, + injectSelection, + onAfterCapture, + logger: { error: errorLog }, + }); + await handler(makeEvent()); + expect(errorLog).toHaveBeenCalledWith('[nexpath] onAfterCapture failed:', expect.any(Error)); + // pipeline still proceeds to the popup + injection + expect(spawnStop).toHaveBeenCalledOnce(); + expect(injectSelection).toHaveBeenCalledOnce(); + }); + + it('logs and returns when spawnStop rejects — the armed fallback stands', async () => { + spawnStop.mockRejectedValueOnce(new Error('stop blew up')); + const handler = createChatEventHandler({ + spawnAuto, + spawnStop, + injectSelection, + onAfterCapture, + logger: { error: errorLog }, + }); + await handler(makeEvent()); + expect(onAfterCapture).toHaveBeenCalledOnce(); // armed before the popup + expect(injectSelection).not.toHaveBeenCalled(); + expect(errorLog).toHaveBeenCalledWith('[nexpath] spawnStop failed:', expect.any(Error)); + }); + + it('logs and swallows when injectSelection throws (does not propagate to watcher)', async () => { + spawnStop.mockResolvedValueOnce(fakeSelection); + injectSelection.mockRejectedValueOnce(new Error('inject blew up')); + const handler = createChatEventHandler({ + spawnAuto, + spawnStop, + injectSelection, + logger: { error: errorLog }, + }); + await expect(handler(makeEvent())).resolves.toBeUndefined(); + expect(errorLog).toHaveBeenCalledWith('[nexpath] injectSelection failed:', expect.any(Error)); + }); + + it('handler always resolves — never propagates exceptions to the watcher', async () => { + spawnAuto.mockRejectedValueOnce(new Error('a')); + spawnStop.mockRejectedValueOnce(new Error('b')); + injectSelection.mockRejectedValueOnce(new Error('c')); + onAfterCapture.mockRejectedValueOnce(new Error('d')); + const handler = createChatEventHandler({ + spawnAuto, + spawnStop, + injectSelection, + onAfterCapture, + logger: { error: errorLog }, + }); + await expect(handler(makeEvent())).resolves.toBeUndefined(); + }); +}); diff --git a/src/ext-vscode/src/chat-pipeline.ts b/src/ext-vscode/src/chat-pipeline.ts new file mode 100644 index 00000000..3751a394 --- /dev/null +++ b/src/ext-vscode/src/chat-pipeline.ts @@ -0,0 +1,136 @@ +import type { ChatHistoryEvent } from './chat-history-types.js'; +import type { StopSelection } from './ipc.js'; + +/** + * Chat pipeline orchestrator (M13 of M2 Branch 5). + * + * Glues the chat-history watcher to the existing nexpath pipeline (`auto` + * + `stop`). For each user prompt the watcher captures, this handler: + * + * 1. Calls `nexpath auto ` via ipc — forwards the + * prompt to the existing capture/classify/advisory stages + * (Layer C, untouched). + * 2. Calls `onAfterCapture` — the caller checks the store and arms the + * in-editor advisory fallback if `auto` just parked one. This runs BEFORE + * the (possibly blocking) terminal popup so the user always has a path to + * the advisory even when the popup can't open — e.g. on macOS the popup is + * `osascript` → Terminal.app, which BLOCKS on the Automation-permission + * dialog and may never return. + * 3. Calls `nexpath stop ` via ipc — Layer C opens its terminal + * popup and blocks until the user acts. + * 4. If the user SELECTED an option in the popup, `stop` returns a + * `StopSelection`; the handler injects that prompt into the host's chat + * input (`injectSelection`) and the wiring clears the now-redundant + * fallback. No selection (dismissed / no-advisory / popup couldn't open) → + * the armed fallback stays as the escape hatch. + * + * **Timing simplification for B5 smoke test:** `auto` and `stop` are + * called back-to-back. In production we'd want `stop` to fire after the + * agent's response completes (so the advisory has the latest context), + * but our chat-history watcher only emits user-prompt events — we don't + * yet detect "agent response done". This is acceptable for B5 because: + * - The legacy Claude Code hook flow also fires `auto` then `stop` + * and works fine. + * - Layer C's classifier doesn't require the assistant response to + * be present — it advises based on the user prompt alone. + * - A future M5 hardening pass can add "response done" detection via + * state.vscdb assistant-message extractors. + * + * **Resilience:** the handler catches everything. A failed spawn / + * malformed payload / view-provider error never propagates to the + * watcher (the watcher uses `void this.handleMessage(raw)` patterns + * where unhandled rejections would crash the extension host). + */ + +export interface ChatPipelineDeps { + /** + * Inject `ipc.spawnAuto`. Tests pass a mock. + * + * The third argument carries the originating `ChatHistoryEvent` so the + * caller can derive a per-event `cwd` from `event.sourcePath` — necessary + * for multi-workspace correctness, since one extension instance may watch + * `state.vscdb` files belonging to other workspaces and must attribute + * each prompt to its true project root, not to its own `workspaceCwd`. + */ + spawnAuto: ( + prompt: string, + sessionId: string, + event: ChatHistoryEvent, + ) => Promise; + /** Inject `ipc.spawnStop`. Tests pass a mock. */ + spawnStop: ( + sessionId: string, + event: ChatHistoryEvent, + ) => Promise; + /** + * Inject the selected prompt back into the host's chat input. Called only + * when the user picked an option in Layer C's terminal popup. + */ + injectSelection: (selectedPrompt: string, event: ChatHistoryEvent) => Promise | void; + /** + * Optional: called right after `auto` succeeds, BEFORE the terminal popup. + * The caller checks the store and arms the in-editor advisory fallback if an + * advisory was just parked — so it's available even if the popup blocks (the + * macOS Automation-dialog case) or can't open at all. + */ + onAfterCapture?: (event: ChatHistoryEvent) => Promise | void; + /** + * Optional session-id composer. Production passes a function that prefixes + * the host workspace id; tests omit this and just use `event.rawSessionId`. + */ + composeSessionId?: (event: ChatHistoryEvent) => string; + /** Optional logger override (tests). */ + logger?: { error: (msg: string, err: unknown) => void }; +} + +const defaultLogger = { + error: (msg: string, err: unknown) => console.error(msg, err), +}; + +/** + * Build the handler the watcher calls for every captured prompt. + * + * The returned function takes a `ChatHistoryEvent` and runs the auto → + * stop → publish pipeline. It never throws. + */ +export function createChatEventHandler( + deps: ChatPipelineDeps, +): (event: ChatHistoryEvent) => Promise { + const composeSessionId = + deps.composeSessionId ?? ((e: ChatHistoryEvent) => e.rawSessionId); + const logger = deps.logger ?? defaultLogger; + + return async (event: ChatHistoryEvent): Promise => { + const sessionId = composeSessionId(event); + try { + await deps.spawnAuto(event.prompt, sessionId, event); + } catch (err) { + logger.error('[nexpath] spawnAuto failed:', err); + return; + } + // Arm the in-editor fallback BEFORE the popup. `stop` can block indefinitely + // on macOS (osascript waiting on the Automation-permission dialog), so the + // fallback must not depend on `stop` returning. + if (deps.onAfterCapture) { + try { + await deps.onAfterCapture(event); + } catch (err) { + logger.error('[nexpath] onAfterCapture failed:', err); + } + } + let selection: StopSelection | null; + try { + selection = await deps.spawnStop(sessionId, event); + } catch (err) { + // The popup couldn't deliver a selection — the fallback armed above stands. + logger.error('[nexpath] spawnStop failed:', err); + return; + } + if (selection === null) return; // dismissed / no advisory / no TTY — fallback stands + try { + await deps.injectSelection(selection.selectedPrompt, event); + } catch (err) { + logger.error('[nexpath] injectSelection failed:', err); + } + }; +} diff --git a/src/ext-vscode/src/cursor-state-dump-helpers.test.ts b/src/ext-vscode/src/cursor-state-dump-helpers.test.ts new file mode 100644 index 00000000..d2f1357e --- /dev/null +++ b/src/ext-vscode/src/cursor-state-dump-helpers.test.ts @@ -0,0 +1,253 @@ +import { describe, it, expect } from 'vitest'; +import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import { + KEEP_ITEMTABLE_PREFIXES, + shouldKeepItemTable, + redactValue, + cursorConfigRoot, + discoverAllStateVscdb, + parseArgs, +} from './cursor-state-dump-helpers.js'; + +describe('shouldKeepItemTable', () => { + it('keeps every prefix in the default allowlist', () => { + for (const p of KEEP_ITEMTABLE_PREFIXES) { + expect(shouldKeepItemTable(`${p}some-suffix`)).toBe(true); + } + }); + + it('drops keys that do not match any prefix', () => { + expect(shouldKeepItemTable('workbench.editor.recent')).toBe(false); + expect(shouldKeepItemTable('terminal.integrated.layoutInfo')).toBe(false); + expect(shouldKeepItemTable('debug.selectedroot')).toBe(false); + expect(shouldKeepItemTable('')).toBe(false); + }); + + it('respects a custom prefix list when supplied', () => { + expect(shouldKeepItemTable('foo.bar', ['foo.'])).toBe(true); + expect(shouldKeepItemTable('foo.bar', ['baz.'])).toBe(false); + }); + + it('matches by prefix, not exact string', () => { + expect(shouldKeepItemTable('aiService.prompts')).toBe(true); + expect(shouldKeepItemTable('aiService.generations.extra')).toBe(true); + }); +}); + +describe('redactValue', () => { + it('keeps strings of length <= 8 unchanged', () => { + const v = JSON.stringify({ role: 'user', type: 'assistant', short: 'hi' }); + const out = redactValue(v); + // 'user' (4), 'assistant' (9 — gets redacted!), 'hi' (2) — boundary matters + const parsed = JSON.parse(out); + expect(parsed.role).toBe('user'); + expect(parsed.short).toBe('hi'); + // 'assistant' is 9 chars → over threshold → redacted + expect(parsed.type).toBe('*********'); + }); + + it('redacts strings of length > 8 inside JSON', () => { + const v = JSON.stringify({ prompt: 'sensitive-secret-text' }); + const out = redactValue(v); + const parsed = JSON.parse(out); + expect(parsed.prompt).toBe('*'.repeat('sensitive-secret-text'.length)); + }); + + it('recurses into nested JSON objects and arrays', () => { + const v = JSON.stringify({ + conv: [ + { role: 'user', text: 'a long sensitive prompt' }, + { role: 'assistant', text: 'a long sensitive reply' }, + ], + }); + const parsed = JSON.parse(redactValue(v)); + expect(parsed.conv[0].role).toBe('user'); + expect(parsed.conv[0].text).toBe('*'.repeat('a long sensitive prompt'.length)); + expect(parsed.conv[1].text).toBe('*'.repeat('a long sensitive reply'.length)); + }); + + it('preserves non-string values (numbers, booleans, null) in JSON', () => { + const v = JSON.stringify({ count: 42, ok: true, missing: null, x: false }); + const parsed = JSON.parse(redactValue(v)); + expect(parsed.count).toBe(42); + expect(parsed.ok).toBe(true); + expect(parsed.missing).toBeNull(); + expect(parsed.x).toBe(false); + }); + + it('bulk-redacts the entire string when the value is not JSON', () => { + const v = 'this-is-not-json-and-should-vanish'; + expect(redactValue(v)).toBe('*'.repeat(v.length)); + }); + + it('handles a bare JSON string (still JSON, but the root is a long string)', () => { + const v = JSON.stringify('a fairly long string here'); + const out = redactValue(v); + // Result is also JSON-encoded — parse to check + const parsed = JSON.parse(out); + expect(parsed).toBe('*'.repeat('a fairly long string here'.length)); + }); + + it('redacts at exactly the 9-char threshold (boundary)', () => { + // 8 chars → kept + expect(JSON.parse(redactValue(JSON.stringify({ k: '12345678' }))).k).toBe('12345678'); + // 9 chars → redacted + expect(JSON.parse(redactValue(JSON.stringify({ k: '123456789' }))).k).toBe('*'.repeat(9)); + }); +}); + +describe('cursorConfigRoot', () => { + it('returns the linux path under ~/.config/Cursor', () => { + expect( + cursorConfigRoot({ platform: 'linux', home: '/home/u' }), + ).toBe('/home/u/.config/Cursor'); + }); + + it('returns the macOS path under ~/Library/Application Support/Cursor', () => { + expect( + cursorConfigRoot({ platform: 'darwin', home: '/Users/u' }), + ).toBe('/Users/u/Library/Application Support/Cursor'); + }); + + it('returns the Windows path under %APPDATA%/Cursor when APPDATA is provided', () => { + expect( + cursorConfigRoot({ + platform: 'win32', + home: 'C:\\Users\\u', + appdata: 'C:\\Users\\u\\AppData\\Roaming', + }), + ).toBe('C:\\Users\\u\\AppData\\Roaming/Cursor'); + }); + + it('falls back to /AppData/Roaming/Cursor on Windows when APPDATA missing', () => { + expect( + cursorConfigRoot({ platform: 'win32', home: 'C:/U' }), + ).toContain('Cursor'); + }); + + it('treats unknown platforms as linux-style', () => { + expect( + cursorConfigRoot({ platform: 'freebsd' as NodeJS.Platform, home: '/home/u' }), + ).toBe('/home/u/.config/Cursor'); + }); +}); + +describe('discoverAllStateVscdb', () => { + it('returns empty when no Cursor tree exists', () => { + const tmp = mkdtempSync(join(tmpdir(), 'cursor-disc-empty-')); + const result = discoverAllStateVscdb(tmp); + expect(result).toEqual([]); + rmSync(tmp, { recursive: true, force: true }); + }); + + it('finds the global state.vscdb when only it exists', () => { + const tmp = mkdtempSync(join(tmpdir(), 'cursor-disc-glob-')); + mkdirSync(join(tmp, 'User', 'globalStorage'), { recursive: true }); + writeFileSync(join(tmp, 'User', 'globalStorage', 'state.vscdb'), ''); + const result = discoverAllStateVscdb(tmp); + expect(result).toHaveLength(1); + expect(result[0]!.label).toBe('global'); + expect(result[0]!.path.endsWith('state.vscdb')).toBe(true); + rmSync(tmp, { recursive: true, force: true }); + }); + + it('finds workspace state.vscdb files alongside the global one', () => { + const tmp = mkdtempSync(join(tmpdir(), 'cursor-disc-ws-')); + mkdirSync(join(tmp, 'User', 'globalStorage'), { recursive: true }); + writeFileSync(join(tmp, 'User', 'globalStorage', 'state.vscdb'), ''); + for (const ws of ['1234567890', 'abc-def', 'empty-window']) { + mkdirSync(join(tmp, 'User', 'workspaceStorage', ws), { recursive: true }); + writeFileSync(join(tmp, 'User', 'workspaceStorage', ws, 'state.vscdb'), ''); + } + const result = discoverAllStateVscdb(tmp); + expect(result).toHaveLength(4); + const labels = result.map((r) => r.label).sort(); + expect(labels).toEqual([ + 'global', + 'workspace-1234567890', + 'workspace-abc-def', + 'workspace-empty-window', + ]); + rmSync(tmp, { recursive: true, force: true }); + }); + + it('skips workspace dirs that do not actually contain state.vscdb', () => { + const tmp = mkdtempSync(join(tmpdir(), 'cursor-disc-skip-')); + mkdirSync(join(tmp, 'User', 'workspaceStorage', 'has-db'), { recursive: true }); + writeFileSync(join(tmp, 'User', 'workspaceStorage', 'has-db', 'state.vscdb'), ''); + mkdirSync(join(tmp, 'User', 'workspaceStorage', 'no-db'), { recursive: true }); + const result = discoverAllStateVscdb(tmp); + expect(result).toHaveLength(1); + expect(result[0]!.label).toBe('workspace-has-db'); + rmSync(tmp, { recursive: true, force: true }); + }); + + it('uses injected fs helpers when provided', () => { + const fakeFs = { + existsSync: (p: string) => p.includes('workspaceStorage') || p.includes('state.vscdb'), + readdirSync: () => ['ws1'], + }; + const result = discoverAllStateVscdb('/fake/root', fakeFs); + expect(result.map((r) => r.label)).toContain('workspace-ws1'); + }); +}); + +describe('parseArgs', () => { + it('accepts --name and produces ok=true', () => { + const r = parseArgs(['--name', 'mine']); + expect(r.ok).toBe(true); + if (r.ok) { + expect(r.args.name).toBe('mine'); + expect(r.args.redact).toBe(false); + expect(r.args.src).toBeUndefined(); + } + }); + + it('parses --src and --redact', () => { + const r = parseArgs(['--name', 'x', '--src', '/p', '--redact']); + expect(r.ok).toBe(true); + if (r.ok) { + expect(r.args.src).toBe('/p'); + expect(r.args.redact).toBe(true); + } + }); + + it('rejects with an error when --name is missing', () => { + const r = parseArgs(['--redact']); + expect(r.ok).toBe(false); + if (!r.ok) { + expect(r.error).toContain('--name'); + } + }); + + it('rejects with an error when --name has no value', () => { + const r = parseArgs(['--name']); + expect(r.ok).toBe(false); + if (!r.ok) { + expect(r.error).toContain('--name requires a value'); + } + }); + + it('rejects with an error when --src has no value', () => { + const r = parseArgs(['--name', 'a', '--src']); + expect(r.ok).toBe(false); + if (!r.ok) { + expect(r.error).toContain('--src requires a value'); + } + }); + + it('signals help when --help or -h is passed', () => { + expect(parseArgs(['--help'])).toMatchObject({ ok: false, help: true }); + expect(parseArgs(['-h'])).toMatchObject({ ok: false, help: true }); + }); + + it('rejects unknown arguments', () => { + const r = parseArgs(['--name', 'x', '--bogus']); + expect(r.ok).toBe(false); + if (!r.ok) { + expect(r.error).toContain('--bogus'); + } + }); +}); diff --git a/src/ext-vscode/src/cursor-state-dump-helpers.ts b/src/ext-vscode/src/cursor-state-dump-helpers.ts new file mode 100644 index 00000000..31db3aca --- /dev/null +++ b/src/ext-vscode/src/cursor-state-dump-helpers.ts @@ -0,0 +1,183 @@ +/** + * Pure / near-pure helpers used by `scripts/dump-cursor-state.ts`. + * + * These live in `src/` (and not next to the script) for two reasons: + * 1. They're typechecked by the sub-package's main `tsconfig.json` + * (rootDir = `./src`). + * 2. They're picked up by vitest for co-located unit testing via the + * `cursor-state-dump-helpers.test.ts` sibling. + * + * The script in `scripts/dump-cursor-state.ts` imports these helpers and + * adds the I/O orchestration on top. + * + * None of these helpers are referenced by `src/extension.ts`, so the + * production esbuild bundle does not include them. + */ + +import { existsSync, readdirSync } from 'node:fs'; +import { homedir } from 'node:os'; +import { join } from 'node:path'; + +/** ItemTable key prefixes the dump should keep — everything else is dropped. */ +export const KEEP_ITEMTABLE_PREFIXES = [ + 'aiService.', + 'composer.', + 'composerData.', + 'cursorAIService.', + 'cursorAIChatService.', + 'cascade.', + 'workbench.panel.composerChatViewPane.', + 'workbench.panel.aichat.', + 'workbench.backgroundComposer.', +] as const; + +/** + * Return true iff the ItemTable row key should be retained in the dump. + * Filters out unrelated VS Code state (recents, themes, terminal, etc.). + */ +export function shouldKeepItemTable( + key: string, + prefixes: readonly string[] = KEEP_ITEMTABLE_PREFIXES, +): boolean { + return prefixes.some((p) => key.startsWith(p)); +} + +/** + * Redact long string values inside a row's `value` field. + * + * Behaviour: + * - If the value parses as JSON: recursively replace every string longer + * than 8 chars with same-length asterisks. Numbers, booleans, nulls, + * and short strings are preserved. + * - If the value is not JSON: replace the whole string with asterisks of + * the same length. + * + * The 8-char threshold preserves common short markers (`user`, `assistant`, + * `linux`, etc.) while obscuring prompt text, tab IDs, paths, etc. + */ +export function redactValue(value: string): string { + try { + const parsed = JSON.parse(value); + const redacted = JSON.parse(JSON.stringify(parsed), (_k, v) => { + if (typeof v === 'string' && v.length > 8) return '*'.repeat(v.length); + return v; + }); + return JSON.stringify(redacted); + } catch { + return '*'.repeat(value.length); + } +} + +export interface CursorConfigRootInputs { + platform?: NodeJS.Platform; + home?: string; + appdata?: string; +} + +/** + * Resolve the OS-specific Cursor configuration root. + * + * Linux: ~/.config/Cursor + * macOS: ~/Library/Application Support/Cursor + * Windows: %APPDATA%/Cursor (falls back to /AppData/Roaming/Cursor) + * + * Inputs are injectable for testability — defaults to the live process + * environment. + */ +export function cursorConfigRoot(inputs: CursorConfigRootInputs = {}): string { + const platform = inputs.platform ?? process.platform; + const home = inputs.home ?? homedir(); + switch (platform) { + case 'darwin': + return join(home, 'Library', 'Application Support', 'Cursor'); + case 'win32': { + const appdata = + inputs.appdata ?? process.env.APPDATA ?? join(home, 'AppData', 'Roaming'); + return join(appdata, 'Cursor'); + } + default: + return join(home, '.config', 'Cursor'); + } +} + +export interface DiscoveredDb { + path: string; + /** Short label for the output filename, e.g. 'global' or 'workspace-'. */ + label: string; +} + +export interface DiscoveryFs { + existsSync: (p: string) => boolean; + readdirSync: (p: string) => string[]; +} + +/** + * Discover every `state.vscdb` under a Cursor config root — both the global + * one and one per workspace under `User/workspaceStorage/`. + * + * Returns labelled entries so each gets a distinct output filename when the + * dump script writes its fixtures. + */ +export function discoverAllStateVscdb( + root: string, + fsHelpers: DiscoveryFs = { existsSync, readdirSync }, +): DiscoveredDb[] { + const found: DiscoveredDb[] = []; + const globalPath = join(root, 'User', 'globalStorage', 'state.vscdb'); + if (fsHelpers.existsSync(globalPath)) { + found.push({ path: globalPath, label: 'global' }); + } + const wsDir = join(root, 'User', 'workspaceStorage'); + if (fsHelpers.existsSync(wsDir)) { + for (const entry of fsHelpers.readdirSync(wsDir)) { + const dbPath = join(wsDir, entry, 'state.vscdb'); + if (fsHelpers.existsSync(dbPath)) { + found.push({ path: dbPath, label: `workspace-${entry}` }); + } + } + } + return found; +} + +export interface CliArgs { + name: string; + src?: string; + redact: boolean; +} + +export type ParseArgsResult = + | { ok: true; args: CliArgs } + | { ok: false; error: string; help?: true }; + +/** + * Parse the `dump-cursor-state` CLI arguments without ever calling + * `process.exit` directly — so unit tests can exercise the error paths. + * + * The script entry-point handles `result.ok === false` by printing the + * error / usage and exiting with the appropriate code. + */ +export function parseArgs(argv: readonly string[]): ParseArgsResult { + const args: Partial = { redact: false }; + for (let i = 0; i < argv.length; i++) { + const a = argv[i]; + if (a === '--name') { + const v = argv[++i]; + if (v === undefined) return { ok: false, error: '--name requires a value' }; + args.name = v; + } else if (a === '--src') { + const v = argv[++i]; + if (v === undefined) return { ok: false, error: '--src requires a value' }; + args.src = v; + } else if (a === '--redact') { + args.redact = true; + } else if (a === '--help' || a === '-h') { + return { ok: false, error: '', help: true }; + } else { + return { ok: false, error: `Unknown argument: ${a}` }; + } + } + if (!args.name) { + return { ok: false, error: 'Missing required --name ' }; + } + return { ok: true, args: args as CliArgs }; +} diff --git a/src/ext-vscode/src/extension.test.ts b/src/ext-vscode/src/extension.test.ts new file mode 100644 index 00000000..cf001485 --- /dev/null +++ b/src/ext-vscode/src/extension.test.ts @@ -0,0 +1,596 @@ +import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; + +// vi.hoisted lets the mocks declared here be referenced from the vi.mock +// factories below (which are themselves hoisted above the import block). +const { + mockShowOnboarding, + mockRegisterWebviewViewProvider, + mockProviderCtor, + mockDetectHost, + mockWorkspaceStorageDir, + mockWindsurfCodeiumDir, + mockEnumerateStateVscdbPaths, + mockCreateChatHistoryWatcher, + mockWatcherStart, + mockWatcherStop, + mockCreateChatEventHandler, + mockShowInformationMessage, + mockExistsSync, + mockExecuteCommand, +} = vi.hoisted(() => ({ + mockShowOnboarding: vi.fn(), + mockRegisterWebviewViewProvider: vi.fn(), + mockProviderCtor: vi.fn(), + mockDetectHost: vi.fn(() => 'vscode-generic'), + mockWorkspaceStorageDir: vi.fn(() => null), + mockWindsurfCodeiumDir: vi.fn(() => '/home/u/.codeium/windsurf'), + mockEnumerateStateVscdbPaths: vi.fn(() => []), + mockCreateChatHistoryWatcher: vi.fn(), + mockWatcherStart: vi.fn(), + mockWatcherStop: vi.fn(), + mockCreateChatEventHandler: vi.fn(() => vi.fn()), + mockShowInformationMessage: vi.fn(), + mockExistsSync: vi.fn(() => false), + mockExecuteCommand: vi.fn(), +})); + +vi.mock('vscode', () => ({ + window: { + registerWebviewViewProvider: mockRegisterWebviewViewProvider, + showInformationMessage: mockShowInformationMessage, + createOutputChannel: vi.fn(() => ({ + appendLine: vi.fn(), + dispose: vi.fn(), + })), + createStatusBarItem: vi.fn(() => ({ + text: '', + tooltip: '', + command: '', + show: vi.fn(), + hide: vi.fn(), + dispose: vi.fn(), + })), + }, + workspace: { + workspaceFolders: undefined, + }, + env: { appName: 'Visual Studio Code' }, + commands: { + executeCommand: mockExecuteCommand, + getCommands: vi.fn().mockResolvedValue([]), + registerCommand: vi.fn(() => ({ dispose: vi.fn() })), + }, + StatusBarAlignment: { Left: 1, Right: 2 }, +})); +vi.mock('./onboarding.js', () => ({ + CONSENT_KEY: 'nexpath.consentGranted', + showOnboardingIfNeeded: mockShowOnboarding, +})); +vi.mock('./webview/view-provider.js', () => ({ + VIEW_ID: 'nexpath.status', + NexpathDecisionSessionViewProvider: class { + constructor(...args: unknown[]) { + mockProviderCtor(...args); + } + publishPayload(): void {} + }, +})); +vi.mock('./webview/prompt-injection.js', () => ({ + handleOptionSelection: vi.fn(), +})); +vi.mock('node:fs', async (importOriginal) => { + const actual = await importOriginal(); + return { ...actual, existsSync: mockExistsSync }; +}); +vi.mock('./host-detector.js', () => ({ + detectHost: mockDetectHost, + workspaceStorageDir: mockWorkspaceStorageDir, + windsurfCodeiumDir: mockWindsurfCodeiumDir, +})); +vi.mock('./chat-input-injector.js', () => ({ + chatInputInject: vi.fn(), +})); +vi.mock('./path-enumerator.js', () => ({ + enumerateStateVscdbPaths: mockEnumerateStateVscdbPaths, + globalStorageStateVscdbPath: () => null, +})); +vi.mock('./chat-history-watcher.js', () => ({ + createChatHistoryWatcher: mockCreateChatHistoryWatcher, +})); +vi.mock('./chat-pipeline.js', () => ({ + createChatEventHandler: mockCreateChatEventHandler, +})); +vi.mock('./ipc.js', () => ({ + spawnAuto: vi.fn(), + spawnStop: vi.fn(), +})); +vi.mock('./advisory-fallback.js', () => ({ + createAdvisoryFallback: vi.fn(() => ({ + armIfPending: vi.fn(), + clear: vi.fn(), + showAdvisory: vi.fn(), + })), +})); + +import { activate, deactivate, getViewProvider } from './extension.js'; + +interface FakeContext { + extensionUri: { __uri: true }; + subscriptions: unknown[]; + globalState: { get: (k: string) => T | undefined }; +} + +function makeCtx(consent: boolean | undefined = undefined): FakeContext { + return { + extensionUri: { __uri: true }, + subscriptions: [], + globalState: { + get: (k: string) => + (k === 'nexpath.consentGranted' ? (consent as T) : undefined) as + | T + | undefined, + }, + }; +} + +describe('activate', () => { + let logSpy: ReturnType; + let errorSpy: ReturnType; + + beforeEach(() => { + mockShowOnboarding.mockReset(); + mockRegisterWebviewViewProvider.mockReset(); + mockProviderCtor.mockReset(); + mockDetectHost.mockReset().mockReturnValue('vscode-generic'); + mockWorkspaceStorageDir.mockReset().mockReturnValue(null); + mockWindsurfCodeiumDir.mockReset().mockReturnValue('/home/u/.codeium/windsurf'); + mockEnumerateStateVscdbPaths.mockReset().mockReturnValue([]); + mockCreateChatHistoryWatcher.mockReset().mockReturnValue({ + start: mockWatcherStart, + stop: mockWatcherStop, + }); + mockWatcherStart.mockReset(); + mockWatcherStop.mockReset(); + mockCreateChatEventHandler.mockReset().mockReturnValue(vi.fn()); + mockShowInformationMessage.mockReset(); + mockExistsSync.mockReset().mockReturnValue(false); + mockExecuteCommand.mockReset().mockResolvedValue(undefined); + mockRegisterWebviewViewProvider.mockReturnValue({ dispose: vi.fn() }); + logSpy = vi.spyOn(console, 'log').mockImplementation(() => {}); + errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); + // Reset module-level state in extension.ts (deactivate clears viewProvider + watcher) + deactivate(); + }); + + afterEach(() => { + logSpy.mockRestore(); + errorSpy.mockRestore(); + }); + + it('logs the activation message', async () => { + mockShowOnboarding.mockResolvedValueOnce(undefined); + await activate(makeCtx() as never); + expect(logSpy).toHaveBeenCalledWith('[nexpath] extension activated'); + }); + + it('forwards the ExtensionContext to showOnboardingIfNeeded', async () => { + mockShowOnboarding.mockResolvedValueOnce(undefined); + const ctx = makeCtx(); + await activate(ctx as never); + expect(mockShowOnboarding).toHaveBeenCalledOnce(); + expect(mockShowOnboarding).toHaveBeenCalledWith(ctx); + }); + + it('registers the view provider on every activation regardless of consent', async () => { + mockShowOnboarding.mockResolvedValueOnce(undefined); + await activate(makeCtx(false) as never); // user denied + expect(mockProviderCtor).toHaveBeenCalledOnce(); + expect(mockRegisterWebviewViewProvider).toHaveBeenCalledOnce(); + }); + + it('does NOT start the watcher when consent is undefined (first launch, user has not answered)', async () => { + mockShowOnboarding.mockResolvedValueOnce(undefined); + mockDetectHost.mockReturnValueOnce('cursor'); + await activate(makeCtx(undefined) as never); + expect(mockCreateChatHistoryWatcher).not.toHaveBeenCalled(); + expect(mockWatcherStart).not.toHaveBeenCalled(); + }); + + it('does NOT start the watcher when consent is explicitly false (user denied)', async () => { + mockShowOnboarding.mockResolvedValueOnce(undefined); + mockDetectHost.mockReturnValueOnce('cursor'); + await activate(makeCtx(false) as never); + expect(mockCreateChatHistoryWatcher).not.toHaveBeenCalled(); + }); + + it('does NOT start the watcher on plain VS Code host even if consent is true', async () => { + mockShowOnboarding.mockResolvedValueOnce(undefined); + mockDetectHost.mockReturnValueOnce('vscode-generic'); + await activate(makeCtx(true) as never); + expect(mockCreateChatHistoryWatcher).not.toHaveBeenCalled(); + }); + + it('does NOT start the watcher when no state.vscdb files are found under workspaceStorage', async () => { + mockShowOnboarding.mockResolvedValueOnce(undefined); + mockDetectHost.mockReturnValueOnce('cursor'); + mockWorkspaceStorageDir.mockReturnValueOnce('/fake/workspaceStorage'); + mockEnumerateStateVscdbPaths.mockReturnValueOnce([]); + await activate(makeCtx(true) as never); + expect(mockCreateChatHistoryWatcher).not.toHaveBeenCalled(); + expect(logSpy).toHaveBeenCalledWith( + expect.stringContaining('no workspace state.vscdb'), + ); + }); + + it('starts the watcher when consent=true + host=cursor + dbs present', async () => { + mockShowOnboarding.mockResolvedValueOnce(undefined); + mockDetectHost.mockReturnValueOnce('cursor'); + mockWorkspaceStorageDir.mockReturnValueOnce('/fake/workspaceStorage'); + mockEnumerateStateVscdbPaths.mockReturnValueOnce([ + '/fake/workspaceStorage/wsA/state.vscdb', + '/fake/workspaceStorage/wsB/state.vscdb', + ]); + const ctx = makeCtx(true); + await activate(ctx as never); + expect(mockCreateChatHistoryWatcher).toHaveBeenCalledOnce(); + const watcherOpts = mockCreateChatHistoryWatcher.mock.calls[0]![0] as { + targets: Array<{ path: string; kind: string }>; + }; + expect(watcherOpts.targets).toHaveLength(2); + expect(watcherOpts.targets[0]!.kind).toBe('cursor-sqlite'); + expect(mockWatcherStart).toHaveBeenCalledOnce(); + // Cleanup disposable pushed onto subscriptions + expect(ctx.subscriptions.length).toBeGreaterThanOrEqual(2); + }); + + it('builds the chat-event handler with the right composer (workspace-prefixed session id)', async () => { + mockShowOnboarding.mockResolvedValueOnce(undefined); + mockDetectHost.mockReturnValueOnce('cursor'); + mockWorkspaceStorageDir.mockReturnValueOnce('/fake/ws'); + mockEnumerateStateVscdbPaths.mockReturnValueOnce(['/fake/ws/a/state.vscdb']); + await activate(makeCtx(true) as never); + expect(mockCreateChatEventHandler).toHaveBeenCalledOnce(); + const deps = mockCreateChatEventHandler.mock.calls[0]![0] as { + composeSessionId?: (e: { + rawSessionId: string; + sourcePath: string; + prompt: string; + capturedAt: Date; + extractorId: string; + }) => string; + }; + expect(typeof deps.composeSessionId).toBe('function'); + // R4.3 fix: composer now derives cwd per-event from event.sourcePath via + // sibling workspace.json lookup. The fake path here has no real + // workspace.json on disk → helper returns null → composer falls back to + // the extension instance's workspaceCwd (process.cwd() since + // workspaceFolders is undefined in the mock). + const composed = deps.composeSessionId!({ + rawSessionId: 'tab-1', + sourcePath: '/fake/ws/a/state.vscdb', + prompt: 'irrelevant', + capturedAt: new Date(0), + extractorId: 'cursor-v2024-q4', + }); + expect(composed.endsWith('|tab-1')).toBe(true); + expect(composed).toMatch(/.+\|tab-1$/); + }); + + it('does not throw when showOnboardingIfNeeded rejects — logs error and continues', async () => { + mockShowOnboarding.mockRejectedValueOnce(new Error('onboarding boom')); + mockDetectHost.mockReturnValueOnce('cursor'); + await expect(activate(makeCtx(true) as never)).resolves.toBeUndefined(); + expect(errorSpy).toHaveBeenCalledWith( + '[nexpath] onboarding failed:', + expect.any(Error), + ); + }); + + it('exposes the constructed provider via getViewProvider()', async () => { + mockShowOnboarding.mockResolvedValueOnce(undefined); + await activate(makeCtx() as never); + expect(getViewProvider()).toBeDefined(); + }); + + // ── Notification-panel pre-open (Cursor/Windsurf toast visibility) ───────── + // VS Code shows showInformationMessage toasts as transient bottom-right + // popups. Cursor and Windsurf route them to the silent notification stack + // (bell icon) and stay invisible until the user opens the panel. extension.ts + // pre-opens the panel via the `notifications.showList` command on non-VS-Code + // hosts so the consent toast is immediately discoverable. Dev plan §2.2 M11 + // ("consent toast") + §2.5 cursor-quirks compliance. + + it('on host=cursor: pre-opens notification panel before consent toast', async () => { + mockShowOnboarding.mockResolvedValueOnce(undefined); + mockDetectHost.mockReturnValueOnce('cursor'); + await activate(makeCtx() as never); + expect(mockExecuteCommand).toHaveBeenCalledWith('notifications.showList'); + }); + + it('on host=windsurf: pre-opens notification panel before consent toast', async () => { + mockShowOnboarding.mockResolvedValueOnce(undefined); + mockDetectHost.mockReturnValueOnce('windsurf'); + await activate(makeCtx() as never); + expect(mockExecuteCommand).toHaveBeenCalledWith('notifications.showList'); + }); + + it('on host=vscode-generic: does NOT pre-open notification panel (toast surfaces natively)', async () => { + mockShowOnboarding.mockResolvedValueOnce(undefined); + mockDetectHost.mockReturnValueOnce('vscode-generic'); + await activate(makeCtx() as never); + expect(mockExecuteCommand).not.toHaveBeenCalledWith('notifications.showList'); + }); + + it('swallows errors from notifications.showList (best-effort discoverability hint)', async () => { + mockShowOnboarding.mockResolvedValueOnce(undefined); + mockDetectHost.mockReturnValueOnce('cursor'); + mockExecuteCommand.mockRejectedValueOnce(new Error('command not found')); + await expect(activate(makeCtx() as never)).resolves.toBeUndefined(); + // Onboarding still runs even though showList rejected + expect(mockShowOnboarding).toHaveBeenCalledOnce(); + }); + + // ── Windsurf codeium-cascade dir wiring (Drift A fix) ────────────────────── + // Per dev plan §2.3 acceptance #2: when host=windsurf, the watcher must + // monitor BOTH state.vscdb files AND `~/.codeium/windsurf/`. These tests + // verify the extension.ts wiring that adds the codeium dir as a + // windsurf-dir WatchTarget alongside the cursor-sqlite targets. + + it('windsurf host: adds codeium cascade dir as a windsurf-dir target when it exists on disk', async () => { + mockShowOnboarding.mockResolvedValueOnce(undefined); + mockDetectHost.mockReturnValueOnce('windsurf'); + mockWorkspaceStorageDir.mockReturnValueOnce('/fake/ws'); + mockEnumerateStateVscdbPaths.mockReturnValueOnce(['/fake/ws/a/state.vscdb']); + mockWindsurfCodeiumDir.mockReturnValueOnce('/home/u/.codeium/windsurf'); + mockExistsSync.mockImplementation( + (p: string) => p === '/home/u/.codeium/windsurf', + ); + + await activate(makeCtx(true) as never); + + expect(mockCreateChatHistoryWatcher).toHaveBeenCalledOnce(); + const watcherOpts = mockCreateChatHistoryWatcher.mock.calls[0]![0] as { + targets: Array<{ path: string; kind: string }>; + }; + expect(watcherOpts.targets).toHaveLength(2); + expect(watcherOpts.targets[0]).toEqual({ + path: '/fake/ws/a/state.vscdb', + kind: 'cursor-sqlite', + }); + expect(watcherOpts.targets[1]).toEqual({ + path: '/home/u/.codeium/windsurf', + kind: 'windsurf-dir', + }); + }); + + it('windsurf host: skips codeium dir when it does not exist (existsSync = false)', async () => { + mockShowOnboarding.mockResolvedValueOnce(undefined); + mockDetectHost.mockReturnValueOnce('windsurf'); + mockWorkspaceStorageDir.mockReturnValueOnce('/fake/ws'); + mockEnumerateStateVscdbPaths.mockReturnValueOnce(['/fake/ws/a/state.vscdb']); + mockExistsSync.mockReturnValue(false); + + await activate(makeCtx(true) as never); + + const watcherOpts = mockCreateChatHistoryWatcher.mock.calls[0]![0] as { + targets: Array<{ path: string; kind: string }>; + }; + expect(watcherOpts.targets).toHaveLength(1); + expect(watcherOpts.targets[0]!.kind).toBe('cursor-sqlite'); + }); + + it('windsurf host: starts watcher when ONLY the codeium dir exists (no state.vscdb yet)', async () => { + mockShowOnboarding.mockResolvedValueOnce(undefined); + mockDetectHost.mockReturnValueOnce('windsurf'); + mockWorkspaceStorageDir.mockReturnValueOnce('/fake/ws'); + mockEnumerateStateVscdbPaths.mockReturnValueOnce([]); + mockWindsurfCodeiumDir.mockReturnValueOnce('/home/u/.codeium/windsurf'); + mockExistsSync.mockImplementation( + (p: string) => p === '/home/u/.codeium/windsurf', + ); + + await activate(makeCtx(true) as never); + + expect(mockCreateChatHistoryWatcher).toHaveBeenCalledOnce(); + const watcherOpts = mockCreateChatHistoryWatcher.mock.calls[0]![0] as { + targets: Array<{ path: string; kind: string }>; + }; + expect(watcherOpts.targets).toHaveLength(1); + expect(watcherOpts.targets[0]).toEqual({ + path: '/home/u/.codeium/windsurf', + kind: 'windsurf-dir', + }); + }); + + it('cursor host: never adds a windsurf-dir target (no cross-host leakage)', async () => { + mockShowOnboarding.mockResolvedValueOnce(undefined); + mockDetectHost.mockReturnValueOnce('cursor'); + mockWorkspaceStorageDir.mockReturnValueOnce('/fake/ws'); + mockEnumerateStateVscdbPaths.mockReturnValueOnce(['/fake/ws/a/state.vscdb']); + // Even if existsSync would say yes for /home/u/.codeium/windsurf, the + // cursor-host branch must never consult it. + mockExistsSync.mockReturnValue(true); + + await activate(makeCtx(true) as never); + + expect(mockWindsurfCodeiumDir).not.toHaveBeenCalled(); + const watcherOpts = mockCreateChatHistoryWatcher.mock.calls[0]![0] as { + targets: Array<{ path: string; kind: string }>; + }; + expect(watcherOpts.targets.every((t) => t.kind === 'cursor-sqlite')).toBe( + true, + ); + }); + + // ── Watcher-callback wiring (B5 audit follow-up) ─────────────────────────── + // The watcher takes three callbacks at construction time (onEvent, + // onError, onSchemaUnknown). The watcher itself is mocked in these tests, + // so we capture the callbacks from the createChatHistoryWatcher call args + // and invoke them directly — verifying that what extension.ts wires up + // does the right thing. + + /** Helper: drive activate() with watcher-starting conditions + return the watcher opts. */ + async function activateWithWatcher(): Promise<{ + onEvent: (event: unknown) => void; + onError: (err: Error) => void; + onSchemaUnknown: (info: { path: string; observedSampleKeys: readonly string[] }) => void; + }> { + mockShowOnboarding.mockResolvedValueOnce(undefined); + mockDetectHost.mockReturnValueOnce('cursor'); + mockWorkspaceStorageDir.mockReturnValueOnce('/fake/ws'); + mockEnumerateStateVscdbPaths.mockReturnValueOnce(['/fake/ws/a/state.vscdb']); + await activate(makeCtx(true) as never); + const opts = mockCreateChatHistoryWatcher.mock.calls[0]![0] as { + onEvent: (event: unknown) => void; + onError: (err: Error) => void; + onSchemaUnknown: (info: { + path: string; + observedSampleKeys: readonly string[]; + }) => void; + }; + return opts; + } + + it('routes watcher onEvent through the chat-event handler (the integration proof)', async () => { + const trackedHandler = vi.fn(); + mockCreateChatEventHandler.mockReturnValueOnce(trackedHandler); + const opts = await activateWithWatcher(); + const event = { + prompt: 'hi', + rawSessionId: 'tab-7', + capturedAt: new Date(0), + sourcePath: '/fake/ws/a/state.vscdb', + extractorId: 'cursor-v2025-q2', + }; + opts.onEvent(event); + expect(trackedHandler).toHaveBeenCalledOnce(); + expect(trackedHandler).toHaveBeenCalledWith(event); + }); + + it('watcher onSchemaUnknown surfaces a visible info toast with path + observed keys', async () => { + const opts = await activateWithWatcher(); + opts.onSchemaUnknown({ + path: '/fake/ws/a/state.vscdb', + observedSampleKeys: ['unknown.key.1', 'unknown.key.2', 'unknown.key.3'], + }); + expect(mockShowInformationMessage).toHaveBeenCalledOnce(); + const msg = mockShowInformationMessage.mock.calls[0]![0] as string; + expect(msg).toContain('/fake/ws/a/state.vscdb'); + expect(msg).toContain('schema is not recognised'); + expect(msg).toContain('unknown.key.1'); + }); + + it('watcher onError logs to console.error (does not crash the extension)', async () => { + const opts = await activateWithWatcher(); + const watcherErr = new Error('watch boom'); + expect(() => opts.onError(watcherErr)).not.toThrow(); + expect(errorSpy).toHaveBeenCalledWith('[nexpath] watcher error:', watcherErr); + }); + + // ── Pipeline logger wiring (B1.4 follow-up — spawn errors in Output) ────── + // chat-pipeline.ts catches spawnAuto / spawnStop failures and forwards them + // to its `logger.error`. extension.ts must wire a logger that writes to + // BOTH console.error AND the Nexpath OutputChannel (via the local `log` + // helper). Otherwise IPC errors only surface in Developer Tools Console, + // invisible to end users. Verified live during B1.4 (nexpath binary moved + // aside → spawnAuto ENOENT → silent before this fix, surfaces in Output + // after this fix). + + it('wires a logger into createChatEventHandler that writes spawn errors to the Output channel', async () => { + mockShowOnboarding.mockResolvedValueOnce(undefined); + mockDetectHost.mockReturnValueOnce('cursor'); + mockWorkspaceStorageDir.mockReturnValueOnce('/fake/ws'); + mockEnumerateStateVscdbPaths.mockReturnValueOnce(['/fake/ws/a/state.vscdb']); + await activate(makeCtx(true) as never); + const handlerDeps = mockCreateChatEventHandler.mock.calls[0]![0] as { + logger?: { error: (msg: string, err: unknown) => void }; + }; + expect(handlerDeps.logger).toBeDefined(); + expect(typeof handlerDeps.logger!.error).toBe('function'); + + // Calling the wired logger.error should reach console.error (existing + // path) and also call `log()` which writes to console.log (which the + // OutputChannel mock will also receive via appendLine in production). + const enoent = Object.assign(new Error('spawn nexpath ENOENT'), { + code: 'ENOENT', + }); + handlerDeps.logger!.error('[nexpath] spawnAuto failed:', enoent); + expect(errorSpy).toHaveBeenCalledWith( + '[nexpath] spawnAuto failed:', + enoent, + ); + expect(logSpy).toHaveBeenCalledWith( + expect.stringContaining('spawn nexpath ENOENT'), + ); + }); + + it('wired logger.error formats non-Error rejection values cleanly', async () => { + mockShowOnboarding.mockResolvedValueOnce(undefined); + mockDetectHost.mockReturnValueOnce('cursor'); + mockWorkspaceStorageDir.mockReturnValueOnce('/fake/ws'); + mockEnumerateStateVscdbPaths.mockReturnValueOnce(['/fake/ws/a/state.vscdb']); + await activate(makeCtx(true) as never); + const handlerDeps = mockCreateChatEventHandler.mock.calls[0]![0] as { + logger?: { error: (msg: string, err: unknown) => void }; + }; + handlerDeps.logger!.error('[nexpath] spawnStop failed:', 'plain string err'); + expect(logSpy).toHaveBeenCalledWith( + expect.stringContaining('plain string err'), + ); + }); +}); + +describe('deactivate', () => { + let logSpy: ReturnType; + + beforeEach(() => { + mockShowOnboarding.mockReset(); + mockRegisterWebviewViewProvider.mockReset(); + mockProviderCtor.mockReset(); + mockDetectHost.mockReset().mockReturnValue('vscode-generic'); + mockWorkspaceStorageDir.mockReset().mockReturnValue(null); + mockWindsurfCodeiumDir.mockReset().mockReturnValue('/home/u/.codeium/windsurf'); + mockEnumerateStateVscdbPaths.mockReset().mockReturnValue([]); + mockCreateChatHistoryWatcher.mockReset().mockReturnValue({ + start: mockWatcherStart, + stop: mockWatcherStop, + }); + mockWatcherStart.mockReset(); + mockWatcherStop.mockReset(); + mockExistsSync.mockReset().mockReturnValue(false); + mockRegisterWebviewViewProvider.mockReturnValue({ dispose: vi.fn() }); + logSpy = vi.spyOn(console, 'log').mockImplementation(() => {}); + }); + + afterEach(() => { + logSpy.mockRestore(); + }); + + it('logs the deactivation message', () => { + deactivate(); + expect(logSpy).toHaveBeenCalledWith('[nexpath] extension deactivated'); + }); + + it('returns synchronously without throwing', () => { + expect(() => deactivate()).not.toThrow(); + }); + + it('clears module-level viewProvider on deactivate', async () => { + mockShowOnboarding.mockResolvedValueOnce(undefined); + await activate(makeCtx() as never); + expect(getViewProvider()).toBeDefined(); + deactivate(); + expect(getViewProvider()).toBeUndefined(); + }); + + it('stops the watcher on deactivate (if one was started)', async () => { + mockShowOnboarding.mockResolvedValueOnce(undefined); + mockDetectHost.mockReturnValueOnce('cursor'); + mockWorkspaceStorageDir.mockReturnValueOnce('/fake/ws'); + mockEnumerateStateVscdbPaths.mockReturnValueOnce(['/fake/ws/a/state.vscdb']); + await activate(makeCtx(true) as never); + expect(mockWatcherStart).toHaveBeenCalledOnce(); + deactivate(); + expect(mockWatcherStop).toHaveBeenCalled(); + }); +}); diff --git a/src/ext-vscode/src/extension.ts b/src/ext-vscode/src/extension.ts new file mode 100644 index 00000000..aed9131f --- /dev/null +++ b/src/ext-vscode/src/extension.ts @@ -0,0 +1,342 @@ +import * as vscode from 'vscode'; +import { existsSync } from 'node:fs'; +import { CONSENT_KEY, showOnboardingIfNeeded } from './onboarding.js'; +import { + NexpathDecisionSessionViewProvider, + VIEW_ID, +} from './webview/view-provider.js'; +import { handleOptionSelection } from './webview/prompt-injection.js'; +import { + detectHost, + windsurfCodeiumDir, + workspaceStorageDir, +} from './host-detector.js'; +import { chatInputInject } from './chat-input-injector.js'; +import { + enumerateStateVscdbPaths, + globalStorageStateVscdbPath, +} from './path-enumerator.js'; +import { + createChatHistoryWatcher, + type ChatHistoryWatcher, +} from './chat-history-watcher.js'; +import { createChatEventHandler } from './chat-pipeline.js'; +import { spawnAuto, spawnStop } from './ipc.js'; +import { resolveWorkspaceFromDbPath, canonicalizeCwd } from './resolve-db-workspace.js'; +import { createAdvisoryFallback, type AdvisoryFallback } from './advisory-fallback.js'; +import type { ChatHistoryEvent, WatchTarget } from './chat-history-types.js'; + +/** globalState key gating the one-time "use the status bar fallback" hint. */ +const FALLBACK_HINT_KEY = 'nexpath.fallbackHintShown'; + +/** + * Module-level state held across activate / deactivate so the watcher's + * resources can be cleaned up properly. View-provider lookup is exposed + * via `getViewProvider()` so other modules can publish payloads even + * outside the natural watcher → pipeline → view-provider chain. + */ +let viewProvider: NexpathDecisionSessionViewProvider | undefined; +let watcher: ChatHistoryWatcher | undefined; +let logChannel: vscode.OutputChannel | undefined; + +/** + * Dedicated VS Code OutputChannel for nexpath messages — visible to + * engineers and end-users at `View → Output → Nexpath`. Replaces the + * plain console.log path which Cursor / VS Code only surface in the + * Developer Tools Console (hard to discover, easy to miss). All key + * lifecycle + watcher events log through this channel; console.log is + * kept as a secondary destination so the existing extension.test.ts + * console-spy assertions still pass. + */ +function log(line: string): void { + console.log(line); + logChannel?.appendLine(`[${new Date().toISOString()}] ${line}`); +} + +export async function activate(context: vscode.ExtensionContext): Promise { + logChannel = vscode.window.createOutputChannel('Nexpath'); + context.subscriptions.push(logChannel); + log('[nexpath] extension activated'); + + // 1. Detect host (Cursor / Windsurf / vscode-generic). Stable for the + // lifetime of this extension instance. + const host = detectHost(); + + // 2. Construct + register the view provider with the B4 injectFn-aware + // onSelect. injectFn falls through to clipboard when the host has no + // matching command (the safe default; see chat-input-injector.ts). + // The same path injects a terminal-popup selection and a webview-fallback + // selection, so define it once and reuse. + const injectIntoChat = (text: string): Promise => + handleOptionSelection(text, { + injectFn: (t) => chatInputInject(t, { host }), + }); + viewProvider = new NexpathDecisionSessionViewProvider( + context.extensionUri, + injectIntoChat, + ); + context.subscriptions.push( + vscode.window.registerWebviewViewProvider(VIEW_ID, viewProvider), + ); + + // 2b. In-editor advisory fallback. Layer C's terminal popup is the primary + // advisory surface, but it is environment-fragile (needs a GUI terminal + + // $DISPLAY + session bus on Linux, and can land behind the editor). When a + // prompt cycle produces an advisory the popup didn't deliver, this lights a + // status-bar item that reveals the advisory in the webview — a surface that + // works on every OS with no terminal. It reads Layer C's store read-only; + // no Layer C code changes. + const statusBarItem = vscode.window.createStatusBarItem( + vscode.StatusBarAlignment.Right, + 100, + ); + statusBarItem.command = 'nexpath.showAdvisory'; + context.subscriptions.push(statusBarItem); + + const advisoryFallback: AdvisoryFallback = createAdvisoryFallback({ + publishPayload: (payload) => viewProvider?.publishPayload(payload), + statusBar: { + show: (text, tooltip) => { + statusBarItem.text = text; + statusBarItem.tooltip = tooltip; + statusBarItem.show(); + }, + hide: () => statusBarItem.hide(), + }, + showInfoOnce: (message) => { + if (context.globalState.get(FALLBACK_HINT_KEY) === true) return; + void context.globalState.update(FALLBACK_HINT_KEY, true); + void vscode.window.showInformationMessage(message); + }, + }); + context.subscriptions.push( + vscode.commands.registerCommand('nexpath.showAdvisory', () => + advisoryFallback.showAdvisory(), + ), + ); + + // 3. On hosts that route non-modal info messages to the silent + // notification stack (Cursor, Windsurf) instead of surfacing them as + // transient bottom-right toasts (VS Code's default), pre-open the + // notification panel so the consent toast in `showOnboardingIfNeeded` + // is immediately visible. Best-effort — failures are silent because + // the command id is host-dependent and the toast still lands in the + // panel either way. + if (host !== 'vscode-generic') { + try { + await vscode.commands.executeCommand('notifications.showList'); + } catch { + // ignored — discoverability hint, not load-bearing + } + } + + // 4. Show onboarding (consent prompt + macOS FDA guidance). May await + // the user's click; safe — the activate flow is allowed to block. + try { + await showOnboardingIfNeeded(context); + } catch (err) { + log(`[nexpath] onboarding failed: ${err instanceof Error ? err.stack ?? err.message : String(err)}`); + console.error('[nexpath] onboarding failed:', err); + } + + // 5. Watcher start-up — gated on consent + host being recognised. If the + // user denied, the value is `false` (NOT undefined) → watcher does + // not start. If the host is vscode-generic, there's no AI chat to + // watch. + const consent = context.globalState.get(CONSENT_KEY); + log(`[nexpath] consent state: ${JSON.stringify(consent)}, host: ${host}`); + if (consent !== true) { + log('[nexpath] consent not granted — watcher not started'); + return; + } + if (host === 'vscode-generic') { + log('[nexpath] host is plain VS Code — no chat to watch'); + return; + } + + const wsStorage = workspaceStorageDir({ host }); + const allDbPaths = enumerateStateVscdbPaths(wsStorage); + + // Multi-workspace correctness (R4.3 fix): + // Cursor / Windsurf keep ONE workspaceStorage//state.vscdb per open + // workspace, but every running extension instance can see all of them. + // If every instance watched every db, two open windows would each capture + // every prompt — producing duplicate rows in prompt-store.db with one + // row mis-attributed to the wrong window's cwd. + // + // The defensive choice: filter to dbs whose sibling workspace.json#folder + // matches THIS instance's workspaceCwd. Each db ends up watched by + // exactly one instance, so prompts land in prompt-store.db exactly once + // with the correct project_root. + // + // Fallback ladder: + // - No workspace folder open (window-without-folder): can't filter → + // keep current "watch all" behaviour so the user still gets capture. + // - workspace.json missing / multi-root (.code-workspace with + // `configuration` not `folder`) / unparseable: keep that db in the + // watch list, and rely on per-event cwd resolution below to attribute + // correctly. + const ownWorkspaceCwd = + vscode.workspace.workspaceFolders?.[0]?.uri.fsPath ?? null; + const dbPaths = + ownWorkspaceCwd === null + ? allDbPaths + : allDbPaths.filter((p) => { + const folder = resolveWorkspaceFromDbPath(p); + // null → workspace.json missing/unparseable → keep as defensive + // catch-all (per-event resolution will sort attribution). + return folder === null || folder === ownWorkspaceCwd; + }); + if (ownWorkspaceCwd !== null && dbPaths.length < allDbPaths.length) { + log( + `[nexpath] filtered ${allDbPaths.length - dbPaths.length} cross-workspace db(s); ` + + `watching ${dbPaths.length} for own workspace ${ownWorkspaceCwd}`, + ); + } + + // Per dev plan §2.3 acceptance #2, Windsurf's chat data may also live at + // `~/.codeium/windsurf/` (legacy Codeium Cascade store) in addition to + // `state.vscdb`. Watch both when host=windsurf; skip silently if the + // cascade dir doesn't exist (fs.watch on a missing path would throw). + // Existence is captured at activate time — same activate-time-only + // limitation that path-enumerator.ts documents for workspaceStorage. + const codeiumDir = + host === 'windsurf' ? windsurfCodeiumDir() : null; + const codeiumExists = codeiumDir !== null && existsSync(codeiumDir); + + // Cursor's modern Composer / Agent mode stores conversations in + // `globalStorage/state.vscdb` (shared file across all workspaces) under + // the `cursorDiskKV` table — NOT in `workspaceStorage`. Add it as a + // target unconditionally on Cursor hosts so Agent / Composer prompts + // actually reach the pipeline. Multi-window caveat: two open Cursor + // windows will both watch this file and both emit each new bubble (no + // cross-instance dedup), so each event reaches Layer C twice with each + // window's respective `workspaceCwd`. Documented as a known v0.1.3 + // limitation (single-window is the common case). + const globalDbPath = + host === 'cursor' ? globalStorageStateVscdbPath(wsStorage) : null; + log( + `[nexpath] enumerated ${dbPaths.length} state.vscdb file(s) under ${wsStorage}; ` + + `globalStorageDb=${globalDbPath === null ? 'absent' : 'present'}; ` + + `codeiumExists=${codeiumExists}`, + ); + if (dbPaths.length === 0 && globalDbPath === null && !codeiumExists) { + log( + `[nexpath] no workspace state.vscdb, no global state.vscdb, and no codeium dir found — watcher not started. ` + + 'Open at least one workspace in the host and reload the extension to retry.', + ); + return; + } + + const targets: WatchTarget[] = dbPaths.map((path) => ({ + path, + kind: 'cursor-sqlite', + })); + if (globalDbPath !== null) { + targets.push({ path: globalDbPath, kind: 'cursor-sqlite' }); + } + if (codeiumExists) { + targets.push({ path: codeiumDir!, kind: 'windsurf-dir' }); + } + + // 6. Build the pipeline handler (auto → stop → publish) with real + // dependencies (ipc.spawnAuto / ipc.spawnStop / viewProvider.publishPayload). + // Session id is workspace-prefixed so concurrent workspaces don't + // collide on the same chat tab id. + // Workspace folder fsPath drives: + // - the spawn-time `cwd` for nexpath auto/stop (so Layer C resolves + // project root / .env / hook-stats correctly) + // - the session-id prefix so concurrent workspaces don't collide on + // the same chat-tab id + // When VS Code is opened without a folder, fall back to the extension's + // own cwd — Layer C will use its DEFAULT_DB_PATH and skip project-specific + // .env loading. + const workspaceCwd = + vscode.workspace.workspaceFolders?.[0]?.uri.fsPath ?? process.cwd(); + // Per-event cwd resolution (defense-in-depth alongside the enumeration + // filter above). For every captured prompt, derive cwd from the db that + // fired the event — NOT the extension instance's workspaceCwd. Falls + // back to instance cwd when workspace.json is missing / multi-root. + // Canonicalize so the cwd we hand `auto` and `stop` matches the project_root + // `auto` records (= its process.cwd(), which the OS canonicalizes). Without + // this, on macOS the throwaway workspace under `/tmp` (a symlink to + // `/private/tmp`) makes `auto` write project_root=/private/tmp/… while `stop` + // looks up /tmp/… → no match → `stop_no_pending` → the popup never opens. + const cwdForEvent = (event: ChatHistoryEvent): string => + canonicalizeCwd(resolveWorkspaceFromDbPath(event.sourcePath) ?? workspaceCwd); + const handleChatEvent = createChatEventHandler({ + spawnAuto: (prompt, sid, event) => + spawnAuto(prompt, sid, { cwd: cwdForEvent(event) }), + spawnStop: (sid, event) => spawnStop(sid, { cwd: cwdForEvent(event) }), + // The user picked an option in the terminal popup → inject it into the chat + // input and clear any waiting fallback (they've handled this advisory). + injectSelection: async (selectedPrompt) => { + advisoryFallback.clear(); + await injectIntoChat(selectedPrompt); + }, + // After capture, before the popup: arm the in-editor fallback if `auto` + // parked an advisory, so it's available even if the popup blocks (macOS + // Automation dialog) or can't open. + onAfterCapture: (event) => + advisoryFallback.armIfPending(cwdForEvent(event)), + composeSessionId: (event) => `${cwdForEvent(event)}|${event.rawSessionId}`, + // Wire IPC failures (e.g. nexpath binary not on PATH → ENOENT) into + // the Nexpath OutputChannel so they surface to the user. Default logger + // only writes to console.error which is invisible outside Developer + // Tools. Both destinations are kept so existing test assertions on + // console.error continue to pass. + logger: { + error: (msg: string, err: unknown) => { + const detail = err instanceof Error ? err.message : String(err); + log(`${msg} ${detail}`); + console.error(msg, err); + }, + }, + }); + + watcher = createChatHistoryWatcher({ + targets, + // Polling backstop: fs.watch alone is unreliable on Windows for the SQLite + // WAL recreate pattern (fires once then goes silent → only the first prompts + // captured). Re-read every 2s; dedup makes it safe (no re-emit of seen + // prompts). Low latency still comes from fs.watch when it does fire. + pollMs: 2000, + onEvent: (event) => { + log(`[nexpath] watcher event: prompt="${event.prompt.slice(0, 80)}" raw_session_id=${event.rawSessionId} extractor=${event.extractorId}`); + // The watcher's onEvent is sync-fire-and-forget; the handler returns + // a Promise we deliberately don't await. + void handleChatEvent(event); + }, + onError: (err) => { + log(`[nexpath] watcher error: ${err.message}`); + console.error('[nexpath] watcher error:', err); + }, + onSchemaUnknown: ({ path, observedSampleKeys }) => { + log(`[nexpath] schema unknown for ${path}; sample keys: ${observedSampleKeys.slice(0, 3).join(', ')}`); + void vscode.window.showInformationMessage( + `Nexpath: ${path} schema is not recognised. The chat-history extractors may need updating. ` + + `Observed keys: ${observedSampleKeys.slice(0, 3).join(', ')}…`, + ); + }, + }); + + watcher.start(); + context.subscriptions.push({ dispose: () => watcher?.stop() }); + const cascadeNote = codeiumExists ? ' + 1 windsurf-dir' : ''; + log( + `[nexpath] watcher started on ${dbPaths.length} state.vscdb file(s)${cascadeNote} for host=${host}`, + ); +} + +export function deactivate(): void { + log('[nexpath] extension deactivated'); + watcher?.stop(); + watcher = undefined; + viewProvider = undefined; + logChannel = undefined; +} + +/** Lookup for other extension modules that want to publish payloads. */ +export function getViewProvider(): NexpathDecisionSessionViewProvider | undefined { + return viewProvider; +} diff --git a/src/ext-vscode/src/extractors/cursor-composer-bubble.test.ts b/src/ext-vscode/src/extractors/cursor-composer-bubble.test.ts new file mode 100644 index 00000000..e555e4ea --- /dev/null +++ b/src/ext-vscode/src/extractors/cursor-composer-bubble.test.ts @@ -0,0 +1,147 @@ +import { describe, it, expect } from 'vitest'; +import { cursorComposerBubble } from './cursor-composer-bubble.js'; + +const SOURCE = '/home/u/.config/Cursor/User/globalStorage/state.vscdb'; + +function userBubble(text: string, composerId = 'comp-1', bubbleId = 'bub-1'): { + key: string; + value: string; +} { + return { + key: `cursorDiskKV/bubbleId:${composerId}:${bubbleId}`, + value: JSON.stringify({ + _v: 3, + type: 1, + bubbleId, + text, + createdAt: '2026-05-23T09:58:55.791Z', + }), + }; +} + +function assistantBubble(composerId = 'comp-1', bubbleId = 'bub-2'): { + key: string; + value: string; +} { + return { + key: `cursorDiskKV/bubbleId:${composerId}:${bubbleId}`, + value: JSON.stringify({ + _v: 3, + type: 2, + bubbleId, + text: '', // assistant bubbles have empty text in older Cursor versions + createdAt: '2026-05-23T09:59:01.000Z', + }), + }; +} + +describe('cursorComposerBubble', () => { + it('identifies itself with the expected id and label', () => { + expect(cursorComposerBubble.id).toBe('cursor-composer-bubble'); + expect(cursorComposerBubble.label).toMatch(/composer.*agent/i); + }); + + it('owns keys starting with cursorDiskKV/bubbleId: and nothing else', () => { + expect( + cursorComposerBubble.ownsKey('cursorDiskKV/bubbleId:c1:b1'), + ).toBe(true); + expect( + cursorComposerBubble.ownsKey('cursorDiskKV/composerData:c1'), + ).toBe(false); + expect(cursorComposerBubble.ownsKey('aiService.prompts')).toBe(false); + expect(cursorComposerBubble.ownsKey('bubbleId:c1:b1')).toBe(false); // missing prefix + }); + + it('emits one event per user bubble with composerId as rawSessionId', () => { + const row = userBubble('make me a website where i can create invoices'); + const events = cursorComposerBubble.decodeRow(row, SOURCE); + expect(events).toHaveLength(1); + expect(events[0]).toMatchObject({ + prompt: 'make me a website where i can create invoices', + rawSessionId: 'comp-1', + sourcePath: SOURCE, + extractorId: 'cursor-composer-bubble', + }); + expect(events[0].capturedAt).toBeInstanceOf(Date); + }); + + it('skips assistant bubbles (type=2)', () => { + expect(cursorComposerBubble.decodeRow(assistantBubble(), SOURCE)).toEqual( + [], + ); + }); + + it('skips bubbles whose type is not 1 (defensive against schema drift)', () => { + const row = { + key: 'cursorDiskKV/bubbleId:c1:b3', + value: JSON.stringify({ _v: 3, type: 99, text: 'whatever' }), + }; + expect(cursorComposerBubble.decodeRow(row, SOURCE)).toEqual([]); + }); + + it('skips user bubbles with empty / whitespace-only text', () => { + const empty = userBubble('', 'comp-2', 'bub-4'); + const ws = userBubble(' \n \t ', 'comp-2', 'bub-5'); + expect(cursorComposerBubble.decodeRow(empty, SOURCE)).toEqual([]); + expect(cursorComposerBubble.decodeRow(ws, SOURCE)).toEqual([]); + }); + + it('skips rows with malformed JSON without throwing', () => { + const row = { + key: 'cursorDiskKV/bubbleId:c1:b6', + value: '{not valid json', + }; + expect(() => cursorComposerBubble.decodeRow(row, SOURCE)).not.toThrow(); + expect(cursorComposerBubble.decodeRow(row, SOURCE)).toEqual([]); + }); + + it('skips rows whose key does not match the expected shape', () => { + expect( + cursorComposerBubble.decodeRow( + { key: 'aiService.prompts', value: '[]' }, + SOURCE, + ), + ).toEqual([]); + // Missing the second colon → no parseable composerId + expect( + cursorComposerBubble.decodeRow( + { key: 'cursorDiskKV/bubbleId:onlyone', value: '{}' }, + SOURCE, + ), + ).toEqual([]); + }); + + it('different composers produce different rawSessionIds so they do not dedup against each other', () => { + const a = cursorComposerBubble.decodeRow( + userBubble('first conversation prompt', 'comp-A'), + SOURCE, + ); + const b = cursorComposerBubble.decodeRow( + userBubble('second conversation prompt', 'comp-B'), + SOURCE, + ); + expect(a[0].rawSessionId).toBe('comp-A'); + expect(b[0].rawSessionId).toBe('comp-B'); + }); + + it('fingerprintKeys is exactly the table-qualified bubbleId prefix', () => { + expect(cursorComposerBubble.fingerprintKeys).toEqual([ + 'cursorDiskKV/bubbleId:', + ]); + }); + + it('trims whitespace around the prompt text (Cursor sometimes appends a trailing newline)', () => { + const row = { + key: 'cursorDiskKV/bubbleId:comp-X:b1', + value: JSON.stringify({ + _v: 3, + type: 1, + text: ' the login is not working on my phone fix it\n', + }), + }; + const events = cursorComposerBubble.decodeRow(row, SOURCE); + expect(events[0].prompt).toBe( + 'the login is not working on my phone fix it', + ); + }); +}); diff --git a/src/ext-vscode/src/extractors/cursor-composer-bubble.ts b/src/ext-vscode/src/extractors/cursor-composer-bubble.ts new file mode 100644 index 00000000..c2703fee --- /dev/null +++ b/src/ext-vscode/src/extractors/cursor-composer-bubble.ts @@ -0,0 +1,124 @@ +import type { + ChatHistoryEvent, + ChatHistoryExtractor, + ItemTableRow, +} from '../chat-history-types.js'; + +/** + * Cursor Composer / Agent mode — `cursorDiskKV.bubbleId::` + * (Cursor 3.4.20+, observed live 2026-05-25). + * + * Storage location: `~/.config/Cursor/User/globalStorage/state.vscdb` + * (NOT workspaceStorage). One shared file across every workspace the + * user opens in Cursor — Composer/Agent conversations are workspace- + * agnostic. + * + * Row shape: + * key = `bubbleId::` (the reader prefixes this + * with `cursorDiskKV/` so it's distinguishable from any + * `ItemTable` rows that might happen to share a key prefix) + * value = JSON object with shape: + * { + * "_v": 3, + * "type": 1 | 2, // 1 = user, 2 = assistant + * "bubbleId": "", + * "text": "", + * "richText": "", + * "createdAt": "2026-05-23T09:58:55.791Z", + * …many other fields (codebaseContextChunks, attachedFiles, lints, etc.) + * } + * + * Only user-type bubbles (type=1) produce ChatHistoryEvents; assistant + * replies (type=2) are skipped. The user prompt text lives at `.text` + * (plain string) — `.richText` is the same content in Cursor's Lexical + * editor format and is intentionally ignored. + * + * **`rawSessionId` = the composerId.** A composer is one Agent + * conversation; each conversation has its own id and contains multiple + * bubbles. Using composerId per-conversation means two distinct + * conversations in the same workspace don't dedup against each other, + * while the same bubble re-read across fs.watch fires within ONE + * conversation will dedup correctly (signature = sourcePath + + * composerId + prompt text). The bubbleId is NOT used in the signature + * because the prompt text is already content-addressed; including the + * bubbleId would cause a re-fire if Cursor ever rewrites the value + * (which it does — e.g., when `tokenCount` is updated post-response). + * + * **Why this extractor was added (M2 closeout 2026-05-25):** the user's + * manual S01 test run captured 0 prompts despite typing 15 prompts in + * Cursor. Investigation traced the prompts to `cursorDiskKV` in + * globalStorage (not the workspaceStorage paths the watcher was + * monitoring). The Composer/Agent panel is Cursor's default modern UX; + * without this extractor the extension is effectively blind to the + * majority of real-world prompts. Closes the F2 gap in dev plan §2.10. + */ + +const KEY_PREFIX = 'cursorDiskKV/bubbleId:'; + +interface ComposerBubbleValue { + /** Schema version. Currently 3 in Cursor 3.4.20. */ + _v?: number; + /** 1 = user bubble (a prompt), 2 = assistant bubble (a response). */ + type?: number; + /** UUID of the bubble itself. Same as the second UUID in the key. */ + bubbleId?: string; + /** The user prompt text. Empty / missing for assistant bubbles. */ + text?: string; + /** ISO timestamp; not used for capture but useful for future telemetry. */ + createdAt?: string; +} + +/** + * Extract the composerId from a key of the form + * `cursorDiskKV/bubbleId::`. Returns null when the + * key doesn't match the expected shape (defensive — we never reach + * `decodeRow` without `ownsKey` having returned true, but a key that + * `startsWith` the prefix could still be missing the second colon). + */ +function parseComposerId(key: string): string | null { + const rest = key.slice(KEY_PREFIX.length); + const colonIdx = rest.indexOf(':'); + if (colonIdx <= 0) return null; + return rest.slice(0, colonIdx); +} + +export const cursorComposerBubble: ChatHistoryExtractor = { + id: 'cursor-composer-bubble', + label: 'Cursor Composer / Agent mode (cursorDiskKV bubbles)', + fingerprintKeys: [KEY_PREFIX] as const, + + ownsKey(key: string): boolean { + return key.startsWith(KEY_PREFIX); + }, + + decodeRow(row: ItemTableRow, sourcePath: string): ChatHistoryEvent[] { + if (!row.key.startsWith(KEY_PREFIX)) return []; + + const composerId = parseComposerId(row.key); + if (composerId === null) return []; + + let parsed: ComposerBubbleValue; + try { + parsed = JSON.parse(row.value) as ComposerBubbleValue; + } catch { + return []; + } + + // Only user-type bubbles produce capture events. type=2 is the + // assistant; type=undefined / other values are skipped defensively. + if (parsed.type !== 1) return []; + + const text = typeof parsed.text === 'string' ? parsed.text.trim() : ''; + if (text.length === 0) return []; + + return [ + { + prompt: text, + rawSessionId: composerId, + capturedAt: new Date(), + sourcePath, + extractorId: 'cursor-composer-bubble', + }, + ]; + }, +}; diff --git a/src/ext-vscode/src/extractors/cursor-v2024-q4.test.ts b/src/ext-vscode/src/extractors/cursor-v2024-q4.test.ts new file mode 100644 index 00000000..25298f02 --- /dev/null +++ b/src/ext-vscode/src/extractors/cursor-v2024-q4.test.ts @@ -0,0 +1,116 @@ +import { describe, it, expect } from 'vitest'; +import { cursorV2024Q4 } from './cursor-v2024-q4.js'; +import type { ItemTableRow } from '../chat-history-types.js'; + +const SRC = '/fake/state.vscdb'; + +describe('cursorV2024Q4 extractor', () => { + describe('static fields', () => { + it('has the expected id, label, and fingerprint keys', () => { + expect(cursorV2024Q4.id).toBe('cursor-v2024-q4'); + expect(cursorV2024Q4.label).toContain('v2024-Q4'); + expect(cursorV2024Q4.fingerprintKeys).toEqual(['aiService.prompts']); + }); + }); + + describe('ownsKey', () => { + it('matches exactly aiService.prompts', () => { + expect(cursorV2024Q4.ownsKey('aiService.prompts')).toBe(true); + }); + it('does not match other keys', () => { + expect(cursorV2024Q4.ownsKey('aiService.generations')).toBe(false); + expect(cursorV2024Q4.ownsKey('aiService.prompts.extra')).toBe(false); + expect(cursorV2024Q4.ownsKey('')).toBe(false); + }); + }); + + describe('decodeRow', () => { + const row = (value: unknown): ItemTableRow => ({ + key: 'aiService.prompts', + value: JSON.stringify(value), + }); + + it('returns one event per prompt entry with text', () => { + const events = cursorV2024Q4.decodeRow( + row([{ text: 'first' }, { text: 'second' }, { text: 'third' }]), + SRC, + ); + expect(events).toHaveLength(3); + expect(events.map((e) => e.prompt)).toEqual(['first', 'second', 'third']); + expect(events.every((e) => e.extractorId === 'cursor-v2024-q4')).toBe(true); + expect(events.every((e) => e.sourcePath === SRC)).toBe(true); + }); + + it("uses a stable 'ask-mode' rawSessionId for every prompt (NOT positional index)", () => { + // Regression for the M2 R3 FIFO-shift bug (2026-05-20): the previous + // positional `prompts-index:${i}` rawSessionId caused every Cursor + // restart with a full FIFO to re-emit the entire backlog because + // the dedup signature shifted when a new prompt pushed out the + // oldest. Content-stable rawSessionId means the signature depends + // only on (sourcePath, prompt text), which survives FIFO shifts. + const events = cursorV2024Q4.decodeRow(row([{ text: 'a' }, { text: 'b' }]), SRC); + expect(events.map((e) => e.rawSessionId)).toEqual(['ask-mode', 'ask-mode']); + }); + + it('FIFO-shift regression: same prompts in shifted positions produce the SAME (sourcePath,text)-stable signature', () => { + // Simulates the live M2 R3 scenario: an Ask-mode FIFO of 10 prompts + // shifts left by 1 (oldest dropped, new appended). The text-stable + // rawSessionId means each individual prompt's downstream watcher + // signature `${sourcePath}|${rawSessionId}|${prompt}` is unchanged + // by the shift, so the watcher's seenSignatures dedup correctly + // recognises them as already-seen. + const before = cursorV2024Q4.decodeRow( + row(Array.from({ length: 10 }, (_, i) => ({ text: `prompt-${i}` }))), + SRC, + ); + // FIFO shift: drop prompt-0, append prompt-10. + const after = cursorV2024Q4.decodeRow( + row([ + ...Array.from({ length: 9 }, (_, i) => ({ text: `prompt-${i + 1}` })), + { text: 'prompt-10' }, + ]), + SRC, + ); + // Build the signature for each event the way chat-history-watcher does. + const sigOf = (e: { sourcePath: string; rawSessionId: string; prompt: string }) => + `${e.sourcePath}|${e.rawSessionId}|${e.prompt}`; + const beforeSigs = new Set(before.map(sigOf)); + const afterSigs = after.map(sigOf); + // 9 of the 10 post-shift prompts must hit the dedup set; the new + // tail (`prompt-10`) is the only one that's genuinely new. + const newOnes = afterSigs.filter((s) => !beforeSigs.has(s)); + expect(newOnes).toHaveLength(1); + expect(newOnes[0]).toContain('prompt-10'); + }); + + it('skips entries with no text field', () => { + const events = cursorV2024Q4.decodeRow( + row([{ text: 'ok' }, { commandType: 1 }, { text: '' }, { text: 'still-ok' }]), + SRC, + ); + expect(events.map((e) => e.prompt)).toEqual(['ok', 'still-ok']); + }); + + it('returns [] for foreign keys', () => { + const events = cursorV2024Q4.decodeRow( + { key: 'someOtherKey', value: '[]' }, + SRC, + ); + expect(events).toEqual([]); + }); + + it('returns [] for malformed JSON', () => { + const events = cursorV2024Q4.decodeRow( + { key: 'aiService.prompts', value: 'not-json{{{' }, + SRC, + ); + expect(events).toEqual([]); + }); + + it('returns [] for non-array JSON', () => { + expect(cursorV2024Q4.decodeRow(row({}), SRC)).toEqual([]); + expect(cursorV2024Q4.decodeRow(row(null), SRC)).toEqual([]); + expect(cursorV2024Q4.decodeRow(row('string'), SRC)).toEqual([]); + }); + }); +}); diff --git a/src/ext-vscode/src/extractors/cursor-v2024-q4.ts b/src/ext-vscode/src/extractors/cursor-v2024-q4.ts new file mode 100644 index 00000000..6720c645 --- /dev/null +++ b/src/ext-vscode/src/extractors/cursor-v2024-q4.ts @@ -0,0 +1,80 @@ +import type { + ChatHistoryEvent, + ChatHistoryExtractor, + ItemTableRow, +} from '../chat-history-types.js'; + +/** + * Cursor v2024-Q4 (approx. Cursor versions ~0.30–0.40, pre-Composer release). + * + * Community-documented key: `aiService.prompts` — a single JSON array of all + * prompts globally (not per-tab). Each entry has at minimum a `text` field; + * older entries lack any tab/session identifier. + * + * **`rawSessionId` is a content-stable constant (`ask-mode`), NOT the + * entry's array index.** Live-tested on Cursor 3.4.20 during M2 R3 manual + * testing (2026-05-20): the `aiService.prompts` array is a rolling FIFO + * (capacity ~10). When a new prompt is submitted, the oldest is dropped + * and ALL subsequent prompts shift left by one. Using the positional + * index as the session id meant the dedup signature + * (`sourcePath|rawSessionId|prompt`) changed for every existing prompt + * after each FIFO shift, causing every Cursor restart to re-emit the + * entire backlog. Switching to a stable session id makes the signature + * content-driven (sourcePath + prompt text), so a shifted array is + * correctly recognised as containing the same prompts. + * + * Trade-off: if a user submits the *exact same text* twice within the + * FIFO window, only the first emit fires. Layer C's session state will + * undercount by that one prompt. Acceptable because (a) byte-identical + * re-submission is rare in practice, (b) Layer C tolerates undercount + * gracefully (advisory gating still functions), and (c) it's strictly + * better than the FIFO-shift flood it replaces. + */ + +const PROMPTS_KEY = 'aiService.prompts'; + +interface CursorV2024Q4PromptEntry { + /** The prompt text the user submitted. */ + text?: string; + /** Optional command-type enum (refactor, edit, ask, etc.). Not used for capture. */ + commandType?: number; + /** Optional timestamp; not currently used. */ + timestamp?: number; +} + +export const cursorV2024Q4: ChatHistoryExtractor = { + id: 'cursor-v2024-q4', + label: 'Cursor v2024-Q4 (pre-Composer)', + fingerprintKeys: [PROMPTS_KEY] as const, + + ownsKey(key: string): boolean { + return key === PROMPTS_KEY; + }, + + decodeRow(row: ItemTableRow, sourcePath: string): ChatHistoryEvent[] { + if (row.key !== PROMPTS_KEY) return []; + let parsed: unknown; + try { + parsed = JSON.parse(row.value); + } catch { + return []; + } + if (!Array.isArray(parsed)) return []; + + const events: ChatHistoryEvent[] = []; + const entries = parsed as CursorV2024Q4PromptEntry[]; + for (const entry of entries) { + if (!entry || typeof entry.text !== 'string' || entry.text.length === 0) { + continue; + } + events.push({ + prompt: entry.text, + rawSessionId: 'ask-mode', + capturedAt: new Date(), + sourcePath, + extractorId: 'cursor-v2024-q4', + }); + } + return events; + }, +}; diff --git a/src/ext-vscode/src/extractors/cursor-v2025-q1.test.ts b/src/ext-vscode/src/extractors/cursor-v2025-q1.test.ts new file mode 100644 index 00000000..30703d58 --- /dev/null +++ b/src/ext-vscode/src/extractors/cursor-v2025-q1.test.ts @@ -0,0 +1,120 @@ +import { describe, it, expect } from 'vitest'; +import { cursorV2025Q1 } from './cursor-v2025-q1.js'; +import type { ItemTableRow } from '../chat-history-types.js'; + +const SRC = '/fake/state.vscdb'; +const KEY = 'composer.composerData'; + +const wrap = (composerData: unknown): ItemTableRow => ({ + key: KEY, + value: JSON.stringify(composerData), +}); + +describe('cursorV2025Q1 extractor', () => { + describe('static fields', () => { + it('has the expected id, label, and fingerprint keys', () => { + expect(cursorV2025Q1.id).toBe('cursor-v2025-q1'); + expect(cursorV2025Q1.label).toContain('Composer'); + expect(cursorV2025Q1.fingerprintKeys).toEqual([KEY]); + }); + }); + + describe('ownsKey', () => { + it('matches exactly composer.composerData', () => { + expect(cursorV2025Q1.ownsKey(KEY)).toBe(true); + }); + it('does not match other keys', () => { + expect(cursorV2025Q1.ownsKey('aiService.prompts')).toBe(false); + expect(cursorV2025Q1.ownsKey('composer.other')).toBe(false); + expect(cursorV2025Q1.ownsKey('composerData.composerData')).toBe(false); + }); + }); + + describe('decodeRow', () => { + it('extracts user messages with role=user, ignoring assistant messages', () => { + const events = cursorV2025Q1.decodeRow( + wrap({ + allComposers: { + c1: { + composerId: 'c1', + conversation: [ + { role: 'user', text: 'hello' }, + { role: 'assistant', text: 'hi' }, + { role: 'user', text: 'follow-up' }, + ], + }, + }, + }), + SRC, + ); + expect(events.map((e) => e.prompt)).toEqual(['hello', 'follow-up']); + expect(events.every((e) => e.rawSessionId === 'composer:c1')).toBe(true); + expect(events.every((e) => e.extractorId === 'cursor-v2025-q1')).toBe(true); + }); + + it('supports legacy numeric type=1 for user messages', () => { + const events = cursorV2025Q1.decodeRow( + wrap({ + allComposers: { + c2: { + composerId: 'c2', + conversation: [ + { type: 1, text: 'legacy-user' }, + { type: 2, text: 'legacy-assistant' }, + ], + }, + }, + }), + SRC, + ); + expect(events).toHaveLength(1); + expect(events[0]!.prompt).toBe('legacy-user'); + }); + + it('supports the `content` field as fallback when `text` is missing', () => { + const events = cursorV2025Q1.decodeRow( + wrap({ + allComposers: { + c3: { + composerId: 'c3', + conversation: [{ role: 'user', content: 'from-content' }], + }, + }, + }), + SRC, + ); + expect(events[0]!.prompt).toBe('from-content'); + }); + + it('emits events per composer when multiple conversations exist', () => { + const events = cursorV2025Q1.decodeRow( + wrap({ + allComposers: { + a: { composerId: 'a', conversation: [{ role: 'user', text: 'from-a' }] }, + b: { composerId: 'b', conversation: [{ role: 'user', text: 'from-b' }] }, + }, + }), + SRC, + ); + const sessions = events.map((e) => e.rawSessionId).sort(); + expect(sessions).toEqual(['composer:a', 'composer:b']); + }); + + it('returns [] for malformed JSON', () => { + expect( + cursorV2025Q1.decodeRow({ key: KEY, value: 'not-json' }, SRC), + ).toEqual([]); + }); + + it('returns [] when allComposers is missing or empty', () => { + expect(cursorV2025Q1.decodeRow(wrap({}), SRC)).toEqual([]); + expect(cursorV2025Q1.decodeRow(wrap({ allComposers: {} }), SRC)).toEqual([]); + }); + + it('returns [] for foreign keys', () => { + expect( + cursorV2025Q1.decodeRow({ key: 'unrelated', value: '{}' }, SRC), + ).toEqual([]); + }); + }); +}); diff --git a/src/ext-vscode/src/extractors/cursor-v2025-q1.ts b/src/ext-vscode/src/extractors/cursor-v2025-q1.ts new file mode 100644 index 00000000..e74d6373 --- /dev/null +++ b/src/ext-vscode/src/extractors/cursor-v2025-q1.ts @@ -0,0 +1,113 @@ +import type { + ChatHistoryEvent, + ChatHistoryExtractor, + ItemTableRow, +} from '../chat-history-types.js'; + +/** + * Cursor v2025-Q1 (Composer era). + * + * **Real-machine update (2026-05-15, Cursor 3.4.20):** The fingerprint key + * is `composer.composerData` (NOT `composerData.composerData` as the + * original community docs suggested). Confirmed via + * `scripts/dump-cursor-state.ts` against a live Cursor 3.4.20 workspace DB. + * + * **Open finding:** the value at `composer.composerData` is *metadata only* + * on Cursor 3.4.20: + * `{ selectedComposerIds: string[], lastFocusedComposerIds: string[], + * hasMigratedComposerData: boolean, hasMigratedMultipleComposers: boolean }` + * — it does **not** contain conversation messages. Where modern Composer + * messages are actually stored is **TBD** (the workspace's `cursorDiskKV` + * table was empty on a chat-less DB; messages may land there once a real + * chat occurs, or in a separate file outside SQLite). + * + * The `allComposers / conversation` shape this extractor parses for is + * believed to be correct for an OLDER Composer-era format (~0.45–0.49). On + * a Cursor 3.4.20 DB this extractor will fingerprint-match the key but + * `decodeRow` will return [] because the expected `allComposers` field is + * absent. That's safe — fingerprint matches the modern key, decode falls + * through cleanly, and a follow-up fixture-driven update can refine the + * decode logic once we have a snapshot of a workspace DB with real chats. + * + * TODO(M2/B2): re-run `dump-cursor-state.ts` after submitting a real + * Composer-mode prompt in Cursor and inspect which row(s) the message + * landed in. Update this extractor (or add a sibling) accordingly before + * Branch 6 ships. + */ + +const COMPOSER_KEY = 'composer.composerData'; + +interface ComposerConversation { + /** Composer / tab identifier — used as `rawSessionId`. */ + composerId?: string; + conversation?: ComposerMessage[]; +} + +interface ComposerMessage { + /** Numeric form: 1 = user, 2 = assistant. */ + type?: number | string; + /** String form: 'user' / 'assistant'. */ + role?: string; + /** Either `text` or `content` depending on minor version. */ + text?: string; + content?: string; +} + +interface ComposerData { + allComposers?: Record; +} + +function isUserMessage(msg: ComposerMessage): boolean { + return ( + msg.role === 'user' || + msg.type === 'user' || + msg.type === 1 + ); +} + +function extractText(msg: ComposerMessage): string | undefined { + if (typeof msg.text === 'string' && msg.text.length > 0) return msg.text; + if (typeof msg.content === 'string' && msg.content.length > 0) return msg.content; + return undefined; +} + +export const cursorV2025Q1: ChatHistoryExtractor = { + id: 'cursor-v2025-q1', + label: 'Cursor v2025-Q1 (Composer)', + fingerprintKeys: [COMPOSER_KEY] as const, + + ownsKey(key: string): boolean { + return key === COMPOSER_KEY; + }, + + decodeRow(row: ItemTableRow, sourcePath: string): ChatHistoryEvent[] { + if (row.key !== COMPOSER_KEY) return []; + let parsed: ComposerData; + try { + parsed = JSON.parse(row.value) as ComposerData; + } catch { + return []; + } + if (!parsed || typeof parsed !== 'object') return []; + + const events: ChatHistoryEvent[] = []; + const composers = parsed.allComposers ?? {}; + for (const conv of Object.values(composers)) { + if (!conv || !Array.isArray(conv.conversation)) continue; + const composerId = conv.composerId ?? 'unknown'; + for (const msg of conv.conversation) { + if (!isUserMessage(msg)) continue; + const text = extractText(msg); + if (!text) continue; + events.push({ + prompt: text, + rawSessionId: `composer:${composerId}`, + capturedAt: new Date(), + sourcePath, + extractorId: 'cursor-v2025-q1', + }); + } + } + return events; + }, +}; diff --git a/src/ext-vscode/src/extractors/cursor-v2025-q2.test.ts b/src/ext-vscode/src/extractors/cursor-v2025-q2.test.ts new file mode 100644 index 00000000..42e74aec --- /dev/null +++ b/src/ext-vscode/src/extractors/cursor-v2025-q2.test.ts @@ -0,0 +1,112 @@ +import { describe, it, expect } from 'vitest'; +import { cursorV2025Q2 } from './cursor-v2025-q2.js'; +import type { ItemTableRow } from '../chat-history-types.js'; + +const SRC = '/fake/state.vscdb'; +const KEY_PREFIX = 'cursorAIChatService.chatHistory.'; + +const wrap = (tabId: string, messages: unknown): ItemTableRow => ({ + key: `${KEY_PREFIX}${tabId}`, + value: JSON.stringify(messages), +}); + +describe('cursorV2025Q2 extractor', () => { + describe('static fields', () => { + it('has the expected id, label, and prefix fingerprint', () => { + expect(cursorV2025Q2.id).toBe('cursor-v2025-q2'); + expect(cursorV2025Q2.label).toContain('v2025-Q2'); + expect(cursorV2025Q2.fingerprintKeys).toEqual([KEY_PREFIX]); + }); + }); + + describe('ownsKey', () => { + it('matches keys with the chatHistory prefix', () => { + expect(cursorV2025Q2.ownsKey(`${KEY_PREFIX}abc`)).toBe(true); + expect(cursorV2025Q2.ownsKey(`${KEY_PREFIX}tab-xyz-123`)).toBe(true); + }); + it('does not match other keys', () => { + expect(cursorV2025Q2.ownsKey('cursorAIChatService.other')).toBe(false); + expect(cursorV2025Q2.ownsKey('aiService.prompts')).toBe(false); + }); + }); + + describe('decodeRow', () => { + it('extracts role=user messages and tags rawSessionId with the tab id', () => { + const events = cursorV2025Q2.decodeRow( + wrap('tab-1', [ + { role: 'user', content: 'first' }, + { role: 'assistant', content: 'reply' }, + { role: 'user', content: 'second' }, + ]), + SRC, + ); + expect(events.map((e) => e.prompt)).toEqual(['first', 'second']); + expect(events.every((e) => e.rawSessionId === 'tab:tab-1')).toBe(true); + expect(events.every((e) => e.extractorId === 'cursor-v2025-q2')).toBe(true); + }); + + it('uses `text` as fallback when `content` is missing', () => { + const events = cursorV2025Q2.decodeRow( + wrap('tab-2', [{ role: 'user', text: 'from-text' }]), + SRC, + ); + expect(events[0]!.prompt).toBe('from-text'); + }); + + it('treats type="user" as a user message', () => { + const events = cursorV2025Q2.decodeRow( + wrap('tab-3', [ + { type: 'user', content: 'typed-user' }, + { type: 'assistant', content: 'typed-asst' }, + ]), + SRC, + ); + expect(events.map((e) => e.prompt)).toEqual(['typed-user']); + }); + + it('treats legacy numeric type=1 as a user message', () => { + const events = cursorV2025Q2.decodeRow( + wrap('tab-4', [ + { type: 1, content: 'legacy-user' }, + { type: 2, content: 'legacy-asst' }, + ]), + SRC, + ); + expect(events.map((e) => e.prompt)).toEqual(['legacy-user']); + }); + + it('returns [] for foreign keys', () => { + const events = cursorV2025Q2.decodeRow( + { key: 'some.other.key', value: '[]' }, + SRC, + ); + expect(events).toEqual([]); + }); + + it('returns [] for malformed JSON', () => { + expect( + cursorV2025Q2.decodeRow( + { key: `${KEY_PREFIX}tab-x`, value: 'not-json {{' }, + SRC, + ), + ).toEqual([]); + }); + + it('returns [] for non-array JSON', () => { + expect(cursorV2025Q2.decodeRow(wrap('tab-z', {}), SRC)).toEqual([]); + expect(cursorV2025Q2.decodeRow(wrap('tab-z', null), SRC)).toEqual([]); + }); + + it('skips messages without extractable text', () => { + const events = cursorV2025Q2.decodeRow( + wrap('tab-5', [ + { role: 'user', content: '' }, + { role: 'user' }, + { role: 'user', content: 'kept' }, + ]), + SRC, + ); + expect(events.map((e) => e.prompt)).toEqual(['kept']); + }); + }); +}); diff --git a/src/ext-vscode/src/extractors/cursor-v2025-q2.ts b/src/ext-vscode/src/extractors/cursor-v2025-q2.ts new file mode 100644 index 00000000..39b4f3f9 --- /dev/null +++ b/src/ext-vscode/src/extractors/cursor-v2025-q2.ts @@ -0,0 +1,94 @@ +import type { + ChatHistoryEvent, + ChatHistoryExtractor, + ItemTableRow, +} from '../chat-history-types.js'; + +/** + * Cursor v2025-Q2+ — community-documented per-tab chat history layout. + * + * **Real-machine status (2026-05-15, Cursor 3.4.20):** the prefix + * `cursorAIChatService.chatHistory.` was **NOT** observed in a real + * Cursor 3.4.20 workspace DB. This extractor's fingerprint will only + * match if a Cursor version that does use this key prefix is encountered. + * + * **What we DID see on Cursor 3.4.20** (per + * `scripts/dump-cursor-state.ts` inspection): + * - `aiService.prompts` / `aiService.generations` — covered by the + * `cursor-v2024-q4` extractor. + * - `composer.composerData` — metadata only; covered by + * `cursor-v2025-q1`. + * - No `cursorAIChatService.*` keys at all. + * The Composer-mode message storage location for Cursor 3.4.20 is still + * unknown — likely in `cursorDiskKV` once chats happen, or in a separate + * file. The dump script captures both tables and all chat-prefix keys, so + * a follow-up commit will refine extractors once a chat-bearing snapshot + * arrives. + * + * TODO(M2/B2): either (a) confirm the `cursorAIChatService.chatHistory.` + * prefix in some older Cursor version (and keep this extractor as + * version-pinned), or (b) replace it with a `cursor-v3-x` extractor + * matching Cursor 3.4.20's actual message storage once it's been + * snapshotted. + */ + +const KEY_PREFIX = 'cursorAIChatService.chatHistory.'; + +interface ChatMessage { + role?: string; + type?: string | number; + content?: string; + text?: string; +} + +function isUserMessage(msg: ChatMessage): boolean { + return ( + msg.role === 'user' || + msg.type === 'user' || + msg.type === 1 /* legacy numeric */ + ); +} + +function extractText(msg: ChatMessage): string | undefined { + if (typeof msg.content === 'string' && msg.content.length > 0) return msg.content; + if (typeof msg.text === 'string' && msg.text.length > 0) return msg.text; + return undefined; +} + +export const cursorV2025Q2: ChatHistoryExtractor = { + id: 'cursor-v2025-q2', + label: 'Cursor v2025-Q2+ (per-tab chat history)', + fingerprintKeys: [KEY_PREFIX] as const, + + ownsKey(key: string): boolean { + return key.startsWith(KEY_PREFIX); + }, + + decodeRow(row: ItemTableRow, sourcePath: string): ChatHistoryEvent[] { + if (!row.key.startsWith(KEY_PREFIX)) return []; + const tabId = row.key.slice(KEY_PREFIX.length); + + let parsed: unknown; + try { + parsed = JSON.parse(row.value); + } catch { + return []; + } + if (!Array.isArray(parsed)) return []; + + const events: ChatHistoryEvent[] = []; + for (const msg of parsed as ChatMessage[]) { + if (!msg || !isUserMessage(msg)) continue; + const text = extractText(msg); + if (!text) continue; + events.push({ + prompt: text, + rawSessionId: `tab:${tabId}`, + capturedAt: new Date(), + sourcePath, + extractorId: 'cursor-v2025-q2', + }); + } + return events; + }, +}; diff --git a/src/ext-vscode/src/extractors/index.test.ts b/src/ext-vscode/src/extractors/index.test.ts new file mode 100644 index 00000000..e38974bf --- /dev/null +++ b/src/ext-vscode/src/extractors/index.test.ts @@ -0,0 +1,133 @@ +import { describe, it, expect } from 'vitest'; +import { + ALL_EXTRACTORS, + getExtractorById, + pickExtractor, +} from './index.js'; + +describe('extractor registry', () => { + it('lists all extractors in newest-first order, composer-bubble first (cursorDiskKV-table-owner)', () => { + expect(ALL_EXTRACTORS.map((e) => e.id)).toEqual([ + 'cursor-composer-bubble', + 'cursor-v2025-q2', + 'cursor-v2025-q1', + 'cursor-v2024-q4', + 'windsurf', + ]); + }); + + it('getExtractorById returns the matching extractor', () => { + expect(getExtractorById('cursor-v2025-q2')?.id).toBe('cursor-v2025-q2'); + expect(getExtractorById('cursor-composer-bubble')?.id).toBe( + 'cursor-composer-bubble', + ); + expect(getExtractorById('windsurf')?.id).toBe('windsurf'); + }); + + it('getExtractorById returns undefined for unknown ids', () => { + expect(getExtractorById('nonexistent')).toBeUndefined(); + }); +}); + +describe('pickExtractor (M4 fingerprint)', () => { + it('returns "unknown" for an empty key list', () => { + const result = pickExtractor([]); + expect(result.kind).toBe('unknown'); + if (result.kind === 'unknown') { + expect(result.observedKeyCount).toBe(0); + expect(result.observedSampleKeys).toEqual([]); + } + }); + + it('returns "unknown" when no keys match any extractor', () => { + const result = pickExtractor(['unrelated.key.1', 'another.thing', 'foo.bar']); + expect(result.kind).toBe('unknown'); + if (result.kind === 'unknown') { + expect(result.observedKeyCount).toBe(3); + expect(result.observedSampleKeys).toEqual([ + 'unrelated.key.1', + 'another.thing', + 'foo.bar', + ]); + } + }); + + it('caps observedSampleKeys to 5 entries', () => { + const result = pickExtractor(['a', 'b', 'c', 'd', 'e', 'f', 'g']); + if (result.kind === 'unknown') { + expect(result.observedSampleKeys).toHaveLength(5); + } + }); + + it('picks cursor-v2024-q4 when aiService.prompts is the matching key', () => { + const result = pickExtractor([ + 'aiService.prompts', + 'workbench.editor.recent', + 'foo.bar', + ]); + expect(result.kind).toBe('known'); + if (result.kind === 'known') { + expect(result.extractor.id).toBe('cursor-v2024-q4'); + expect(result.matchedKeys).toEqual(['aiService.prompts']); + } + }); + + it('picks cursor-v2025-q1 when composer.composerData is present', () => { + const result = pickExtractor(['composer.composerData', 'other.key']); + expect(result.kind).toBe('known'); + if (result.kind === 'known') { + expect(result.extractor.id).toBe('cursor-v2025-q1'); + } + }); + + it('picks cursor-v2025-q2 when chatHistory keys are present (prefix match)', () => { + const result = pickExtractor([ + 'cursorAIChatService.chatHistory.tab-1', + 'cursorAIChatService.chatHistory.tab-2', + 'workbench.recent.files', + ]); + expect(result.kind).toBe('known'); + if (result.kind === 'known') { + expect(result.extractor.id).toBe('cursor-v2025-q2'); + expect(result.matchedKeys).toEqual([ + 'cursorAIChatService.chatHistory.tab-1', + 'cursorAIChatService.chatHistory.tab-2', + ]); + } + }); + + it('picks the extractor with the highest match count when multiple match', () => { + // q2 has 3 chatHistory matches; q4 has 1 prompts match — q2 should win. + const result = pickExtractor([ + 'aiService.prompts', + 'cursorAIChatService.chatHistory.t1', + 'cursorAIChatService.chatHistory.t2', + 'cursorAIChatService.chatHistory.t3', + ]); + expect(result.kind).toBe('known'); + if (result.kind === 'known') { + expect(result.extractor.id).toBe('cursor-v2025-q2'); + } + }); + + it('picks the newer extractor on a tie (registry order)', () => { + // q4 has 1 match (aiService.prompts); q1 has 1 match (composerData) + // — q1 wins because it appears earlier in ALL_EXTRACTORS. + const result = pickExtractor([ + 'aiService.prompts', + 'composer.composerData', + ]); + expect(result.kind).toBe('known'); + if (result.kind === 'known') { + expect(result.extractor.id).toBe('cursor-v2025-q1'); + } + }); + + it('picks windsurf when only cascade keys are present', () => { + const result = pickExtractor(['cascade.history', 'cascade.settings']); + expect(result.kind).toBe('known'); + if (result.kind === 'known') { + expect(result.extractor.id).toBe('windsurf'); + } + }); +}); diff --git a/src/ext-vscode/src/extractors/index.ts b/src/ext-vscode/src/extractors/index.ts new file mode 100644 index 00000000..9820c36d --- /dev/null +++ b/src/ext-vscode/src/extractors/index.ts @@ -0,0 +1,81 @@ +import type { + ChatHistoryExtractor, + FingerprintResult, +} from '../chat-history-types.js'; +import { cursorComposerBubble } from './cursor-composer-bubble.js'; +import { cursorV2024Q4 } from './cursor-v2024-q4.js'; +import { cursorV2025Q1 } from './cursor-v2025-q1.js'; +import { cursorV2025Q2 } from './cursor-v2025-q2.js'; +import { windsurf } from './windsurf.js'; + +/** + * Registry of all known per-version chat-history extractors. Ordered + * newest-first so that on a tie in fingerprint match count the latest + * version wins (see `pickExtractor`). cursor-composer-bubble is listed + * first because it's Cursor's current default chat mode (Composer / + * Agent) and the only one that owns rows from the `cursorDiskKV` table + * (everything else owns rows from `ItemTable`). + */ +export const ALL_EXTRACTORS: readonly ChatHistoryExtractor[] = [ + cursorComposerBubble, + cursorV2025Q2, + cursorV2025Q1, + cursorV2024Q4, + windsurf, +]; + +export function getExtractorById(id: string): ChatHistoryExtractor | undefined { + return ALL_EXTRACTORS.find((e) => e.id === id); +} + +/** + * Schema fingerprint detection (M4). + * + * Examine a set of observed ItemTable keys and pick the most-specific + * known extractor. If no extractor's `fingerprintKeys` (treated as + * prefixes) match any observed key, return a `kind: 'unknown'` result + * that the extension layer surfaces as a "schema unknown" toast. + * + * Matching strategy: + * - For each extractor, count the observed keys whose value starts with + * any of the extractor's `fingerprintKeys`. + * - The extractor with the highest match count wins. + * - Ties are broken by registry order — first listed wins (newest-first). + */ +export function pickExtractor( + observedKeys: readonly string[], +): FingerprintResult { + let best: { extractor: ChatHistoryExtractor; matches: string[] } | undefined; + + for (const extractor of ALL_EXTRACTORS) { + const matches = observedKeys.filter((k) => + extractor.fingerprintKeys.some((fp) => k.startsWith(fp)), + ); + if (matches.length === 0) continue; + if (!best || matches.length > best.matches.length) { + best = { extractor, matches }; + } + } + + if (best) { + return { + kind: 'known', + extractor: best.extractor, + matchedKeys: best.matches, + }; + } + + return { + kind: 'unknown', + observedKeyCount: observedKeys.length, + observedSampleKeys: observedKeys.slice(0, 5), + }; +} + +export { + cursorComposerBubble, + cursorV2024Q4, + cursorV2025Q1, + cursorV2025Q2, + windsurf, +}; diff --git a/src/ext-vscode/src/extractors/windsurf.test.ts b/src/ext-vscode/src/extractors/windsurf.test.ts new file mode 100644 index 00000000..9fe2c9d6 --- /dev/null +++ b/src/ext-vscode/src/extractors/windsurf.test.ts @@ -0,0 +1,51 @@ +import { describe, it, expect } from 'vitest'; +import { windsurf, decodeWindsurfJsonFile } from './windsurf.js'; + +describe('windsurf extractor (placeholder)', () => { + it('has the expected id, label, and fingerprint', () => { + expect(windsurf.id).toBe('windsurf'); + expect(windsurf.label).toContain('Cascade'); + expect(windsurf.fingerprintKeys).toEqual(['cascade.']); + }); + + it('matches cascade.* keys', () => { + expect(windsurf.ownsKey('cascade.history')).toBe(true); + expect(windsurf.ownsKey('cascade.something.else')).toBe(true); + }); + + it('does not match non-cascade keys', () => { + expect(windsurf.ownsKey('aiService.prompts')).toBe(false); + expect(windsurf.ownsKey('cursorAIChatService.chatHistory.tab-1')).toBe(false); + }); + + it('returns [] for any row (placeholder — real decoding lands in Branch 4)', () => { + expect(windsurf.decodeRow({ key: 'cascade.history', value: '[]' }, '/p')).toEqual([]); + expect(windsurf.decodeRow({ key: 'cascade.x', value: 'anything' }, '/p')).toEqual([]); + }); +}); + +describe('decodeWindsurfJsonFile (Drift A fix — JSON-file decoder contract)', () => { + // The body is currently a no-op pending live-install schema identification + // (see TODO in src/extractors/windsurf.ts). These tests lock the CONTRACT + // — return type + signature — so when the engineer fills in the real + // decoder, the watcher's expectations don't have to change. If/when the + // decoder gains real behaviour, ADD new tests; don't remove these. + + it('returns [] for an empty object (current stub behaviour)', () => { + expect(decodeWindsurfJsonFile({}, '/p/conv-1.json')).toEqual([]); + }); + + it('returns [] for null / undefined / non-object parsed values', () => { + expect(decodeWindsurfJsonFile(null, '/p/f.json')).toEqual([]); + expect(decodeWindsurfJsonFile(undefined, '/p/f.json')).toEqual([]); + expect(decodeWindsurfJsonFile('a string', '/p/f.json')).toEqual([]); + expect(decodeWindsurfJsonFile(42, '/p/f.json')).toEqual([]); + }); + + it('returns an array regardless of input shape (never throws on the stub path)', () => { + expect(Array.isArray(decodeWindsurfJsonFile({ conversation: [] }, '/p'))).toBe(true); + expect( + Array.isArray(decodeWindsurfJsonFile({ messages: [{ role: 'user', content: 'hi' }] }, '/p')), + ).toBe(true); + }); +}); diff --git a/src/ext-vscode/src/extractors/windsurf.ts b/src/ext-vscode/src/extractors/windsurf.ts new file mode 100644 index 00000000..ed420579 --- /dev/null +++ b/src/ext-vscode/src/extractors/windsurf.ts @@ -0,0 +1,79 @@ +import type { + ChatHistoryEvent, + ChatHistoryExtractor, + ItemTableRow, +} from '../chat-history-types.js'; + +/** + * Windsurf (Codeium Cascade). + * + * FINDING (2026-05-28, live Windsurf 2.0 / SWE-1.6 install on Linux): + * Cascade prompt capture via file-watching is INFEASIBLE, and Windsurf is + * therefore OUT OF SCOPE for v1 capture. Evidence from a live "what is 2 + 2" + * conversation: + * - Conversation CONTENT (the user's prompt + the reply) is written to + * `~/.codeium/windsurf/cascade/.pb`, ENCRYPTED at rest: + * Shannon entropy 8.00 bits/byte, not gzip/zlib/deflate, no recoverable + * strings, prompt text absent in plaintext or base64. + * - The only plaintext is session METADATA in the host's + * `globalStorage/state.vscdb` under `windsurf.acp.metadataCache` + * (`sessionId`, an LLM-generated `title`, `cwd`, timestamps, `status`) + * — NOT the raw prompt. A title is an LLM summary, one per session, with + * no per-prompt granularity; useless as Layer C input. + * - `state.vscdb` (workspaceStorage) holds zero chat content; + * `chat.ChatSessionStore.index` is empty. + * + * There is no readable prompt on disk for any decoder to consume. The + * `windsurf-dir` watch + the no-op decoders below are kept inert (host + * detection + watch wiring stay so a future non-encrypted path could slot in) + * but they will never emit on Windsurf 2.0. Re-capturing Windsurf would need a + * non-file-watching route (Codeium MCP/command/API, or decrypting the `.pb` + * store — high effort, brittle, ToS-sensitive). See architecture doc §5.3. + * + * `decodeRow` is a fingerprint stub for the (unobserved) case where a future + * Windsurf migrates chat to a VS Code-style ItemTable with `cascade.*` keys. + */ + +const CASCADE_KEY_PREFIX = 'cascade.'; + +export const windsurf: ChatHistoryExtractor = { + id: 'windsurf', + label: 'Windsurf (Cascade)', + fingerprintKeys: [CASCADE_KEY_PREFIX] as const, + + ownsKey(key: string): boolean { + return key.startsWith(CASCADE_KEY_PREFIX); + }, + + decodeRow(_row: ItemTableRow, _sourcePath: string): ChatHistoryEvent[] { + return []; + }, +}; + +/** + * Decode a single parsed Windsurf JSON file into zero or more chat events. + * + * Contract: + * - `parsed` is whatever `JSON.parse(fileContents)` returned. Callers + * handle malformed files BEFORE calling this — the function may assume + * a parseable value (still `unknown` because the shape is host-defined). + * - `sourcePath` is the absolute path to the .json file (forwarded into + * the `ChatHistoryEvent.sourcePath` field so dedup + diagnostics stay + * stable across re-reads of the same file). + * - Returns events with `extractorId === 'windsurf'`. `capturedAt` is + * left as `new Date(0)` here; the watcher overwrites it via its + * injected clock so test ordering stays deterministic. + * + * RESOLVED — do NOT try to implement this. The 2026-05-28 live inspection + * (see the module docstring above) showed Cascade conversations are stored + * encrypted (`~/.codeium/windsurf/cascade/.pb`, entropy 8.00) and + * the scanner's premise (plaintext top-level `*.json`) is wrong. No plaintext + * prompt exists on disk, so this stays a no-op and Windsurf capture is out of + * v1 scope. Kept only so the `windsurf-dir` watch dispatch has a target. + */ +export function decodeWindsurfJsonFile( + _parsed: unknown, + _sourcePath: string, +): ChatHistoryEvent[] { + return []; +} diff --git a/src/ext-vscode/src/host-detector.test.ts b/src/ext-vscode/src/host-detector.test.ts new file mode 100644 index 00000000..a8045eac --- /dev/null +++ b/src/ext-vscode/src/host-detector.test.ts @@ -0,0 +1,123 @@ +import { describe, it, expect, vi } from 'vitest'; + +vi.mock('vscode', () => ({ + env: { appName: 'Visual Studio Code' }, +})); + +import { + classifyHost, + detectHost, + chatHistoryBaseDir, + workspaceStorageDir, + windsurfCodeiumDir, +} from './host-detector.js'; + +describe('classifyHost', () => { + it('maps "Cursor" → cursor', () => { + expect(classifyHost('Cursor')).toBe('cursor'); + expect(classifyHost('Cursor 3.4.20')).toBe('cursor'); + }); + + it('maps "Windsurf" → windsurf', () => { + expect(classifyHost('Windsurf')).toBe('windsurf'); + expect(classifyHost('Windsurf 1.0')).toBe('windsurf'); + }); + + it('maps Visual Studio Code / Code / unknown → vscode-generic', () => { + expect(classifyHost('Visual Studio Code')).toBe('vscode-generic'); + expect(classifyHost('Code')).toBe('vscode-generic'); + expect(classifyHost('Code - Insiders')).toBe('vscode-generic'); + expect(classifyHost('Something Else')).toBe('vscode-generic'); + expect(classifyHost('')).toBe('vscode-generic'); + }); + + it('is case-insensitive', () => { + expect(classifyHost('CURSOR')).toBe('cursor'); + expect(classifyHost('cursor')).toBe('cursor'); + expect(classifyHost('WINDSURF')).toBe('windsurf'); + }); +}); + +describe('detectHost', () => { + it('reads from injected appName when provided', () => { + expect(detectHost({ appName: 'Cursor' })).toBe('cursor'); + expect(detectHost({ appName: 'Windsurf' })).toBe('windsurf'); + expect(detectHost({ appName: 'Visual Studio Code' })).toBe('vscode-generic'); + }); + + it('falls back to vscode.env.appName when no override (mocked: VS Code)', () => { + expect(detectHost()).toBe('vscode-generic'); + }); +}); + +describe('chatHistoryBaseDir', () => { + it('returns null for vscode-generic (no chat-history watching)', () => { + expect( + chatHistoryBaseDir({ host: 'vscode-generic', home: '/home/u', platform: 'linux' }), + ).toBeNull(); + }); + + it('cursor on linux → ~/.config/Cursor', () => { + expect( + chatHistoryBaseDir({ host: 'cursor', home: '/home/u', platform: 'linux' }), + ).toBe('/home/u/.config/Cursor'); + }); + + it('cursor on darwin → Library/Application Support', () => { + expect( + chatHistoryBaseDir({ host: 'cursor', home: '/Users/u', platform: 'darwin' }), + ).toBe('/Users/u/Library/Application Support/Cursor'); + }); + + it('cursor on win32 with APPDATA → uses it', () => { + expect( + chatHistoryBaseDir({ + host: 'cursor', + home: 'C:\\Users\\u', + platform: 'win32', + appdata: 'C:\\Users\\u\\AppData\\Roaming', + }), + ).toBe('C:\\Users\\u\\AppData\\Roaming/Cursor'); + }); + + it('windsurf paths mirror cursor paths', () => { + expect( + chatHistoryBaseDir({ host: 'windsurf', home: '/home/u', platform: 'linux' }), + ).toBe('/home/u/.config/Windsurf'); + expect( + chatHistoryBaseDir({ host: 'windsurf', home: '/Users/u', platform: 'darwin' }), + ).toBe('/Users/u/Library/Application Support/Windsurf'); + }); +}); + +describe('workspaceStorageDir', () => { + it('appends User/workspaceStorage to the host base dir', () => { + expect( + workspaceStorageDir({ host: 'cursor', home: '/home/u', platform: 'linux' }), + ).toBe('/home/u/.config/Cursor/User/workspaceStorage'); + }); + + it('returns null for vscode-generic (passes the base null through)', () => { + expect(workspaceStorageDir({ host: 'vscode-generic' })).toBeNull(); + }); +}); + +describe('windsurfCodeiumDir', () => { + it('joins ~/.codeium/windsurf for an explicit home', () => { + expect(windsurfCodeiumDir('/home/u')).toBe('/home/u/.codeium/windsurf'); + }); + + it('matches the windsurfAdapter codeiumCascadeDir convention (cross-file invariant)', () => { + // The CLI-side adapter at src/agents/adapters/windsurf.ts uses + // join(home, '.codeium', 'windsurf'). Locking the same shape here so + // a future rename on either side stays obvious. + expect(windsurfCodeiumDir('/Users/u')).toBe('/Users/u/.codeium/windsurf'); + expect(windsurfCodeiumDir('C:\\Users\\u')).toBe('C:\\Users\\u/.codeium/windsurf'); + }); + + it('defaults to os.homedir() when no argument provided', () => { + // Don't pin a literal value (runner-dependent); just assert structure. + const result = windsurfCodeiumDir(); + expect(result.endsWith('/.codeium/windsurf')).toBe(true); + }); +}); diff --git a/src/ext-vscode/src/host-detector.ts b/src/ext-vscode/src/host-detector.ts new file mode 100644 index 00000000..1d2899dd --- /dev/null +++ b/src/ext-vscode/src/host-detector.ts @@ -0,0 +1,112 @@ +import * as vscode from 'vscode'; +import { homedir, platform as osPlatform } from 'node:os'; +import { join } from 'node:path'; + +/** + * Host detection (B4 wiring). + * + * The same VS Code extension ships to Cursor, Windsurf, and plain VS Code + * via Open VSX. At runtime the extension needs to know which host it's + * inside so it can look up the correct `state.vscdb` paths (and, in + * Branch 4 of the prompt-injection work, which `vscode.commands.*` id to + * try for chat-input injection). + * + * VS Code's `vscode.env.appName` reports a host-specific string. Empirical + * values across host versions: + * - Plain VS Code: "Visual Studio Code" / "Code" + * - Cursor: "Cursor" + * - Windsurf: "Windsurf" + * + * We treat anything we don't recognise as a generic VS Code host (no + * special chat-history handling). + */ + +export type Host = 'cursor' | 'windsurf' | 'vscode-generic'; + +/** Pure mapping from `appName` to a recognised host id. */ +export function classifyHost(appName: string): Host { + const normalised = appName.toLowerCase(); + if (normalised.includes('cursor')) return 'cursor'; + if (normalised.includes('windsurf')) return 'windsurf'; + return 'vscode-generic'; +} + +/** + * Read the current host from the live `vscode.env.appName`. Injectable for + * tests via `deps.appName`. + */ +export function detectHost(deps: { appName?: string } = {}): Host { + return classifyHost(deps.appName ?? vscode.env.appName); +} + +/** + * Per-host base directories where `state.vscdb` is expected to live. + * + * Mirrors the CLI-side adapter logic in `src/agents/adapters/cursor.ts` + * and `src/agents/adapters/windsurf.ts` — the same OS rules apply, but + * the extension can resolve them itself without going through the + * adapter (extension and CLI are different processes). + */ +export interface HostStorageInputs { + host?: Host; + home?: string; + platform?: NodeJS.Platform; + appdata?: string; +} + +export function chatHistoryBaseDir(inputs: HostStorageInputs = {}): string | null { + const host = inputs.host ?? detectHost(); + const home = inputs.home ?? homedir(); + const platform = inputs.platform ?? osPlatform(); + + if (host === 'cursor') { + return hostConfigDir('Cursor', home, platform, inputs.appdata); + } + if (host === 'windsurf') { + return hostConfigDir('Windsurf', home, platform, inputs.appdata); + } + // Plain VS Code host: no chat-history watching (no AI chat panel to read). + return null; +} + +function hostConfigDir( + hostName: string, + home: string, + platform: NodeJS.Platform, + appdata?: string, +): string { + switch (platform) { + case 'darwin': + return join(home, 'Library', 'Application Support', hostName); + case 'win32': + return join( + appdata ?? process.env.APPDATA ?? join(home, 'AppData', 'Roaming'), + hostName, + ); + default: + return join(home, '.config', hostName); + } +} + +/** Returns the `User/workspaceStorage` directory (parent of per-workspace state.vscdb files). */ +export function workspaceStorageDir(inputs: HostStorageInputs = {}): string | null { + const base = chatHistoryBaseDir(inputs); + return base === null ? null : join(base, 'User', 'workspaceStorage'); +} + +/** + * Windsurf's legacy Codeium Cascade chat-data directory: `~/.codeium/windsurf`. + * + * Per dev plan §2.3 acceptance #2, the watcher monitors BOTH `state.vscdb` + * (under `workspaceStorageDir`) AND this directory for Windsurf. The + * directory holds per-session JSON files; the watcher dispatches it as a + * `windsurf-dir` `WatchTarget`. + * + * Mirrors `codeiumCascadeDir` in `src/agents/adapters/windsurf.ts`. Kept as + * a local copy because the sub-package's tsconfig rootDir prevents importing + * from the root `src/agents/` tree. The `marketplace-id.test.ts` pattern is + * the model for catching cross-file drift if either side moves. + */ +export function windsurfCodeiumDir(home: string = homedir()): string { + return join(home, '.codeium', 'windsurf'); +} diff --git a/src/ext-vscode/src/ipc.test.ts b/src/ext-vscode/src/ipc.test.ts new file mode 100644 index 00000000..d3e9c31d --- /dev/null +++ b/src/ext-vscode/src/ipc.test.ts @@ -0,0 +1,313 @@ +import { describe, it, expect, vi } from 'vitest'; +import { EventEmitter } from 'node:events'; +import { Readable, Writable } from 'node:stream'; +import { + spawnAuto, + spawnStop, + resolveSpawnEnv, + NexpathBinaryNotFoundError, + NexpathMalformedPayloadError, +} from './ipc.js'; + +interface FakeChildOptions { + stdoutChunks?: string[]; + stderrChunks?: string[]; + exitCode?: number; + errorBeforeClose?: Error; +} + +function makeFakeChild(opts: FakeChildOptions = {}) { + const child = new EventEmitter() as EventEmitter & { + stdin: Writable; + stdout: Readable; + stderr: Readable; + }; + child.stdin = new Writable({ + write(_chunk, _enc, cb) { + cb(); + }, + }); + child.stdout = new Readable({ read() {} }); + child.stderr = new Readable({ read() {} }); + + // `emit('data', ...)` synchronously invokes any registered listeners on the + // stream, which is what we need so the consumer's data handler has accumulated + // its buffer before we emit 'close'. `push()` buffers asynchronously and + // would deliver the data after 'close' fires. + queueMicrotask(() => { + if (opts.errorBeforeClose) { + child.emit('error', opts.errorBeforeClose); + return; + } + for (const c of opts.stdoutChunks ?? []) child.stdout.emit('data', Buffer.from(c)); + for (const c of opts.stderrChunks ?? []) child.stderr.emit('data', Buffer.from(c)); + child.emit('close', opts.exitCode ?? 0); + }); + + return child; +} + +describe('spawnAuto', () => { + it('resolves when the process exits with code 0', async () => { + const spawnFn = vi.fn(() => makeFakeChild({ exitCode: 0 })); + await expect( + spawnAuto('hi', 'sess-1', { spawnFn: spawnFn as never }), + ).resolves.toBeUndefined(); + expect(spawnFn).toHaveBeenCalledWith( + 'nexpath', + ['auto'], + expect.any(Object), + ); + }); + + it('includes --db argument when dbPath is provided', async () => { + const spawnFn = vi.fn(() => makeFakeChild({ exitCode: 0 })); + await spawnAuto('p', 's', { spawnFn: spawnFn as never, dbPath: '/tmp/x.db' }); + expect(spawnFn).toHaveBeenCalledWith( + 'nexpath', + ['auto', '--db', '/tmp/x.db'], + expect.any(Object), + ); + }); + + it('uses binaryPath override over default "nexpath"', async () => { + const spawnFn = vi.fn(() => makeFakeChild({ exitCode: 0 })); + await spawnAuto('p', 's', { + spawnFn: spawnFn as never, + binaryPath: '/usr/local/bin/nexpath', + }); + expect(spawnFn).toHaveBeenCalledWith( + '/usr/local/bin/nexpath', + ['auto'], + expect.any(Object), + ); + }); + + it('passes opts.cwd as the spawned process cwd (so Layer C resolves --project correctly)', async () => { + const spawnFn = vi.fn(() => makeFakeChild({ exitCode: 0 })); + await spawnAuto('p', 's', { + spawnFn: spawnFn as never, + cwd: '/some/workspace/path', + }); + const opts = spawnFn.mock.calls[0]![2] as { cwd?: string }; + expect(opts.cwd).toBe('/some/workspace/path'); + }); + + it('defaults the spawned process cwd to process.cwd() when opts.cwd is omitted', async () => { + const spawnFn = vi.fn(() => makeFakeChild({ exitCode: 0 })); + await spawnAuto('p', 's', { spawnFn: spawnFn as never }); + const opts = spawnFn.mock.calls[0]![2] as { cwd?: string }; + expect(opts.cwd).toBe(process.cwd()); + }); + + it('rejects with NexpathBinaryNotFoundError when the child emits error', async () => { + const enoent = Object.assign(new Error('spawn nexpath ENOENT'), { + code: 'ENOENT', + }); + const spawnFn = vi.fn(() => makeFakeChild({ errorBeforeClose: enoent })); + await expect( + spawnAuto('p', 's', { spawnFn: spawnFn as never }), + ).rejects.toBeInstanceOf(NexpathBinaryNotFoundError); + }); + + it('rejects with a clear message when the process exits non-zero', async () => { + const spawnFn = vi.fn(() => + makeFakeChild({ exitCode: 1, stderrChunks: ['bad stuff\n'] }), + ); + await expect( + spawnAuto('p', 's', { spawnFn: spawnFn as never }), + ).rejects.toThrow(/exited with code 1/); + }); +}); + +describe('spawnStop', () => { + it('returns the selected prompt from Layer C\'s {decision:"block", reason} output', async () => { + // This is the ONLY shape Layer C's `nexpath stop` writes to stdout — on + // selection in the terminal popup. `reason` is the chosen option text. + const out = { decision: 'block', reason: 'Run the full test suite for this phase.' }; + const spawnFn = vi.fn(() => + makeFakeChild({ + stdoutChunks: [JSON.stringify(out)], + exitCode: 0, + }), + ); + const result = await spawnStop('s', { spawnFn: spawnFn as never }); + expect(result).toEqual({ selectedPrompt: 'Run the full test suite for this phase.' }); + }); + + it('resolves null when stdout is valid JSON but not the selection shape', async () => { + // Any non-{decision:block} JSON carries no actionable selection. + const spawnFn = vi.fn(() => + makeFakeChild({ stdoutChunks: [JSON.stringify({ decision: 'approve' })], exitCode: 0 }), + ); + const result = await spawnStop('s', { spawnFn: spawnFn as never }); + expect(result).toBeNull(); + }); + + it('resolves null when {decision:"block"} has an empty reason', async () => { + const spawnFn = vi.fn(() => + makeFakeChild({ stdoutChunks: [JSON.stringify({ decision: 'block', reason: '' })], exitCode: 0 }), + ); + const result = await spawnStop('s', { spawnFn: spawnFn as never }); + expect(result).toBeNull(); + }); + + it('resolves null when stdout is empty (no selection / no advisory)', async () => { + const spawnFn = vi.fn(() => makeFakeChild({ stdoutChunks: [], exitCode: 0 })); + const result = await spawnStop('s', { spawnFn: spawnFn as never }); + expect(result).toBeNull(); + }); + + it('resolves null when stdout is only whitespace', async () => { + const spawnFn = vi.fn(() => + makeFakeChild({ stdoutChunks: [' \n \t '], exitCode: 0 }), + ); + const result = await spawnStop('s', { spawnFn: spawnFn as never }); + expect(result).toBeNull(); + }); + + it('rejects with NexpathMalformedPayloadError when stdout is non-empty but not JSON', async () => { + const spawnFn = vi.fn(() => + makeFakeChild({ stdoutChunks: ['not json {{'], exitCode: 0 }), + ); + await expect( + spawnStop('s', { spawnFn: spawnFn as never }), + ).rejects.toBeInstanceOf(NexpathMalformedPayloadError); + }); + + it('rejects when nexpath stop exits non-zero AND nothing can be recovered', async () => { + const spawnFn = vi.fn(() => + makeFakeChild({ exitCode: 2, stderrChunks: ['boom\n'] }), + ); + await expect( + spawnStop('s', { spawnFn: spawnFn as never, recoverSelection: async () => null }), + ).rejects.toThrow(/exited with code 2/); + }); + + it('recovers the selection from the store when stop crashes on exit (Windows libuv)', async () => { + // 3221226505 = 0xC0000409 — the Windows fail-fast code from stop.ts process.exit. + const spawnFn = vi.fn(() => + makeFakeChild({ exitCode: 3221226505, stdoutChunks: [], stderrChunks: ['[nexpath] Prompt sent to Claude\n'] }), + ); + const result = await spawnStop('s', { + spawnFn: spawnFn as never, + cwd: '/proj', + recoverSelection: async (cwd) => (cwd === '/proj' ? 'Run the test suite for this phase.' : null), + }); + expect(result).toEqual({ selectedPrompt: 'Run the test suite for this phase.' }); + }); + + it('prefers a stdout selection even on a non-zero exit (stdout survived the crash)', async () => { + const out = { decision: 'block', reason: 'Cross-confirm the spec first.' }; + const spawnFn = vi.fn(() => + makeFakeChild({ exitCode: 3221226505, stdoutChunks: [JSON.stringify(out)] }), + ); + const recover = vi.fn(async () => 'should-not-be-used'); + const result = await spawnStop('s', { spawnFn: spawnFn as never, recoverSelection: recover }); + expect(result).toEqual({ selectedPrompt: 'Cross-confirm the spec first.' }); + expect(recover).not.toHaveBeenCalled(); + }); + + it('non-zero exit with no recovery + no stdout → rejects', async () => { + const spawnFn = vi.fn(() => makeFakeChild({ exitCode: 1, stdoutChunks: [] })); + await expect( + spawnStop('s', { spawnFn: spawnFn as never, recoverSelection: async () => null }), + ).rejects.toThrow(/exited with code 1/); + }); + + it('rejects with NexpathBinaryNotFoundError when spawn emits error', async () => { + const spawnFn = vi.fn(() => + makeFakeChild({ errorBeforeClose: new Error('nope') }), + ); + await expect( + spawnStop('s', { spawnFn: spawnFn as never }), + ).rejects.toBeInstanceOf(NexpathBinaryNotFoundError); + }); + + // ── Stdin payload contract — must match Layer C's StopPayload shape ────────── + // `src/cli/commands/stop.ts` lines 37-43 declare StopPayload requires + // `cwd`, `hook_event_name`, and `stop_hook_active` in addition to the + // optional `session_id`. We must send the full shape so Layer C's + // `runStop` works correctly. + + it('writes the full StopPayload shape to stdin (session_id + cwd + hook_event_name + stop_hook_active)', async () => { + let captured = ''; + const spawnFn = vi.fn(() => { + const c = makeFakeChild({ stdoutChunks: [''], exitCode: 0 }); + const origEnd = c.stdin.end.bind(c.stdin); + c.stdin.end = ((chunk: unknown, ...args: unknown[]) => { + if (chunk !== undefined) captured = String(chunk); + return origEnd(chunk as never, ...(args as never[])); + }) as typeof c.stdin.end; + return c; + }); + await spawnStop('sess-X', { + spawnFn: spawnFn as never, + cwd: '/workspace/abc', + }); + const parsed = JSON.parse(captured.trim()); + expect(parsed).toEqual({ + session_id: 'sess-X', + cwd: '/workspace/abc', + hook_event_name: 'Stop', + stop_hook_active: false, + }); + }); + + it('defaults the stdin cwd to process.cwd() when opts.cwd is omitted', async () => { + let captured = ''; + const spawnFn = vi.fn(() => { + const c = makeFakeChild({ stdoutChunks: [''], exitCode: 0 }); + const origEnd = c.stdin.end.bind(c.stdin); + c.stdin.end = ((chunk: unknown, ...args: unknown[]) => { + if (chunk !== undefined) captured = String(chunk); + return origEnd(chunk as never, ...(args as never[])); + }) as typeof c.stdin.end; + return c; + }); + await spawnStop('sess-Y', { spawnFn: spawnFn as never }); + const parsed = JSON.parse(captured.trim()); + expect(parsed.cwd).toBe(process.cwd()); + expect(parsed.hook_event_name).toBe('Stop'); + expect(parsed.stop_hook_active).toBe(false); + }); + + it('passes opts.cwd as the spawned process cwd', async () => { + const spawnFn = vi.fn(() => + makeFakeChild({ stdoutChunks: [''], exitCode: 0 }), + ); + await spawnStop('s', { spawnFn: spawnFn as never, cwd: '/spawn/cwd' }); + const opts = spawnFn.mock.calls[0]![2] as { cwd?: string }; + expect(opts.cwd).toBe('/spawn/cwd'); + }); +}); + +describe('resolveSpawnEnv — DBus session bus restoration', () => { + it('injects the standard per-user bus when DBUS_SESSION_BUS_ADDRESS is absent and the socket exists', () => { + const env = resolveSpawnEnv({ + env: { PATH: '/usr/bin' }, + getuid: () => 1000, + existsSync: (p) => p === '/run/user/1000/bus', + }); + expect(env.DBUS_SESSION_BUS_ADDRESS).toBe('unix:path=/run/user/1000/bus'); + expect(env.PATH).toBe('/usr/bin'); // existing vars preserved + }); + + it('never overrides an address that is already set', () => { + const env = resolveSpawnEnv({ + env: { DBUS_SESSION_BUS_ADDRESS: 'unix:path=/already/here' }, + getuid: () => 1000, + existsSync: () => true, + }); + expect(env.DBUS_SESSION_BUS_ADDRESS).toBe('unix:path=/already/here'); + }); + + it('leaves env unchanged when the bus socket does not exist', () => { + const env = resolveSpawnEnv({ + env: { PATH: '/usr/bin' }, + getuid: () => 1000, + existsSync: () => false, + }); + expect(env.DBUS_SESSION_BUS_ADDRESS).toBeUndefined(); + }); +}); diff --git a/src/ext-vscode/src/ipc.ts b/src/ext-vscode/src/ipc.ts new file mode 100644 index 00000000..cb4f1a52 --- /dev/null +++ b/src/ext-vscode/src/ipc.ts @@ -0,0 +1,335 @@ +import { spawn } from 'node:child_process'; +import type { SpawnOptions } from 'node:child_process'; +import { existsSync } from 'node:fs'; +import { bringPopupToFront } from './popup-foreground.js'; +import { readInjectedPrompt } from './advisory-store-reader.js'; + +/** + * IPC layer between the VS Code extension (Layer B) and the existing nexpath + * CLI pipeline (Layer C). The extension never imports from Layer C directly; + * instead it spawns `nexpath auto` and `nexpath stop` as subprocesses and + * communicates via stdin/stdout JSON envelopes. + * + * This module is a Branch 1 STUB — it defines the spawn + parse contract and + * the error taxonomy. The actual chat-history watcher (Branch 2) and webview + * payload renderer (Branch 3) call into this module; the Cursor / Windsurf + * adapters (Branch 4) supply concrete binary-path resolution. + * + * Binary path resolution order (highest priority first): + * 1. `opts.binaryPath` — explicit override (test fixtures, dev setups) + * 2. `process.env.NEXPATH_BIN` — for users who install via a non-standard PATH + * 3. `'nexpath'` — fall back to PATH lookup + */ + +export interface DecisionSessionPayload { + /** Advisory text to show in the webview / notification. */ + advisory: string; + /** Selectable options the user may pick. */ + options: Array<{ id: string; label: string }>; +} + +/** + * The result of `nexpath stop` when the user picked an option in Layer C's + * terminal popup. Layer C emits `{ decision: 'block', reason: