From b36b81087d809014475422808d6f20d23f5567f5 Mon Sep 17 00:00:00 2001 From: Aditya Mathur <57684218+MathurAditya724@users.noreply.github.com> Date: Wed, 29 Apr 2026 20:39:41 +0000 Subject: [PATCH 01/46] feat(local): add command to run a local Spotlight sidecar Adds 'sentry local', a long-running command that starts a minimal Hono HTTP server wire-compatible with the Spotlight sidecar protocol. The server uses @spotlightjs/spotlight/sdk's createSpotlightBuffer + pushToSpotlightBuffer helpers to ingest envelopes from any Sentry SDK running in the user's dev stack and tails them to the terminal. Endpoints exposed: POST /stream - Spotlight ingest POST /api/{projectId}/envelope/ - Sentry SDK ingest path GET /stream - SSE feed for the Spotlight overlay GET /health - liveness check Why a thin in-tree server instead of spawning npx @spotlightjs/spotlight: the SDK helpers give us decompression + lazy parsing for free while keeping the surface focused on a CLI-friendly tail UX, and bundling through esbuild keeps the published binary self-contained per the no-runtime-dependencies rule. The command runs without auth (it's a local dev tool) and shuts down gracefully on SIGINT/SIGTERM, force-closing keep-alive connections so SSE subscribers don't block exit. --- bun.lock | 323 ++++++++++++--- docs/src/content/docs/contributing.md | 1 + docs/src/fragments/commands/local.md | 50 +++ package.json | 3 + plugins/sentry-cli/skills/sentry-cli/SKILL.md | 8 + .../skills/sentry-cli/references/local.md | 42 ++ src/app.ts | 2 + src/commands/local.ts | 369 ++++++++++++++++++ 8 files changed, 754 insertions(+), 44 deletions(-) create mode 100644 docs/src/fragments/commands/local.md create mode 100644 plugins/sentry-cli/skills/sentry-cli/references/local.md create mode 100644 src/commands/local.ts diff --git a/bun.lock b/bun.lock index ead0d2419..ff961af11 100644 --- a/bun.lock +++ b/bun.lock @@ -8,10 +8,12 @@ "@anthropic-ai/sdk": "^0.39.0", "@biomejs/biome": "2.3.8", "@clack/prompts": "^0.11.0", + "@hono/node-server": "^2.0.0", "@mastra/client-js": "^1.4.0", "@sentry/api": "^0.113.0", "@sentry/node-core": "10.50.0", "@sentry/sqlish": "^1.0.0", + "@spotlightjs/spotlight": "^4.11.3", "@stricli/auto-complete": "^1.2.4", "@stricli/core": "^1.2.4", "@types/bun": "latest", @@ -26,6 +28,7 @@ "consola": "^3.4.2", "esbuild": "^0.25.0", "fast-check": "^4.5.3", + "hono": "^4.12.15", "http-cache-semantics": "^4.2.0", "ignore": "^7.0.5", "marked": "^15", @@ -143,7 +146,11 @@ "@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.12", "", { "os": "win32", "cpu": "x64" }, "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA=="], - "@hono/node-server": ["@hono/node-server@1.19.9", "", { "peerDependencies": { "hono": "^4" } }, "sha512-vHL6w3ecZsky+8P5MD+eFfaGTyCeOHUIFYMGpQGbrBTSmNNoxv0if69rEZ5giu36weC5saFuznL411gRX7bJDw=="], + "@fastify/otel": ["@fastify/otel@0.18.0", "", { "dependencies": { "@opentelemetry/core": "^2.0.0", "@opentelemetry/instrumentation": "^0.212.0", "@opentelemetry/semantic-conventions": "^1.28.0", "minimatch": "^10.2.4" }, "peerDependencies": { "@opentelemetry/api": "^1.9.0" } }, "sha512-3TASCATfw+ctICSb4ymrv7iCm0qJ0N9CarB+CZ7zIJ7KqNbwI5JjyDL1/sxoC0ccTO1Zyd1iQ+oqncPg5FJXaA=="], + + "@hono/mcp": ["@hono/mcp@0.2.5", "", { "dependencies": { "pkce-challenge": "^5.0.0" }, "peerDependencies": { "@modelcontextprotocol/sdk": "^1.25.1", "hono": "*", "hono-rate-limiter": "^0.4.2", "zod": "^3.25.0 || ^4.0.0" } }, "sha512-JsaJes7VlNvUrUQ9j2b9C13xjFLvyKQY515aWtsdJ9cwhBmWz5od2yUCbDu7cX38GeADmlLmpu4BKNNAV6G27w=="], + + "@hono/node-server": ["@hono/node-server@2.0.0", "", { "peerDependencies": { "hono": "^4" } }, "sha512-n3GfHwwCvHCkGmOwKfxUPOlbfzuO64Sbc5XC4NGPIXxkuOnJrdgExdRKmHfF924r914WRJPT397GdqLvdYTeyQ=="], "@isaacs/balanced-match": ["@isaacs/balanced-match@4.0.1", "", {}, "sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ=="], @@ -151,6 +158,12 @@ "@isaacs/ttlcache": ["@isaacs/ttlcache@2.1.4", "", {}, "sha512-7kMz0BJpMvgAMkyglums7B2vtrn5g0a0am77JY0GjkZZNetOBCFn7AG7gKCwT0QPiXyxW7YIQSgtARknUEOcxQ=="], + "@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="], + + "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="], + + "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], + "@lukeed/csprng": ["@lukeed/csprng@1.1.0", "", {}, "sha512-Z7C/xXCiGWsg0KuKsHTKJxbWhpI3Vs5GwLfOean7MGyVFGqdRgBbAjOCh6u4bbjPc/8MJ2pZmK/0DLdCbivLDA=="], "@lukeed/uuid": ["@lukeed/uuid@2.0.1", "", { "dependencies": { "@lukeed/csprng": "^1.1.0" } }, "sha512-qC72D4+CDdjGqJvkFMMEAtancHUQ7/d/tAiHf64z8MopFDmcrtbcJuerDtFceuAfQJ2pDSfCKCtbqoGBNnwg0w=="], @@ -165,20 +178,72 @@ "@opentelemetry/api": ["@opentelemetry/api@1.9.0", "", {}, "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg=="], + "@opentelemetry/api-logs": ["@opentelemetry/api-logs@0.214.0", "", { "dependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-40lSJeqYO8Uz2Yj7u94/SJWE/wONa7rmMKjI1ZcIjgf3MHNHv1OZUCrCETGuaRF62d5pQD1wKIW+L4lmSMTzZA=="], + "@opentelemetry/core": ["@opentelemetry/core@2.5.0", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-ka4H8OM6+DlUhSAZpONu0cPBtPPTQKxbxVzC4CzVx5+K4JnroJVBtDzLAMx4/3CDTJXRvVFhpFjtl4SaiTNoyQ=="], + "@opentelemetry/instrumentation": ["@opentelemetry/instrumentation@0.214.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.214.0", "import-in-the-middle": "^3.0.0", "require-in-the-middle": "^8.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-MHqEX5Dk59cqVah5LiARMACku7jXSVk9iVDWOea4x3cr7VfdByeDCURK6o1lntT1JS/Tsovw01UJrBhN3/uC5w=="], + + "@opentelemetry/instrumentation-amqplib": ["@opentelemetry/instrumentation-amqplib@0.61.0", "", { "dependencies": { "@opentelemetry/core": "^2.0.0", "@opentelemetry/instrumentation": "^0.214.0", "@opentelemetry/semantic-conventions": "^1.33.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-mCKoyTGfRNisge4br0NpOFSy2Z1NnEW8hbCJdUDdJFHrPqVzc4IIBPA/vX0U+LUcQqrQvJX+HMIU0dbDRe0i0Q=="], + + "@opentelemetry/instrumentation-connect": ["@opentelemetry/instrumentation-connect@0.57.0", "", { "dependencies": { "@opentelemetry/core": "^2.0.0", "@opentelemetry/instrumentation": "^0.214.0", "@opentelemetry/semantic-conventions": "^1.27.0", "@types/connect": "3.4.38" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-FMEBChnI4FLN5TE9DHwfH7QpNir1JzXno1uz/TAucVdLCyrG0jTrKIcNHt/i30A0M2AunNBCkcd8Ei26dIPKdg=="], + + "@opentelemetry/instrumentation-dataloader": ["@opentelemetry/instrumentation-dataloader@0.31.0", "", { "dependencies": { "@opentelemetry/instrumentation": "^0.214.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-f654tZFQXS5YeLDNb9KySrwtg7SnqZN119FauD7acBoTzuLduaiGTNz88ixcVSOOMGZ+EjJu/RFtx5klObC95g=="], + + "@opentelemetry/instrumentation-fs": ["@opentelemetry/instrumentation-fs@0.33.0", "", { "dependencies": { "@opentelemetry/core": "^2.0.0", "@opentelemetry/instrumentation": "^0.214.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-sCZWXGalQ01wr3tAhSR9ucqFJ0phidpAle6/17HVjD6gN8FLmZMK/8sKxdXYHy3PbnlV1P4zeiSVFNKpbFMNLA=="], + + "@opentelemetry/instrumentation-generic-pool": ["@opentelemetry/instrumentation-generic-pool@0.57.0", "", { "dependencies": { "@opentelemetry/instrumentation": "^0.214.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-orhmlaK+ZIW9hKU+nHTbXrCSXZcH83AescTqmpamHRobRmYSQwRbD0a1odc0yAzuzOtxYiHiXAnpnIpaSSY7Ow=="], + + "@opentelemetry/instrumentation-graphql": ["@opentelemetry/instrumentation-graphql@0.62.0", "", { "dependencies": { "@opentelemetry/instrumentation": "^0.214.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-3YNuLVPUxafXkH1jBAbGsKNsP3XVzcFDhCDCE3OqBwCwShlqQbLMRMFh1T/d5jaVZiGVmSsfof+ICKD2iOV8xg=="], + + "@opentelemetry/instrumentation-hapi": ["@opentelemetry/instrumentation-hapi@0.60.0", "", { "dependencies": { "@opentelemetry/core": "^2.0.0", "@opentelemetry/instrumentation": "^0.214.0", "@opentelemetry/semantic-conventions": "^1.27.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-aNljZKYrEa7obLAxd1bCEDxF7kzCLGXTuTJZ8lMR9rIVEjmuKBXN1gfqpm/OB//Zc2zP4iIve1jBp7sr3mQV6w=="], + + "@opentelemetry/instrumentation-http": ["@opentelemetry/instrumentation-http@0.214.0", "", { "dependencies": { "@opentelemetry/core": "2.6.1", "@opentelemetry/instrumentation": "0.214.0", "@opentelemetry/semantic-conventions": "^1.29.0", "forwarded-parse": "2.1.2" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-FlkDhZDRjDJDcO2LcSCtjRpkal1NJ8y0fBqBhTvfAR3JSYY2jAIj1kSS5IjmEBt4c3aWv+u/lqLuoCDrrKCSKg=="], + + "@opentelemetry/instrumentation-ioredis": ["@opentelemetry/instrumentation-ioredis@0.62.0", "", { "dependencies": { "@opentelemetry/instrumentation": "^0.214.0", "@opentelemetry/redis-common": "^0.38.2", "@opentelemetry/semantic-conventions": "^1.33.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-ZYt//zcPve8qklaZX+5Z4MkU7UpEkFRrxsf2cnaKYBitqDnsCN69CPAuuMOX6NYdW2rG9sFy7V/QWtBlP5XiNQ=="], + + "@opentelemetry/instrumentation-kafkajs": ["@opentelemetry/instrumentation-kafkajs@0.23.0", "", { "dependencies": { "@opentelemetry/instrumentation": "^0.214.0", "@opentelemetry/semantic-conventions": "^1.30.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-4K+nVo+zI+aDz0Z85SObwbdixIbzS9moIuKJaYsdlzcHYnKOPtB7ya8r8Ezivy/GVIBHiKJVq4tv+BEkgOMLaQ=="], + + "@opentelemetry/instrumentation-knex": ["@opentelemetry/instrumentation-knex@0.58.0", "", { "dependencies": { "@opentelemetry/instrumentation": "^0.214.0", "@opentelemetry/semantic-conventions": "^1.33.1" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-Hc/o8fSsaWxZ8r1Yw4rNDLwTpUopTf4X32y4W6UhlHmW8Wizz8wfhgOKIelSeqFVTKBBPIDUOsQWuIMxBmu8Bw=="], + + "@opentelemetry/instrumentation-koa": ["@opentelemetry/instrumentation-koa@0.62.0", "", { "dependencies": { "@opentelemetry/core": "^2.0.0", "@opentelemetry/instrumentation": "^0.214.0", "@opentelemetry/semantic-conventions": "^1.36.0" }, "peerDependencies": { "@opentelemetry/api": "^1.9.0" } }, "sha512-uVip0VuGUQXZ+vFxkKxAUNq8qNl+VFlyHDh/U6IQ8COOEDfbEchdaHnpFrMYF3psZRUuoSIgb7xOeXj00RdwDA=="], + + "@opentelemetry/instrumentation-lru-memoizer": ["@opentelemetry/instrumentation-lru-memoizer@0.58.0", "", { "dependencies": { "@opentelemetry/instrumentation": "^0.214.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-6grM3TdMyHzlGY1cUA+mwoPueB1F3dYKgKtZIH6jOFXqfHAByyLTc+6PFjGM9tKh52CFBJaDwodNlL/Td39z7Q=="], + + "@opentelemetry/instrumentation-mongodb": ["@opentelemetry/instrumentation-mongodb@0.67.0", "", { "dependencies": { "@opentelemetry/instrumentation": "^0.214.0", "@opentelemetry/semantic-conventions": "^1.33.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-1WJp5N1lYfHq2IhECOTewFs5Tf2NfUOwQRqs/rZdXKTezArMlucxgzAaqcgp3A3YREXopXTpXHsxZTGHjNhMdQ=="], + + "@opentelemetry/instrumentation-mongoose": ["@opentelemetry/instrumentation-mongoose@0.60.0", "", { "dependencies": { "@opentelemetry/core": "^2.0.0", "@opentelemetry/instrumentation": "^0.214.0", "@opentelemetry/semantic-conventions": "^1.33.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-8BahAZpKsOoc+lrZGb7Ofn4g3z8qtp5IxDfvAVpKXsEheQN7ONMH5djT5ihy6yf8yyeQJGS0gXFfpEAEeEHqQg=="], + + "@opentelemetry/instrumentation-mysql": ["@opentelemetry/instrumentation-mysql@0.60.0", "", { "dependencies": { "@opentelemetry/instrumentation": "^0.214.0", "@opentelemetry/semantic-conventions": "^1.33.0", "@types/mysql": "2.15.27" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-08pO8GFPEIz2zquKDGteBZDNmwketdgH8hTe9rVYgW9kCJXq1Psj3wPQGx+VaX4ZJKCfPeoLMYup9+cxHvZyVQ=="], + + "@opentelemetry/instrumentation-mysql2": ["@opentelemetry/instrumentation-mysql2@0.60.0", "", { "dependencies": { "@opentelemetry/instrumentation": "^0.214.0", "@opentelemetry/semantic-conventions": "^1.33.0", "@opentelemetry/sql-common": "^0.41.2" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-m/5d3bxQALllCzezYDk/6vajh0tj5OijMMvOZGr+qN1NMXm1dzMNwyJ0gNZW7Fo3YFRyj/jJMxIw+W7d525dlw=="], + + "@opentelemetry/instrumentation-pg": ["@opentelemetry/instrumentation-pg@0.66.0", "", { "dependencies": { "@opentelemetry/core": "^2.0.0", "@opentelemetry/instrumentation": "^0.214.0", "@opentelemetry/semantic-conventions": "^1.34.0", "@opentelemetry/sql-common": "^0.41.2", "@types/pg": "8.15.6", "@types/pg-pool": "2.0.7" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-KxfLGXBb7k2ueaPJfq2GXBDXBly8P+SpR/4Mj410hhNgmQF3sCqwXvUBQxZQkDAmsdBAoenM+yV1LhtsMRamcA=="], + + "@opentelemetry/instrumentation-redis": ["@opentelemetry/instrumentation-redis@0.62.0", "", { "dependencies": { "@opentelemetry/instrumentation": "^0.214.0", "@opentelemetry/redis-common": "^0.38.2", "@opentelemetry/semantic-conventions": "^1.27.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-y3pPpot7WzR/8JtHcYlTYsyY8g+pbFhAqbwAuG5bLPnR6v6pt1rQc0DpH0OlGP/9CZbWBP+Zhwp9yFoygf/ZXQ=="], + + "@opentelemetry/instrumentation-tedious": ["@opentelemetry/instrumentation-tedious@0.33.0", "", { "dependencies": { "@opentelemetry/instrumentation": "^0.214.0", "@opentelemetry/semantic-conventions": "^1.33.0", "@types/tedious": "^4.0.14" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-Q6WQwAD01MMTub31GlejoiFACYNw26J426wyjvU7by7fDIr2nZXNW4vhTGs7i7F0TnXBO3xN688g1tdUgYwJ5w=="], + + "@opentelemetry/redis-common": ["@opentelemetry/redis-common@0.38.3", "", {}, "sha512-VCghU1JYs/4gP6Gqf/xro9MEsZ7LrMv2uONVsaESKL38ZOB9BqnI98FfS23wjMnHlpuE+TTaWSoAVNpTwYXzjw=="], + "@opentelemetry/resources": ["@opentelemetry/resources@2.5.0", "", { "dependencies": { "@opentelemetry/core": "2.5.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-F8W52ApePshpoSrfsSk1H2yJn9aKjCrbpQF1M9Qii0GHzbfVeFUB+rc3X4aggyZD8x9Gu3Slua+s6krmq6Dt8g=="], "@opentelemetry/sdk-trace-base": ["@opentelemetry/sdk-trace-base@2.5.0", "", { "dependencies": { "@opentelemetry/core": "2.5.0", "@opentelemetry/resources": "2.5.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-VzRf8LzotASEyNDUxTdaJ9IRJ1/h692WyArDBInf5puLCjxbICD6XkHgpuudis56EndyS7LYFmtTMny6UABNdQ=="], "@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.39.0", "", {}, "sha512-R5R9tb2AXs2IRLNKLBJDynhkfmx7mX0vi8NkhZb3gUkPWHn6HXk5J8iQ/dql0U3ApfWym4kXXmBDRGO+oeOfjg=="], + "@opentelemetry/sql-common": ["@opentelemetry/sql-common@0.41.2", "", { "dependencies": { "@opentelemetry/core": "^2.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0" } }, "sha512-4mhWm3Z8z+i508zQJ7r6Xi7y4mmoJpdvH0fZPFRkWrdp5fq7hhZ2HhYokEOLkfqSMgPR4Z9EyB3DBkbKGOqZiQ=="], + "@peggyjs/from-mem": ["@peggyjs/from-mem@3.1.3", "", { "dependencies": { "semver": "7.7.4" } }, "sha512-LLlgtfXIaeYXoOYovOI0spLM8ZXaqkAlmcRRrLzHJzLMqkU6Sw0R4KMoCoHx1PjaP815pSCBlS+BN6aD8t1Jgg=="], + "@prisma/instrumentation": ["@prisma/instrumentation@7.6.0", "", { "dependencies": { "@opentelemetry/instrumentation": "^0.207.0" }, "peerDependencies": { "@opentelemetry/api": "^1.8" } }, "sha512-ZPW2gRiwpPzEfgeZgaekhqXrbW+Y2RJKHVqUmlhZhKzRNCcvR6DykzylDrynpArKKRQtLxoZy36fK7U0p3pdgQ=="], + "@sentry/api": ["@sentry/api@0.113.0", "", {}, "sha512-28W0Oykb/O+6kH8F+OEd8070N4z7ctawlyUtEvnNZNlaLviDC9Is1X/0JiK2Xb9y2ZNbkWf+/H1y5hXr0WTIOw=="], "@sentry/core": ["@sentry/core@10.50.0", "", {}, "sha512-J4A+vzUO3adl0TkFCjaN1+4miamrjHiEIYuLHiuu1lmAjq5WIVw32ObvAh4yMwNtxyaEMosTrrh5M6f12XSJFg=="], + "@sentry/node": ["@sentry/node@10.51.0", "", { "dependencies": { "@fastify/otel": "0.18.0", "@opentelemetry/api": "^1.9.1", "@opentelemetry/core": "^2.6.1", "@opentelemetry/instrumentation": "^0.214.0", "@opentelemetry/instrumentation-amqplib": "0.61.0", "@opentelemetry/instrumentation-connect": "0.57.0", "@opentelemetry/instrumentation-dataloader": "0.31.0", "@opentelemetry/instrumentation-fs": "0.33.0", "@opentelemetry/instrumentation-generic-pool": "0.57.0", "@opentelemetry/instrumentation-graphql": "0.62.0", "@opentelemetry/instrumentation-hapi": "0.60.0", "@opentelemetry/instrumentation-http": "0.214.0", "@opentelemetry/instrumentation-ioredis": "0.62.0", "@opentelemetry/instrumentation-kafkajs": "0.23.0", "@opentelemetry/instrumentation-knex": "0.58.0", "@opentelemetry/instrumentation-koa": "0.62.0", "@opentelemetry/instrumentation-lru-memoizer": "0.58.0", "@opentelemetry/instrumentation-mongodb": "0.67.0", "@opentelemetry/instrumentation-mongoose": "0.60.0", "@opentelemetry/instrumentation-mysql": "0.60.0", "@opentelemetry/instrumentation-mysql2": "0.60.0", "@opentelemetry/instrumentation-pg": "0.66.0", "@opentelemetry/instrumentation-redis": "0.62.0", "@opentelemetry/instrumentation-tedious": "0.33.0", "@opentelemetry/sdk-trace-base": "^2.6.1", "@opentelemetry/semantic-conventions": "^1.40.0", "@prisma/instrumentation": "7.6.0", "@sentry/core": "10.51.0", "@sentry/node-core": "10.51.0", "@sentry/opentelemetry": "10.51.0", "import-in-the-middle": "^3.0.0" } }, "sha512-2yZLRZwS1dKG8/4eOTpGSo/gO/EgmT9aPj6lAzUkRa7bZCTTdW4BraaHU0leX5T94909Qfhbr3W5AVTfDOCKiQ=="], + "@sentry/node-core": ["@sentry/node-core@10.50.0", "", { "dependencies": { "@sentry/core": "10.50.0", "@sentry/opentelemetry": "10.50.0", "import-in-the-middle": "^3.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.9.0", "@opentelemetry/core": "^1.30.1 || ^2.1.0", "@opentelemetry/exporter-trace-otlp-http": ">=0.57.0 <1", "@opentelemetry/instrumentation": ">=0.57.1 <1", "@opentelemetry/sdk-trace-base": "^1.30.1 || ^2.1.0", "@opentelemetry/semantic-conventions": "^1.39.0" }, "optionalPeers": ["@opentelemetry/api", "@opentelemetry/core", "@opentelemetry/exporter-trace-otlp-http", "@opentelemetry/instrumentation", "@opentelemetry/sdk-trace-base", "@opentelemetry/semantic-conventions"] }, "sha512-Eb1BYf4Lc7ZYmdX3acKP6SgyGikrBA370gbGHaWI5jRu7G7vig8sIu1ghPmY5AlvqBPOetado7GniXr6fAXbTw=="], "@sentry/opentelemetry": ["@sentry/opentelemetry@10.50.0", "", { "dependencies": { "@sentry/core": "10.50.0" }, "peerDependencies": { "@opentelemetry/api": "^1.9.0", "@opentelemetry/core": "^1.30.1 || ^2.1.0", "@opentelemetry/sdk-trace-base": "^1.30.1 || ^2.1.0", "@opentelemetry/semantic-conventions": "^1.39.0" } }, "sha512-axn3pgDPveGdaMUC0abMCmFN7ux2pA5ebPufCef4lMIsyg7BBQvaEJ+vE19wjstMaBCAJGsdZlL3eeP2rtgRMw=="], @@ -189,6 +254,8 @@ "@sindresorhus/transliterate": ["@sindresorhus/transliterate@1.6.0", "", { "dependencies": { "escape-string-regexp": "^5.0.0" } }, "sha512-doH1gimEu3A46VX6aVxpHTeHrytJAG6HgdxntYnCFiIFHEM/ZGpG8KiZGBChchjQmG0XFIBL552kBTjVcMZXwQ=="], + "@spotlightjs/spotlight": ["@spotlightjs/spotlight@4.11.3", "", { "dependencies": { "@hono/mcp": "^0.2.2", "@hono/node-server": "^1.19.10", "@jridgewell/trace-mapping": "^0.3.25", "@modelcontextprotocol/sdk": "^1.26.0", "@sentry/core": "^10.31.0", "@sentry/node": "^10.31.0", "anser": "^2.3.3", "chalk": "^5.6.2", "eventsource": "^4.0.0", "fast-fuzzy": "^1.12.0", "hono": "^4.12.7", "launch-editor": "^2.9.1", "logfmt": "^1.4.0", "mcp-proxy": "^5.6.0", "semver": "^7.7.3", "uuidv7": "^1.0.2", "yaml": "^2.8.1", "zod": "^4" }, "bin": { "spotlight": "dist/run.js" } }, "sha512-0vSx3r1qkScyJtilISn2+vfMCq1M9LEGhKfAv5Wgd4soWEwhtSpU8obCd/dWrNdPdpyD5OwiUuynxqxvq0cVpg=="], + "@standard-community/standard-json": ["@standard-community/standard-json@0.3.5", "", { "peerDependencies": { "@standard-schema/spec": "^1.0.0", "@types/json-schema": "^7.0.15", "@valibot/to-json-schema": "^1.3.0", "arktype": "^2.1.20", "effect": "^3.16.8", "quansync": "^0.2.11", "sury": "^10.0.0", "typebox": "^1.0.17", "valibot": "^1.1.0", "zod": "^3.25.0 || ^4.0.0", "zod-to-json-schema": "^3.24.5" }, "optionalPeers": ["@valibot/to-json-schema", "arktype", "effect", "sury", "typebox", "valibot", "zod", "zod-to-json-schema"] }, "sha512-4+ZPorwDRt47i+O7RjyuaxHRK/37QY/LmgxlGrRrSTLYoFatEOzvqIc85GTlM18SFZ5E91C+v0o/M37wZPpUHA=="], "@standard-community/standard-openapi": ["@standard-community/standard-openapi@0.2.9", "", { "peerDependencies": { "@standard-community/standard-json": "^0.3.5", "@standard-schema/spec": "^1.0.0", "arktype": "^2.1.20", "effect": "^3.17.14", "openapi-types": "^12.1.3", "sury": "^10.0.0", "typebox": "^1.0.0", "valibot": "^1.1.0", "zod": "^3.25.0 || ^4.0.0", "zod-openapi": "^4" }, "optionalPeers": ["arktype", "effect", "sury", "typebox", "valibot", "zod", "zod-openapi"] }, "sha512-htj+yldvN1XncyZi4rehbf9kLbu8os2Ke/rfqoZHCMHuw34kiF3LP/yQPdA0tQ940y8nDq3Iou8R3wG+AGGyvg=="], @@ -221,10 +288,16 @@ "@types/mime": ["@types/mime@1.3.5", "", {}, "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w=="], + "@types/mysql": ["@types/mysql@2.15.27", "", { "dependencies": { "@types/node": "*" } }, "sha512-YfWiV16IY0OeBfBCk8+hXKmdTKrKlwKN1MNKAPBu5JYxLwBEZl7QzeEpGnlZb3VMGJrrGmB84gXiH+ofs/TezA=="], + "@types/node": ["@types/node@22.19.7", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-MciR4AKGHWl7xwxkBa6xUGxQJ4VBOmPTF7sL+iGzuahOFaO0jHCsuEfS80pan1ef4gWId1oWOweIhrDEYLuaOw=="], "@types/node-fetch": ["@types/node-fetch@2.6.13", "", { "dependencies": { "@types/node": "*", "form-data": "^4.0.4" } }, "sha512-QGpRVpzSaUs30JBSGPjOg4Uveu384erbHBoT1zeONvyCfwQxIkUshLAOqN/k9EjGviPRmWTTe6aH2qySWKTVSw=="], + "@types/pg": ["@types/pg@8.15.6", "", { "dependencies": { "@types/node": "*", "pg-protocol": "*", "pg-types": "^2.2.0" } }, "sha512-NoaMtzhxOrubeL/7UZuNTrejB4MPAJ0RpxZqXQf2qXuVlTPuG6Y8p4u9dKRaue4yjmC7ZhzVO2/Yyyn25znrPQ=="], + + "@types/pg-pool": ["@types/pg-pool@2.0.7", "", { "dependencies": { "@types/pg": "*" } }, "sha512-U4CwmGVQcbEuqpyju8/ptOKg6gEC+Tqsvj2xS9o1g71bUh8twxnC6ZL5rZKCsGN0iyH0CwgUyc9VR5owNQF9Ng=="], + "@types/picomatch": ["@types/picomatch@4.0.3", "", {}, "sha512-iG0T6+nYJ9FAPmx9SsUlnwcq1ZVRuCXcVEvWnntoPlrOpwtSTKNDC9uVAxTsC3PUvJ+99n4RpAcNgBbHX3JSnQ=="], "@types/qrcode-terminal": ["@types/qrcode-terminal@0.12.2", "", {}, "sha512-v+RcIEJ+Uhd6ygSQ0u5YYY7ZM+la7GgPbs0V/7l/kFs2uO4S8BcIUEMoP7za4DNIqNnUD5npf0A/7kBhrCKG5Q=="], @@ -239,9 +312,11 @@ "@types/serve-static": ["@types/serve-static@1.15.10", "", { "dependencies": { "@types/http-errors": "*", "@types/node": "*", "@types/send": "<1" } }, "sha512-tRs1dB+g8Itk72rlSI2ZrW6vZg0YrLI81iQSTkMmOqnqCaNr/8Ek4VwWcN5vZgCYWbg/JJSGBlUaYGAOP73qBw=="], + "@types/tedious": ["@types/tedious@4.0.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-KHPsfX/FoVbUGbyYvk1q9MMQHLPeRZhRJZdO45Q4YjvFkv4hMNghCWTvy7rdKessBsmtz4euWCWAB6/tVpI1Iw=="], + "abort-controller": ["abort-controller@3.0.0", "", { "dependencies": { "event-target-shim": "^5.0.0" } }, "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg=="], - "accepts": ["accepts@1.3.8", "", { "dependencies": { "mime-types": "~2.1.34", "negotiator": "0.6.3" } }, "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw=="], + "accepts": ["accepts@2.0.0", "", { "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="], "acorn": ["acorn@8.15.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="], @@ -253,6 +328,8 @@ "ajv-formats": ["ajv-formats@3.0.1", "", { "dependencies": { "ajv": "^8.0.0" } }, "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ=="], + "anser": ["anser@2.3.5", "", {}, "sha512-vcZjxvvVoxTeR5XBNJB38oTu/7eDCZlwdz32N1eNgpyPF7j/Z7Idf+CUwQOkKKpJ7RJyjxgLHCM7vdIK0iCNMQ=="], + "ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="], "ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="], @@ -265,12 +342,16 @@ "asynckit": ["asynckit@0.4.0", "", {}, "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="], + "balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="], + "base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="], "binpunch": ["binpunch@1.0.0", "", { "bin": { "binpunch": "dist/cli.js" } }, "sha512-ghxdoerLN3WN64kteDJuL4d9dy7gbvcqoADNRWBk6aQ5FrYH1EmPmREAdcdIdTNAA3uW3V38Env5OqH2lj+i+g=="], "body-parser": ["body-parser@2.2.2", "", { "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", "debug": "^4.4.3", "http-errors": "^2.0.0", "iconv-lite": "^0.7.0", "on-finished": "^2.4.1", "qs": "^6.14.1", "raw-body": "^3.0.1", "type-is": "^2.0.1" } }, "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA=="], + "brace-expansion": ["brace-expansion@5.0.5", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ=="], + "bun-types": ["bun-types@1.3.13", "", { "dependencies": { "@types/node": "*" } }, "sha512-QXKeHLlOLqQX9LgYaHJfzdBaV21T63HhFJnvuRCcjZiaUDpbs5ED1MgxbMra71CsryN/1dAoXuJJJwIv/2drVA=="], "bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="], @@ -299,13 +380,13 @@ "consola": ["consola@3.4.2", "", {}, "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA=="], - "content-disposition": ["content-disposition@0.5.4", "", { "dependencies": { "safe-buffer": "5.2.1" } }, "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ=="], + "content-disposition": ["content-disposition@1.0.1", "", {}, "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q=="], "content-type": ["content-type@1.0.5", "", {}, "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA=="], "cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="], - "cookie-signature": ["cookie-signature@1.0.7", "", {}, "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA=="], + "cookie-signature": ["cookie-signature@1.2.2", "", {}, "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg=="], "cors": ["cors@2.8.6", "", { "dependencies": { "object-assign": "^4", "vary": "^1" } }, "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw=="], @@ -353,11 +434,11 @@ "event-target-shim": ["event-target-shim@5.0.1", "", {}, "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ=="], - "eventsource": ["eventsource@3.0.7", "", { "dependencies": { "eventsource-parser": "^3.0.1" } }, "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA=="], + "eventsource": ["eventsource@4.1.0", "", { "dependencies": { "eventsource-parser": "^3.0.1" } }, "sha512-2GuF51iuHX6A9xdTccMTsNb7VO0lHZihApxhvQzJB5A03DvHDd2FQepodbMaztPBmBcE/ox7o2gqaxGhYB9LhQ=="], "eventsource-parser": ["eventsource-parser@3.0.6", "", {}, "sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg=="], - "express": ["express@4.22.1", "", { "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", "body-parser": "~1.20.3", "content-disposition": "~0.5.4", "content-type": "~1.0.4", "cookie": "~0.7.1", "cookie-signature": "~1.0.6", "debug": "2.6.9", "depd": "2.0.0", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", "finalhandler": "~1.3.1", "fresh": "~0.5.2", "http-errors": "~2.0.0", "merge-descriptors": "1.0.3", "methods": "~1.1.2", "on-finished": "~2.4.1", "parseurl": "~1.3.3", "path-to-regexp": "~0.1.12", "proxy-addr": "~2.0.7", "qs": "~6.14.0", "range-parser": "~1.2.1", "safe-buffer": "5.2.1", "send": "~0.19.0", "serve-static": "~1.16.2", "setprototypeof": "1.2.0", "statuses": "~2.0.1", "type-is": "~1.6.18", "utils-merge": "1.0.1", "vary": "~1.1.2" } }, "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g=="], + "express": ["express@5.2.1", "", { "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.1", "content-disposition": "^1.0.0", "content-type": "^1.0.5", "cookie": "^0.7.1", "cookie-signature": "^1.2.1", "debug": "^4.4.0", "depd": "^2.0.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "finalhandler": "^2.1.0", "fresh": "^2.0.0", "http-errors": "^2.0.0", "merge-descriptors": "^2.0.0", "mime-types": "^3.0.0", "on-finished": "^2.4.1", "once": "^1.4.0", "parseurl": "^1.3.3", "proxy-addr": "^2.0.7", "qs": "^6.14.0", "range-parser": "^1.2.1", "router": "^2.2.0", "send": "^1.1.0", "serve-static": "^2.2.0", "statuses": "^2.0.1", "type-is": "^2.0.1", "vary": "^1.1.2" } }, "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw=="], "express-rate-limit": ["express-rate-limit@8.2.1", "", { "dependencies": { "ip-address": "10.0.1" }, "peerDependencies": { "express": ">= 4.11" } }, "sha512-PCZEIEIxqwhzw4KF0n7QF4QqruVTcF73O5kFKUnGOyjbCCgizBBiFaYpd/fnBLUMPw/BWw9OsiN7GgrNYr7j6g=="], @@ -367,11 +448,13 @@ "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], + "fast-fuzzy": ["fast-fuzzy@1.12.0", "", { "dependencies": { "graphemesplit": "^2.4.1" } }, "sha512-sXxGgHS+ubYpsdLnvOvJ9w5GYYZrtL9mkosG3nfuD446ahvoWEsSKBP7ieGmWIKVLnaxRDgUJkZMdxRgA2Ni+Q=="], + "fast-uri": ["fast-uri@3.1.0", "", {}, "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA=="], "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], - "finalhandler": ["finalhandler@1.3.2", "", { "dependencies": { "debug": "2.6.9", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "on-finished": "~2.4.1", "parseurl": "~1.3.3", "statuses": "~2.0.2", "unpipe": "~1.0.0" } }, "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg=="], + "finalhandler": ["finalhandler@2.1.1", "", { "dependencies": { "debug": "^4.4.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "on-finished": "^2.4.1", "parseurl": "^1.3.3", "statuses": "^2.0.1" } }, "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA=="], "form-data": ["form-data@4.0.5", "", { "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" } }, "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w=="], @@ -381,7 +464,9 @@ "forwarded": ["forwarded@0.2.0", "", {}, "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="], - "fresh": ["fresh@0.5.2", "", {}, "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q=="], + "forwarded-parse": ["forwarded-parse@2.1.2", "", {}, "sha512-alTFZZQDKMporBH77856pXgzhEzaUVmLCDk+egLgIgHst3Tpndzz8MnKe+GzRJRfvVdn69HhpW7cmXzvtLvJAw=="], + + "fresh": ["fresh@2.0.0", "", {}, "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A=="], "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="], @@ -397,6 +482,8 @@ "gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="], + "graphemesplit": ["graphemesplit@2.6.0", "", { "dependencies": { "js-base64": "^3.6.0", "unicode-trie": "^2.0.0" } }, "sha512-rG9w2wAfkpg0DILa1pjnjNfucng3usON360shisqIMUBw/87pojcBSrHmeE4UwryAuBih7g8m1oilf5/u8EWdQ=="], + "gray-matter": ["gray-matter@4.0.3", "", { "dependencies": { "js-yaml": "^3.13.1", "kind-of": "^6.0.2", "section-matter": "^1.0.0", "strip-bom-string": "^1.0.0" } }, "sha512-5v6yZd4JK3eMI3FqqCouswVqwugaA9r4dNZB1wwcmrD02QkV5H0y7XBQW8QwQqEaZY1pM9aqORSORhJRdNK44Q=="], "has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], @@ -409,10 +496,12 @@ "highlight.js": ["highlight.js@10.7.3", "", {}, "sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A=="], - "hono": ["hono@4.12.3", "", {}, "sha512-SFsVSjp8sj5UumXOOFlkZOG6XS9SJDKw0TbwFeV+AJ8xlST8kxK5Z/5EYa111UY8732lK2S/xB653ceuaoGwpg=="], + "hono": ["hono@4.12.15", "", {}, "sha512-qM0jDhFEaCBb4TxoW7f53Qrpv9RBiayUHo0S52JudprkhvpjIrGoU1mnnr29Fvd1U335ZFPZQY1wlkqgfGXyLg=="], "hono-openapi": ["hono-openapi@1.3.0", "", { "peerDependencies": { "@hono/standard-validator": "^0.2.0", "@standard-community/standard-json": "^0.3.5", "@standard-community/standard-openapi": "^0.2.9", "@types/json-schema": "^7.0.15", "hono": "^4.8.3", "openapi-types": "^12.1.3" }, "optionalPeers": ["@hono/standard-validator", "hono"] }, "sha512-xDvCWpWEIv0weEmnl3EjRQzqbHIO8LnfzMuYOCmbuyE5aes6aXxLg4vM3ybnoZD5TiTUkA6PuRQPJs3R7WRBig=="], + "hono-rate-limiter": ["hono-rate-limiter@0.4.2", "", { "peerDependencies": { "hono": "^4.1.1" } }, "sha512-AAtFqgADyrmbDijcRTT/HJfwqfvhalya2Zo+MgfdrMPas3zSMD8SU03cv+ZsYwRU1swv7zgVt0shwN059yzhjw=="], + "http-cache-semantics": ["http-cache-semantics@4.2.0", "", {}, "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ=="], "http-errors": ["http-errors@2.0.1", "", { "dependencies": { "depd": "~2.0.0", "inherits": "~2.0.4", "setprototypeof": "~1.2.0", "statuses": "~2.0.2", "toidentifier": "~1.0.1" } }, "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ=="], @@ -443,6 +532,8 @@ "jose": ["jose@6.1.3", "", {}, "sha512-0TpaTfihd4QMNwrz/ob2Bp7X04yuxJkjRGi4aKmOqwhov54i6u79oCv7T+C7lo70MKH6BesI3vscD1yb/yzKXQ=="], + "js-base64": ["js-base64@3.7.8", "", {}, "sha512-hNngCeKxIUQiEUN3GPJOkz4wF/YvdUdbNL9hsBcMQTkKzboD7T/q3OYOuuPZLUE6dBxSGpwhk5mwuDud7JVAow=="], + "js-tiktoken": ["js-tiktoken@1.0.21", "", { "dependencies": { "base64-js": "^1.5.1" } }, "sha512-biOj/6M5qdgx5TKjDnFT1ymSpM5tbd3ylwDtrQvFQSu0Z7bBYko2dF+W/aUkXUPuk6IVpRxk/3Q2sHOzGlS36g=="], "js-yaml": ["js-yaml@3.14.2", "", { "dependencies": { "argparse": "^1.0.7", "esprima": "^4.0.0" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg=="], @@ -459,15 +550,21 @@ "kind-of": ["kind-of@6.0.3", "", {}, "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw=="], + "launch-editor": ["launch-editor@2.13.2", "", { "dependencies": { "picocolors": "^1.1.1", "shell-quote": "^1.8.3" } }, "sha512-4VVDnbOpLXy/s8rdRCSXb+zfMeFR0WlJWpET1iA9CQdlZDfwyLjUuGQzXU4VeOoey6AicSAluWan7Etga6Kcmg=="], + + "logfmt": ["logfmt@1.4.0", "", { "dependencies": { "split": "0.2.x", "through": "2.3.x" }, "bin": { "logfmt": "bin/logfmt" } }, "sha512-p1Ow0C2dDJYaQBhRHt+HVMP6ELuBm4jYSYNHPMfz0J5wJ9qA6/7oBOlBZBfT1InqguTYcvJzNea5FItDxTcbyw=="], + "lru-cache": ["lru-cache@11.2.6", "", {}, "sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ=="], "marked": ["marked@15.0.12", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-8dD6FusOQSrpv9Z1rdNMdlSgQOIP880DHqnohobOmYLElGEqAL/JvxvuxZO16r4HtjTlfPRDC1hbvxC9dPN2nA=="], "math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="], + "mcp-proxy": ["mcp-proxy@5.12.5", "", { "bin": { "mcp-proxy": "dist/bin/mcp-proxy.mjs" } }, "sha512-Vawdc8vi36fXxKCaDpluRvbGcmrUXJdvXcDhkh30HYsws8XqX2rWPBflZpavzeS+6SwijRFV7g+9ypQRJZlrEQ=="], + "media-typer": ["media-typer@1.1.0", "", {}, "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw=="], - "merge-descriptors": ["merge-descriptors@1.0.3", "", {}, "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ=="], + "merge-descriptors": ["merge-descriptors@2.0.0", "", {}, "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g=="], "methods": ["methods@1.1.2", "", {}, "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w=="], @@ -489,7 +586,7 @@ "nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], - "negotiator": ["negotiator@0.6.3", "", {}, "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg=="], + "negotiator": ["negotiator@1.0.0", "", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="], "node-domexception": ["node-domexception@1.0.0", "", {}, "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ=="], @@ -513,6 +610,8 @@ "p-retry": ["p-retry@7.1.1", "", { "dependencies": { "is-network-error": "^1.1.0" } }, "sha512-J5ApzjyRkkf601HpEeykoiCvzHQjWxPAHhyjFcEUP2SWq0+35NKh8TLhpLw+Dkq5TZBFvUM6UigdE9hIVYTl5w=="], + "pako": ["pako@0.2.9", "", {}, "sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA=="], + "parse-ms": ["parse-ms@4.0.0", "", {}, "sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw=="], "parse5": ["parse5@5.1.1", "", {}, "sha512-ugq4DFI0Ptb+WWjAdOK16+u/nHfiIrcE+sh8kZMaM0WllQKLI9rOUq6c2b7cwPkXdzfQESqvoqK6ug7U/Yyzug=="], @@ -531,12 +630,26 @@ "peggy": ["peggy@5.1.0", "", { "dependencies": { "@peggyjs/from-mem": "3.1.3", "commander": "^14.0.3", "source-map-generator": "2.0.6" }, "bin": { "peggy": "bin/peggy.js" } }, "sha512-IEo5aYRZ2kXH4Qby06cjtL114PZnwLoTiA41vUmg2vPZgANn+c87m5BUurhuDr5/cu758ZlpgsAfBVx+hhO5+w=="], + "pg-int8": ["pg-int8@1.0.1", "", {}, "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw=="], + + "pg-protocol": ["pg-protocol@1.13.0", "", {}, "sha512-zzdvXfS6v89r6v7OcFCHfHlyG/wvry1ALxZo4LqgUoy7W9xhBDMaqOuMiF3qEV45VqsN6rdlcehHrfDtlCPc8w=="], + + "pg-types": ["pg-types@2.2.0", "", { "dependencies": { "pg-int8": "1.0.1", "postgres-array": "~2.0.0", "postgres-bytea": "~1.0.0", "postgres-date": "~1.0.4", "postgres-interval": "^1.1.0" } }, "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA=="], + "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], "picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="], "pkce-challenge": ["pkce-challenge@5.0.1", "", {}, "sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ=="], + "postgres-array": ["postgres-array@2.0.0", "", {}, "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA=="], + + "postgres-bytea": ["postgres-bytea@1.0.1", "", {}, "sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ=="], + + "postgres-date": ["postgres-date@1.0.7", "", {}, "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q=="], + + "postgres-interval": ["postgres-interval@1.2.0", "", { "dependencies": { "xtend": "^4.0.0" } }, "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ=="], + "pretty-ms": ["pretty-ms@9.3.0", "", { "dependencies": { "parse-ms": "^4.0.0" } }, "sha512-gjVS5hOP+M3wMm5nmNOucbIrqudzs9v/57bWRHQWLYklXqoXKrVfYW2W9+glfGsqtPgpiz5WwyEEB+ksXIx3gQ=="], "proxy-addr": ["proxy-addr@2.0.7", "", { "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" } }, "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg=="], @@ -559,6 +672,8 @@ "require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="], + "require-in-the-middle": ["require-in-the-middle@8.0.1", "", { "dependencies": { "debug": "^4.3.5", "module-details-from-path": "^1.0.3" } }, "sha512-QT7FVMXfWOYFbeRBF6nu+I6tr2Tf3u0q8RIEjNob/heKY/nh7drD/k7eeMFmSQgnTtCzLDcCu/XEnpW2wk4xCQ=="], + "router": ["router@2.2.0", "", { "dependencies": { "debug": "^4.4.0", "depd": "^2.0.0", "is-promise": "^4.0.0", "parseurl": "^1.3.3", "path-to-regexp": "^8.0.0" } }, "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ=="], "safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="], @@ -571,9 +686,9 @@ "semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="], - "send": ["send@0.19.2", "", { "dependencies": { "debug": "2.6.9", "depd": "2.0.0", "destroy": "1.2.0", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", "fresh": "~0.5.2", "http-errors": "~2.0.1", "mime": "1.6.0", "ms": "2.1.3", "on-finished": "~2.4.1", "range-parser": "~1.2.1", "statuses": "~2.0.2" } }, "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg=="], + "send": ["send@1.2.1", "", { "dependencies": { "debug": "^4.4.3", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "fresh": "^2.0.0", "http-errors": "^2.0.1", "mime-types": "^3.0.2", "ms": "^2.1.3", "on-finished": "^2.4.1", "range-parser": "^1.2.1", "statuses": "^2.0.2" } }, "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ=="], - "serve-static": ["serve-static@1.16.3", "", { "dependencies": { "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "parseurl": "~1.3.3", "send": "~0.19.1" } }, "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA=="], + "serve-static": ["serve-static@2.2.1", "", { "dependencies": { "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "parseurl": "^1.3.3", "send": "^1.2.0" } }, "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw=="], "setprototypeof": ["setprototypeof@1.2.0", "", {}, "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="], @@ -581,6 +696,8 @@ "shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="], + "shell-quote": ["shell-quote@1.8.3", "", {}, "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw=="], + "side-channel": ["side-channel@1.1.0", "", { "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" } }, "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw=="], "side-channel-list": ["side-channel-list@1.0.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3" } }, "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA=="], @@ -593,6 +710,8 @@ "source-map-generator": ["source-map-generator@2.0.6", "", {}, "sha512-IlassDs1Ve8nV6uyQZXF9kdkJpVKnMte2JZQXu13M0A5zwc+vu6+LNHfmxsHBMDtoZE21RHiKI0/xvpecZRCNg=="], + "split": ["split@0.2.10", "", { "dependencies": { "through": "2" } }, "sha512-e0pKq+UUH2Xq/sXbYpZBZc3BawsfDZ7dgv+JtRTUPNcvF5CMR4Y9cvJqkMY0MoxWzTHvZuz1beg6pNEKlszPiQ=="], + "sprintf-js": ["sprintf-js@1.0.3", "", {}, "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g=="], "statuses": ["statuses@2.0.2", "", {}, "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw=="], @@ -609,6 +728,10 @@ "thenify-all": ["thenify-all@1.6.0", "", { "dependencies": { "thenify": ">= 3.1.0 < 4" } }, "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA=="], + "through": ["through@2.3.8", "", {}, "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg=="], + + "tiny-inflate": ["tiny-inflate@1.0.3", "", {}, "sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw=="], + "tinyexec": ["tinyexec@1.0.2", "", {}, "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg=="], "tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="], @@ -627,6 +750,8 @@ "undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], + "unicode-trie": ["unicode-trie@2.0.0", "", { "dependencies": { "pako": "^0.2.5", "tiny-inflate": "^1.0.0" } }, "sha512-x7bc76x0bm4prf1VLg79uhAzKw8DVboClSN5VxJuQ+LKDOVEW9CdH+VY7SP+vX7xCYQqzzgQpFqz15zeLvAtZQ=="], + "unpipe": ["unpipe@1.0.0", "", {}, "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="], "utils-merge": ["utils-merge@1.0.1", "", {}, "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA=="], @@ -649,10 +774,14 @@ "wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="], + "xtend": ["xtend@4.0.2", "", {}, "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ=="], + "xxhash-wasm": ["xxhash-wasm@1.1.0", "", {}, "sha512-147y/6YNh+tlp6nd/2pWq38i9h6mz/EuQ6njIrmW8D1BS5nCqs0P6DG+m6zTGnNz5I+uhZ0SHxBs9BsPrwcKDA=="], "y18n": ["y18n@5.0.8", "", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="], + "yaml": ["yaml@2.8.3", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg=="], + "yargs": ["yargs@16.2.0", "", { "dependencies": { "cliui": "^7.0.2", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.0", "y18n": "^5.0.5", "yargs-parser": "^20.2.2" } }, "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw=="], "yargs-parser": ["yargs-parser@20.2.9", "", {}, "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w=="], @@ -667,6 +796,8 @@ "zod-to-json-schema": ["zod-to-json-schema@3.25.1", "", { "peerDependencies": { "zod": "^3.25 || ^4" } }, "sha512-pM/SU9d3YAggzi6MtR4h7ruuQlqKtad8e9S0fmxcMi+ueAK5Korys/aWcV9LIIHTVbj01NdzxcnXSN+O74ZIVA=="], + "@a2a-js/sdk/express": ["express@4.22.1", "", { "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", "body-parser": "~1.20.3", "content-disposition": "~0.5.4", "content-type": "~1.0.4", "cookie": "~0.7.1", "cookie-signature": "~1.0.6", "debug": "2.6.9", "depd": "2.0.0", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", "finalhandler": "~1.3.1", "fresh": "~0.5.2", "http-errors": "~2.0.0", "merge-descriptors": "1.0.3", "methods": "~1.1.2", "on-finished": "~2.4.1", "parseurl": "~1.3.3", "path-to-regexp": "~0.1.12", "proxy-addr": "~2.0.7", "qs": "~6.14.0", "range-parser": "~1.2.1", "safe-buffer": "5.2.1", "send": "~0.19.0", "serve-static": "~1.16.2", "setprototypeof": "1.2.0", "statuses": "~2.0.1", "type-is": "~1.6.18", "utils-merge": "1.0.1", "vary": "~1.1.2" } }, "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g=="], + "@ai-sdk/provider-utils/@ai-sdk/provider": ["@ai-sdk/provider@1.1.3", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-qZMxYJ0qqX/RfnuIaab+zp8UAeJn/ygXXAffR5I4N0n1IrvA6qBsjc8hXLmBiMV2zoXlifkacF7sEFnYnjBcqg=="], "@ai-sdk/provider-utils-v6/@ai-sdk/provider": ["@ai-sdk/provider@3.0.0", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-m9ka3ptkPQbaHHZHqDXDF9C9B5/Mav0KTdky1k2HZ3/nrW2t1AgObxIVPyGDWQNS9FXT/FS6PIoSjpcP/No8rQ=="], @@ -675,29 +806,109 @@ "@anthropic-ai/sdk/@types/node": ["@types/node@18.19.130", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg=="], - "@modelcontextprotocol/sdk/express": ["express@5.2.1", "", { "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.1", "content-disposition": "^1.0.0", "content-type": "^1.0.5", "cookie": "^0.7.1", "cookie-signature": "^1.2.1", "debug": "^4.4.0", "depd": "^2.0.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "finalhandler": "^2.1.0", "fresh": "^2.0.0", "http-errors": "^2.0.0", "merge-descriptors": "^2.0.0", "mime-types": "^3.0.0", "on-finished": "^2.4.1", "once": "^1.4.0", "parseurl": "^1.3.3", "proxy-addr": "^2.0.7", "qs": "^6.14.0", "range-parser": "^1.2.1", "router": "^2.2.0", "send": "^1.1.0", "serve-static": "^2.2.0", "statuses": "^2.0.1", "type-is": "^2.0.1", "vary": "^1.1.2" } }, "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw=="], + "@fastify/otel/@opentelemetry/core": ["@opentelemetry/core@2.7.1", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-QAqIj32AtK6+pEVNG7EOVxHdE06RP+FM5qpiEJ4RtDcFIqKUZHYhl7/7UY5efhwmwNAg7j8QbJVBLxMerc0+gw=="], + + "@fastify/otel/@opentelemetry/instrumentation": ["@opentelemetry/instrumentation@0.212.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.212.0", "import-in-the-middle": "^2.0.6", "require-in-the-middle": "^8.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-IyXmpNnifNouMOe0I/gX7ENfv2ZCNdYTF0FpCsoBcpbIHzk81Ww9rQTYTnvghszCg7qGrIhNvWC8dhEifgX9Jg=="], + + "@fastify/otel/@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.40.0", "", {}, "sha512-cifvXDhcqMwwTlTK04GBNeIe7yyo28Mfby85QXFe1Yk8nmi36Ab/5UQwptOx84SsoGNRg+EVSjwzfSZMy6pmlw=="], + + "@fastify/otel/minimatch": ["minimatch@10.2.5", "", { "dependencies": { "brace-expansion": "^5.0.5" } }, "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg=="], + + "@mastra/core/hono": ["hono@4.12.3", "", {}, "sha512-SFsVSjp8sj5UumXOOFlkZOG6XS9SJDKw0TbwFeV+AJ8xlST8kxK5Z/5EYa111UY8732lK2S/xB653ceuaoGwpg=="], + + "@modelcontextprotocol/sdk/@hono/node-server": ["@hono/node-server@1.19.9", "", { "peerDependencies": { "hono": "^4" } }, "sha512-vHL6w3ecZsky+8P5MD+eFfaGTyCeOHUIFYMGpQGbrBTSmNNoxv0if69rEZ5giu36weC5saFuznL411gRX7bJDw=="], + + "@modelcontextprotocol/sdk/eventsource": ["eventsource@3.0.7", "", { "dependencies": { "eventsource-parser": "^3.0.1" } }, "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA=="], + + "@modelcontextprotocol/sdk/hono": ["hono@4.12.3", "", {}, "sha512-SFsVSjp8sj5UumXOOFlkZOG6XS9SJDKw0TbwFeV+AJ8xlST8kxK5Z/5EYa111UY8732lK2S/xB653ceuaoGwpg=="], "@modelcontextprotocol/sdk/zod": ["zod@4.3.5", "", {}, "sha512-k7Nwx6vuWx1IJ9Bjuf4Zt1PEllcwe7cls3VNzm4CQ1/hgtFUK2bRNG3rvnpPUhFjmqJKAKtjV576KnUkHocg/g=="], + "@opentelemetry/api-logs/@opentelemetry/api": ["@opentelemetry/api@1.9.1", "", {}, "sha512-gLyJlPHPZYdAk1JENA9LeHejZe1Ti77/pTeFm/nMXmQH/HFZlcS/O2XJB+L8fkbrNSqhdtlvjBVjxwUYanNH5Q=="], + + "@opentelemetry/instrumentation-amqplib/@opentelemetry/core": ["@opentelemetry/core@2.7.1", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-QAqIj32AtK6+pEVNG7EOVxHdE06RP+FM5qpiEJ4RtDcFIqKUZHYhl7/7UY5efhwmwNAg7j8QbJVBLxMerc0+gw=="], + + "@opentelemetry/instrumentation-amqplib/@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.40.0", "", {}, "sha512-cifvXDhcqMwwTlTK04GBNeIe7yyo28Mfby85QXFe1Yk8nmi36Ab/5UQwptOx84SsoGNRg+EVSjwzfSZMy6pmlw=="], + + "@opentelemetry/instrumentation-connect/@opentelemetry/core": ["@opentelemetry/core@2.7.1", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-QAqIj32AtK6+pEVNG7EOVxHdE06RP+FM5qpiEJ4RtDcFIqKUZHYhl7/7UY5efhwmwNAg7j8QbJVBLxMerc0+gw=="], + + "@opentelemetry/instrumentation-connect/@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.40.0", "", {}, "sha512-cifvXDhcqMwwTlTK04GBNeIe7yyo28Mfby85QXFe1Yk8nmi36Ab/5UQwptOx84SsoGNRg+EVSjwzfSZMy6pmlw=="], + + "@opentelemetry/instrumentation-fs/@opentelemetry/core": ["@opentelemetry/core@2.7.1", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-QAqIj32AtK6+pEVNG7EOVxHdE06RP+FM5qpiEJ4RtDcFIqKUZHYhl7/7UY5efhwmwNAg7j8QbJVBLxMerc0+gw=="], + + "@opentelemetry/instrumentation-hapi/@opentelemetry/core": ["@opentelemetry/core@2.7.1", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-QAqIj32AtK6+pEVNG7EOVxHdE06RP+FM5qpiEJ4RtDcFIqKUZHYhl7/7UY5efhwmwNAg7j8QbJVBLxMerc0+gw=="], + + "@opentelemetry/instrumentation-hapi/@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.40.0", "", {}, "sha512-cifvXDhcqMwwTlTK04GBNeIe7yyo28Mfby85QXFe1Yk8nmi36Ab/5UQwptOx84SsoGNRg+EVSjwzfSZMy6pmlw=="], + + "@opentelemetry/instrumentation-http/@opentelemetry/core": ["@opentelemetry/core@2.6.1", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-8xHSGWpJP9wBxgBpnqGL0R3PbdWQndL1Qp50qrg71+B28zK5OQmUgcDKLJgzyAAV38t4tOyLMGDD60LneR5W8g=="], + + "@opentelemetry/instrumentation-http/@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.40.0", "", {}, "sha512-cifvXDhcqMwwTlTK04GBNeIe7yyo28Mfby85QXFe1Yk8nmi36Ab/5UQwptOx84SsoGNRg+EVSjwzfSZMy6pmlw=="], + + "@opentelemetry/instrumentation-ioredis/@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.40.0", "", {}, "sha512-cifvXDhcqMwwTlTK04GBNeIe7yyo28Mfby85QXFe1Yk8nmi36Ab/5UQwptOx84SsoGNRg+EVSjwzfSZMy6pmlw=="], + + "@opentelemetry/instrumentation-kafkajs/@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.40.0", "", {}, "sha512-cifvXDhcqMwwTlTK04GBNeIe7yyo28Mfby85QXFe1Yk8nmi36Ab/5UQwptOx84SsoGNRg+EVSjwzfSZMy6pmlw=="], + + "@opentelemetry/instrumentation-knex/@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.40.0", "", {}, "sha512-cifvXDhcqMwwTlTK04GBNeIe7yyo28Mfby85QXFe1Yk8nmi36Ab/5UQwptOx84SsoGNRg+EVSjwzfSZMy6pmlw=="], + + "@opentelemetry/instrumentation-koa/@opentelemetry/core": ["@opentelemetry/core@2.7.1", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-QAqIj32AtK6+pEVNG7EOVxHdE06RP+FM5qpiEJ4RtDcFIqKUZHYhl7/7UY5efhwmwNAg7j8QbJVBLxMerc0+gw=="], + + "@opentelemetry/instrumentation-koa/@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.40.0", "", {}, "sha512-cifvXDhcqMwwTlTK04GBNeIe7yyo28Mfby85QXFe1Yk8nmi36Ab/5UQwptOx84SsoGNRg+EVSjwzfSZMy6pmlw=="], + + "@opentelemetry/instrumentation-mongodb/@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.40.0", "", {}, "sha512-cifvXDhcqMwwTlTK04GBNeIe7yyo28Mfby85QXFe1Yk8nmi36Ab/5UQwptOx84SsoGNRg+EVSjwzfSZMy6pmlw=="], + + "@opentelemetry/instrumentation-mongoose/@opentelemetry/core": ["@opentelemetry/core@2.7.1", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-QAqIj32AtK6+pEVNG7EOVxHdE06RP+FM5qpiEJ4RtDcFIqKUZHYhl7/7UY5efhwmwNAg7j8QbJVBLxMerc0+gw=="], + + "@opentelemetry/instrumentation-mongoose/@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.40.0", "", {}, "sha512-cifvXDhcqMwwTlTK04GBNeIe7yyo28Mfby85QXFe1Yk8nmi36Ab/5UQwptOx84SsoGNRg+EVSjwzfSZMy6pmlw=="], + + "@opentelemetry/instrumentation-mysql/@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.40.0", "", {}, "sha512-cifvXDhcqMwwTlTK04GBNeIe7yyo28Mfby85QXFe1Yk8nmi36Ab/5UQwptOx84SsoGNRg+EVSjwzfSZMy6pmlw=="], + + "@opentelemetry/instrumentation-mysql2/@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.40.0", "", {}, "sha512-cifvXDhcqMwwTlTK04GBNeIe7yyo28Mfby85QXFe1Yk8nmi36Ab/5UQwptOx84SsoGNRg+EVSjwzfSZMy6pmlw=="], + + "@opentelemetry/instrumentation-pg/@opentelemetry/core": ["@opentelemetry/core@2.7.1", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-QAqIj32AtK6+pEVNG7EOVxHdE06RP+FM5qpiEJ4RtDcFIqKUZHYhl7/7UY5efhwmwNAg7j8QbJVBLxMerc0+gw=="], + + "@opentelemetry/instrumentation-pg/@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.40.0", "", {}, "sha512-cifvXDhcqMwwTlTK04GBNeIe7yyo28Mfby85QXFe1Yk8nmi36Ab/5UQwptOx84SsoGNRg+EVSjwzfSZMy6pmlw=="], + + "@opentelemetry/instrumentation-redis/@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.40.0", "", {}, "sha512-cifvXDhcqMwwTlTK04GBNeIe7yyo28Mfby85QXFe1Yk8nmi36Ab/5UQwptOx84SsoGNRg+EVSjwzfSZMy6pmlw=="], + + "@opentelemetry/instrumentation-tedious/@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.40.0", "", {}, "sha512-cifvXDhcqMwwTlTK04GBNeIe7yyo28Mfby85QXFe1Yk8nmi36Ab/5UQwptOx84SsoGNRg+EVSjwzfSZMy6pmlw=="], + + "@opentelemetry/sql-common/@opentelemetry/core": ["@opentelemetry/core@2.7.1", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-QAqIj32AtK6+pEVNG7EOVxHdE06RP+FM5qpiEJ4RtDcFIqKUZHYhl7/7UY5efhwmwNAg7j8QbJVBLxMerc0+gw=="], + "@peggyjs/from-mem/semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="], - "cli-highlight/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], + "@prisma/instrumentation/@opentelemetry/instrumentation": ["@opentelemetry/instrumentation@0.207.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.207.0", "import-in-the-middle": "^2.0.0", "require-in-the-middle": "^8.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-y6eeli9+TLKnznrR8AZlQMSJT7wILpXH+6EYq5Vf/4Ao+huI7EedxQHwRgVUOMLFbe7VFDvHJrX9/f4lcwnJsA=="], - "cliui/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], + "@sentry/node/@opentelemetry/api": ["@opentelemetry/api@1.9.1", "", {}, "sha512-gLyJlPHPZYdAk1JENA9LeHejZe1Ti77/pTeFm/nMXmQH/HFZlcS/O2XJB+L8fkbrNSqhdtlvjBVjxwUYanNH5Q=="], - "cliui/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + "@sentry/node/@opentelemetry/core": ["@opentelemetry/core@2.7.1", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-QAqIj32AtK6+pEVNG7EOVxHdE06RP+FM5qpiEJ4RtDcFIqKUZHYhl7/7UY5efhwmwNAg7j8QbJVBLxMerc0+gw=="], - "cliui/wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="], + "@sentry/node/@opentelemetry/sdk-trace-base": ["@opentelemetry/sdk-trace-base@2.7.1", "", { "dependencies": { "@opentelemetry/core": "2.7.1", "@opentelemetry/resources": "2.7.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-NAYIlsF8MPUsKqJMiDQJTMPOmlbawC1Iz/omMLygZ1C9am8fTKYjTaI+OZM+WTY3t3Glo0wnOg/6/pac6RGPPw=="], + + "@sentry/node/@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.40.0", "", {}, "sha512-cifvXDhcqMwwTlTK04GBNeIe7yyo28Mfby85QXFe1Yk8nmi36Ab/5UQwptOx84SsoGNRg+EVSjwzfSZMy6pmlw=="], + + "@sentry/node/@sentry/core": ["@sentry/core@10.51.0", "", {}, "sha512-Y45V/YXvVLEXmOdkbD1oG1gkRWFi9guCEGg3PlIlIpRjAbZUrvLGgjRJIc1E7XpSzmOnWbs5BbUxMv4PDaPj2w=="], + + "@sentry/node/@sentry/node-core": ["@sentry/node-core@10.51.0", "", { "dependencies": { "@sentry/core": "10.51.0", "@sentry/opentelemetry": "10.51.0", "import-in-the-middle": "^3.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.9.0", "@opentelemetry/core": "^1.30.1 || ^2.1.0", "@opentelemetry/exporter-trace-otlp-http": ">=0.57.0 <1", "@opentelemetry/instrumentation": ">=0.57.1 <1", "@opentelemetry/sdk-trace-base": "^1.30.1 || ^2.1.0", "@opentelemetry/semantic-conventions": "^1.39.0" }, "optionalPeers": ["@opentelemetry/api", "@opentelemetry/core", "@opentelemetry/exporter-trace-otlp-http", "@opentelemetry/instrumentation", "@opentelemetry/sdk-trace-base", "@opentelemetry/semantic-conventions"] }, "sha512-VP9DMEzBEuauABrfDHYz/pRYa74M09uRJLz0ls3yel3sKhYHMyCB29ZxbKcciUhD4d33dwgi8DbaPZV2H/wnfQ=="], + + "@sentry/node/@sentry/opentelemetry": ["@sentry/opentelemetry@10.51.0", "", { "dependencies": { "@sentry/core": "10.51.0" }, "peerDependencies": { "@opentelemetry/api": "^1.9.0", "@opentelemetry/core": "^1.30.1 || ^2.1.0", "@opentelemetry/sdk-trace-base": "^1.30.1 || ^2.1.0", "@opentelemetry/semantic-conventions": "^1.39.0" } }, "sha512-Qc7AlCE4uhB+SvHLqah4RgR1WdY7wmmr/hx9g/prDP9R1ocshmUEMrZK9qjuwaklW7/fmkFCXI8ETxo5L1bHIA=="], + + "@spotlightjs/spotlight/@hono/node-server": ["@hono/node-server@1.19.14", "", { "peerDependencies": { "hono": "^4" } }, "sha512-GwtvgtXxnWsucXvbQXkRgqksiH2Qed37H9xHZocE5sA3N8O8O8/8FA3uclQXxXVzc9XBZuEOMK7+r02FmSpHtw=="], + + "@spotlightjs/spotlight/semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="], - "express/body-parser": ["body-parser@1.20.4", "", { "dependencies": { "bytes": "~3.1.2", "content-type": "~1.0.5", "debug": "2.6.9", "depd": "2.0.0", "destroy": "~1.2.0", "http-errors": "~2.0.1", "iconv-lite": "~0.4.24", "on-finished": "~2.4.1", "qs": "~6.14.0", "raw-body": "~2.5.3", "type-is": "~1.6.18", "unpipe": "~1.0.0" } }, "sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA=="], + "@spotlightjs/spotlight/zod": ["zod@4.3.5", "", {}, "sha512-k7Nwx6vuWx1IJ9Bjuf4Zt1PEllcwe7cls3VNzm4CQ1/hgtFUK2bRNG3rvnpPUhFjmqJKAKtjV576KnUkHocg/g=="], - "express/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], + "accepts/mime-types": ["mime-types@3.0.2", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A=="], - "express/qs": ["qs@6.14.2", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q=="], + "cli-highlight/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], + + "cliui/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], + + "cliui/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], - "express/type-is": ["type-is@1.6.18", "", { "dependencies": { "media-typer": "0.3.0", "mime-types": "~2.1.24" } }, "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g=="], + "cliui/wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="], - "finalhandler/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], + "express/mime-types": ["mime-types@3.0.2", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A=="], "parse5-htmlparser2-tree-adapter/parse5": ["parse5@6.0.1", "", {}, "sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw=="], @@ -705,7 +916,7 @@ "router/path-to-regexp": ["path-to-regexp@8.3.0", "", {}, "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA=="], - "send/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], + "send/mime-types": ["mime-types@3.0.2", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A=="], "trpc-cli/commander": ["commander@14.0.2", "", {}, "sha512-TywoWNNRbhoD0BXs1P3ZEScW8W5iKrnbithIl0YH+uCmBd0QpPOA8yc82DS3BIE5Ma6FnBVUsJ7wVUDz4dvOWQ=="], @@ -717,25 +928,47 @@ "zod-from-json-schema/zod": ["zod@4.3.5", "", {}, "sha512-k7Nwx6vuWx1IJ9Bjuf4Zt1PEllcwe7cls3VNzm4CQ1/hgtFUK2bRNG3rvnpPUhFjmqJKAKtjV576KnUkHocg/g=="], - "@anthropic-ai/sdk/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="], + "@a2a-js/sdk/express/accepts": ["accepts@1.3.8", "", { "dependencies": { "mime-types": "~2.1.34", "negotiator": "0.6.3" } }, "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw=="], + + "@a2a-js/sdk/express/body-parser": ["body-parser@1.20.4", "", { "dependencies": { "bytes": "~3.1.2", "content-type": "~1.0.5", "debug": "2.6.9", "depd": "2.0.0", "destroy": "~1.2.0", "http-errors": "~2.0.1", "iconv-lite": "~0.4.24", "on-finished": "~2.4.1", "qs": "~6.14.0", "raw-body": "~2.5.3", "type-is": "~1.6.18", "unpipe": "~1.0.0" } }, "sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA=="], - "@modelcontextprotocol/sdk/express/accepts": ["accepts@2.0.0", "", { "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="], + "@a2a-js/sdk/express/content-disposition": ["content-disposition@0.5.4", "", { "dependencies": { "safe-buffer": "5.2.1" } }, "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ=="], - "@modelcontextprotocol/sdk/express/content-disposition": ["content-disposition@1.0.1", "", {}, "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q=="], + "@a2a-js/sdk/express/cookie-signature": ["cookie-signature@1.0.7", "", {}, "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA=="], - "@modelcontextprotocol/sdk/express/cookie-signature": ["cookie-signature@1.2.2", "", {}, "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg=="], + "@a2a-js/sdk/express/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], - "@modelcontextprotocol/sdk/express/finalhandler": ["finalhandler@2.1.1", "", { "dependencies": { "debug": "^4.4.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "on-finished": "^2.4.1", "parseurl": "^1.3.3", "statuses": "^2.0.1" } }, "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA=="], + "@a2a-js/sdk/express/finalhandler": ["finalhandler@1.3.2", "", { "dependencies": { "debug": "2.6.9", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "on-finished": "~2.4.1", "parseurl": "~1.3.3", "statuses": "~2.0.2", "unpipe": "~1.0.0" } }, "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg=="], - "@modelcontextprotocol/sdk/express/fresh": ["fresh@2.0.0", "", {}, "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A=="], + "@a2a-js/sdk/express/fresh": ["fresh@0.5.2", "", {}, "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q=="], - "@modelcontextprotocol/sdk/express/merge-descriptors": ["merge-descriptors@2.0.0", "", {}, "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g=="], + "@a2a-js/sdk/express/merge-descriptors": ["merge-descriptors@1.0.3", "", {}, "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ=="], - "@modelcontextprotocol/sdk/express/mime-types": ["mime-types@3.0.2", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A=="], + "@a2a-js/sdk/express/qs": ["qs@6.14.2", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q=="], - "@modelcontextprotocol/sdk/express/send": ["send@1.2.1", "", { "dependencies": { "debug": "^4.4.3", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "fresh": "^2.0.0", "http-errors": "^2.0.1", "mime-types": "^3.0.2", "ms": "^2.1.3", "on-finished": "^2.4.1", "range-parser": "^1.2.1", "statuses": "^2.0.2" } }, "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ=="], + "@a2a-js/sdk/express/send": ["send@0.19.2", "", { "dependencies": { "debug": "2.6.9", "depd": "2.0.0", "destroy": "1.2.0", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", "fresh": "~0.5.2", "http-errors": "~2.0.1", "mime": "1.6.0", "ms": "2.1.3", "on-finished": "~2.4.1", "range-parser": "~1.2.1", "statuses": "~2.0.2" } }, "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg=="], - "@modelcontextprotocol/sdk/express/serve-static": ["serve-static@2.2.1", "", { "dependencies": { "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "parseurl": "^1.3.3", "send": "^1.2.0" } }, "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw=="], + "@a2a-js/sdk/express/serve-static": ["serve-static@1.16.3", "", { "dependencies": { "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "parseurl": "~1.3.3", "send": "~0.19.1" } }, "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA=="], + + "@a2a-js/sdk/express/type-is": ["type-is@1.6.18", "", { "dependencies": { "media-typer": "0.3.0", "mime-types": "~2.1.24" } }, "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g=="], + + "@anthropic-ai/sdk/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="], + + "@fastify/otel/@opentelemetry/instrumentation/@opentelemetry/api-logs": ["@opentelemetry/api-logs@0.212.0", "", { "dependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-TEEVrLbNROUkYY51sBJGk7lO/OLjuepch8+hmpM6ffMJQ2z/KVCjdHuCFX6fJj8OkJP2zckPjrJzQtXU3IAsFg=="], + + "@fastify/otel/@opentelemetry/instrumentation/import-in-the-middle": ["import-in-the-middle@2.0.6", "", { "dependencies": { "acorn": "^8.15.0", "acorn-import-attributes": "^1.9.5", "cjs-module-lexer": "^2.2.0", "module-details-from-path": "^1.0.4" } }, "sha512-3vZV3jX0XRFW3EJDTwzWoZa+RH1b8eTTx6YOCjglrLyPuepwoBti1k3L2dKwdCUrnVEfc5CuRuGstaC/uQJJaw=="], + + "@opentelemetry/instrumentation-fs/@opentelemetry/core/@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.40.0", "", {}, "sha512-cifvXDhcqMwwTlTK04GBNeIe7yyo28Mfby85QXFe1Yk8nmi36Ab/5UQwptOx84SsoGNRg+EVSjwzfSZMy6pmlw=="], + + "@opentelemetry/sql-common/@opentelemetry/core/@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.40.0", "", {}, "sha512-cifvXDhcqMwwTlTK04GBNeIe7yyo28Mfby85QXFe1Yk8nmi36Ab/5UQwptOx84SsoGNRg+EVSjwzfSZMy6pmlw=="], + + "@prisma/instrumentation/@opentelemetry/instrumentation/@opentelemetry/api-logs": ["@opentelemetry/api-logs@0.207.0", "", { "dependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-lAb0jQRVyleQQGiuuvCOTDVspc14nx6XJjP4FspJ1sNARo3Regq4ZZbrc3rN4b1TYSuUCvgH+UXUPug4SLOqEQ=="], + + "@prisma/instrumentation/@opentelemetry/instrumentation/import-in-the-middle": ["import-in-the-middle@2.0.6", "", { "dependencies": { "acorn": "^8.15.0", "acorn-import-attributes": "^1.9.5", "cjs-module-lexer": "^2.2.0", "module-details-from-path": "^1.0.4" } }, "sha512-3vZV3jX0XRFW3EJDTwzWoZa+RH1b8eTTx6YOCjglrLyPuepwoBti1k3L2dKwdCUrnVEfc5CuRuGstaC/uQJJaw=="], + + "@sentry/node/@opentelemetry/sdk-trace-base/@opentelemetry/resources": ["@opentelemetry/resources@2.7.1", "", { "dependencies": { "@opentelemetry/core": "2.7.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-DeT6KKolmC4e/dRQvMQ/RwlnzhaqeiFOXY5ngoOPJ07GgVVKxZOg9EcrNZb5aTzUn+iCrJldAgOfQm1O/QfPAQ=="], + + "accepts/mime-types/mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="], "cli-highlight/chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], @@ -743,25 +976,27 @@ "cliui/wrap-ansi/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], - "express/body-parser/iconv-lite": ["iconv-lite@0.4.24", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3" } }, "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA=="], + "express/mime-types/mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="], + + "send/mime-types/mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="], - "express/body-parser/raw-body": ["raw-body@2.5.3", "", { "dependencies": { "bytes": "~3.1.2", "http-errors": "~2.0.1", "iconv-lite": "~0.4.24", "unpipe": "~1.0.0" } }, "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA=="], + "type-is/mime-types/mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="], - "express/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], + "yargs/string-width/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], - "express/type-is/media-typer": ["media-typer@0.3.0", "", {}, "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ=="], + "@a2a-js/sdk/express/accepts/negotiator": ["negotiator@0.6.3", "", {}, "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg=="], - "finalhandler/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], + "@a2a-js/sdk/express/body-parser/iconv-lite": ["iconv-lite@0.4.24", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3" } }, "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA=="], - "send/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], + "@a2a-js/sdk/express/body-parser/raw-body": ["raw-body@2.5.3", "", { "dependencies": { "bytes": "~3.1.2", "http-errors": "~2.0.1", "iconv-lite": "~0.4.24", "unpipe": "~1.0.0" } }, "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA=="], - "type-is/mime-types/mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="], + "@a2a-js/sdk/express/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], - "yargs/string-width/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + "@a2a-js/sdk/express/type-is/media-typer": ["media-typer@0.3.0", "", {}, "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ=="], - "@modelcontextprotocol/sdk/express/accepts/negotiator": ["negotiator@1.0.0", "", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="], + "@fastify/otel/@opentelemetry/instrumentation/@opentelemetry/api-logs/@opentelemetry/api": ["@opentelemetry/api@1.9.1", "", {}, "sha512-gLyJlPHPZYdAk1JENA9LeHejZe1Ti77/pTeFm/nMXmQH/HFZlcS/O2XJB+L8fkbrNSqhdtlvjBVjxwUYanNH5Q=="], - "@modelcontextprotocol/sdk/express/mime-types/mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="], + "@prisma/instrumentation/@opentelemetry/instrumentation/@opentelemetry/api-logs/@opentelemetry/api": ["@opentelemetry/api@1.9.1", "", {}, "sha512-gLyJlPHPZYdAk1JENA9LeHejZe1Ti77/pTeFm/nMXmQH/HFZlcS/O2XJB+L8fkbrNSqhdtlvjBVjxwUYanNH5Q=="], "yargs/string-width/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], } diff --git a/docs/src/content/docs/contributing.md b/docs/src/content/docs/contributing.md index c4ee61408..37e39de3c 100644 --- a/docs/src/content/docs/contributing.md +++ b/docs/src/content/docs/contributing.md @@ -70,6 +70,7 @@ cli/ │ │ ├── explore.ts # Query aggregate event data (Explore) │ │ ├── help.ts # Help command │ │ ├── init.ts # Initialize Sentry in your project (experimental) +│ │ ├── local.ts # Run a local Spotlight sidecar to capture dev SDK events │ │ └── schema.ts # Browse the Sentry API schema │ ├── lib/ # Shared utilities │ └── types/ # TypeScript types and Zod schemas diff --git a/docs/src/fragments/commands/local.md b/docs/src/fragments/commands/local.md new file mode 100644 index 000000000..ea78aac95 --- /dev/null +++ b/docs/src/fragments/commands/local.md @@ -0,0 +1,50 @@ + + +[Spotlight](https://spotlightjs.com) is "Sentry for Development" — a lightweight local proxy that ingests Sentry envelopes from SDKs running in your dev stack and surfaces them in real time. `sentry local` runs a minimal [Hono](https://hono.dev/) HTTP server that's wire-compatible with Spotlight's sidecar protocol, so your existing SDKs and the [Spotlight overlay](https://spotlightjs.com/about/) work without any changes. + +No authentication is required — the sidecar binds to `localhost` by default and is purely a development tool. + +## Examples + +```bash +# Start the sidecar on the default port (8969) +sentry local + +# Use a custom port and bind to all interfaces +sentry local --port 9000 --host 0.0.0.0 + +# Run quietly (suppress per-envelope tail output) +sentry local --quiet + +# Open the SSE endpoint in a browser on startup +sentry local --open +``` + +## Endpoints + +| Method | Path | Description | +|--------|---------------------------------|----------------------------------------------------| +| `POST` | `/stream` | Spotlight-compatible envelope ingest | +| `POST` | `/api/{projectId}/envelope/` | Sentry SDK ingest path | +| `GET` | `/stream` | Server-Sent Events feed of incoming envelopes | +| `GET` | `/health` | Liveness check (returns `OK`) | + +## Pointing your SDK at the sidecar + +Set a localhost DSN that resolves to the sidecar's port — the public key and project ID can be any non-empty value because the sidecar accepts everything: + +```bash +SENTRY_DSN=http://public@localhost:8969/1 +``` + +Or configure your SDK's transport explicitly to send envelopes to `http://localhost:8969/stream`. + +## Tail output + +By default, every envelope received is logged as a single line: + +``` +14:32:01.456 • event+attachment +``` + +The label is the joined list of envelope item types (`event`, `transaction`, `log`, `attachment`, etc.). Use `--quiet` to suppress this output if you only need the SSE stream for the Spotlight overlay. diff --git a/package.json b/package.json index 54722e2b9..c996578bb 100644 --- a/package.json +++ b/package.json @@ -10,10 +10,12 @@ "@anthropic-ai/sdk": "^0.39.0", "@biomejs/biome": "2.3.8", "@clack/prompts": "^0.11.0", + "@hono/node-server": "^2.0.0", "@mastra/client-js": "^1.4.0", "@sentry/api": "^0.113.0", "@sentry/node-core": "10.50.0", "@sentry/sqlish": "^1.0.0", + "@spotlightjs/spotlight": "^4.11.3", "@stricli/auto-complete": "^1.2.4", "@stricli/core": "^1.2.4", "@types/bun": "latest", @@ -28,6 +30,7 @@ "consola": "^3.4.2", "esbuild": "^0.25.0", "fast-check": "^4.5.3", + "hono": "^4.12.15", "http-cache-semantics": "^4.2.0", "ignore": "^7.0.5", "marked": "^15", diff --git a/plugins/sentry-cli/skills/sentry-cli/SKILL.md b/plugins/sentry-cli/skills/sentry-cli/SKILL.md index 5a7ae22b9..f2e7d7b8c 100644 --- a/plugins/sentry-cli/skills/sentry-cli/SKILL.md +++ b/plugins/sentry-cli/skills/sentry-cli/SKILL.md @@ -456,6 +456,14 @@ Initialize Sentry in your project (experimental) → Full flags and examples: `references/init.md` +### Local + +Run a local Spotlight sidecar to capture dev SDK events + +- `sentry local` — Run a local Spotlight sidecar to capture dev SDK events + +→ Full flags and examples: `references/local.md` + ### Schema Browse the Sentry API schema diff --git a/plugins/sentry-cli/skills/sentry-cli/references/local.md b/plugins/sentry-cli/skills/sentry-cli/references/local.md new file mode 100644 index 000000000..92ee5ab9a --- /dev/null +++ b/plugins/sentry-cli/skills/sentry-cli/references/local.md @@ -0,0 +1,42 @@ +--- +name: sentry-cli-local +version: 0.31.0-dev.0 +description: Run a local Spotlight sidecar to capture dev SDK events +requires: + bins: ["sentry"] + auth: true +--- + +# Local Commands + +Run a local Spotlight sidecar to capture dev SDK events + +### `sentry local` + +Run a local Spotlight sidecar to capture dev SDK events + +**Flags:** +- `-p, --port - Port to listen on (default 8969) - (default: "8969")` +- `-H, --host - Hostname to bind to (default localhost) - (default: "localhost")` +- `-o, --open - Open the sidecar SSE URL in a browser` +- `-q, --quiet - Suppress per-envelope tail output` + +**Examples:** + +```bash +# Start the sidecar on the default port (8969) +sentry local + +# Use a custom port and bind to all interfaces +sentry local --port 9000 --host 0.0.0.0 + +# Run quietly (suppress per-envelope tail output) +sentry local --quiet + +# Open the SSE endpoint in a browser on startup +sentry local --open + +SENTRY_DSN=http://public@localhost:8969/1 +``` + +All commands also support `--json`, `--fields`, `--help`, `--log-level`, and `--verbose` flags. diff --git a/src/app.ts b/src/app.ts index 52174001e..e5871191d 100644 --- a/src/app.ts +++ b/src/app.ts @@ -18,6 +18,7 @@ import { helpCommand } from "./commands/help.js"; import { initCommand } from "./commands/init.js"; import { issueRoute } from "./commands/issue/index.js"; import { listCommand as issueListCommand } from "./commands/issue/list.js"; +import { localCommand } from "./commands/local.js"; import { logRoute } from "./commands/log/index.js"; import { listCommand as logListCommand } from "./commands/log/list.js"; import { orgRoute } from "./commands/org/index.js"; @@ -99,6 +100,7 @@ export const routes = buildRouteMap({ trace: traceRoute, trial: trialRoute, init: initCommand, + local: localCommand, api: apiCommand, schema: schemaCommand, dashboards: dashboardListCommand, diff --git a/src/commands/local.ts b/src/commands/local.ts new file mode 100644 index 000000000..02791b951 --- /dev/null +++ b/src/commands/local.ts @@ -0,0 +1,369 @@ +/** + * sentry local + * + * Run a local Spotlight-compatible sidecar server. + * + * Spotlight (https://github.com/getsentry/spotlight) is "Sentry for + * Development" — a small local proxy that ingests Sentry envelopes from + * SDKs running in your dev stack and surfaces them in real time. + * + * This command starts a minimal Hono HTTP server that: + * + * 1. Accepts envelopes from Sentry SDKs at the standard sidecar endpoints: + * - `POST /stream` (Spotlight-compatible) + * - `POST /api/{projectId}/envelope/` (Sentry SDK ingest path) + * 2. Pushes them into the buffer provided by `@spotlightjs/spotlight/sdk`, + * which lazily parses each envelope. + * 3. Streams new envelopes back to subscribers via Server-Sent Events at + * `GET /stream` — compatible with the Spotlight overlay/UI. + * 4. Tails events to the terminal as they arrive so you can see what your + * app is sending without leaving the CLI. + * + * To point your SDK at the local sidecar, use a placeholder DSN that + * resolves to localhost — for example: + * + * SENTRY_DSN=http://public@localhost:8969/1 + * + * Or configure your SDK's transport to send to `http://localhost:8969/stream`. + * + * The command runs until interrupted (Ctrl-C / SIGTERM). + */ + +import type { Server } from "node:http"; +import { serve } from "@hono/node-server"; +import { + createSpotlightBuffer, + pushToSpotlightBuffer, +} from "@spotlightjs/spotlight/sdk"; +import { Hono } from "hono"; +import { cors } from "hono/cors"; +import { streamSSE } from "hono/streaming"; +import type { SentryContext } from "../context.js"; +import { openOrShowUrl } from "../lib/browser.js"; +import { buildCommand, numberParser } from "../lib/command.js"; +import { ValidationError } from "../lib/errors.js"; +import { bold, cyan, muted } from "../lib/formatters/colors.js"; +import { logger } from "../lib/logger.js"; + +const log = logger.withTag("local"); + +/** Default port matches Spotlight's `DEFAULT_PORT`. */ +const DEFAULT_PORT = 8969; + +/** Buffer size: how many recent envelopes to retain for late subscribers. */ +const BUFFER_SIZE = 500; + +/** SSE event payload — what we send to GET /stream subscribers. */ +type EventPayload = { + contentType: string; + data: string; // base64-encoded raw envelope bytes +}; + +type LocalFlags = { + readonly port: number; + readonly host: string; + readonly open: boolean; + readonly quiet: boolean; +}; + +/** + * Validate a port number from `--port`. + * + * Hard-fails on out-of-range values so users get a clean error rather than + * a `listen EADDRNOTAVAIL` from the kernel. + */ +function parsePort(value: string): number { + const port = numberParser(value); + if (!Number.isInteger(port) || port < 0 || port > 65_535) { + throw new ValidationError( + `Invalid port: ${value}. Must be an integer between 0 and 65535.`, + "port" + ); + } + return port; +} + +/** + * Build the Hono application that backs the sidecar. + * + * We expose three concerns: + * - CORS: open to `*` because dev stacks send from arbitrary `localhost:*` + * origins (Vite, Next, Astro, etc.). The sidecar binds to localhost by + * default, so this isn't a security regression. + * - Ingest: `POST /stream` and `POST /api/.../envelope/` accept envelope + * bodies. We hand the raw buffer to `pushToSpotlightBuffer`, which + * decompresses (gzip/deflate/br) and decodes lazily. + * - Subscribe: `GET /stream` opens an SSE stream of every envelope that + * enters the buffer, including those buffered before the subscriber + * connected (so a freshly-opened Spotlight overlay can still see + * recent events). + */ +function buildSidecarApp( + spotlightBuffer: ReturnType, + onEnvelope: (contentType: string, data: Buffer) => void +): Hono { + const app = new Hono(); + + // Open CORS — sidecar binds to localhost; this is a dev-only tool. + app.use( + "*", + cors({ + origin: "*", + allowMethods: ["GET", "POST", "OPTIONS"], + allowHeaders: ["Content-Type", "Content-Encoding", "User-Agent"], + }) + ); + + /** Health check — useful for `curl` and for SDKs that probe before sending. */ + app.get("/health", (c) => c.text("OK")); + + /** Ingest handler shared by `/stream` and `/api/.../envelope/`. */ + const ingest = async (c: { + req: { + arrayBuffer: () => Promise; + header: (name: string) => string | undefined; + }; + body: (data: null, status: number) => Response; + }) => { + const arrayBuf = await c.req.arrayBuffer(); + const body = Buffer.from(arrayBuf); + const contentType = c.req.header("content-type") ?? ""; + const contentEncoding = c.req.header("content-encoding") as + | "gzip" + | "deflate" + | "br" + | undefined; + const userAgent = c.req.header("user-agent"); + + const container = pushToSpotlightBuffer({ + spotlightBuffer, + body, + encoding: contentEncoding, + contentType, + userAgent, + }); + + if (container) { + // Surface the decoded payload to the tail/subscribe pipeline. We push + // the (potentially decompressed) raw body so SSE subscribers don't + // have to redo the work and so the tail formatter can rely on a + // single representation. + onEnvelope(container.getContentType(), container.getData()); + } + + return c.body(null, 204); + }; + + app.post("/stream", ingest); + // SDK-style envelope ingestion: /api/{projectId}/envelope/?... + app.post("/api/:projectId/envelope/", ingest); + app.post("/api/:projectId/envelope", ingest); + + /** + * SSE stream — Spotlight overlay / UI clients connect here to receive a + * live feed of envelopes. Each event is emitted as a JSON object with + * the content type and base64-encoded body. + */ + app.get("/stream", (c) => + streamSSE(c, async (stream) => { + // Tie the subscriber lifetime to the response stream. We unsubscribe + // when the client disconnects so the buffer doesn't leak readers. + const readerId = spotlightBuffer.subscribe((container) => { + const payload: EventPayload = { + contentType: container.getContentType(), + data: container.getData().toString("base64"), + }; + stream + .writeSSE({ + event: "envelope", + data: JSON.stringify(payload), + }) + .catch((err: unknown) => { + log.debug( + `SSE write failed (client likely disconnected): ${ + err instanceof Error ? err.message : String(err) + }` + ); + }); + }); + + stream.onAbort(() => { + spotlightBuffer.unsubscribe(readerId); + }); + + // Keep the stream open until the client disconnects. + // hono/streaming resolves the promise on abort. + await new Promise((resolve) => { + stream.onAbort(() => resolve()); + }); + }) + ); + + return app; +} + +/** + * Resolve the human label for an envelope. + * + * - When the envelope parses cleanly, we use the joined item types + * (e.g. `event+attachment`). + * - Otherwise we fall back to the content type, with a friendly alias + * for the canonical Sentry envelope mime type. + */ +function describeEnvelope(contentType: string, eventTypes: string[]): string { + if (eventTypes.length > 0) { + return eventTypes.join("+"); + } + if (contentType === "application/x-sentry-envelope") { + return "envelope"; + } + return contentType; +} + +/** + * Format a freshly received envelope for terminal output. + * + * Keeps the formatting deliberately minimal — this is a tail, not a UI. + * If users want rich rendering, they can point the Spotlight overlay at + * `http://localhost:/stream` instead. + */ +function formatTailLine(contentType: string, eventTypes: string[]): string { + const ts = new Date().toISOString().slice(11, 23); // HH:MM:SS.sss + const label = describeEnvelope(contentType, eventTypes); + return `${muted(ts)} ${cyan("•")} ${bold(label)}`; +} + +/** + * Install signal handlers that stop the HTTP server on Ctrl-C / SIGTERM. + * + * Returns a Promise that resolves when shutdown is complete. The command + * awaits this so the generator stays alive until the user interrupts. + */ +function waitForShutdown(server: Server): Promise { + return new Promise((resolve) => { + let shuttingDown = false; + const shutdown = (signal: NodeJS.Signals) => { + if (shuttingDown) { + // Second signal — force exit. Bypasses the `process.exit` hook so + // we don't dangle on stuck connections. + process.exit(0); + } + shuttingDown = true; + log.info(`Received ${signal}, shutting down...`); + server.close(() => resolve()); + // Force-close keep-alive connections so we don't wait on long-lived + // SSE subscribers. + if (typeof server.closeAllConnections === "function") { + server.closeAllConnections(); + } + }; + + process.once("SIGINT", () => shutdown("SIGINT")); + process.once("SIGTERM", () => shutdown("SIGTERM")); + }); +} + +export const localCommand = buildCommand({ + docs: { + brief: "Run a local Spotlight sidecar to capture dev SDK events", + fullDescription: + "Start a local Spotlight-compatible sidecar server.\n\n" + + "Spotlight is Sentry for Development — it gives you a live view of\n" + + "errors, traces, and logs emitted by Sentry SDKs in your dev stack.\n" + + "This command runs a minimal Hono server that ingests envelopes\n" + + "from any Sentry SDK and tails them to your terminal.\n\n" + + "Endpoints:\n" + + " POST /stream — Spotlight ingest\n" + + " POST /api/{projectId}/envelope/ — Sentry SDK ingest\n" + + " GET /stream — SSE feed (for the Spotlight overlay)\n" + + " GET /health — health check\n\n" + + "Configure your SDK to send to the sidecar with a localhost DSN, e.g.:\n" + + " SENTRY_DSN=http://public@localhost:8969/1\n\n" + + "Press Ctrl-C to stop the server.", + }, + // No `output` config: this is a long-running server, not a data command. + // We write progress directly to stderr via the logger. + parameters: { + flags: { + port: { + kind: "parsed", + parse: parsePort, + brief: `Port to listen on (default ${DEFAULT_PORT})`, + default: String(DEFAULT_PORT), + }, + host: { + kind: "parsed", + parse: String, + brief: "Hostname to bind to (default localhost)", + default: "localhost", + }, + open: { + kind: "boolean", + brief: "Open the sidecar SSE URL in a browser", + default: false, + }, + quiet: { + kind: "boolean", + brief: "Suppress per-envelope tail output", + default: false, + }, + }, + aliases: { + p: "port", + H: "host", + o: "open", + q: "quiet", + }, + }, + // No auth required — this is a local-only dev server. + auth: false, + async *func(this: SentryContext, flags: LocalFlags) { + const buffer = createSpotlightBuffer(BUFFER_SIZE); + + // Tail subscriber: prints a one-line summary for each envelope. We + // route through the logger (stderr) rather than stdout so the tail + // doesn't pollute pipelines that consume the CLI's stdout, and so it + // honors `--log-level` / `SENTRY_LOG_LEVEL` like the rest of the CLI. + // Skipped entirely when `--quiet` is set. + if (!flags.quiet) { + buffer.subscribe((container) => { + const types = container.getEventTypes() ?? []; + log.info(formatTailLine(container.getContentType(), types)); + }); + } + + const app = buildSidecarApp(buffer, () => { + // Tail output is driven by the buffer subscriber above so we don't + // have to repeat the formatting work. This callback is a no-op for + // now; future hooks (e.g. metrics, file logging) can plug in here. + }); + + // `serve` returns a Node http.Server — we use it for graceful shutdown. + const server = serve({ + fetch: app.fetch, + port: flags.port, + hostname: flags.host, + }) as unknown as Server; + + const url = `http://${flags.host}:${flags.port}`; + log.info(`Spotlight sidecar listening on ${bold(url)}`); + log.info(` ${muted("Ingest:")} POST ${url}/stream`); + log.info(` ${muted("Stream:")} GET ${url}/stream`); + log.info(` ${muted("Health:")} GET ${url}/health`); + log.info(`Point your SDK at ${bold(`${url}/stream`)} or use a DSN like:`); + log.info( + ` ${muted(`SENTRY_DSN=${url.replace("http://", "http://public@")}/1`)}` + ); + log.info("Press Ctrl-C to stop."); + + if (flags.open) { + // Best-effort — never blocks shutdown. + await openOrShowUrl(`${url}/stream`); + } + + // Block until the user interrupts. We don't yield any CommandOutput + // because there's no structured payload — this command is a server. + await waitForShutdown(server); + log.info("Sidecar stopped."); + }, +}); From 3beec45f04c81fd0ffb051123865f9c1b2317544 Mon Sep 17 00:00:00 2001 From: Aditya Mathur <57684218+MathurAditya724@users.noreply.github.com> Date: Tue, 5 May 2026 20:13:36 +0000 Subject: [PATCH 02/46] feat(local): pretty-print errors, transactions, and logs in tail output MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace the minimal 'timestamp • type' one-liner with rich formatted output that shows actual event content: error type/message with stack location, transaction name/op/duration/span count, and log messages with attributes. Uses the CLI's own color system since Spotlight's humanFormatters aren't publicly exported from the package. --- AGENTS.md | 83 +---- .../skills/sentry-cli/references/dashboard.md | 2 +- .../skills/sentry-cli/references/event.md | 2 +- .../skills/sentry-cli/references/explore.md | 2 +- .../skills/sentry-cli/references/issue.md | 4 +- .../skills/sentry-cli/references/log.md | 2 +- .../skills/sentry-cli/references/span.md | 2 +- .../skills/sentry-cli/references/trace.md | 4 +- src/commands/local.ts | 309 ++++++++++++++++-- 9 files changed, 298 insertions(+), 112 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 1bfd4b629..5050a2838 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1001,86 +1001,11 @@ mock.module("./some-module", () => ({ ### Architecture - -* **@sentry/api SDK integration: type wrapping pattern and pagination helpers**: @sentry/api SDK integration: wrap SDK types at \`src/lib/api/\*.ts\` boundaries with \`as unknown as SentryX\` casts; never leak SDK types to commands. Wrappers in \`src/types/sentry.ts\` use \`Partial\ & RequiredCore\`. \`src/lib/region.ts\` imports \`retrieveAnOrganization\` directly to avoid circular dep with api-client. \`unwrapResult\`/\`unwrapPaginatedResult\` MUST stay CLI-owned — SDK versions throw plain \`Error\`, breaking the 'all errors are CliError subclasses' invariant (see also 365e4299). Body-shape casts use \`Parameters\\[0]\["body"]\`. - - -* **apiRequestToRegion/rawApiRequest options shape — no timeout/signal/headers on the typed API**: \`ApiRequestOptions\\` in \`src/lib/api/infrastructure.ts\` has only \`{ method, body, params, schema }\`. \`rawApiRequest\` adds \`headers?\`; neither exposes \`timeout\`/\`signal\`. Call sites pass \`(url, init: RequestInit)\` to authenticated fetch — never a \`Request\` (only @sentry/api SDK does). \`apiRequestToRegion\` auto-sets JSON Content-Type and \`JSON.stringify\`s body; \`rawApiRequest\` preserves string bodies, only sets JSON Content-Type when body is object and caller didn't provide one. 204/205 throw \`ApiError\` rather than crashing on \`response.json()\` — bulk-mutate callers must catch. - - -* **Completion fast-path skips Sentry SDK via SENTRY\_CLI\_NO\_TELEMETRY and SQLite telemetry queue**: Shell completions (\`\_\_complete\`) set \`SENTRY\_CLI\_NO\_TELEMETRY=1\` in \`bin.ts\` before any imports, skipping \`createTracedDatabase\` and avoiding \`@sentry/node-core/light\` load (~85ms). Completion timing queued to \`completion\_telemetry\_queue\` SQLite table (~1ms); normal runs drain via \`DELETE FROM ... RETURNING\` and emit as \`Sentry.metrics.distribution\`. Achieves ~60ms dev / ~140ms CI within 200ms e2e budget. - - -* **Fuzzy recovery auto-resolves dash/underscore slug mismatches without original-slug fallback**: Display-name project input (contains spaces) skips slug lookup, goes to name-based fuzzy search across four resolution sites: \`resolveProjectBySlug\`, \`resolveOrgProjectTarget\` (project-search), \`org-list.ts#handleProjectSearch\`, \`project/list.ts#handleProjectNotFound\`. \`parseOrgProjectArg\` detects spaces via \`looksLikeDisplayName()\` and sets \`originalSlug\` on \`project-search\`; sites check \`isDisplayName = originalSlug !== undefined\` and skip \`findProjectsBySlug\` (404s on URL-encoded spaces), going directly to \`triageProjectNotFound\` → \`findSimilarProjectsAcrossOrgs\`. \*\*Critical\*\*: recursive fuzzy recovery calls must NOT pass \`originalSlug\` — otherwise the recovered slug also skips lookup, causing infinite skip→empty→not-found loop. - - -* **Project cache is org-scoped with three key formats and three population paths**: \`project\_cache\` SQLite table uses three key shapes: \`{orgId}:{projectId}\` (DSN resolution), \`dsn:{publicKey}\` (DSN without orgId), \`list:{orgSlug}/{projectSlug}\` (batch from API). Helpers: \`getCachedProject\`, \`getCachedProjectByDsnKey\`, \`getCachedProjectsForOrg\` (completions), \`getCachedProjectBySlug\` (queries all three shapes for hot-path slug lookups; used by \`fetchProjectId\` to skip \`GET /projects/{org}/{project}/\`). Population paths: DSN resolution in resolve-target.ts, \`listProjects()\` batch via \`cacheProjectsForOrg\`, \`fetchProjectId\` seeds on API success. Resolution errors use live API via \`findSimilarProjectsAcrossOrgs\` — no cross-org cache search. - - -* **Sentry API: events require org+project, issues have legacy global endpoint**: Sentry API scoping/auth quirks: (1) Events require org+project (\`/projects/{org}/{project}/events/{id}/\`); issues use legacy global \`/api/0/issues/{id}/\`; traces need only org. Cross-project search via Discover \`/organizations/{org}/events/?query=id:{eventId}\`. (2) \`/users/me/\` returns 403 for OAuth tokens — use \`/auth/\` instead (all token types, control silo). \`getControlSiloUrl()\` routes; \`SentryUserSchema\` uses \`.passthrough()\` since \`/auth/\` only requires \`id\`. (3) Chunk upload endpoint returns camelCase (\`chunkSize\`, \`chunksPerRequest\`, \`maxRequestSize\`, \`hashAlgorithm\`); \`AssembleResponse\` also camelCase — exception to snake\_case convention. - - -* **Sentry CLI authenticated fetch architecture with response caching**: Authenticated fetch (\`createAuthenticatedFetch\` in \`src/lib/sentry-client.ts\`): auth headers, 30s \`REQUEST\_TIMEOUT\_MS\`, retry max 2, 401 refresh, span tracing. Dual input: SDK \`Request\` vs \`(url, init)\`. \`buildAttemptFactory\` yields fresh \`(input, init)\` per attempt; \`Request\` clones; \`FormData\`/\`Blob\`/\`URLSearchParams\` pass through. Only bare \`ReadableStream\` needs materialization. Do NOT materialize FormData — strips multipart boundary. Internal aborts tagged \`INTERNAL\_TIMEOUT\_MARKER\` Symbol; last attempt throws \`TimeoutError\`. Per-endpoint \`ENDPOINT\_TIMEOUT\_OVERRIDES\` (e.g. \`/autofix/\` 120s). Response cache: \`http-cache-semantics\` RFC 7234 at \`~/.sentry/cache/responses/\`; GET 2xx only. On 4xx/5xx, \`apiRequestToRegion\` attaches allow-listed response headers to Sentry scope as \`api\_response\_headers\` context. Cache hit invisibility solved via module-level \`lastCacheHitAgeMs\` (set on hit, cleared per-call); \`src/lib/cache-hint.ts\` provides \`formatCacheHint()\`/\`appendCacheHint()\`, wired in \`buildCommand\` only when generator returns \`CommandReturn\` (bare \`return;\` paths skip). - - -* **Sentry CLI resolve-target cascade has 5 priority levels with env var support**: resolve-target.ts cascade has 5 priority levels: (1) Explicit CLI flags, (2) SENTRY\_ORG/SENTRY\_PROJECT env vars, (3) SQLite config defaults, (4) DSN auto-detection, (5) Directory name inference. SENTRY\_PROJECT supports combo notation \`org/project\` — when used, SENTRY\_ORG is ignored. If combo parse fails (e.g. \`org/\`), the entire value is discarded. \`resolveFromEnvVars()\` helper is injected into all four resolution functions. - -### Decision - - -* **Issue list global limit with fair per-project distribution and representation guarantees**: \`issue list --limit\` is a global total across all detected projects. \`fetchWithBudget\` Phase 1 divides evenly, Phase 2 redistributes surplus via cursor resume. \`trimWithProjectGuarantee\` ensures ≥1 issue per project before filling remaining slots. JSON output wraps in \`{ data, hasMore }\` with optional \`errors\` array. Compound cursor (pipe-separated) enables \`-c last\` for multi-target pagination, keyed by sorted target fingerprint. - - -* **Prefer dedicated SQLite tables + migrations over metadata KV for non-trivial caches**: Prefer dedicated SQLite tables + migrations over \`metadata\` KV for non-trivial caches. Schema migrations are cheap — don't shoehorn structured caches into \`metadata\` with dotted-prefix keys. Dedicated tables give clearer schema, proper indexes, simpler bulk-clear, no prefix collisions. \`metadata\` KV is fine for small scalars (defaults.\*, install.\*). Example: \`issue\_org\_cache\` (schema v15) replaced \`metadata\` keys \`issue\_org.{numericId}\`. Migration pattern: bump \`CURRENT\_SCHEMA\_VERSION\`, add \`EXPECTED\_TABLES.foo\`, add \`if (currentVersion < N) db.exec(EXPECTED\_TABLES.foo)\`. HTTP response cache (URL+headers, short TTLs) can't answer structural questions like 'which org owns issue 123?' — use dedicated tables for structural/mapping questions, HTTP cache for content. - - -* **Top-level --help stays terse Stricli output, not branded help**: \`sentry --help\` and \`sentry -h\` MUST render Stricli's terse default template, NOT the branded help (Flags + Environment Variables sections). Agents parse \`--help\` output and branding wastes tokens. Branded help is reserved for human discovery paths: \`sentry\` (no-args, via \`defaultCommand: "help"\`) and \`sentry help\`. Do NOT add interception logic in \`src/cli.ts\` to rewrite \`--help\` → \`help\`. TTY/agent detection is not worth the complexity — agents have skills documentation; humans get the footer hint pointing to \`sentry help\`. Subcommand help (e.g. \`sentry issue --help\`) is also left to Stricli for command-specific flag rendering. + +* **sentry local command uses getParsedEnvelope() for envelope item dispatch**: \`src/commands/local.ts\` receives raw Sentry envelopes via a Hono HTTP server, pushes them into a Spotlight \`MessageBuffer\\`, then in the subscriber calls \`container.getParsedEnvelope()\` to get \`\[header, items\[]]\`. Each item's \`\[itemHeader, itemPayload]\` is dispatched: \`event\`/\`error\` types → \`formatErrorItem\`, \`transaction\` → \`formatTransactionItem\`, \`log\` → \`formatLogItem\` (returns multiple lines), all others fall back to a minimal \`timestamp • type\` line. Formatters are inline in \`local.ts\` and use the CLI's color helpers. ### Gotcha - -* **@sentry/api SDK can return non-array data for empty/edge responses**: \`@sentry/api\` SDK (in \`node\_modules/@sentry/api/dist/index.js\`) returns \`data = {}\` (not \`\[]\`) when response body is empty, has \`Content-Length: 0\`, or status 204; and returns a \`ReadableStream\` when \`Content-Type\` is missing. \`unwrapResult\` from \`src/lib/api/infrastructure.ts\` returns \`data\` as-is, and \`as unknown as SentryX\[]\` casts silently lie. Always guard array-typed SDK results with \`Array.isArray(data)\` before \`.map()\` — applied in \`listOrganizationsInRegion\` (CLI-1CQ). Self-hosted instances behind reverse proxies (nginx, Cloudflare, WAFs) commonly trigger this by stripping bodies or wrapping responses. Throw a descriptive \`ApiError\` on mismatch rather than letting \`TypeError: x.map is not a function\` bubble up minified. - - -* **Bun bytecode: true crashes esbuild→compile ESM bundles (Bun 1.3.11)**: Bun build flags for compiled CLI (\`script/build.ts\`): (1) Do NOT enable \`bytecode: true\` with esbuild→\`Bun.build({ compile })\` pipeline. Still broken on Bun 1.3.13 — crashes \`TypeError: Expected CommonJS module to have a function wrapper\` at entry.instantiate (esbuild emits ESM; bytecode loader mis-caches as CJS). Exit 0, no output. Upstream: oven-sh/bun#21097, #23490. (2) Pass \`autoloadDotenv: false\` and \`autoloadBunfig: false\` — otherwise user's \`.env\`/\`bunfig.toml\` silently injects into \`process.env\` (e.g. Next.js \`.env.local\` could override stored OAuth token). Shell env vars still work; suggest direnv for dir-scoped vars. - - -* **dist/bin.cjs runtime Node version check must match engines.node**: \`engines.node >=22.12\` matches Astro 6 floor. CI builds matrix \`\["22","24"]\`; docs jobs pin \`actions/setup-node@v6\` with \`node-version: "24"\` after \`setup-bun\`. The npm package's \`dist/bin.cjs\` (from \`script/bundle.ts\`) contains an inline Node guard that MUST match \`engines.node\`. Simple \`parseInt(process.versions.node) < 22\` misses 22.0.0–22.11.x — use explicit major+minor: \`let v=process.versions.node.split('.').map(Number);if(v\[0]<22||(v\[0]===22&\&v\[1]<12)){...}\`. When bumping, update BIN\_WRAPPER string AND error message in lockstep. Without \`engine-strict=true\`, npm only warns — the runtime guard is real enforcement. - - -* **Making clearAuth() async breaks model-based tests — use non-async Promise\ return instead**: Making \`clearAuth()\` \`async\` breaks fast-check model-based tests — real async yields during \`asyncModelRun\` cause \`createIsolatedDbContext\` cleanup to interleave. Keep non-async; return \`clearResponseCache().catch(...)\` directly. Model-based tests also need explicit timeouts (e.g., \`30\_000\`) — Bun's default 5s causes false failures during shrinking. - - -* **script/generate-api-schema.ts regex is brittle against SDK bundler output changes**: \`script/generate-api-schema.ts\` parses \`node\_modules/@sentry/api/dist/index.js\` with a regex (\`/var (\w+) = \\(options\S\*\\) => \\(options\S\*client \\?\\? client\\)\\.(\w+)\\(/g\`) to map SDK function names to URL+method pairs, producing \`src/generated/api-schema.json\`. If the SDK changes its generator's bundle format (e.g., switches to \`const\`, arrow vs function, different client-selection pattern), schema generation silently produces empty \`fn\` fields. When bumping \`@sentry/api\`, verify \`sentry schema\` output still populates function names. \`src/generated/api-schema.json\` is gitignored — regenerates on every dev/build/typecheck via \`bun run generate:schema\`. - - -* **Source Map v3 spec allows null entries in sources array**: The Source Map v3 spec allows \`null\` entries in the \`sources\` array, and bundlers like esbuild actually produce them. Any code iterating over \`sources\` and calling string methods (e.g., \`.replaceAll()\`) must guard against null: \`map.sources.map((s) => typeof s === "string" ? s.replaceAll("\\\\", "/") : s)\`. Without the guard, \`null.replaceAll()\` throws \`TypeError\`. This applies to \`src/lib/sourcemap/debug-id.ts\` and any future sourcemap manipulation code. - - -* **Starlight 0.33+ route data moved from Astro.props to Astro.locals.starlightRoute**: Starlight 0.33+ / Astro 6 docs migration: (1) Route data moved from \`Astro.props\` to \`Astro.locals.starlightRoute\` — old \`Astro.props.sidebar\` is \`undefined\`. Field rename: \`slug\` → \`id\`. Import types via \`@astrojs/starlight/route-data\`. Built-in children (SiteTitle, Search, SocialIcons) take no props. \`starlight.social\` is array-form. (2) Content collections must migrate to Content Layer API: rename \`src/content/config.ts\` → \`src/content.config.ts\`, use \`docsLoader()\` + \`docsSchema()\`. Landing-page detection: \`id === ""\` (\`normalizeIndexSlug\` maps \`"index"\` → \`""\`). - -### Pattern - - -* **Bun global installs use .bun path segment for detection**: Bun global installs place scripts under \`~/.bun/install/global/node\_modules/\`. In \`detectPackageManagerFromPath()\`, check \`segments.includes('.bun')\` before npm fallback. Order: \`.pnpm\` → pnpm, \`.bun\` → bun, other \`node\_modules\` → npm. Yarn classic shares npm layout so falls through — acceptable because path detection is \*\*fallback\*\* after subprocess calls (which identify yarn correctly). Path detection must NOT override stored DB info, only serve as fallback when subprocess fails (e.g., Windows \`.cmd\` ENOENT). - - -* **Evict-then-read pattern: return cacheEvicted flag from helpers that clear cache on 404**: When a helper function transparently evicts a stale cache entry on 404 and falls back to an unscoped call, callers holding the now-stale cached value will let it win \`??\` chains. Fix: helper must return \`{ result, cacheEvicted }\` so callers compute \`effectiveCachedValue = cacheEvicted ? null : cachedValue\` before the \`??\` fallback, and re-cache the freshly-derived value. Applied in \`fetchIssueByNumericId\` in \`src/commands/issue/utils.ts\` — both \`resolveNumericIssue\` and \`resolveShareIssue\` consume the flag. A local cached variable outliving its DB entry is the common shape of this bug; always audit post-eviction read paths. - - -* **Non-essential DB cache writes should be guarded with try-catch**: Non-essential DB cache writes (e.g., \`setUserInfo()\`, \`setInstallInfo()\`) must be wrapped in try-catch so a broken/read-only DB doesn't crash a command whose primary operation succeeded. Pattern: \`try { setInstallInfo(...) } catch { log.debug(...) }\`. In login.ts, \`getCurrentUser()\` failure after token save must not block auth — log warning, continue. In upgrade.ts, \`setInstallInfo\` after legacy detection is guarded same way. Exception: \`getUserRegions()\` failure should \`clearAuth()\` and fail hard (indicates invalid token). This is enforced by BugBot reviews — any \`setInstallInfo\`/\`setUserInfo\` call outside setup.ts's \`bestEffort()\` wrapper needs its own try-catch. - - -* **Sentry CLI command docs are auto-generated from Stricli route tree with CI freshness check**: Sentry CLI command docs are auto-generated from Stricli route tree: Docs in \`docs/src/content/docs/commands/\*.md\` and skill files in \`plugins/sentry-cli/skills/sentry-cli/references/\*.md\` are generated via \`bun run generate:docs\`. Content between \`\\` markers is regenerated; hand-written examples go in \`docs/src/fragments/commands/\`. CI checks \`check:command-docs\` and \`check:skill\` fail if stale. Run generators after changing command parameters/flags/docs. - - -* **Stricli buildCommand output config injects json flag into func params**: Stricli command gotchas: (1) In \`func()\` handlers use \`this.stdout\`/\`this.stderr\` directly — NOT \`this.process.stdout\`. \`SentryContext\` has \`process\` and \`stdout\`/\`stderr\` as separate top-level properties; test mocks omit full \`process\` so \`this.process.stdout\` throws \`TypeError\` at runtime (TS doesn't catch). (2) \`output: { json: true, human: formatFn }\` auto-injects \`--json\`/\`--fields\` flags — type flags explicitly (\`flags: { json?: boolean }\`). Commands with interactive side effects (prompts, QR codes) should check \`flags.json\` and skip. (3) Route maps with \`defaultCommand\` blend the default command's flags into subcommand completions — completion tests must track \`hasDefaultCommand\` and skip strict subcommand-matching. - - -* **Token-type classification via literal prefix match (classifySentryToken)**: Token-type classification via literal prefix match: \`src/lib/token-type.ts\` \`classifySentryToken(token)\` returns \`'org-auth-token'\` (\`sntrys\_\` prefix), \`'user-auth-token'\` (\`sntryu\_\` prefix), or \`'oauth-or-legacy'\`. Case-sensitive \`startsWith\` matches Sentry backend's \`SENTRY\_ORG\_AUTH\_TOKEN\_PREFIX\`. Use to short-circuit commands where a token type is semantically invalid (e.g. \`whoami\` with org token — \`/auth/\` rejects \`sntrys\_\`) before a confusing API failure. \`getAuthToken()\` from \`db/auth\` returns the effective token (env + DB fallback). - -### Preference - - -* **PR workflow: address review comments, resolve threads, wait for CI**: PR workflow: (1) wait for CI; (2) check unresolved comments via \`gh api repos/.../pulls/N/comments\`; (3) fix in follow-up commits (NEVER amend a pushed commit without explicit user request + force push); (4) reply explaining fix; (5) resolve thread via \`gh api graphql resolveReviewThread\`; (6) push + re-check CI. BugBot/Seer/Warden/Cursor post new comments per-commit and often catch bugs in fix commits — re-check after each push. Dispatch a subagent review before declaring merge-ready. Branches: \`fix/\*\` or \`feat/\*\`. Style: \`Array.from(set)\` over spreads; 'allowlist' not 'whitelist'; \`arr.at(-1)\` over index math. Reviewer questions may be inquiries — confirm intent before changing. After reverts/changes affecting PR scope, update the PR description to match. + +* **@spotlightjs/spotlight exports only two paths — no formatter/parser access**: The \`@spotlightjs/spotlight\` package's \`exports\` map exposes only \`.\` (main server) and \`./sdk\` (buffer API). Formatter registries (\`humanFormatters\`, \`applyFormatter\`) and parser helpers (\`isErrorEvent\`, type guards) live under \`dist/server/formatters/\` and \`dist/server/parser/\` but are not in \`exports\`. Bun's strict module resolution blocks deep \`dist/\` imports at runtime. Workaround: write inline formatters using the CLI's own color system (\`muted\`, \`bold\`, \`cyan\`, etc.) following the same pattern as Spotlight's human formatters. diff --git a/plugins/sentry-cli/skills/sentry-cli/references/dashboard.md b/plugins/sentry-cli/skills/sentry-cli/references/dashboard.md index a6dd162e9..799575e39 100644 --- a/plugins/sentry-cli/skills/sentry-cli/references/dashboard.md +++ b/plugins/sentry-cli/skills/sentry-cli/references/dashboard.md @@ -42,7 +42,7 @@ View a dashboard - `-w, --web - Open in browser` - `-f, --fresh - Bypass cache, re-detect projects, and fetch fresh data` - `-r, --refresh - Auto-refresh interval in seconds (default: 60, min: 10)` -- `-t, --period - Time range: "7d", "2026-03-01..2026-04-01", ">=2026-03-01"` +- `-t, --period - Time range: "7d", "2026-04-01..2026-05-01", ">=2026-04-01"` **Examples:** diff --git a/plugins/sentry-cli/skills/sentry-cli/references/event.md b/plugins/sentry-cli/skills/sentry-cli/references/event.md index 43c3e7c07..05dd3fbd1 100644 --- a/plugins/sentry-cli/skills/sentry-cli/references/event.md +++ b/plugins/sentry-cli/skills/sentry-cli/references/event.md @@ -37,7 +37,7 @@ List events for an issue - `-n, --limit - Number of events (1-1000) - (default: "25")` - `-q, --query - Search query (Sentry search syntax)` - `--full - Include full event body (stacktraces)` -- `-t, --period - Time range: "7d", "2026-03-01..2026-04-01", ">=2026-03-01" - (default: "7d")` +- `-t, --period - Time range: "7d", "2026-04-01..2026-05-01", ">=2026-04-01" - (default: "7d")` - `-f, --fresh - Bypass cache, re-detect projects, and fetch fresh data` - `-c, --cursor - Navigate pages: "next", "prev", "first" (or raw cursor string)` diff --git a/plugins/sentry-cli/skills/sentry-cli/references/explore.md b/plugins/sentry-cli/skills/sentry-cli/references/explore.md index 7c58e148d..91c0c05c2 100644 --- a/plugins/sentry-cli/skills/sentry-cli/references/explore.md +++ b/plugins/sentry-cli/skills/sentry-cli/references/explore.md @@ -21,7 +21,7 @@ Query aggregate event data (Explore) - `-q, --query - Search query (Sentry search syntax)` - `-s, --sort - Sort field (prefix with - for desc, e.g., "-count()")` - `-n, --limit - Number of rows (1-1000) - (default: "25")` -- `-t, --period - Time range: "7d", "2026-03-01..2026-04-01", ">=2026-03-01" - (default: "24h")` +- `-t, --period - Time range: "7d", "2026-04-01..2026-05-01", ">=2026-04-01" - (default: "24h")` - `-f, --fresh - Bypass cache, re-detect projects, and fetch fresh data` - `-c, --cursor - Navigate pages: "next", "prev", "first" (or raw cursor string)` diff --git a/plugins/sentry-cli/skills/sentry-cli/references/issue.md b/plugins/sentry-cli/skills/sentry-cli/references/issue.md index 03beef4ab..83a254cc1 100644 --- a/plugins/sentry-cli/skills/sentry-cli/references/issue.md +++ b/plugins/sentry-cli/skills/sentry-cli/references/issue.md @@ -19,7 +19,7 @@ List issues in a project - `-q, --query - Search query (Sentry syntax, implicit AND, no OR operator)` - `-n, --limit - Maximum number of issues to list - (default: "25")` - `-s, --sort - Sort by: date, new, freq, user - (default: "date")` -- `-t, --period - Time range: "7d", "2026-03-01..2026-04-01", ">=2026-03-01" - (default: "90d")` +- `-t, --period - Time range: "7d", "2026-04-01..2026-05-01", ">=2026-04-01" - (default: "90d")` - `-c, --cursor - Pagination cursor (use "next" for next page, "prev" for previous)` - `--compact - Single-line rows for compact output (auto-detects if omitted)` - `-f, --fresh - Bypass cache, re-detect projects, and fetch fresh data` @@ -87,7 +87,7 @@ List events for a specific issue - `-n, --limit - Number of events (1-1000) - (default: "25")` - `-q, --query - Search query (Sentry search syntax)` - `--full - Include full event body (stacktraces)` -- `-t, --period - Time range: "7d", "2026-03-01..2026-04-01", ">=2026-03-01" - (default: "7d")` +- `-t, --period - Time range: "7d", "2026-04-01..2026-05-01", ">=2026-04-01" - (default: "7d")` - `-f, --fresh - Bypass cache, re-detect projects, and fetch fresh data` - `-c, --cursor - Navigate pages: "next", "prev", "first" (or raw cursor string)` diff --git a/plugins/sentry-cli/skills/sentry-cli/references/log.md b/plugins/sentry-cli/skills/sentry-cli/references/log.md index 05fb7c1d3..fbfe59c81 100644 --- a/plugins/sentry-cli/skills/sentry-cli/references/log.md +++ b/plugins/sentry-cli/skills/sentry-cli/references/log.md @@ -19,7 +19,7 @@ List logs from a project - `-n, --limit - Number of log entries (1-1000) - (default: "100")` - `-q, --query - Filter query (e.g., "level:error", "project:backend", "project:[a,b]")` - `-f, --follow - Stream logs (optionally specify poll interval in seconds)` -- `-t, --period - Time range: "7d", "2026-03-01..2026-04-01", ">=2026-03-01"` +- `-t, --period - Time range: "7d", "2026-04-01..2026-05-01", ">=2026-04-01"` - `-s, --sort - Sort order: "newest" (default) or "oldest" - (default: "newest")` - `--fresh - Bypass cache, re-detect projects, and fetch fresh data` diff --git a/plugins/sentry-cli/skills/sentry-cli/references/span.md b/plugins/sentry-cli/skills/sentry-cli/references/span.md index 057f34877..f503d51e8 100644 --- a/plugins/sentry-cli/skills/sentry-cli/references/span.md +++ b/plugins/sentry-cli/skills/sentry-cli/references/span.md @@ -19,7 +19,7 @@ List spans in a project or trace - `-n, --limit - Number of spans (<=1000) - (default: "25")` - `-q, --query - Filter spans (e.g., "op:db", "project:backend", "project:[cli,api]")` - `-s, --sort - Sort order: date, duration - (default: "date")` -- `-t, --period - Time range: "7d", "2026-03-01..2026-04-01", ">=2026-03-01" - (default: "7d")` +- `-t, --period - Time range: "7d", "2026-04-01..2026-05-01", ">=2026-04-01" - (default: "7d")` - `-f, --fresh - Bypass cache, re-detect projects, and fetch fresh data` - `-c, --cursor - Navigate pages: "next", "prev", "first" (or raw cursor string)` diff --git a/plugins/sentry-cli/skills/sentry-cli/references/trace.md b/plugins/sentry-cli/skills/sentry-cli/references/trace.md index 92611d6d5..b7ea69603 100644 --- a/plugins/sentry-cli/skills/sentry-cli/references/trace.md +++ b/plugins/sentry-cli/skills/sentry-cli/references/trace.md @@ -19,7 +19,7 @@ List recent traces in a project - `-n, --limit - Number of traces (1-1000) - (default: "25")` - `-q, --query - Search query (Sentry search syntax)` - `-s, --sort - Sort by: date, duration - (default: "date")` -- `-t, --period - Time range: "7d", "2026-03-01..2026-04-01", ">=2026-03-01" - (default: "7d")` +- `-t, --period - Time range: "7d", "2026-04-01..2026-05-01", ">=2026-04-01" - (default: "7d")` - `-f, --fresh - Bypass cache, re-detect projects, and fetch fresh data` - `-c, --cursor - Navigate pages: "next", "prev", "first" (or raw cursor string)` @@ -91,7 +91,7 @@ View logs associated with a trace **Flags:** - `-w, --web - Open trace in browser` -- `-t, --period - Time range: "7d", "2026-03-01..2026-04-01", ">=2026-03-01" - (default: "14d")` +- `-t, --period - Time range: "7d", "2026-04-01..2026-05-01", ">=2026-04-01" - (default: "14d")` - `-n, --limit - Number of log entries (<=1000) - (default: "100")` - `-q, --query - Filter query (e.g., "level:error", "project:backend", "project:[a,b]")` - `-s, --sort - Sort order: "newest" (default) or "oldest" - (default: "newest")` diff --git a/src/commands/local.ts b/src/commands/local.ts index 02791b951..c0d0b1d9a 100644 --- a/src/commands/local.ts +++ b/src/commands/local.ts @@ -42,7 +42,15 @@ import type { SentryContext } from "../context.js"; import { openOrShowUrl } from "../lib/browser.js"; import { buildCommand, numberParser } from "../lib/command.js"; import { ValidationError } from "../lib/errors.js"; -import { bold, cyan, muted } from "../lib/formatters/colors.js"; +import { + bold, + cyan, + green, + magenta, + muted, + red, + yellow, +} from "../lib/formatters/colors.js"; import { logger } from "../lib/logger.js"; const log = logger.withTag("local"); @@ -202,37 +210,289 @@ function buildSidecarApp( return app; } +/** Format a local timestamp as HH:MM:SS from a Sentry timestamp. */ +function formatTime(timestamp?: number | string): string { + let date: Date; + if (!timestamp) { + date = new Date(); + } else if (typeof timestamp === "string") { + date = new Date(timestamp); + } else { + date = new Date(timestamp * 1000); + } + if (Number.isNaN(date.getTime())) { + return "??:??:??"; + } + return date.toLocaleTimeString("en-US", { hour12: false }); +} + +/** Level → color map for tail output. */ +const LEVEL_COLORS: Record string> = { + error: (s) => red(bold(s)), + fatal: (s) => red(bold(s)), + warning: yellow, + info: cyan, + trace: green, + debug: muted, +}; + +/** Colorize a log/event level label. */ +function colorLevel(level: string): string { + const colorFn = LEVEL_COLORS[level]; + return colorFn ? colorFn(level) : level; +} + +/** Mobile SDK name substrings. */ +const MOBILE_MARKERS = ["cocoa", "android", "react-native", "flutter"]; + +/** Server-side JS SDK name substrings — exclude from browser detection. */ +const SERVER_JS_MARKERS = [ + "node", + "bun", + "deno", + "nextjs", + "remix", + "astro", + "nuxt", + "sveltekit", +]; + +/** + * Infer the source platform from the envelope header's `sdk.name` field. + * Returns a short colored label like "server", "browser", or "mobile". + */ +function inferSource(header: Record): string { + const sdk = header.sdk as { name?: string } | undefined; + const name = sdk?.name ?? ""; + if (MOBILE_MARKERS.some((m) => name.includes(m))) { + return magenta("mobile"); + } + if ( + name.startsWith("sentry.javascript.") && + !SERVER_JS_MARKERS.some((m) => name.includes(m)) + ) { + return yellow("browser"); + } + return cyan("server"); +} + +/** Shape of a single stack frame in the exception value. */ +type StackFrame = { + filename?: string; + lineno?: number; + colno?: number; + function?: string; + in_app?: boolean; +}; + +/** Build the `[file:line:col] [func]` suffix for the best stack frame. */ +function formatFrameHint(frames: StackFrame[]): string { + const frame = frames.find((f) => f.in_app) ?? frames.at(-1); + if (!frame) { + return ""; + } + let hint = ""; + if (frame.filename && frame.lineno) { + const loc = frame.colno + ? `${frame.filename}:${frame.lineno}:${frame.colno}` + : `${frame.filename}:${frame.lineno}`; + hint += ` ${muted(`[${loc}]`)}`; + } + if (frame.function) { + hint += ` ${muted(`[${frame.function}]`)}`; + } + return hint; +} + +/** + * Format an error event item into a colored one-liner. + * + * Output: `HH:MM:SS error server TypeError: x is not a function [file.ts:42:5] [handleRequest]` + */ +function formatErrorItem( + event: Record, + header: Record +): string { + const exception = event.exception as + | { + values?: { + type?: string; + value?: string; + stacktrace?: { frames?: StackFrame[] }; + }[]; + } + | undefined; + const first = exception?.values?.[0]; + const errorType = first?.type ?? "Error"; + const errorValue = + first?.value ?? (event.message as string | undefined) ?? "Unknown error"; + + let msg = `${errorType}: ${errorValue}`; + + const frames = first?.stacktrace?.frames; + if (frames?.length) { + msg += formatFrameHint(frames); + } + + const ts = formatTime(event.timestamp as number | undefined); + return `${muted(ts)} ${colorLevel("error")} ${inferSource(header)} ${msg}`; +} + /** - * Resolve the human label for an envelope. + * Format a transaction event item into a colored one-liner. * - * - When the envelope parses cleanly, we use the joined item types - * (e.g. `event+attachment`). - * - Otherwise we fall back to the content type, with a friendly alias - * for the canonical Sentry envelope mime type. + * Output: `HH:MM:SS trace browser [http.client] GET /api/users [245ms] [3 spans]` */ -function describeEnvelope(contentType: string, eventTypes: string[]): string { - if (eventTypes.length > 0) { - return eventTypes.join("+"); +function formatTransactionItem( + event: Record, + header: Record +): string { + const trace = (event.contexts as Record | undefined) + ?.trace as + | { op?: string; status?: string; description?: string } + | undefined; + let msg = + (event.transaction as string) ?? trace?.description ?? "Transaction"; + + const op = trace?.op; + if (op && op !== "default" && op !== "unknown") { + msg = `[${op}] ${msg}`; } - if (contentType === "application/x-sentry-envelope") { - return "envelope"; + + const start = event.start_timestamp as number | undefined; + const end = event.timestamp as number | undefined; + if (start !== undefined && end !== undefined) { + const durationMs = Math.round((end - start) * 1000); + msg += ` ${muted(`[${durationMs}ms]`)}`; } - return contentType; + + const status = trace?.status; + if (status && status !== "ok") { + msg += ` ${muted(`[${status}]`)}`; + } + + const spans = event.spans as unknown[] | undefined; + if (spans?.length) { + msg += ` ${muted(`[${spans.length} span${spans.length === 1 ? "" : "s"}]`)}`; + } + + const ts = formatTime(event.timestamp as number | undefined); + return `${muted(ts)} ${colorLevel("trace")} ${inferSource(header)} ${msg}`; +} + +/** Shape of a single log entry inside a log envelope item. */ +type LogEntry = { + level?: string; + body?: string; + timestamp?: number; + attributes?: Record; +}; + +/** Format one log entry into a colored tail line. */ +function formatSingleLog(logEntry: LogEntry, source: string): string { + const level = logEntry.level ?? "log"; + let msg = logEntry.body ?? ""; + + if (logEntry.attributes) { + const attrs = Object.entries(logEntry.attributes) + .filter( + ([k, v]) => + !k.startsWith("sentry.") && v.value !== null && v.value !== undefined + ) + .map(([k, v]) => `${k}=${v.value}`); + if (attrs.length > 0) { + msg += ` ${muted(`[${attrs.join(", ")}]`)}`; + } + } + + const ts = formatTime(logEntry.timestamp); + return `${muted(ts)} ${colorLevel(level)} ${source} ${msg}`; } /** - * Format a freshly received envelope for terminal output. + * Format a log event item. A log envelope item contains an `items` array + * of individual log entries; each gets its own line. * - * Keeps the formatting deliberately minimal — this is a tail, not a UI. - * If users want rich rendering, they can point the Spotlight overlay at - * `http://localhost:/stream` instead. + * Output: `HH:MM:SS info server User logged in [user_id=1234]` */ -function formatTailLine(contentType: string, eventTypes: string[]): string { - const ts = new Date().toISOString().slice(11, 23); // HH:MM:SS.sss - const label = describeEnvelope(contentType, eventTypes); +function formatLogItem( + event: Record, + header: Record +): string[] { + const items = event.items as LogEntry[] | undefined; + if (!items?.length) { + return []; + } + + const source = inferSource(header); + return items.map((logEntry) => formatSingleLog(logEntry, source)); +} + +/** Item types that map to the error formatter. */ +const ERROR_TYPES = new Set(["event", "error"]); + +/** Produce a fallback one-liner for unparseable or unsupported items. */ +function formatFallbackLine(label: string): string { + const ts = new Date().toISOString().slice(11, 23); return `${muted(ts)} ${cyan("•")} ${bold(label)}`; } +/** Resolve a human label for a completely unparseable envelope. */ +function resolveUnparseableLabel(container: { + getContentType: () => string; + getEventTypes: () => string[] | null; +}): string { + const types = container.getEventTypes(); + if (types && types.length > 0) { + return types.join("+"); + } + const ct = container.getContentType(); + return ct === "application/x-sentry-envelope" ? "envelope" : ct; +} + +/** + * Format a freshly received envelope for terminal output. + * + * For recognized item types (errors, transactions, logs), produces richly + * colored one-liners with context (stack location, span count, duration, + * log attributes, etc.). Items without a dedicated formatter (attachments, + * profiles, sessions, check-ins) fall back to a minimal timestamp + type line. + */ +function formatEnvelopeLines(container: { + getParsedEnvelope: () => { + envelope: [Record, [{ type?: string }, unknown][]]; + } | null; + getContentType: () => string; + getEventTypes: () => string[] | null; +}): string[] { + const parsed = container.getParsedEnvelope(); + if (!parsed) { + return [formatFallbackLine(resolveUnparseableLabel(container))]; + } + + const [header, items] = parsed.envelope; + const lines: string[] = []; + for (const [itemHeader, itemPayload] of items) { + const itemType = itemHeader.type; + const payload = itemPayload as Record; + + if (itemType && ERROR_TYPES.has(itemType)) { + lines.push(formatErrorItem(payload, header)); + } else if (itemType === "transaction") { + lines.push(formatTransactionItem(payload, header)); + } else if (itemType === "log") { + lines.push(...formatLogItem(payload, header)); + } else { + lines.push(formatFallbackLine(itemType ?? container.getContentType())); + } + } + + if (lines.length > 0) { + return lines; + } + return [formatFallbackLine(resolveUnparseableLabel(container))]; +} + /** * Install signal handlers that stop the HTTP server on Ctrl-C / SIGTERM. * @@ -320,15 +580,16 @@ export const localCommand = buildCommand({ async *func(this: SentryContext, flags: LocalFlags) { const buffer = createSpotlightBuffer(BUFFER_SIZE); - // Tail subscriber: prints a one-line summary for each envelope. We - // route through the logger (stderr) rather than stdout so the tail - // doesn't pollute pipelines that consume the CLI's stdout, and so it + // Tail subscriber: pretty-prints each envelope item using Spotlight's + // human formatters. Routes through the logger (stderr) so the tail + // doesn't pollute pipelines that consume the CLI's stdout, and // honors `--log-level` / `SENTRY_LOG_LEVEL` like the rest of the CLI. // Skipped entirely when `--quiet` is set. if (!flags.quiet) { buffer.subscribe((container) => { - const types = container.getEventTypes() ?? []; - log.info(formatTailLine(container.getContentType(), types)); + for (const line of formatEnvelopeLines(container)) { + log.info(line); + } }); } From d030cabba901825f73fb6e65be8aa0811b950320 Mon Sep 17 00:00:00 2001 From: Aditya Mathur <57684218+MathurAditya724@users.noreply.github.com> Date: Tue, 5 May 2026 20:24:52 +0000 Subject: [PATCH 03/46] feat(local): add --filter/-f flag to show only specific event types Adds a repeatable --filter flag that accepts error, transaction, or log. When set, only matching envelope items are rendered in the tail output; non-matching items are silently dropped. No filter = show everything. Usage: sentry local -f error # errors only sentry local -f error -f log # errors and logs sentry local -f transaction # transactions only --- .../skills/sentry-cli/references/local.md | 1 + src/commands/local.ts | 146 ++++++++++++++---- 2 files changed, 119 insertions(+), 28 deletions(-) diff --git a/plugins/sentry-cli/skills/sentry-cli/references/local.md b/plugins/sentry-cli/skills/sentry-cli/references/local.md index 92ee5ab9a..2f7d40f31 100644 --- a/plugins/sentry-cli/skills/sentry-cli/references/local.md +++ b/plugins/sentry-cli/skills/sentry-cli/references/local.md @@ -20,6 +20,7 @@ Run a local Spotlight sidecar to capture dev SDK events - `-H, --host - Hostname to bind to (default localhost) - (default: "localhost")` - `-o, --open - Open the sidecar SSE URL in a browser` - `-q, --quiet - Suppress per-envelope tail output` +- `-f, --filter ... - Only show items of this type (repeatable: error, transaction, log)` **Examples:** diff --git a/src/commands/local.ts b/src/commands/local.ts index c0d0b1d9a..26e48a9db 100644 --- a/src/commands/local.ts +++ b/src/commands/local.ts @@ -67,11 +67,31 @@ type EventPayload = { data: string; // base64-encoded raw envelope bytes }; +/** Envelope item categories that can be filtered via `--filter`. */ +const FILTER_VALUES = ["error", "transaction", "log"] as const; +type FilterValue = (typeof FILTER_VALUES)[number]; + +/** + * Parse and validate a `--filter` value. + * Accepts the canonical names: error, transaction, log. + */ +function parseFilter(value: string): FilterValue { + const lower = value.toLowerCase(); + if (!FILTER_VALUES.includes(lower as FilterValue)) { + throw new ValidationError( + `Invalid filter "${value}". Valid values: ${FILTER_VALUES.join(", ")}`, + "filter" + ); + } + return lower as FilterValue; +} + type LocalFlags = { readonly port: number; readonly host: string; readonly open: boolean; readonly quiet: boolean; + readonly filter: FilterValue[]; }; /** @@ -431,6 +451,24 @@ function formatLogItem( /** Item types that map to the error formatter. */ const ERROR_TYPES = new Set(["event", "error"]); +/** + * Map envelope item `type` to the corresponding `FilterValue`. + * Returns undefined for item types that don't map to a filter category. + */ +function itemTypeToFilterCategory( + itemType: string | undefined +): FilterValue | undefined { + if (!itemType) { + return; + } + if (ERROR_TYPES.has(itemType)) { + return "error"; + } + if (itemType === "transaction" || itemType === "log") { + return itemType; + } +} + /** Produce a fallback one-liner for unparseable or unsupported items. */ function formatFallbackLine(label: string): string { const ts = new Date().toISOString().slice(11, 23); @@ -450,46 +488,84 @@ function resolveUnparseableLabel(container: { return ct === "application/x-sentry-envelope" ? "envelope" : ct; } +/** Format a single envelope item into one or more output lines. */ +function formatItem( + itemType: string | undefined, + payload: Record, + header: Record, + fallbackLabel: string +): string[] { + if (itemType && ERROR_TYPES.has(itemType)) { + return [formatErrorItem(payload, header)]; + } + if (itemType === "transaction") { + return [formatTransactionItem(payload, header)]; + } + if (itemType === "log") { + return formatLogItem(payload, header); + } + return [formatFallbackLine(fallbackLabel)]; +} + +/** Check whether an item should be shown given active filters. */ +function isItemIncluded( + itemType: string | undefined, + activeFilters: ReadonlySet +): boolean { + if (activeFilters.size === 0) { + return true; + } + const category = itemTypeToFilterCategory(itemType); + return category !== undefined && activeFilters.has(category); +} + /** * Format a freshly received envelope for terminal output. * - * For recognized item types (errors, transactions, logs), produces richly - * colored one-liners with context (stack location, span count, duration, - * log attributes, etc.). Items without a dedicated formatter (attachments, - * profiles, sessions, check-ins) fall back to a minimal timestamp + type line. + * When `activeFilters` is non-empty, only items whose category matches + * one of the filter values are rendered; non-matching items are silently + * dropped. When empty, all items are shown. */ -function formatEnvelopeLines(container: { - getParsedEnvelope: () => { - envelope: [Record, [{ type?: string }, unknown][]]; - } | null; - getContentType: () => string; - getEventTypes: () => string[] | null; -}): string[] { +function formatEnvelopeLines( + container: { + getParsedEnvelope: () => { + envelope: [Record, [{ type?: string }, unknown][]]; + } | null; + getContentType: () => string; + getEventTypes: () => string[] | null; + }, + activeFilters: ReadonlySet +): string[] { const parsed = container.getParsedEnvelope(); if (!parsed) { + if (activeFilters.size > 0) { + return []; + } return [formatFallbackLine(resolveUnparseableLabel(container))]; } const [header, items] = parsed.envelope; const lines: string[] = []; for (const [itemHeader, itemPayload] of items) { - const itemType = itemHeader.type; - const payload = itemPayload as Record; - - if (itemType && ERROR_TYPES.has(itemType)) { - lines.push(formatErrorItem(payload, header)); - } else if (itemType === "transaction") { - lines.push(formatTransactionItem(payload, header)); - } else if (itemType === "log") { - lines.push(...formatLogItem(payload, header)); - } else { - lines.push(formatFallbackLine(itemType ?? container.getContentType())); + if (!isItemIncluded(itemHeader.type, activeFilters)) { + continue; } + lines.push( + ...formatItem( + itemHeader.type, + itemPayload as Record, + header, + itemHeader.type ?? container.getContentType() + ) + ); } if (lines.length > 0) { return lines; } + if (activeFilters.size > 0) { + return []; + } return [formatFallbackLine(resolveUnparseableLabel(container))]; } @@ -567,12 +643,21 @@ export const localCommand = buildCommand({ brief: "Suppress per-envelope tail output", default: false, }, + filter: { + kind: "parsed", + parse: parseFilter, + brief: + "Only show items of this type (repeatable: error, transaction, log)", + variadic: true, + optional: true, + }, }, aliases: { p: "port", H: "host", o: "open", q: "quiet", + f: "filter", }, }, // No auth required — this is a local-only dev server. @@ -580,14 +665,16 @@ export const localCommand = buildCommand({ async *func(this: SentryContext, flags: LocalFlags) { const buffer = createSpotlightBuffer(BUFFER_SIZE); - // Tail subscriber: pretty-prints each envelope item using Spotlight's - // human formatters. Routes through the logger (stderr) so the tail - // doesn't pollute pipelines that consume the CLI's stdout, and - // honors `--log-level` / `SENTRY_LOG_LEVEL` like the rest of the CLI. - // Skipped entirely when `--quiet` is set. + // Build the active filter set once — empty set means "show everything". + const activeFilters: ReadonlySet = new Set(flags.filter); + + // Tail subscriber: pretty-prints each envelope item. Routes through + // the logger (stderr) so the tail doesn't pollute pipelines that + // consume the CLI's stdout, and honors `--log-level` / `SENTRY_LOG_LEVEL` + // like the rest of the CLI. Skipped entirely when `--quiet` is set. if (!flags.quiet) { buffer.subscribe((container) => { - for (const line of formatEnvelopeLines(container)) { + for (const line of formatEnvelopeLines(container, activeFilters)) { log.info(line); } }); @@ -615,6 +702,9 @@ export const localCommand = buildCommand({ log.info( ` ${muted(`SENTRY_DSN=${url.replace("http://", "http://public@")}/1`)}` ); + if (activeFilters.size > 0) { + log.info(`Filtering: ${[...activeFilters].join(", ")}`); + } log.info("Press Ctrl-C to stop."); if (flags.open) { From bda0e68ef3d1f1678de47990a63977fdf8b434a7 Mon Sep 17 00:00:00 2001 From: Aditya Mathur <57684218+MathurAditya724@users.noreply.github.com> Date: Tue, 5 May 2026 20:59:28 +0000 Subject: [PATCH 04/46] fix(local): fix signal handling, SSE protocol, and browser SDK compat Three fixes based on audit against Spotlight's reference implementation: 1. Signal handling: process.once -> process.on so the 'second signal = force exit' code path is reachable (process.once unregisters after the first signal, making the shuttingDown check dead code). 2. SSE format: match the Spotlight protocol so the overlay UI works. - event name is the content type (not 'envelope') - id field is the Spotlight-assigned envelope UUID - data is the parsed envelope JSON (not base64-encoded raw bytes) - Last-Event-ID reconnection is now supported 3. Browser SDK: detect sendBeacon() payloads (Content-Type: text/plain with sentry_client query param) and override to the canonical application/x-sentry-envelope, matching Spotlight's workaround. --- src/commands/local.ts | 49 ++++++++++++++++++++++++++----------------- 1 file changed, 30 insertions(+), 19 deletions(-) diff --git a/src/commands/local.ts b/src/commands/local.ts index 26e48a9db..228d724d7 100644 --- a/src/commands/local.ts +++ b/src/commands/local.ts @@ -61,11 +61,8 @@ const DEFAULT_PORT = 8969; /** Buffer size: how many recent envelopes to retain for late subscribers. */ const BUFFER_SIZE = 500; -/** SSE event payload — what we send to GET /stream subscribers. */ -type EventPayload = { - contentType: string; - data: string; // base64-encoded raw envelope bytes -}; +/** Canonical content type for Sentry envelopes. */ +const SENTRY_CONTENT_TYPE = "application/x-sentry-envelope"; /** Envelope item categories that can be filtered via `--filter`. */ const FILTER_VALUES = ["error", "transaction", "log"] as const; @@ -150,12 +147,22 @@ function buildSidecarApp( req: { arrayBuffer: () => Promise; header: (name: string) => string | undefined; + query: (name: string) => string | undefined; }; body: (data: null, status: number) => Response; }) => { const arrayBuf = await c.req.arrayBuffer(); const body = Buffer.from(arrayBuf); - const contentType = c.req.header("content-type") ?? ""; + // Browser SDKs using sendBeacon() set Content-Type to text/plain to + // avoid CORS preflight. Detect this via the sentry_client query param + // and override to the canonical Sentry envelope content type. + let contentType = c.req.header("content-type") ?? ""; + if ( + c.req.query("sentry_client")?.startsWith("sentry.javascript.browser") && + c.req.header("origin") + ) { + contentType = SENTRY_CONTENT_TYPE; + } const contentEncoding = c.req.header("content-encoding") as | "gzip" | "deflate" @@ -189,22 +196,26 @@ function buildSidecarApp( /** * SSE stream — Spotlight overlay / UI clients connect here to receive a - * live feed of envelopes. Each event is emitted as a JSON object with - * the content type and base64-encoded body. + * live feed of envelopes. The event format matches Spotlight's protocol: + * - `event` is the content type (e.g., "application/x-sentry-envelope") + * - `id` is the Spotlight-assigned envelope UUID (enables reconnection) + * - `data` is the parsed envelope JSON ([header, items]) */ app.get("/stream", (c) => streamSSE(c, async (stream) => { - // Tie the subscriber lifetime to the response stream. We unsubscribe - // when the client disconnects so the buffer doesn't leak readers. + const lastEventId = c.req.header("Last-Event-ID"); const readerId = spotlightBuffer.subscribe((container) => { - const payload: EventPayload = { - contentType: container.getContentType(), - data: container.getData().toString("base64"), - }; + const parsed = container.getParsedEnvelope(); + if (!parsed) { + return; + } + const header = parsed.envelope[0] as Record; + const envelopeId = header.__spotlight_envelope_id; stream .writeSSE({ - event: "envelope", - data: JSON.stringify(payload), + id: envelopeId ? String(envelopeId) : undefined, + event: container.getContentType(), + data: JSON.stringify(parsed.envelope), }) .catch((err: unknown) => { log.debug( @@ -213,7 +224,7 @@ function buildSidecarApp( }` ); }); - }); + }, lastEventId); stream.onAbort(() => { spotlightBuffer.unsubscribe(readerId); @@ -594,8 +605,8 @@ function waitForShutdown(server: Server): Promise { } }; - process.once("SIGINT", () => shutdown("SIGINT")); - process.once("SIGTERM", () => shutdown("SIGTERM")); + process.on("SIGINT", () => shutdown("SIGINT")); + process.on("SIGTERM", () => shutdown("SIGTERM")); }); } From 935b0604024b37e95a5f389b384e50ba912a3df5 Mon Sep 17 00:00:00 2001 From: Aditya Mathur <57684218+MathurAditya724@users.noreply.github.com> Date: Tue, 5 May 2026 21:29:02 +0000 Subject: [PATCH 05/46] refactor(local): remove --open flag and update docs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The --open flag opened the raw SSE endpoint in a browser, which just shows streaming text — not useful without the Spotlight overlay UI. Removed it and updated the fragment docs to document the new pretty-print tail output and --filter flag instead. --- docs/src/fragments/commands/local.md | 20 +++++++++++++++----- src/commands/local.ts | 13 ------------- 2 files changed, 15 insertions(+), 18 deletions(-) diff --git a/docs/src/fragments/commands/local.md b/docs/src/fragments/commands/local.md index ea78aac95..5275d58f5 100644 --- a/docs/src/fragments/commands/local.md +++ b/docs/src/fragments/commands/local.md @@ -16,8 +16,8 @@ sentry local --port 9000 --host 0.0.0.0 # Run quietly (suppress per-envelope tail output) sentry local --quiet -# Open the SSE endpoint in a browser on startup -sentry local --open +# Only show errors and logs (filter out transactions) +sentry local -f error -f log ``` ## Endpoints @@ -41,10 +41,20 @@ Or configure your SDK's transport explicitly to send envelopes to `http://localh ## Tail output -By default, every envelope received is logged as a single line: +By default, incoming envelopes are pretty-printed to the terminal: ``` -14:32:01.456 • event+attachment +14:32:01 error server TypeError: x is not a function [app.ts:42:5] [handleRequest] +14:32:02 trace browser [http.client] GET /api/users [245ms] [3 spans] +14:32:03 info server User logged in [user_id=1234] ``` -The label is the joined list of envelope item types (`event`, `transaction`, `log`, `attachment`, etc.). Use `--quiet` to suppress this output if you only need the SSE stream for the Spotlight overlay. +Errors show the exception type, message, and top stack frame. Transactions show the operation, duration, and span count. Logs show the severity level, message, and custom attributes. + +Use `--filter` / `-f` to narrow the output to specific event types (repeatable): + +```bash +sentry local -f error -f log # only errors and logs +``` + +Use `--quiet` to suppress tail output entirely if you only need the SSE stream for the Spotlight overlay. diff --git a/src/commands/local.ts b/src/commands/local.ts index 228d724d7..32c6521a6 100644 --- a/src/commands/local.ts +++ b/src/commands/local.ts @@ -39,7 +39,6 @@ import { Hono } from "hono"; import { cors } from "hono/cors"; import { streamSSE } from "hono/streaming"; import type { SentryContext } from "../context.js"; -import { openOrShowUrl } from "../lib/browser.js"; import { buildCommand, numberParser } from "../lib/command.js"; import { ValidationError } from "../lib/errors.js"; import { @@ -86,7 +85,6 @@ function parseFilter(value: string): FilterValue { type LocalFlags = { readonly port: number; readonly host: string; - readonly open: boolean; readonly quiet: boolean; readonly filter: FilterValue[]; }; @@ -644,11 +642,6 @@ export const localCommand = buildCommand({ brief: "Hostname to bind to (default localhost)", default: "localhost", }, - open: { - kind: "boolean", - brief: "Open the sidecar SSE URL in a browser", - default: false, - }, quiet: { kind: "boolean", brief: "Suppress per-envelope tail output", @@ -666,7 +659,6 @@ export const localCommand = buildCommand({ aliases: { p: "port", H: "host", - o: "open", q: "quiet", f: "filter", }, @@ -718,11 +710,6 @@ export const localCommand = buildCommand({ } log.info("Press Ctrl-C to stop."); - if (flags.open) { - // Best-effort — never blocks shutdown. - await openOrShowUrl(`${url}/stream`); - } - // Block until the user interrupts. We don't yield any CommandOutput // because there's no structured payload — this command is a server. await waitForShutdown(server); From 0afaf5a001e672f65071b348ae3a2f90fa4a169d Mon Sep 17 00:00:00 2001 From: Aditya Mathur <57684218+MathurAditya724@users.noreply.github.com> Date: Fri, 15 May 2026 09:50:42 +0000 Subject: [PATCH 06/46] fix(local): clean up startup banner, add port auto-retry - Remove logger tag so 'local ' no longer clutters every line - Remove 'Spotlight sidecar' wording, use 'Listening on ' instead - Remove endpoint listing and DSN instructions from banner - Add Spotlight docs link for getting started - Auto-increment port on EADDRINUSE (up to 10 attempts) --- docs/src/content/docs/contributing.md | 2 +- docs/src/fragments/commands/local.md | 18 +-- plugins/sentry-cli/skills/sentry-cli/SKILL.md | 4 +- .../skills/sentry-cli/references/local.md | 10 +- src/commands/local.ts | 106 ++++++++++++------ 5 files changed, 86 insertions(+), 54 deletions(-) diff --git a/docs/src/content/docs/contributing.md b/docs/src/content/docs/contributing.md index 700fcbfd3..0e2c2f5b5 100644 --- a/docs/src/content/docs/contributing.md +++ b/docs/src/content/docs/contributing.md @@ -71,7 +71,7 @@ cli/ │ │ ├── explore.ts # Query aggregate event data (Explore) │ │ ├── help.ts # Help command │ │ ├── init.ts # Initialize Sentry in your project (experimental) -│ │ ├── local.ts # Run a local Spotlight sidecar to capture dev SDK events +│ │ ├── local.ts # Run a local Spotlight server to capture dev SDK events │ │ └── schema.ts # Browse the Sentry API schema │ ├── lib/ # Shared utilities │ └── types/ # TypeScript types and Zod schemas diff --git a/docs/src/fragments/commands/local.md b/docs/src/fragments/commands/local.md index 5275d58f5..70f30faf9 100644 --- a/docs/src/fragments/commands/local.md +++ b/docs/src/fragments/commands/local.md @@ -1,13 +1,15 @@ -[Spotlight](https://spotlightjs.com) is "Sentry for Development" — a lightweight local proxy that ingests Sentry envelopes from SDKs running in your dev stack and surfaces them in real time. `sentry local` runs a minimal [Hono](https://hono.dev/) HTTP server that's wire-compatible with Spotlight's sidecar protocol, so your existing SDKs and the [Spotlight overlay](https://spotlightjs.com/about/) work without any changes. +[Spotlight](https://spotlightjs.com) is "Sentry for Development" — a lightweight local proxy that ingests Sentry envelopes from SDKs running in your dev stack and surfaces them in real time. `sentry local` runs a minimal [Hono](https://hono.dev/) HTTP server that's wire-compatible with Spotlight's protocol, so your existing SDKs and the [Spotlight overlay](https://spotlightjs.com/about/) work without any changes. -No authentication is required — the sidecar binds to `localhost` by default and is purely a development tool. +No authentication is required — the server binds to `localhost` by default and is purely a development tool. + +Learn more about Spotlight at [spotlightjs.com/docs/getting-started](https://spotlightjs.com/docs/getting-started/). ## Examples ```bash -# Start the sidecar on the default port (8969) +# Start the server on the default port (8969) sentry local # Use a custom port and bind to all interfaces @@ -29,16 +31,6 @@ sentry local -f error -f log | `GET` | `/stream` | Server-Sent Events feed of incoming envelopes | | `GET` | `/health` | Liveness check (returns `OK`) | -## Pointing your SDK at the sidecar - -Set a localhost DSN that resolves to the sidecar's port — the public key and project ID can be any non-empty value because the sidecar accepts everything: - -```bash -SENTRY_DSN=http://public@localhost:8969/1 -``` - -Or configure your SDK's transport explicitly to send envelopes to `http://localhost:8969/stream`. - ## Tail output By default, incoming envelopes are pretty-printed to the terminal: diff --git a/plugins/sentry-cli/skills/sentry-cli/SKILL.md b/plugins/sentry-cli/skills/sentry-cli/SKILL.md index 602a64d25..9254ab07c 100644 --- a/plugins/sentry-cli/skills/sentry-cli/SKILL.md +++ b/plugins/sentry-cli/skills/sentry-cli/SKILL.md @@ -470,9 +470,9 @@ Initialize Sentry in your project (experimental) ### Local -Run a local Spotlight sidecar to capture dev SDK events +Run a local Spotlight server to capture dev SDK events -- `sentry local` — Run a local Spotlight sidecar to capture dev SDK events +- `sentry local` — Run a local Spotlight server to capture dev SDK events → Full flags and examples: `references/local.md` diff --git a/plugins/sentry-cli/skills/sentry-cli/references/local.md b/plugins/sentry-cli/skills/sentry-cli/references/local.md index 6f1ea1759..4bb11aea8 100644 --- a/plugins/sentry-cli/skills/sentry-cli/references/local.md +++ b/plugins/sentry-cli/skills/sentry-cli/references/local.md @@ -1,7 +1,7 @@ --- name: sentry-cli-local version: 0.34.0-dev.0 -description: Run a local Spotlight sidecar to capture dev SDK events +description: Run a local Spotlight server to capture dev SDK events requires: bins: ["sentry"] auth: true @@ -9,11 +9,11 @@ requires: # Local Commands -Run a local Spotlight sidecar to capture dev SDK events +Run a local Spotlight server to capture dev SDK events ### `sentry local` -Run a local Spotlight sidecar to capture dev SDK events +Run a local Spotlight server to capture dev SDK events **Flags:** - `-p, --port - Port to listen on (default 8969) - (default: "8969")` @@ -24,7 +24,7 @@ Run a local Spotlight sidecar to capture dev SDK events **Examples:** ```bash -# Start the sidecar on the default port (8969) +# Start the server on the default port (8969) sentry local # Use a custom port and bind to all interfaces @@ -36,8 +36,6 @@ sentry local --quiet # Only show errors and logs (filter out transactions) sentry local -f error -f log -SENTRY_DSN=http://public@localhost:8969/1 - sentry local -f error -f log # only errors and logs ``` diff --git a/src/commands/local.ts b/src/commands/local.ts index 32c6521a6..565dcfcfa 100644 --- a/src/commands/local.ts +++ b/src/commands/local.ts @@ -1,15 +1,15 @@ /** * sentry local * - * Run a local Spotlight-compatible sidecar server. + * Run a local Spotlight-compatible server. * - * Spotlight (https://github.com/getsentry/spotlight) is "Sentry for - * Development" — a small local proxy that ingests Sentry envelopes from - * SDKs running in your dev stack and surfaces them in real time. + * Spotlight (https://spotlightjs.com/) is "Sentry for Development" — a small + * local proxy that ingests Sentry envelopes from SDKs running in your dev + * stack and surfaces them in real time. * * This command starts a minimal Hono HTTP server that: * - * 1. Accepts envelopes from Sentry SDKs at the standard sidecar endpoints: + * 1. Accepts envelopes from Sentry SDKs at the standard endpoints: * - `POST /stream` (Spotlight-compatible) * - `POST /api/{projectId}/envelope/` (Sentry SDK ingest path) * 2. Pushes them into the buffer provided by `@spotlightjs/spotlight/sdk`, @@ -19,12 +19,7 @@ * 4. Tails events to the terminal as they arrive so you can see what your * app is sending without leaving the CLI. * - * To point your SDK at the local sidecar, use a placeholder DSN that - * resolves to localhost — for example: - * - * SENTRY_DSN=http://public@localhost:8969/1 - * - * Or configure your SDK's transport to send to `http://localhost:8969/stream`. + * Learn more: https://spotlightjs.com/docs/getting-started/ * * The command runs until interrupted (Ctrl-C / SIGTERM). */ @@ -52,7 +47,7 @@ import { } from "../lib/formatters/colors.js"; import { logger } from "../lib/logger.js"; -const log = logger.withTag("local"); +const log = logger; /** Default port matches Spotlight's `DEFAULT_PORT`. */ const DEFAULT_PORT = 8969; @@ -608,11 +603,64 @@ function waitForShutdown(server: Server): Promise { }); } +/** Maximum number of consecutive ports to try before giving up. */ +const MAX_PORT_ATTEMPTS = 10; + +/** + * Try to start the HTTP server, auto-incrementing the port on EADDRINUSE. + * + * `@hono/node-server`'s `serve()` calls `server.listen()` synchronously and + * returns immediately — the actual bind happens asynchronously. We wrap it in + * a Promise that resolves on the `listening` event and rejects on `error`. + * When the port is busy we bump the port number and retry up to + * {@link MAX_PORT_ATTEMPTS} times, warning the user on each bump. + */ +function tryListen( + app: Hono, + startPort: number, + hostname: string +): Promise<{ server: Server; port: number }> { + let port = startPort; + let attempts = 0; + + const attempt = (): Promise<{ server: Server; port: number }> => + new Promise((resolve, reject) => { + const server = serve({ + fetch: app.fetch, + port, + hostname, + }) as unknown as Server; + + server.once("listening", () => resolve({ server, port })); + server.once("error", (err: NodeJS.ErrnoException) => { + if (err.code === "EADDRINUSE") { + attempts += 1; + if (attempts >= MAX_PORT_ATTEMPTS) { + reject( + new ValidationError( + `Port ${startPort} is in use and no open port found after ${MAX_PORT_ATTEMPTS} attempts`, + "port" + ) + ); + return; + } + log.warn(`Port ${port} is in use, trying ${port + 1}...`); + port += 1; + resolve(attempt()); + return; + } + reject(err); + }); + }); + + return attempt(); +} + export const localCommand = buildCommand({ docs: { - brief: "Run a local Spotlight sidecar to capture dev SDK events", + brief: "Run a local Spotlight server to capture dev SDK events", fullDescription: - "Start a local Spotlight-compatible sidecar server.\n\n" + + "Start a local Spotlight-compatible server.\n\n" + "Spotlight is Sentry for Development — it gives you a live view of\n" + "errors, traces, and logs emitted by Sentry SDKs in your dev stack.\n" + "This command runs a minimal Hono server that ingests envelopes\n" + @@ -622,8 +670,7 @@ export const localCommand = buildCommand({ " POST /api/{projectId}/envelope/ — Sentry SDK ingest\n" + " GET /stream — SSE feed (for the Spotlight overlay)\n" + " GET /health — health check\n\n" + - "Configure your SDK to send to the sidecar with a localhost DSN, e.g.:\n" + - " SENTRY_DSN=http://public@localhost:8969/1\n\n" + + "Learn more: https://spotlightjs.com/docs/getting-started/\n\n" + "Press Ctrl-C to stop the server.", }, // No `output` config: this is a long-running server, not a data command. @@ -689,30 +736,25 @@ export const localCommand = buildCommand({ // now; future hooks (e.g. metrics, file logging) can plug in here. }); - // `serve` returns a Node http.Server — we use it for graceful shutdown. - const server = serve({ - fetch: app.fetch, - port: flags.port, - hostname: flags.host, - }) as unknown as Server; - - const url = `http://${flags.host}:${flags.port}`; - log.info(`Spotlight sidecar listening on ${bold(url)}`); - log.info(` ${muted("Ingest:")} POST ${url}/stream`); - log.info(` ${muted("Stream:")} GET ${url}/stream`); - log.info(` ${muted("Health:")} GET ${url}/health`); - log.info(`Point your SDK at ${bold(`${url}/stream`)} or use a DSN like:`); - log.info( - ` ${muted(`SENTRY_DSN=${url.replace("http://", "http://public@")}/1`)}` + const { server, port: boundPort } = await tryListen( + app, + flags.port, + flags.host ); + + const url = `http://${flags.host}:${boundPort}`; + log.info(`Listening on ${bold(url)}`); if (activeFilters.size > 0) { log.info(`Filtering: ${[...activeFilters].join(", ")}`); } + log.info( + `Learn more about Spotlight: ${bold("https://spotlightjs.com/docs/getting-started/")}` + ); log.info("Press Ctrl-C to stop."); // Block until the user interrupts. We don't yield any CommandOutput // because there's no structured payload — this command is a server. await waitForShutdown(server); - log.info("Sidecar stopped."); + log.info("Server stopped."); }, }); From d0e70ad756f9dab64487074c29e278ba67d98771 Mon Sep 17 00:00:00 2001 From: Aditya Mathur <57684218+MathurAditya724@users.noreply.github.com> Date: Fri, 15 May 2026 10:00:16 +0000 Subject: [PATCH 07/46] chore(local): remove code slop from local.ts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Remove pointless `log = logger` alias; use `logger` directly - Strip narrating comments that restate the code - Rename buildSidecarApp → buildApp; drop remaining 'sidecar' references - Make onEnvelope callback optional instead of passing noop - Remove redundant type annotation on activeFilters --- src/commands/local.ts | 72 +++++++++++-------------------------------- 1 file changed, 18 insertions(+), 54 deletions(-) diff --git a/src/commands/local.ts b/src/commands/local.ts index 565dcfcfa..26a79776b 100644 --- a/src/commands/local.ts +++ b/src/commands/local.ts @@ -47,8 +47,6 @@ import { } from "../lib/formatters/colors.js"; import { logger } from "../lib/logger.js"; -const log = logger; - /** Default port matches Spotlight's `DEFAULT_PORT`. */ const DEFAULT_PORT = 8969; @@ -64,7 +62,7 @@ type FilterValue = (typeof FILTER_VALUES)[number]; /** * Parse and validate a `--filter` value. - * Accepts the canonical names: error, transaction, log. + * Accepts the canonical names: error, transaction, logger. */ function parseFilter(value: string): FilterValue { const lower = value.toLowerCase(); @@ -102,27 +100,17 @@ function parsePort(value: string): number { } /** - * Build the Hono application that backs the sidecar. + * Build the Hono application. * - * We expose three concerns: - * - CORS: open to `*` because dev stacks send from arbitrary `localhost:*` - * origins (Vite, Next, Astro, etc.). The sidecar binds to localhost by - * default, so this isn't a security regression. - * - Ingest: `POST /stream` and `POST /api/.../envelope/` accept envelope - * bodies. We hand the raw buffer to `pushToSpotlightBuffer`, which - * decompresses (gzip/deflate/br) and decodes lazily. - * - Subscribe: `GET /stream` opens an SSE stream of every envelope that - * enters the buffer, including those buffered before the subscriber - * connected (so a freshly-opened Spotlight overlay can still see - * recent events). + * CORS is open to `*` because dev stacks send from arbitrary `localhost:*` + * origins (Vite, Next, Astro, etc.) and we only bind to localhost. */ -function buildSidecarApp( +function buildApp( spotlightBuffer: ReturnType, - onEnvelope: (contentType: string, data: Buffer) => void + onEnvelope?: (contentType: string, data: Buffer) => void ): Hono { const app = new Hono(); - // Open CORS — sidecar binds to localhost; this is a dev-only tool. app.use( "*", cors({ @@ -132,10 +120,8 @@ function buildSidecarApp( }) ); - /** Health check — useful for `curl` and for SDKs that probe before sending. */ app.get("/health", (c) => c.text("OK")); - /** Ingest handler shared by `/stream` and `/api/.../envelope/`. */ const ingest = async (c: { req: { arrayBuffer: () => Promise; @@ -172,18 +158,13 @@ function buildSidecarApp( }); if (container) { - // Surface the decoded payload to the tail/subscribe pipeline. We push - // the (potentially decompressed) raw body so SSE subscribers don't - // have to redo the work and so the tail formatter can rely on a - // single representation. - onEnvelope(container.getContentType(), container.getData()); + onEnvelope?.(container.getContentType(), container.getData()); } return c.body(null, 204); }; app.post("/stream", ingest); - // SDK-style envelope ingestion: /api/{projectId}/envelope/?... app.post("/api/:projectId/envelope/", ingest); app.post("/api/:projectId/envelope", ingest); @@ -211,7 +192,7 @@ function buildSidecarApp( data: JSON.stringify(parsed.envelope), }) .catch((err: unknown) => { - log.debug( + logger.debug( `SSE write failed (client likely disconnected): ${ err instanceof Error ? err.message : String(err) }` @@ -223,8 +204,6 @@ function buildSidecarApp( spotlightBuffer.unsubscribe(readerId); }); - // Keep the stream open until the client disconnects. - // hono/streaming resolves the promise on abort. await new Promise((resolve) => { stream.onAbort(() => resolve()); }); @@ -589,7 +568,7 @@ function waitForShutdown(server: Server): Promise { process.exit(0); } shuttingDown = true; - log.info(`Received ${signal}, shutting down...`); + logger.info(`Received ${signal}, shutting down...`); server.close(() => resolve()); // Force-close keep-alive connections so we don't wait on long-lived // SSE subscribers. @@ -644,7 +623,7 @@ function tryListen( ); return; } - log.warn(`Port ${port} is in use, trying ${port + 1}...`); + logger.warn(`Port ${port} is in use, trying ${port + 1}...`); port += 1; resolve(attempt()); return; @@ -673,8 +652,6 @@ export const localCommand = buildCommand({ "Learn more: https://spotlightjs.com/docs/getting-started/\n\n" + "Press Ctrl-C to stop the server.", }, - // No `output` config: this is a long-running server, not a data command. - // We write progress directly to stderr via the logger. parameters: { flags: { port: { @@ -710,31 +687,20 @@ export const localCommand = buildCommand({ f: "filter", }, }, - // No auth required — this is a local-only dev server. auth: false, async *func(this: SentryContext, flags: LocalFlags) { const buffer = createSpotlightBuffer(BUFFER_SIZE); + const activeFilters = new Set(flags.filter); - // Build the active filter set once — empty set means "show everything". - const activeFilters: ReadonlySet = new Set(flags.filter); - - // Tail subscriber: pretty-prints each envelope item. Routes through - // the logger (stderr) so the tail doesn't pollute pipelines that - // consume the CLI's stdout, and honors `--log-level` / `SENTRY_LOG_LEVEL` - // like the rest of the CLI. Skipped entirely when `--quiet` is set. if (!flags.quiet) { buffer.subscribe((container) => { for (const line of formatEnvelopeLines(container, activeFilters)) { - log.info(line); + logger.info(line); } }); } - const app = buildSidecarApp(buffer, () => { - // Tail output is driven by the buffer subscriber above so we don't - // have to repeat the formatting work. This callback is a no-op for - // now; future hooks (e.g. metrics, file logging) can plug in here. - }); + const app = buildApp(buffer); const { server, port: boundPort } = await tryListen( app, @@ -743,18 +709,16 @@ export const localCommand = buildCommand({ ); const url = `http://${flags.host}:${boundPort}`; - log.info(`Listening on ${bold(url)}`); + logger.info(`Listening on ${bold(url)}`); if (activeFilters.size > 0) { - log.info(`Filtering: ${[...activeFilters].join(", ")}`); + logger.info(`Filtering: ${[...activeFilters].join(", ")}`); } - log.info( + logger.info( `Learn more about Spotlight: ${bold("https://spotlightjs.com/docs/getting-started/")}` ); - log.info("Press Ctrl-C to stop."); + logger.info("Press Ctrl-C to stop."); - // Block until the user interrupts. We don't yield any CommandOutput - // because there's no structured payload — this command is a server. await waitForShutdown(server); - log.info("Server stopped."); + logger.info("Server stopped."); }, }); From 736985fefd04c9d72966df2d60120ce0f1313e59 Mon Sep 17 00:00:00 2001 From: Aditya Mathur <57684218+MathurAditya724@users.noreply.github.com> Date: Fri, 15 May 2026 10:11:37 +0000 Subject: [PATCH 08/46] fix(local): use logger.log for tail output to drop icon prefix MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Startup banner keeps logger.info (shows ℹ icon), while tail output and shutdown messages use logger.log (no icon prefix). --- src/commands/local.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/commands/local.ts b/src/commands/local.ts index 26a79776b..ffb73ad5b 100644 --- a/src/commands/local.ts +++ b/src/commands/local.ts @@ -568,7 +568,7 @@ function waitForShutdown(server: Server): Promise { process.exit(0); } shuttingDown = true; - logger.info(`Received ${signal}, shutting down...`); + logger.log(`Received ${signal}, shutting down...`); server.close(() => resolve()); // Force-close keep-alive connections so we don't wait on long-lived // SSE subscribers. @@ -695,7 +695,7 @@ export const localCommand = buildCommand({ if (!flags.quiet) { buffer.subscribe((container) => { for (const line of formatEnvelopeLines(container, activeFilters)) { - logger.info(line); + logger.log(line); } }); } @@ -719,6 +719,6 @@ export const localCommand = buildCommand({ logger.info("Press Ctrl-C to stop."); await waitForShutdown(server); - logger.info("Server stopped."); + logger.log("Server stopped."); }, }); From 51d1538853beda057a3ef6746fe6c6f73fb54040 Mon Sep 17 00:00:00 2001 From: Aditya Mathur <57684218+MathurAditya724@users.noreply.github.com> Date: Fri, 15 May 2026 10:15:54 +0000 Subject: [PATCH 09/46] fix(local): restrict CORS to localhost origins only The wildcard origin allowed any webpage to connect to the SSE stream and exfiltrate envelope data. Restrict to localhost/127.0.0.1 origins which is sufficient for local dev stacks (Vite, Next, Astro, etc.). --- src/commands/local.ts | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/commands/local.ts b/src/commands/local.ts index ffb73ad5b..4b7bc10e6 100644 --- a/src/commands/local.ts +++ b/src/commands/local.ts @@ -99,11 +99,15 @@ function parsePort(value: string): number { return port; } +/** Match localhost origins on any port (http or https). */ +const LOCALHOST_ORIGIN_RE = /^https?:\/\/(localhost|127\.0\.0\.1)(:\d+)?$/; + /** * Build the Hono application. * - * CORS is open to `*` because dev stacks send from arbitrary `localhost:*` - * origins (Vite, Next, Astro, etc.) and we only bind to localhost. + * CORS is restricted to localhost origins — dev stacks send from arbitrary + * `localhost:*` ports (Vite, Next, Astro, etc.) but we must not allow + * arbitrary remote origins to read the SSE envelope stream. */ function buildApp( spotlightBuffer: ReturnType, @@ -114,7 +118,8 @@ function buildApp( app.use( "*", cors({ - origin: "*", + origin: (origin) => + LOCALHOST_ORIGIN_RE.test(origin) ? origin : "http://localhost", allowMethods: ["GET", "POST", "OPTIONS"], allowHeaders: ["Content-Type", "Content-Encoding", "User-Agent"], }) From 5d69091ce6a7b887d83df7600f62397438f4dd0d Mon Sep 17 00:00:00 2001 From: Aditya Mathur <57684218+MathurAditya724@users.noreply.github.com> Date: Fri, 15 May 2026 10:15:09 +0000 Subject: [PATCH 10/46] fix(local): address Warden security findings - Fix subscription leak: merge dual stream.onAbort() into one callback so unsubscribe and promise resolution both fire on disconnect - Sanitize envelope content with stripAnsi() before rendering to terminal to prevent ANSI escape injection from crafted payloads - Add 10 MB body size guard on ingest to reject oversized payloads (returns 413) --- src/commands/local.ts | 42 +++++++++++++++++++++++++++--------------- 1 file changed, 27 insertions(+), 15 deletions(-) diff --git a/src/commands/local.ts b/src/commands/local.ts index 4b7bc10e6..d4960270a 100644 --- a/src/commands/local.ts +++ b/src/commands/local.ts @@ -45,6 +45,7 @@ import { red, yellow, } from "../lib/formatters/colors.js"; +import { stripAnsi } from "../lib/formatters/plain-detect.js"; import { logger } from "../lib/logger.js"; /** Default port matches Spotlight's `DEFAULT_PORT`. */ @@ -56,6 +57,9 @@ const BUFFER_SIZE = 500; /** Canonical content type for Sentry envelopes. */ const SENTRY_CONTENT_TYPE = "application/x-sentry-envelope"; +/** Maximum ingest body size (10 MB). Rejects oversized payloads early. */ +const MAX_BODY_BYTES = 10 * 1024 * 1024; + /** Envelope item categories that can be filtered via `--filter`. */ const FILTER_VALUES = ["error", "transaction", "log"] as const; type FilterValue = (typeof FILTER_VALUES)[number]; @@ -135,7 +139,14 @@ function buildApp( }; body: (data: null, status: number) => Response; }) => { + const contentLength = Number(c.req.header("content-length") ?? 0); + if (contentLength > MAX_BODY_BYTES) { + return c.body(null, 413); + } const arrayBuf = await c.req.arrayBuffer(); + if (arrayBuf.byteLength > MAX_BODY_BYTES) { + return c.body(null, 413); + } const body = Buffer.from(arrayBuf); // Browser SDKs using sendBeacon() set Content-Type to text/plain to // avoid CORS preflight. Detect this via the sentry_client query param @@ -205,12 +216,11 @@ function buildApp( }); }, lastEventId); - stream.onAbort(() => { - spotlightBuffer.unsubscribe(readerId); - }); - await new Promise((resolve) => { - stream.onAbort(() => resolve()); + stream.onAbort(() => { + spotlightBuffer.unsubscribe(readerId); + resolve(); + }); }); }) ); @@ -302,12 +312,12 @@ function formatFrameHint(frames: StackFrame[]): string { let hint = ""; if (frame.filename && frame.lineno) { const loc = frame.colno - ? `${frame.filename}:${frame.lineno}:${frame.colno}` - : `${frame.filename}:${frame.lineno}`; + ? `${stripAnsi(frame.filename)}:${frame.lineno}:${frame.colno}` + : `${stripAnsi(frame.filename)}:${frame.lineno}`; hint += ` ${muted(`[${loc}]`)}`; } if (frame.function) { - hint += ` ${muted(`[${frame.function}]`)}`; + hint += ` ${muted(`[${stripAnsi(frame.function)}]`)}`; } return hint; } @@ -331,9 +341,10 @@ function formatErrorItem( } | undefined; const first = exception?.values?.[0]; - const errorType = first?.type ?? "Error"; - const errorValue = - first?.value ?? (event.message as string | undefined) ?? "Unknown error"; + const errorType = stripAnsi(first?.type ?? "Error"); + const errorValue = stripAnsi( + first?.value ?? (event.message as string | undefined) ?? "Unknown error" + ); let msg = `${errorType}: ${errorValue}`; @@ -359,8 +370,9 @@ function formatTransactionItem( ?.trace as | { op?: string; status?: string; description?: string } | undefined; - let msg = - (event.transaction as string) ?? trace?.description ?? "Transaction"; + let msg = stripAnsi( + (event.transaction as string) ?? trace?.description ?? "Transaction" + ); const op = trace?.op; if (op && op !== "default" && op !== "unknown") { @@ -399,7 +411,7 @@ type LogEntry = { /** Format one log entry into a colored tail line. */ function formatSingleLog(logEntry: LogEntry, source: string): string { const level = logEntry.level ?? "log"; - let msg = logEntry.body ?? ""; + let msg = stripAnsi(logEntry.body ?? ""); if (logEntry.attributes) { const attrs = Object.entries(logEntry.attributes) @@ -407,7 +419,7 @@ function formatSingleLog(logEntry: LogEntry, source: string): string { ([k, v]) => !k.startsWith("sentry.") && v.value !== null && v.value !== undefined ) - .map(([k, v]) => `${k}=${v.value}`); + .map(([k, v]) => `${stripAnsi(k)}=${stripAnsi(String(v.value))}`); if (attrs.length > 0) { msg += ` ${muted(`[${attrs.join(", ")}]`)}`; } From 6676aea64071258fd2b314b7f800adba86f0db37 Mon Sep 17 00:00:00 2001 From: Aditya Mathur <57684218+MathurAditya724@users.noreply.github.com> Date: Fri, 15 May 2026 10:24:33 +0000 Subject: [PATCH 11/46] feat(local): align tail formatting with Spotlight's human formatter MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Type labels: uppercase, bracketed, padded — [ERROR] [TRACE] [INFO] - Source labels: uppercase, bracketed, padded — [SERVER] [BROWSER] [MOBILE] - Source colors: match Spotlight Sentinel theme (mobile=blue) - Log attributes: per-attribute brackets [key=value] [key=value] - Update docs fragment example output to match --- docs/src/fragments/commands/local.md | 6 +-- src/commands/local.ts | 57 ++++++++++++++++++---------- 2 files changed, 41 insertions(+), 22 deletions(-) diff --git a/docs/src/fragments/commands/local.md b/docs/src/fragments/commands/local.md index 70f30faf9..e56b4b104 100644 --- a/docs/src/fragments/commands/local.md +++ b/docs/src/fragments/commands/local.md @@ -36,9 +36,9 @@ sentry local -f error -f log By default, incoming envelopes are pretty-printed to the terminal: ``` -14:32:01 error server TypeError: x is not a function [app.ts:42:5] [handleRequest] -14:32:02 trace browser [http.client] GET /api/users [245ms] [3 spans] -14:32:03 info server User logged in [user_id=1234] +14:32:01 [ERROR] [SERVER] TypeError: x is not a function [app.ts:42:5] [handleRequest] +14:32:02 [TRACE] [BROWSER] [http.client] GET /api/users [245ms] [3 spans] +14:32:03 [INFO] [SERVER] User logged in [user_id=1234] [region=us] ``` Errors show the exception type, message, and top stack frame. Transactions show the operation, duration, and span count. Logs show the severity level, message, and custom attributes. diff --git a/src/commands/local.ts b/src/commands/local.ts index d4960270a..3a1293ead 100644 --- a/src/commands/local.ts +++ b/src/commands/local.ts @@ -37,10 +37,10 @@ import type { SentryContext } from "../context.js"; import { buildCommand, numberParser } from "../lib/command.js"; import { ValidationError } from "../lib/errors.js"; import { + blue, bold, cyan, green, - magenta, muted, red, yellow, @@ -244,7 +244,7 @@ function formatTime(timestamp?: number | string): string { return date.toLocaleTimeString("en-US", { hour12: false }); } -/** Level → color map for tail output. */ +/** Level → color map for tail output, matching Spotlight's Sentinel theme. */ const LEVEL_COLORS: Record string> = { error: (s) => red(bold(s)), fatal: (s) => red(bold(s)), @@ -254,10 +254,18 @@ const LEVEL_COLORS: Record string> = { debug: muted, }; -/** Colorize a log/event level label. */ -function colorLevel(level: string): string { +/** Longest bracketed type label: `[WARNING]` = 9 chars. */ +const TYPE_WIDTH = 9; + +/** Longest bracketed source label: `[BROWSER]` = 9 chars. */ +const SOURCE_WIDTH = 9; + +/** Format a type/level label as `[TYPE]` padded to fixed width. */ +function formatType(level: string): string { + const tag = `[${level.toUpperCase()}]`; const colorFn = LEVEL_COLORS[level]; - return colorFn ? colorFn(level) : level; + const colored = colorFn ? colorFn(tag) : tag; + return colored + " ".repeat(Math.max(0, TYPE_WIDTH - tag.length)); } /** Mobile SDK name substrings. */ @@ -275,23 +283,32 @@ const SERVER_JS_MARKERS = [ "sveltekit", ]; +/** Source color map matching Spotlight's Sentinel theme. */ +const SOURCE_COLORS: Record string> = { + browser: yellow, + mobile: blue, + server: cyan, +}; + /** * Infer the source platform from the envelope header's `sdk.name` field. - * Returns a short colored label like "server", "browser", or "mobile". + * Returns a colored, bracketed, padded label like `[SERVER] `. */ function inferSource(header: Record): string { const sdk = header.sdk as { name?: string } | undefined; const name = sdk?.name ?? ""; + let source = "server"; if (MOBILE_MARKERS.some((m) => name.includes(m))) { - return magenta("mobile"); - } - if ( + source = "mobile"; + } else if ( name.startsWith("sentry.javascript.") && !SERVER_JS_MARKERS.some((m) => name.includes(m)) ) { - return yellow("browser"); + source = "browser"; } - return cyan("server"); + const tag = `[${source.toUpperCase()}]`; + const colorFn = SOURCE_COLORS[source] ?? cyan; + return colorFn(tag) + " ".repeat(Math.max(0, SOURCE_WIDTH - tag.length)); } /** Shape of a single stack frame in the exception value. */ @@ -325,7 +342,7 @@ function formatFrameHint(frames: StackFrame[]): string { /** * Format an error event item into a colored one-liner. * - * Output: `HH:MM:SS error server TypeError: x is not a function [file.ts:42:5] [handleRequest]` + * Output: `HH:MM:SS [ERROR] [SERVER] TypeError: x is not a function [file.ts:42:5] [handleRequest]` */ function formatErrorItem( event: Record, @@ -354,13 +371,13 @@ function formatErrorItem( } const ts = formatTime(event.timestamp as number | undefined); - return `${muted(ts)} ${colorLevel("error")} ${inferSource(header)} ${msg}`; + return `${muted(ts)} ${formatType("error")} ${inferSource(header)} ${msg}`; } /** * Format a transaction event item into a colored one-liner. * - * Output: `HH:MM:SS trace browser [http.client] GET /api/users [245ms] [3 spans]` + * Output: `HH:MM:SS [TRACE] [BROWSER] [http.client] GET /api/users [245ms] [3 spans]` */ function formatTransactionItem( event: Record, @@ -397,7 +414,7 @@ function formatTransactionItem( } const ts = formatTime(event.timestamp as number | undefined); - return `${muted(ts)} ${colorLevel("trace")} ${inferSource(header)} ${msg}`; + return `${muted(ts)} ${formatType("trace")} ${inferSource(header)} ${msg}`; } /** Shape of a single log entry inside a log envelope item. */ @@ -419,21 +436,23 @@ function formatSingleLog(logEntry: LogEntry, source: string): string { ([k, v]) => !k.startsWith("sentry.") && v.value !== null && v.value !== undefined ) - .map(([k, v]) => `${stripAnsi(k)}=${stripAnsi(String(v.value))}`); + .map(([k, v]) => + muted(`[${stripAnsi(k)}=${stripAnsi(String(v.value))}]`) + ); if (attrs.length > 0) { - msg += ` ${muted(`[${attrs.join(", ")}]`)}`; + msg += ` ${attrs.join(" ")}`; } } const ts = formatTime(logEntry.timestamp); - return `${muted(ts)} ${colorLevel(level)} ${source} ${msg}`; + return `${muted(ts)} ${formatType(level)} ${source} ${msg}`; } /** * Format a log event item. A log envelope item contains an `items` array * of individual log entries; each gets its own line. * - * Output: `HH:MM:SS info server User logged in [user_id=1234]` + * Output: `HH:MM:SS [INFO] [SERVER] User logged in [user_id=1234]` */ function formatLogItem( event: Record, From b6e7734c36f8785b74ff6df8ff45b75a493766dc Mon Sep 17 00:00:00 2001 From: Aditya Mathur <57684218+MathurAditya724@users.noreply.github.com> Date: Fri, 15 May 2026 10:31:28 +0000 Subject: [PATCH 12/46] fix(local): show outermost exception for chained errors MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit exception.values is ordered oldest→newest per the Sentry protocol, so values[0] is the root cause. Use .at(-1) to display the outermost exception, matching Sentry UI and Spotlight behavior. --- src/commands/local.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/commands/local.ts b/src/commands/local.ts index 3a1293ead..b82434995 100644 --- a/src/commands/local.ts +++ b/src/commands/local.ts @@ -357,7 +357,8 @@ function formatErrorItem( }[]; } | undefined; - const first = exception?.values?.[0]; + // values is ordered oldest→newest; show the outermost (last) exception + const first = exception?.values?.at(-1); const errorType = stripAnsi(first?.type ?? "Error"); const errorValue = stripAnsi( first?.value ?? (event.message as string | undefined) ?? "Unknown error" From 32643be2645d46ed9743ca88ceda27bc6907d6b4 Mon Sep 17 00:00:00 2001 From: Aditya Mathur <57684218+MathurAditya724@users.noreply.github.com> Date: Fri, 15 May 2026 10:37:11 +0000 Subject: [PATCH 13/46] fix(test): use case-insensitive comparison in colorizeSql property test MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit @sentry/sqlish uppercases SQL keywords (e.g. "by" → "BY"), so identifiers that happen to match keywords fail the strict equality check. Compare lowercased strings instead. --- test/lib/formatters/sql.property.test.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/test/lib/formatters/sql.property.test.ts b/test/lib/formatters/sql.property.test.ts index 9c806eada..3e1f45dd9 100644 --- a/test/lib/formatters/sql.property.test.ts +++ b/test/lib/formatters/sql.property.test.ts @@ -126,13 +126,14 @@ describe("property: isDbSpanOp", () => { }); describe("property: colorizeSql", () => { - test("stripping ANSI preserves original text content", () => { + test("stripping ANSI preserves original text content (case-insensitive)", () => { process.env.SENTRY_PLAIN_OUTPUT = "0"; fcAssert( property(sqlStringArb, (sql) => { const colorized = colorizeSql(sql); const stripped = stripAnsi(colorized); - expect(stripped).toBe(sql); + // Case-insensitive: @sentry/sqlish uppercases SQL keywords (e.g. "by" → "BY") + expect(stripped.toLowerCase()).toBe(sql.toLowerCase()); }), { numRuns: DEFAULT_NUM_RUNS } ); From 16731e3d41db394f785df0d5ba84c7a1715c5a28 Mon Sep 17 00:00:00 2001 From: Aditya Mathur <57684218+MathurAditya724@users.noreply.github.com> Date: Fri, 15 May 2026 11:40:22 +0000 Subject: [PATCH 14/46] feat(local): connect to existing server, backoff port retry - On startup, probe the target port for an existing Spotlight server. If one is running, attach as an SSE consumer instead of starting a duplicate server. Uses fetch-based SSE parsing since Bun lacks global EventSource. - Last-Event-ID reconnection already supported via the Spotlight SDK's subscribe(callback, lastEventId) parameter. - Port retry now uses 3 retries with 5s backoff (matching Spotlight) instead of 10 sequential port increments. --- src/commands/local.ts | 210 +++++++++++++++++++++++++++++++++--------- 1 file changed, 166 insertions(+), 44 deletions(-) diff --git a/src/commands/local.ts b/src/commands/local.ts index b82434995..c67999a2c 100644 --- a/src/commands/local.ts +++ b/src/commands/local.ts @@ -1,23 +1,12 @@ /** * sentry local * - * Run a local Spotlight-compatible server. + * Run a local Spotlight-compatible server, or attach to one already running. * - * Spotlight (https://spotlightjs.com/) is "Sentry for Development" — a small - * local proxy that ingests Sentry envelopes from SDKs running in your dev - * stack and surfaces them in real time. - * - * This command starts a minimal Hono HTTP server that: - * - * 1. Accepts envelopes from Sentry SDKs at the standard endpoints: - * - `POST /stream` (Spotlight-compatible) - * - `POST /api/{projectId}/envelope/` (Sentry SDK ingest path) - * 2. Pushes them into the buffer provided by `@spotlightjs/spotlight/sdk`, - * which lazily parses each envelope. - * 3. Streams new envelopes back to subscribers via Server-Sent Events at - * `GET /stream` — compatible with the Spotlight overlay/UI. - * 4. Tails events to the terminal as they arrive so you can see what your - * app is sending without leaving the CLI. + * On startup the command probes `http://:/health`. If a server + * is already listening (e.g. a Spotlight sidecar or another `sentry local`), + * the command attaches as an SSE consumer and tails events from it. Otherwise + * it starts its own Hono HTTP server. * * Learn more: https://spotlightjs.com/docs/getting-started/ * @@ -57,6 +46,9 @@ const BUFFER_SIZE = 500; /** Canonical content type for Sentry envelopes. */ const SENTRY_CONTENT_TYPE = "application/x-sentry-envelope"; +/** Trailing carriage return — stripped from SSE lines. */ +const CR_RE = /\r$/; + /** Maximum ingest body size (10 MB). Rejects oversized payloads early. */ const MAX_BODY_BYTES = 10 * 1024 * 1024; @@ -619,24 +611,23 @@ function waitForShutdown(server: Server): Promise { }); } -/** Maximum number of consecutive ports to try before giving up. */ -const MAX_PORT_ATTEMPTS = 10; +/** Maximum retries on EADDRINUSE before giving up. */ +const MAX_PORT_RETRIES = 3; + +/** Delay between EADDRINUSE retries in milliseconds. */ +const PORT_RETRY_DELAY_MS = 5000; /** - * Try to start the HTTP server, auto-incrementing the port on EADDRINUSE. + * Try to start the HTTP server, retrying with backoff on EADDRINUSE. * - * `@hono/node-server`'s `serve()` calls `server.listen()` synchronously and - * returns immediately — the actual bind happens asynchronously. We wrap it in - * a Promise that resolves on the `listening` event and rejects on `error`. - * When the port is busy we bump the port number and retry up to - * {@link MAX_PORT_ATTEMPTS} times, warning the user on each bump. + * Retries up to {@link MAX_PORT_RETRIES} times with a {@link PORT_RETRY_DELAY_MS} + * delay between attempts, matching Spotlight's retry strategy. */ function tryListen( app: Hono, - startPort: number, + port: number, hostname: string ): Promise<{ server: Server; port: number }> { - let port = startPort; let attempts = 0; const attempt = (): Promise<{ server: Server; port: number }> => @@ -648,20 +639,22 @@ function tryListen( }) as unknown as Server; server.once("listening", () => resolve({ server, port })); - server.once("error", (err: NodeJS.ErrnoException) => { + server.once("error", async (err: NodeJS.ErrnoException) => { if (err.code === "EADDRINUSE") { attempts += 1; - if (attempts >= MAX_PORT_ATTEMPTS) { + if (attempts > MAX_PORT_RETRIES) { reject( new ValidationError( - `Port ${startPort} is in use and no open port found after ${MAX_PORT_ATTEMPTS} attempts`, + `Port ${port} is in use after ${MAX_PORT_RETRIES} retries`, "port" ) ); return; } - logger.warn(`Port ${port} is in use, trying ${port + 1}...`); - port += 1; + logger.warn( + `Port ${port} is in use, retrying in ${PORT_RETRY_DELAY_MS / 1000}s (attempt ${attempts}/${MAX_PORT_RETRIES})...` + ); + await Bun.sleep(PORT_RETRY_DELAY_MS); resolve(attempt()); return; } @@ -672,22 +665,120 @@ function tryListen( return attempt(); } +/** + * Check whether a Spotlight server is already running on the given URL. + * Returns `true` if the health endpoint responds successfully. + */ +async function isServerRunning(url: string): Promise { + try { + const res = await fetch(`${url}/health`); + return res.ok; + } catch { + return false; + } +} + +/** Mutable state for the SSE line parser. */ +type SSEParserState = { + eventType: string; + dataLines: string[]; +}; + +/** Process a single SSE line, dispatching complete events via callback. */ +function feedSSELine( + line: string, + state: SSEParserState, + onEvent: (type: string, data: string) => void +): void { + if (line.startsWith("event:")) { + state.eventType = line.slice(6).trim(); + } else if (line.startsWith("data:")) { + state.dataLines.push(line.slice(5).trimStart()); + } else if (line === "" && state.dataLines.length > 0) { + onEvent(state.eventType, state.dataLines.join("\n")); + state.eventType = ""; + state.dataLines = []; + } +} + +/** + * Consume SSE events from an upstream Spotlight server and print them. + * + * Bun doesn't have a global `EventSource`, so we use `fetch` with a + * streaming body and parse the SSE wire format manually. + */ +async function consumeSSE( + url: string, + activeFilters: ReadonlySet, + signal: AbortSignal +): Promise { + const res = await fetch(`${url}/stream`, { + headers: { Accept: "text/event-stream" }, + signal, + }); + if (!res.body) { + return; + } + + const decoder = new TextDecoder(); + const state: SSEParserState = { eventType: "", dataLines: [] }; + + for await (const chunk of res.body) { + const text = decoder.decode(chunk as Uint8Array, { stream: true }); + for (const rawLine of text.split("\n")) { + feedSSELine(rawLine.replace(CR_RE, ""), state, (type, data) => { + if (type === SENTRY_CONTENT_TYPE) { + processSSEEvent(data, activeFilters); + } + }); + } + } +} + +/** Parse and format a single SSE data payload from upstream. */ +function processSSEEvent( + data: string, + activeFilters: ReadonlySet +): void { + try { + const envelope = JSON.parse(data) as [ + Record, + [{ type?: string }, unknown][], + ]; + const [header, items] = envelope; + for (const [itemHeader, itemPayload] of items) { + if (!isItemIncluded(itemHeader.type, activeFilters)) { + continue; + } + for (const line of formatItem( + itemHeader.type, + itemPayload as Record, + header, + itemHeader.type ?? "envelope" + )) { + logger.log(line); + } + } + } catch (err) { + logger.debug( + `Failed to parse SSE event: ${err instanceof Error ? err.message : String(err)}` + ); + } +} + export const localCommand = buildCommand({ docs: { brief: "Run a local Spotlight server to capture dev SDK events", fullDescription: - "Start a local Spotlight-compatible server.\n\n" + + "Start a local Spotlight-compatible server, or attach to one\n" + + "already running on the same port.\n\n" + "Spotlight is Sentry for Development — it gives you a live view of\n" + - "errors, traces, and logs emitted by Sentry SDKs in your dev stack.\n" + - "This command runs a minimal Hono server that ingests envelopes\n" + - "from any Sentry SDK and tails them to your terminal.\n\n" + - "Endpoints:\n" + - " POST /stream — Spotlight ingest\n" + - " POST /api/{projectId}/envelope/ — Sentry SDK ingest\n" + - " GET /stream — SSE feed (for the Spotlight overlay)\n" + - " GET /health — health check\n\n" + + "errors, traces, and logs emitted by Sentry SDKs in your dev stack.\n\n" + + "If a server is already listening on the port, the command connects\n" + + "as an SSE consumer and tails events from it. Otherwise it starts\n" + + "its own server.\n\n" + "Learn more: https://spotlightjs.com/docs/getting-started/\n\n" + - "Press Ctrl-C to stop the server.", + "Press Ctrl-C to stop.", }, parameters: { flags: { @@ -726,8 +817,39 @@ export const localCommand = buildCommand({ }, auth: false, async *func(this: SentryContext, flags: LocalFlags) { - const buffer = createSpotlightBuffer(BUFFER_SIZE); const activeFilters = new Set(flags.filter); + const url = `http://${flags.host}:${flags.port}`; + + if (await isServerRunning(url)) { + logger.info(`Connected to existing server at ${bold(url)}`); + if (activeFilters.size > 0) { + logger.info(`Filtering: ${[...activeFilters].join(", ")}`); + } + logger.info("Press Ctrl-C to stop."); + + const ac = new AbortController(); + const stop = () => ac.abort(); + process.on("SIGINT", stop); + process.on("SIGTERM", stop); + + if (flags.quiet) { + await new Promise((resolve) => { + ac.signal.addEventListener("abort", () => resolve()); + }); + } else { + await consumeSSE(url, activeFilters, ac.signal).catch( + (err: unknown) => { + if (!(err instanceof DOMException && err.name === "AbortError")) { + throw err; + } + } + ); + } + logger.log("Disconnected."); + return; + } + + const buffer = createSpotlightBuffer(BUFFER_SIZE); if (!flags.quiet) { buffer.subscribe((container) => { @@ -745,8 +867,8 @@ export const localCommand = buildCommand({ flags.host ); - const url = `http://${flags.host}:${boundPort}`; - logger.info(`Listening on ${bold(url)}`); + const listenUrl = `http://${flags.host}:${boundPort}`; + logger.info(`Listening on ${bold(listenUrl)}`); if (activeFilters.size > 0) { logger.info(`Filtering: ${[...activeFilters].join(", ")}`); } From f411cf5b1adf01082e53841160d87102d4620503 Mon Sep 17 00:00:00 2001 From: Aditya Mathur <57684218+MathurAditya724@users.noreply.github.com> Date: Fri, 15 May 2026 11:45:16 +0000 Subject: [PATCH 15/46] fix(local): return null from CORS origin callback for non-localhost origins Returning a mismatched string still worked (browser blocks on mismatch) but returning null correctly omits the Access-Control-Allow-Origin header per Hono's CORS middleware API. --- src/commands/local.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/commands/local.ts b/src/commands/local.ts index c67999a2c..e7c03c3ea 100644 --- a/src/commands/local.ts +++ b/src/commands/local.ts @@ -115,7 +115,7 @@ function buildApp( "*", cors({ origin: (origin) => - LOCALHOST_ORIGIN_RE.test(origin) ? origin : "http://localhost", + LOCALHOST_ORIGIN_RE.test(origin) ? origin : null, allowMethods: ["GET", "POST", "OPTIONS"], allowHeaders: ["Content-Type", "Content-Encoding", "User-Agent"], }) From 45fb65ddccf6c193fdae7f422182d54ef323d503 Mon Sep 17 00:00:00 2001 From: Aditya Mathur <57684218+MathurAditya724@users.noreply.github.com> Date: Fri, 15 May 2026 11:49:33 +0000 Subject: [PATCH 16/46] fix(local): address review findings, remove learn-more line - Close failed server on EADDRINUSE before retrying (prevents fd leak) - Fix SSE chunk boundary: use partial line buffer across chunks - Fix SSE data field: spec-compliant single-space strip instead of trimStart - Add logger.debug to isServerRunning catch block (repo policy) - Use process.once for signal handlers in consumer mode - Guard abort race in quiet consumer mode - Remove dead onEnvelope parameter from buildApp - Remove 'Learn more about Spotlight' line from startup banner --- src/commands/local.ts | 56 +++++++++++++++++++++++++------------------ 1 file changed, 33 insertions(+), 23 deletions(-) diff --git a/src/commands/local.ts b/src/commands/local.ts index e7c03c3ea..0a4e737e0 100644 --- a/src/commands/local.ts +++ b/src/commands/local.ts @@ -106,8 +106,7 @@ const LOCALHOST_ORIGIN_RE = /^https?:\/\/(localhost|127\.0\.0\.1)(:\d+)?$/; * arbitrary remote origins to read the SSE envelope stream. */ function buildApp( - spotlightBuffer: ReturnType, - onEnvelope?: (contentType: string, data: Buffer) => void + spotlightBuffer: ReturnType ): Hono { const app = new Hono(); @@ -157,7 +156,7 @@ function buildApp( | undefined; const userAgent = c.req.header("user-agent"); - const container = pushToSpotlightBuffer({ + pushToSpotlightBuffer({ spotlightBuffer, body, encoding: contentEncoding, @@ -165,10 +164,6 @@ function buildApp( userAgent, }); - if (container) { - onEnvelope?.(container.getContentType(), container.getData()); - } - return c.body(null, 204); }; @@ -640,6 +635,7 @@ function tryListen( server.once("listening", () => resolve({ server, port })); server.once("error", async (err: NodeJS.ErrnoException) => { + server.close(); if (err.code === "EADDRINUSE") { attempts += 1; if (attempts > MAX_PORT_RETRIES) { @@ -673,7 +669,11 @@ async function isServerRunning(url: string): Promise { try { const res = await fetch(`${url}/health`); return res.ok; - } catch { + } catch (err) { + logger.debug( + `No existing server at ${url}`, + err instanceof Error ? err.message : String(err) + ); return false; } } @@ -691,9 +691,11 @@ function feedSSELine( onEvent: (type: string, data: string) => void ): void { if (line.startsWith("event:")) { - state.eventType = line.slice(6).trim(); + const value = line.slice(6); + state.eventType = value.startsWith(" ") ? value.slice(1) : value; } else if (line.startsWith("data:")) { - state.dataLines.push(line.slice(5).trimStart()); + const value = line.slice(5); + state.dataLines.push(value.startsWith(" ") ? value.slice(1) : value); } else if (line === "" && state.dataLines.length > 0) { onEvent(state.eventType, state.dataLines.join("\n")); state.eventType = ""; @@ -722,17 +724,25 @@ async function consumeSSE( const decoder = new TextDecoder(); const state: SSEParserState = { eventType: "", dataLines: [] }; + const onEvent = (type: string, data: string) => { + if (type === SENTRY_CONTENT_TYPE) { + processSSEEvent(data, activeFilters); + } + }; + let partial = ""; for await (const chunk of res.body) { - const text = decoder.decode(chunk as Uint8Array, { stream: true }); - for (const rawLine of text.split("\n")) { - feedSSELine(rawLine.replace(CR_RE, ""), state, (type, data) => { - if (type === SENTRY_CONTENT_TYPE) { - processSSEEvent(data, activeFilters); - } - }); + const text = + partial + decoder.decode(chunk as Uint8Array, { stream: true }); + const lines = text.split("\n"); + partial = lines.pop() ?? ""; + for (const rawLine of lines) { + feedSSELine(rawLine.replace(CR_RE, ""), state, onEvent); } } + if (partial) { + feedSSELine(partial.replace(CR_RE, ""), state, onEvent); + } } /** Parse and format a single SSE data payload from upstream. */ @@ -777,7 +787,6 @@ export const localCommand = buildCommand({ "If a server is already listening on the port, the command connects\n" + "as an SSE consumer and tails events from it. Otherwise it starts\n" + "its own server.\n\n" + - "Learn more: https://spotlightjs.com/docs/getting-started/\n\n" + "Press Ctrl-C to stop.", }, parameters: { @@ -829,11 +838,15 @@ export const localCommand = buildCommand({ const ac = new AbortController(); const stop = () => ac.abort(); - process.on("SIGINT", stop); - process.on("SIGTERM", stop); + process.once("SIGINT", stop); + process.once("SIGTERM", stop); if (flags.quiet) { await new Promise((resolve) => { + if (ac.signal.aborted) { + resolve(); + return; + } ac.signal.addEventListener("abort", () => resolve()); }); } else { @@ -872,9 +885,6 @@ export const localCommand = buildCommand({ if (activeFilters.size > 0) { logger.info(`Filtering: ${[...activeFilters].join(", ")}`); } - logger.info( - `Learn more about Spotlight: ${bold("https://spotlightjs.com/docs/getting-started/")}` - ); logger.info("Press Ctrl-C to stop."); await waitForShutdown(server); From 5aa1a83eabdd4334e8c8e04c62328cd298d7b5b6 Mon Sep 17 00:00:00 2001 From: Aditya Mathur <57684218+MathurAditya724@users.noreply.github.com> Date: Fri, 15 May 2026 11:51:28 +0000 Subject: [PATCH 17/46] fix(local): biome formatting for cors origin ternary --- src/commands/local.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/commands/local.ts b/src/commands/local.ts index 0a4e737e0..3a59ba5f3 100644 --- a/src/commands/local.ts +++ b/src/commands/local.ts @@ -113,8 +113,7 @@ function buildApp( app.use( "*", cors({ - origin: (origin) => - LOCALHOST_ORIGIN_RE.test(origin) ? origin : null, + origin: (origin) => (LOCALHOST_ORIGIN_RE.test(origin) ? origin : null), allowMethods: ["GET", "POST", "OPTIONS"], allowHeaders: ["Content-Type", "Content-Encoding", "User-Agent"], }) From 580b6fe3a2365afe7779f66d2abb26035717f99c Mon Sep 17 00:00:00 2001 From: Aditya Mathur <57684218+MathurAditya724@users.noreply.github.com> Date: Fri, 15 May 2026 11:55:34 +0000 Subject: [PATCH 18/46] fix(local): add 2s timeout to health check fetch Prevents the command from hanging indefinitely if a non-HTTP service is listening on the target port. --- src/commands/local.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/commands/local.ts b/src/commands/local.ts index 3a59ba5f3..da00d876f 100644 --- a/src/commands/local.ts +++ b/src/commands/local.ts @@ -666,7 +666,9 @@ function tryListen( */ async function isServerRunning(url: string): Promise { try { - const res = await fetch(`${url}/health`); + const res = await fetch(`${url}/health`, { + signal: AbortSignal.timeout(2000), + }); return res.ok; } catch (err) { logger.debug( From 722294bcf1462c2748f86b5df19ebe189a4b1a65 Mon Sep 17 00:00:00 2001 From: Aditya Mathur <57684218+MathurAditya724@users.noreply.github.com> Date: Fri, 15 May 2026 12:02:14 +0000 Subject: [PATCH 19/46] fix(local): sanitize trace.op and trace.status with stripAnsi These fields were interpolated into terminal output without sanitization, unlike other user-controlled fields in the same function. --- src/commands/local.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/commands/local.ts b/src/commands/local.ts index da00d876f..e536766d4 100644 --- a/src/commands/local.ts +++ b/src/commands/local.ts @@ -380,7 +380,7 @@ function formatTransactionItem( const op = trace?.op; if (op && op !== "default" && op !== "unknown") { - msg = `[${op}] ${msg}`; + msg = `[${stripAnsi(op)}] ${msg}`; } const start = event.start_timestamp as number | undefined; @@ -392,7 +392,7 @@ function formatTransactionItem( const status = trace?.status; if (status && status !== "ok") { - msg += ` ${muted(`[${status}]`)}`; + msg += ` ${muted(`[${stripAnsi(status)}]`)}`; } const spans = event.spans as unknown[] | undefined; From 5c32850a3cdd34516ca675a203c53f8e6f859de2 Mon Sep 17 00:00:00 2001 From: Aditya Mathur <57684218+MathurAditya724@users.noreply.github.com> Date: Fri, 15 May 2026 22:26:32 +0000 Subject: [PATCH 20/46] merge: regenerate pnpm-lock.yaml after merge with main --- .../skills/sentry-cli/references/local.md | 2 +- pnpm-lock.yaml | 817 +++++++++++++++++- 2 files changed, 815 insertions(+), 4 deletions(-) diff --git a/plugins/sentry-cli/skills/sentry-cli/references/local.md b/plugins/sentry-cli/skills/sentry-cli/references/local.md index 4bb11aea8..b349a52db 100644 --- a/plugins/sentry-cli/skills/sentry-cli/references/local.md +++ b/plugins/sentry-cli/skills/sentry-cli/references/local.md @@ -1,6 +1,6 @@ --- name: sentry-cli-local -version: 0.34.0-dev.0 +version: 0.35.0-dev.0 description: Run a local Spotlight server to capture dev SDK events requires: bins: ["sentry"] diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a6873e6de..6aa469854 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -28,6 +28,9 @@ importers: '@clack/prompts': specifier: 0.11.0 version: 0.11.0 + '@hono/node-server': + specifier: ^2.0.0 + version: 2.0.2(hono@4.12.18) '@mastra/client-js': specifier: ^1.4.0 version: 1.19.0(@standard-community/standard-json@0.3.5(@standard-schema/spec@1.1.0)(@types/json-schema@7.0.15)(quansync@0.2.11)(zod-to-json-schema@3.25.2(zod@3.25.76))(zod@3.25.76))(@standard-community/standard-openapi@0.2.9(@standard-community/standard-json@0.3.5(@standard-schema/spec@1.1.0)(@types/json-schema@7.0.15)(quansync@0.2.11)(zod-to-json-schema@3.25.2(zod@3.25.76))(zod@3.25.76))(@standard-schema/spec@1.1.0)(openapi-types@12.1.3)(zod@3.25.76))(@types/json-schema@7.0.15)(express@5.2.1)(openapi-types@12.1.3)(zod@3.25.76) @@ -39,10 +42,13 @@ importers: version: 10.50.0(patch_hash=2351f28c53bf19ae9eb2f6d741b6cb880bc9c6e60bf7ddd14fde6a0e25bde762) '@sentry/node-core': specifier: 10.50.0 - version: 10.50.0(patch_hash=70711b63f77a0c4b4fe10c70350be7126dc05b220ea303cda74090bce733f467)(@opentelemetry/api@1.9.1)(@opentelemetry/core@2.7.1(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.7.1(@opentelemetry/api@1.9.1))(@opentelemetry/semantic-conventions@1.41.1) + version: 10.50.0(patch_hash=70711b63f77a0c4b4fe10c70350be7126dc05b220ea303cda74090bce733f467)(@opentelemetry/api@1.9.1)(@opentelemetry/core@2.7.1(@opentelemetry/api@1.9.1))(@opentelemetry/instrumentation@0.214.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.7.1(@opentelemetry/api@1.9.1))(@opentelemetry/semantic-conventions@1.41.1) '@sentry/sqlish': specifier: ^1.0.0 version: 1.0.1(react@19.2.6) + '@spotlightjs/spotlight': + specifier: ^4.11.3 + version: 4.11.4(hono-rate-limiter@0.4.2(hono@4.12.18)) '@stricli/auto-complete': specifier: ^1.2.4 version: 1.2.7 @@ -88,6 +94,9 @@ importers: fast-check: specifier: ^4.5.3 version: 4.8.0 + hono: + specifier: ^4.12.15 + version: 4.12.18 http-cache-semantics: specifier: ^4.2.0 version: 4.2.0 @@ -424,16 +433,45 @@ packages: cpu: [x64] os: [win32] + '@fastify/otel@0.18.0': + resolution: {integrity: sha512-3TASCATfw+ctICSb4ymrv7iCm0qJ0N9CarB+CZ7zIJ7KqNbwI5JjyDL1/sxoC0ccTO1Zyd1iQ+oqncPg5FJXaA==} + peerDependencies: + '@opentelemetry/api': ^1.9.0 + + '@hono/mcp@0.2.5': + resolution: {integrity: sha512-JsaJes7VlNvUrUQ9j2b9C13xjFLvyKQY515aWtsdJ9cwhBmWz5od2yUCbDu7cX38GeADmlLmpu4BKNNAV6G27w==} + peerDependencies: + '@modelcontextprotocol/sdk': ^1.25.1 + hono: '*' + hono-rate-limiter: ^0.4.2 + zod: ^3.25.0 || ^4.0.0 + '@hono/node-server@1.19.14': resolution: {integrity: sha512-GwtvgtXxnWsucXvbQXkRgqksiH2Qed37H9xHZocE5sA3N8O8O8/8FA3uclQXxXVzc9XBZuEOMK7+r02FmSpHtw==} engines: {node: '>=18.14.1'} peerDependencies: hono: ^4 + '@hono/node-server@2.0.2': + resolution: {integrity: sha512-tXlTi1h/4V7sDe7i97IVP+9re9ZU7wXZZggnR5ucCRclf1+AX6YhGStrR5w8bLj+3Mlyl0pKfBh9gqTqqnGKfQ==} + engines: {node: '>=20'} + peerDependencies: + hono: ^4 + '@isaacs/ttlcache@2.1.4': resolution: {integrity: sha512-7kMz0BJpMvgAMkyglums7B2vtrn5g0a0am77JY0GjkZZNetOBCFn7AG7gKCwT0QPiXyxW7YIQSgtARknUEOcxQ==} engines: {node: '>=12'} + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + + '@jridgewell/trace-mapping@0.3.31': + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + '@lukeed/csprng@1.1.0': resolution: {integrity: sha512-Z7C/xXCiGWsg0KuKsHTKJxbWhpI3Vs5GwLfOean7MGyVFGqdRgBbAjOCh6u4bbjPc/8MJ2pZmK/0DLdCbivLDA==} engines: {node: '>=8'} @@ -470,16 +508,160 @@ packages: '@cfworker/json-schema': optional: true + '@opentelemetry/api-logs@0.207.0': + resolution: {integrity: sha512-lAb0jQRVyleQQGiuuvCOTDVspc14nx6XJjP4FspJ1sNARo3Regq4ZZbrc3rN4b1TYSuUCvgH+UXUPug4SLOqEQ==} + engines: {node: '>=8.0.0'} + + '@opentelemetry/api-logs@0.212.0': + resolution: {integrity: sha512-TEEVrLbNROUkYY51sBJGk7lO/OLjuepch8+hmpM6ffMJQ2z/KVCjdHuCFX6fJj8OkJP2zckPjrJzQtXU3IAsFg==} + engines: {node: '>=8.0.0'} + + '@opentelemetry/api-logs@0.214.0': + resolution: {integrity: sha512-40lSJeqYO8Uz2Yj7u94/SJWE/wONa7rmMKjI1ZcIjgf3MHNHv1OZUCrCETGuaRF62d5pQD1wKIW+L4lmSMTzZA==} + engines: {node: '>=8.0.0'} + '@opentelemetry/api@1.9.1': resolution: {integrity: sha512-gLyJlPHPZYdAk1JENA9LeHejZe1Ti77/pTeFm/nMXmQH/HFZlcS/O2XJB+L8fkbrNSqhdtlvjBVjxwUYanNH5Q==} engines: {node: '>=8.0.0'} + '@opentelemetry/core@2.6.1': + resolution: {integrity: sha512-8xHSGWpJP9wBxgBpnqGL0R3PbdWQndL1Qp50qrg71+B28zK5OQmUgcDKLJgzyAAV38t4tOyLMGDD60LneR5W8g==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.10.0' + '@opentelemetry/core@2.7.1': resolution: {integrity: sha512-QAqIj32AtK6+pEVNG7EOVxHdE06RP+FM5qpiEJ4RtDcFIqKUZHYhl7/7UY5efhwmwNAg7j8QbJVBLxMerc0+gw==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': '>=1.0.0 <1.10.0' + '@opentelemetry/instrumentation-amqplib@0.61.0': + resolution: {integrity: sha512-mCKoyTGfRNisge4br0NpOFSy2Z1NnEW8hbCJdUDdJFHrPqVzc4IIBPA/vX0U+LUcQqrQvJX+HMIU0dbDRe0i0Q==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-connect@0.57.0': + resolution: {integrity: sha512-FMEBChnI4FLN5TE9DHwfH7QpNir1JzXno1uz/TAucVdLCyrG0jTrKIcNHt/i30A0M2AunNBCkcd8Ei26dIPKdg==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-dataloader@0.31.0': + resolution: {integrity: sha512-f654tZFQXS5YeLDNb9KySrwtg7SnqZN119FauD7acBoTzuLduaiGTNz88ixcVSOOMGZ+EjJu/RFtx5klObC95g==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-fs@0.33.0': + resolution: {integrity: sha512-sCZWXGalQ01wr3tAhSR9ucqFJ0phidpAle6/17HVjD6gN8FLmZMK/8sKxdXYHy3PbnlV1P4zeiSVFNKpbFMNLA==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-generic-pool@0.57.0': + resolution: {integrity: sha512-orhmlaK+ZIW9hKU+nHTbXrCSXZcH83AescTqmpamHRobRmYSQwRbD0a1odc0yAzuzOtxYiHiXAnpnIpaSSY7Ow==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-graphql@0.62.0': + resolution: {integrity: sha512-3YNuLVPUxafXkH1jBAbGsKNsP3XVzcFDhCDCE3OqBwCwShlqQbLMRMFh1T/d5jaVZiGVmSsfof+ICKD2iOV8xg==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-hapi@0.60.0': + resolution: {integrity: sha512-aNljZKYrEa7obLAxd1bCEDxF7kzCLGXTuTJZ8lMR9rIVEjmuKBXN1gfqpm/OB//Zc2zP4iIve1jBp7sr3mQV6w==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-http@0.214.0': + resolution: {integrity: sha512-FlkDhZDRjDJDcO2LcSCtjRpkal1NJ8y0fBqBhTvfAR3JSYY2jAIj1kSS5IjmEBt4c3aWv+u/lqLuoCDrrKCSKg==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-kafkajs@0.23.0': + resolution: {integrity: sha512-4K+nVo+zI+aDz0Z85SObwbdixIbzS9moIuKJaYsdlzcHYnKOPtB7ya8r8Ezivy/GVIBHiKJVq4tv+BEkgOMLaQ==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-knex@0.58.0': + resolution: {integrity: sha512-Hc/o8fSsaWxZ8r1Yw4rNDLwTpUopTf4X32y4W6UhlHmW8Wizz8wfhgOKIelSeqFVTKBBPIDUOsQWuIMxBmu8Bw==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-koa@0.62.0': + resolution: {integrity: sha512-uVip0VuGUQXZ+vFxkKxAUNq8qNl+VFlyHDh/U6IQ8COOEDfbEchdaHnpFrMYF3psZRUuoSIgb7xOeXj00RdwDA==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.9.0 + + '@opentelemetry/instrumentation-lru-memoizer@0.58.0': + resolution: {integrity: sha512-6grM3TdMyHzlGY1cUA+mwoPueB1F3dYKgKtZIH6jOFXqfHAByyLTc+6PFjGM9tKh52CFBJaDwodNlL/Td39z7Q==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-mongodb@0.67.0': + resolution: {integrity: sha512-1WJp5N1lYfHq2IhECOTewFs5Tf2NfUOwQRqs/rZdXKTezArMlucxgzAaqcgp3A3YREXopXTpXHsxZTGHjNhMdQ==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-mongoose@0.60.0': + resolution: {integrity: sha512-8BahAZpKsOoc+lrZGb7Ofn4g3z8qtp5IxDfvAVpKXsEheQN7ONMH5djT5ihy6yf8yyeQJGS0gXFfpEAEeEHqQg==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-mysql2@0.60.0': + resolution: {integrity: sha512-m/5d3bxQALllCzezYDk/6vajh0tj5OijMMvOZGr+qN1NMXm1dzMNwyJ0gNZW7Fo3YFRyj/jJMxIw+W7d525dlw==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-mysql@0.60.0': + resolution: {integrity: sha512-08pO8GFPEIz2zquKDGteBZDNmwketdgH8hTe9rVYgW9kCJXq1Psj3wPQGx+VaX4ZJKCfPeoLMYup9+cxHvZyVQ==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-pg@0.66.0': + resolution: {integrity: sha512-KxfLGXBb7k2ueaPJfq2GXBDXBly8P+SpR/4Mj410hhNgmQF3sCqwXvUBQxZQkDAmsdBAoenM+yV1LhtsMRamcA==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-tedious@0.33.0': + resolution: {integrity: sha512-Q6WQwAD01MMTub31GlejoiFACYNw26J426wyjvU7by7fDIr2nZXNW4vhTGs7i7F0TnXBO3xN688g1tdUgYwJ5w==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation@0.207.0': + resolution: {integrity: sha512-y6eeli9+TLKnznrR8AZlQMSJT7wILpXH+6EYq5Vf/4Ao+huI7EedxQHwRgVUOMLFbe7VFDvHJrX9/f4lcwnJsA==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation@0.212.0': + resolution: {integrity: sha512-IyXmpNnifNouMOe0I/gX7ENfv2ZCNdYTF0FpCsoBcpbIHzk81Ww9rQTYTnvghszCg7qGrIhNvWC8dhEifgX9Jg==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation@0.214.0': + resolution: {integrity: sha512-MHqEX5Dk59cqVah5LiARMACku7jXSVk9iVDWOea4x3cr7VfdByeDCURK6o1lntT1JS/Tsovw01UJrBhN3/uC5w==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + '@opentelemetry/resources@2.7.1': resolution: {integrity: sha512-DeT6KKolmC4e/dRQvMQ/RwlnzhaqeiFOXY5ngoOPJ07GgVVKxZOg9EcrNZb5aTzUn+iCrJldAgOfQm1O/QfPAQ==} engines: {node: ^18.19.0 || >=20.6.0} @@ -496,10 +678,21 @@ packages: resolution: {integrity: sha512-/UhIkaZgPutTFmQ7RnIJGgDXZmtEJ7Dvi86xNTFWcnRxVRNk/aotsqDJYeEvDP+FSMB2SdW+pQzNMcWP0rwuNA==} engines: {node: '>=14'} + '@opentelemetry/sql-common@0.41.2': + resolution: {integrity: sha512-4mhWm3Z8z+i508zQJ7r6Xi7y4mmoJpdvH0fZPFRkWrdp5fq7hhZ2HhYokEOLkfqSMgPR4Z9EyB3DBkbKGOqZiQ==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.1.0 + '@peggyjs/from-mem@3.1.3': resolution: {integrity: sha512-LLlgtfXIaeYXoOYovOI0spLM8ZXaqkAlmcRRrLzHJzLMqkU6Sw0R4KMoCoHx1PjaP815pSCBlS+BN6aD8t1Jgg==} engines: {node: '>=20.8'} + '@prisma/instrumentation@7.6.0': + resolution: {integrity: sha512-ZPW2gRiwpPzEfgeZgaekhqXrbW+Y2RJKHVqUmlhZhKzRNCcvR6DykzylDrynpArKKRQtLxoZy36fK7U0p3pdgQ==} + peerDependencies: + '@opentelemetry/api': ^1.8 + '@sec-ant/readable-stream@0.4.1': resolution: {integrity: sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==} @@ -516,6 +709,10 @@ packages: resolution: {integrity: sha512-J4A+vzUO3adl0TkFCjaN1+4miamrjHiEIYuLHiuu1lmAjq5WIVw32ObvAh4yMwNtxyaEMosTrrh5M6f12XSJFg==} engines: {node: '>=18'} + '@sentry/core@10.53.1': + resolution: {integrity: sha512-XG4ezlkyuAPjBC5+9kXC94rXXuqYTw9NRhfaDHssbTFaGnqBR8vQX2UUgZfY7ucbeelRDGfBu1sywoU+mB04uA==} + engines: {node: '>=18'} + '@sentry/node-core@10.50.0': resolution: {integrity: sha512-Eb1BYf4Lc7ZYmdX3acKP6SgyGikrBA370gbGHaWI5jRu7G7vig8sIu1ghPmY5AlvqBPOetado7GniXr6fAXbTw==} engines: {node: '>=18'} @@ -540,6 +737,34 @@ packages: '@opentelemetry/semantic-conventions': optional: true + '@sentry/node-core@10.53.1': + resolution: {integrity: sha512-iH7SMcM/7jPbN+t7+7ussQOiIqI4BMOGt4VYWlV71/z7k0pY+YPaSvlfVkNXfISiDzFAKv0ecCY3BmsLMu1xDQ==} + engines: {node: '>=18'} + peerDependencies: + '@opentelemetry/api': ^1.9.0 + '@opentelemetry/core': ^1.30.1 || ^2.1.0 + '@opentelemetry/exporter-trace-otlp-http': '>=0.57.0 <1' + '@opentelemetry/instrumentation': '>=0.57.1 <1' + '@opentelemetry/sdk-trace-base': ^1.30.1 || ^2.1.0 + '@opentelemetry/semantic-conventions': ^1.39.0 + peerDependenciesMeta: + '@opentelemetry/api': + optional: true + '@opentelemetry/core': + optional: true + '@opentelemetry/exporter-trace-otlp-http': + optional: true + '@opentelemetry/instrumentation': + optional: true + '@opentelemetry/sdk-trace-base': + optional: true + '@opentelemetry/semantic-conventions': + optional: true + + '@sentry/node@10.53.1': + resolution: {integrity: sha512-rxHVil0tJAmz+keFcZCj1LaUdgdkK66E/l0gqh2p1209PNCGoD3lnClFr6vusy1aF3zF8O9JPtuMEJzXOKhs+w==} + engines: {node: '>=18'} + '@sentry/opentelemetry@10.50.0': resolution: {integrity: sha512-axn3pgDPveGdaMUC0abMCmFN7ux2pA5ebPufCef4lMIsyg7BBQvaEJ+vE19wjstMaBCAJGsdZlL3eeP2rtgRMw==} engines: {node: '>=18'} @@ -549,6 +774,15 @@ packages: '@opentelemetry/sdk-trace-base': ^1.30.1 || ^2.1.0 '@opentelemetry/semantic-conventions': ^1.39.0 + '@sentry/opentelemetry@10.53.1': + resolution: {integrity: sha512-Zok6UXla0mFOjd1YnVb1TZtQNOry9v93fXUqx8jmDaygwWM2BwvP8rBQabLz0/OZXo8+35oge+Vmw+QY5aesnA==} + engines: {node: '>=18'} + peerDependencies: + '@opentelemetry/api': ^1.9.0 + '@opentelemetry/core': ^1.30.1 || ^2.1.0 + '@opentelemetry/sdk-trace-base': ^1.30.1 || ^2.1.0 + '@opentelemetry/semantic-conventions': ^1.39.0 + '@sentry/sqlish@1.0.1': resolution: {integrity: sha512-8Ioewv2Qo4Y18T/O8M9FdoUD70VFUs+gu3XKsXQmEqFbgLl1fVkInmk1IL98fJzNADGCyRXsHGptsISPVW8OsA==} peerDependencies: @@ -569,6 +803,11 @@ packages: resolution: {integrity: sha512-doH1gimEu3A46VX6aVxpHTeHrytJAG6HgdxntYnCFiIFHEM/ZGpG8KiZGBChchjQmG0XFIBL552kBTjVcMZXwQ==} engines: {node: '>=12'} + '@spotlightjs/spotlight@4.11.4': + resolution: {integrity: sha512-OVbvc7QvPXVK+6L1MNl/9mqfmO/YIRqTnNik2kcw46OASH1uVZeN3RNHoFcLuh6B3vE+XTl4clCAsbhRHTd7Mg==} + engines: {node: '>=20'} + hasBin: true + '@standard-community/standard-json@0.3.5': resolution: {integrity: sha512-4+ZPorwDRt47i+O7RjyuaxHRK/37QY/LmgxlGrRrSTLYoFatEOzvqIc85GTlM18SFZ5E91C+v0o/M37wZPpUHA==} peerDependencies: @@ -649,6 +888,9 @@ packages: '@types/bun@1.3.14': resolution: {integrity: sha512-h1hFqFVcvAvD9j9K7ZW7vd82aSA+rTdznZa+5bwvCwqSB1jmmfLcbIWhOLx1/+boy/xmjgCs/OMUL8hRJSmnPw==} + '@types/connect@3.4.38': + resolution: {integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==} + '@types/debug@4.1.13': resolution: {integrity: sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw==} @@ -664,6 +906,9 @@ packages: '@types/ms@2.1.0': resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==} + '@types/mysql@2.15.27': + resolution: {integrity: sha512-YfWiV16IY0OeBfBCk8+hXKmdTKrKlwKN1MNKAPBu5JYxLwBEZl7QzeEpGnlZb3VMGJrrGmB84gXiH+ofs/TezA==} + '@types/node-fetch@2.6.13': resolution: {integrity: sha512-QGpRVpzSaUs30JBSGPjOg4Uveu384erbHBoT1zeONvyCfwQxIkUshLAOqN/k9EjGviPRmWTTe6aH2qySWKTVSw==} @@ -673,6 +918,12 @@ packages: '@types/node@22.19.19': resolution: {integrity: sha512-dyh/xO2Fh5bYrfWaaqGrRQQGkNdmYw6AmaAUvYeUMNTWQtvb796ikLdmTchRmOlOiIJ1TDXfWgVx1QkUlQ6Hew==} + '@types/pg-pool@2.0.7': + resolution: {integrity: sha512-U4CwmGVQcbEuqpyju8/ptOKg6gEC+Tqsvj2xS9o1g71bUh8twxnC6ZL5rZKCsGN0iyH0CwgUyc9VR5owNQF9Ng==} + + '@types/pg@8.15.6': + resolution: {integrity: sha512-NoaMtzhxOrubeL/7UZuNTrejB4MPAJ0RpxZqXQf2qXuVlTPuG6Y8p4u9dKRaue4yjmC7ZhzVO2/Yyyn25znrPQ==} + '@types/picomatch@4.0.3': resolution: {integrity: sha512-iG0T6+nYJ9FAPmx9SsUlnwcq1ZVRuCXcVEvWnntoPlrOpwtSTKNDC9uVAxTsC3PUvJ+99n4RpAcNgBbHX3JSnQ==} @@ -685,6 +936,9 @@ packages: '@types/semver@7.7.1': resolution: {integrity: sha512-FmgJfu+MOcQ370SD0ev7EI8TlCAfKYU+B4m5T3yXc1CiRN94g/SZPtsCkk506aUDtlMnFZvasDwHHUcZUEaYuA==} + '@types/tedious@4.0.14': + resolution: {integrity: sha512-KHPsfX/FoVbUGbyYvk1q9MMQHLPeRZhRJZdO45Q4YjvFkv4hMNghCWTvy7rdKessBsmtz4euWCWAB6/tVpI1Iw==} + '@types/unist@3.0.3': resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==} @@ -724,6 +978,9 @@ packages: ajv@8.20.0: resolution: {integrity: sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==} + anser@2.3.5: + resolution: {integrity: sha512-vcZjxvvVoxTeR5XBNJB38oTu/7eDCZlwdz32N1eNgpyPF7j/Z7Idf+CUwQOkKKpJ7RJyjxgLHCM7vdIK0iCNMQ==} + ansi-escapes@7.3.0: resolution: {integrity: sha512-BvU8nYgGQBxcmMuEeUEmNTvrMVjJNSH7RgW24vXexN4Ven6qCvy4TntnvlnwnMLTVlcRQQdbRY8NKnaIoeWDNg==} engines: {node: '>=18'} @@ -1017,6 +1274,10 @@ packages: resolution: {integrity: sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==} engines: {node: '>=18.0.0'} + eventsource@4.1.0: + resolution: {integrity: sha512-2GuF51iuHX6A9xdTccMTsNb7VO0lHZihApxhvQzJB5A03DvHDd2FQepodbMaztPBmBcE/ox7o2gqaxGhYB9LhQ==} + engines: {node: '>=20.0.0'} + execa@9.6.1: resolution: {integrity: sha512-9Be3ZoN4LmYR90tUoVu2te2BsbzHfhJyfEiAVfz7N5/zv+jduIfLrV2xdQXOHbaD6KgpGdO9PRPM1Y4Q9QkPkA==} engines: {node: ^18.19.0 || >=20.5.0} @@ -1045,6 +1306,9 @@ packages: fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + fast-fuzzy@1.12.0: + resolution: {integrity: sha512-sXxGgHS+ubYpsdLnvOvJ9w5GYYZrtL9mkosG3nfuD446ahvoWEsSKBP7ieGmWIKVLnaxRDgUJkZMdxRgA2Ni+Q==} + fast-uri@3.1.2: resolution: {integrity: sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==} @@ -1079,6 +1343,9 @@ packages: resolution: {integrity: sha512-0iirZp3uVDjVGt9p49aTaqjk84TrglENEDuqfdlZQ1roC9CWlPk6Avf8EEnZNcAqPonwkG35x4n3ww/1THYAeQ==} engines: {node: '>= 12.20'} + forwarded-parse@2.1.2: + resolution: {integrity: sha512-alTFZZQDKMporBH77856pXgzhEzaUVmLCDk+egLgIgHst3Tpndzz8MnKe+GzRJRfvVdn69HhpW7cmXzvtLvJAw==} + forwarded@0.2.0: resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} engines: {node: '>= 0.6'} @@ -1118,6 +1385,9 @@ packages: resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} engines: {node: '>= 0.4'} + graphemesplit@2.6.0: + resolution: {integrity: sha512-rG9w2wAfkpg0DILa1pjnjNfucng3usON360shisqIMUBw/87pojcBSrHmeE4UwryAuBih7g8m1oilf5/u8EWdQ==} + gray-matter@4.0.3: resolution: {integrity: sha512-5v6yZd4JK3eMI3FqqCouswVqwugaA9r4dNZB1wwcmrD02QkV5H0y7XBQW8QwQqEaZY1pM9aqORSORhJRdNK44Q==} engines: {node: '>=6.0'} @@ -1156,6 +1426,11 @@ packages: hono: optional: true + hono-rate-limiter@0.4.2: + resolution: {integrity: sha512-AAtFqgADyrmbDijcRTT/HJfwqfvhalya2Zo+MgfdrMPas3zSMD8SU03cv+ZsYwRU1swv7zgVt0shwN059yzhjw==} + peerDependencies: + hono: ^4.1.1 + hono@4.12.18: resolution: {integrity: sha512-RWzP96k/yv0PQfyXnWjs6zot20TqfpfsNXhOnev8d1InAxubW93L11/oNUc3tQqn2G0bSdAOBpX+2uDFHV7kdQ==} engines: {node: '>=16.9.0'} @@ -1182,6 +1457,9 @@ packages: resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==} engines: {node: '>= 4'} + import-in-the-middle@2.0.6: + resolution: {integrity: sha512-3vZV3jX0XRFW3EJDTwzWoZa+RH1b8eTTx6YOCjglrLyPuepwoBti1k3L2dKwdCUrnVEfc5CuRuGstaC/uQJJaw==} + import-in-the-middle@3.0.1: resolution: {integrity: sha512-pYkiyXVL2Mf3pozdlDGV6NAObxQx13Ae8knZk1UJRJ6uRW/ZRmTGHlQYtrsSl7ubuE5F8CD1z+s1n4RHNuTtuA==} engines: {node: '>=18'} @@ -1263,6 +1541,9 @@ packages: jose@6.2.3: resolution: {integrity: sha512-YYVDInQKFJfR/xa3ojUTl8c2KoTwiL1R5Wg9YCydwH0x0B9grbzlg5HC7mMjCtUJjbQ/YnGEZIhI5tCgfTb4Hw==} + js-base64@3.7.8: + resolution: {integrity: sha512-hNngCeKxIUQiEUN3GPJOkz4wF/YvdUdbNL9hsBcMQTkKzboD7T/q3OYOuuPZLUE6dBxSGpwhk5mwuDud7JVAow==} + js-yaml@3.14.2: resolution: {integrity: sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==} hasBin: true @@ -1287,6 +1568,13 @@ packages: resolution: {integrity: sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==} engines: {node: '>=0.10.0'} + launch-editor@2.13.2: + resolution: {integrity: sha512-4VVDnbOpLXy/s8rdRCSXb+zfMeFR0WlJWpET1iA9CQdlZDfwyLjUuGQzXU4VeOoey6AicSAluWan7Etga6Kcmg==} + + logfmt@1.4.0: + resolution: {integrity: sha512-p1Ow0C2dDJYaQBhRHt+HVMP6ELuBm4jYSYNHPMfz0J5wJ9qA6/7oBOlBZBfT1InqguTYcvJzNea5FItDxTcbyw==} + hasBin: true + longest-streak@3.1.0: resolution: {integrity: sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==} @@ -1306,6 +1594,10 @@ packages: resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} engines: {node: '>= 0.4'} + mcp-proxy@5.12.5: + resolution: {integrity: sha512-Vawdc8vi36fXxKCaDpluRvbGcmrUXJdvXcDhkh30HYsws8XqX2rWPBflZpavzeS+6SwijRFV7g+9ypQRJZlrEQ==} + hasBin: true + mdast-util-find-and-replace@3.0.2: resolution: {integrity: sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg==} @@ -1534,6 +1826,9 @@ packages: resolution: {integrity: sha512-J5ApzjyRkkf601HpEeykoiCvzHQjWxPAHhyjFcEUP2SWq0+35NKh8TLhpLw+Dkq5TZBFvUM6UigdE9hIVYTl5w==} engines: {node: '>=20'} + pako@0.2.9: + resolution: {integrity: sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA==} + parse-ms@4.0.0: resolution: {integrity: sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw==} engines: {node: '>=18'} @@ -1578,6 +1873,17 @@ packages: engines: {node: '>=20'} hasBin: true + pg-int8@1.0.1: + resolution: {integrity: sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==} + engines: {node: '>=4.0.0'} + + pg-protocol@1.13.0: + resolution: {integrity: sha512-zzdvXfS6v89r6v7OcFCHfHlyG/wvry1ALxZo4LqgUoy7W9xhBDMaqOuMiF3qEV45VqsN6rdlcehHrfDtlCPc8w==} + + pg-types@2.2.0: + resolution: {integrity: sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==} + engines: {node: '>=4'} + picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} @@ -1589,6 +1895,22 @@ packages: resolution: {integrity: sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==} engines: {node: '>=16.20.0'} + postgres-array@2.0.0: + resolution: {integrity: sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==} + engines: {node: '>=4'} + + postgres-bytea@1.0.1: + resolution: {integrity: sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ==} + engines: {node: '>=0.10.0'} + + postgres-date@1.0.7: + resolution: {integrity: sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==} + engines: {node: '>=0.10.0'} + + postgres-interval@1.2.0: + resolution: {integrity: sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==} + engines: {node: '>=0.10.0'} + pretty-ms@9.3.0: resolution: {integrity: sha512-gjVS5hOP+M3wMm5nmNOucbIrqudzs9v/57bWRHQWLYklXqoXKrVfYW2W9+glfGsqtPgpiz5WwyEEB+ksXIx3gQ==} engines: {node: '>=18'} @@ -1652,6 +1974,10 @@ packages: resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} engines: {node: '>=0.10.0'} + require-in-the-middle@8.0.1: + resolution: {integrity: sha512-QT7FVMXfWOYFbeRBF6nu+I6tr2Tf3u0q8RIEjNob/heKY/nh7drD/k7eeMFmSQgnTtCzLDcCu/XEnpW2wk4xCQ==} + engines: {node: '>=9.3.0 || >=8.10.0 <9.0.0'} + restore-cursor@4.0.0: resolution: {integrity: sha512-I9fPXU9geO9bHOt9pHHOhOkYerIMsmVaWB0rA2AI9ERh/+x/i7MV5HKBNrg+ljO5eoPVgCcnFuRjJ9uH6I/3eg==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} @@ -1744,6 +2070,9 @@ packages: resolution: {integrity: sha512-IlassDs1Ve8nV6uyQZXF9kdkJpVKnMte2JZQXu13M0A5zwc+vu6+LNHfmxsHBMDtoZE21RHiKI0/xvpecZRCNg==} engines: {node: '>=20'} + split@0.2.10: + resolution: {integrity: sha512-e0pKq+UUH2Xq/sXbYpZBZc3BawsfDZ7dgv+JtRTUPNcvF5CMR4Y9cvJqkMY0MoxWzTHvZuz1beg6pNEKlszPiQ==} + sprintf-js@1.0.3: resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} @@ -1798,6 +2127,12 @@ packages: thenify@3.3.1: resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==} + through@2.3.8: + resolution: {integrity: sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==} + + tiny-inflate@1.0.3: + resolution: {integrity: sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw==} + tinyexec@1.1.2: resolution: {integrity: sha512-dAqSqE/RabpBKI8+h26GfLq6Vb3JVXs30XYQjdMjaj/c2tS8IYYMbIzP599KtRj7c57/wYApb3QjgRgXmrCukA==} engines: {node: '>=18'} @@ -1867,6 +2202,9 @@ packages: undici-types@6.21.0: resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} + unicode-trie@2.0.0: + resolution: {integrity: sha512-x7bc76x0bm4prf1VLg79uhAzKw8DVboClSN5VxJuQ+LKDOVEW9CdH+VY7SP+vX7xCYQqzzgQpFqz15zeLvAtZQ==} + unicorn-magic@0.3.0: resolution: {integrity: sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==} engines: {node: '>=18'} @@ -1962,6 +2300,10 @@ packages: utf-8-validate: optional: true + xtend@4.0.2: + resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} + engines: {node: '>=0.4'} + xxhash-wasm@1.1.0: resolution: {integrity: sha512-147y/6YNh+tlp6nd/2pWq38i9h6mz/EuQ6njIrmW8D1BS5nCqs0P6DG+m6zTGnNz5I+uhZ0SHxBs9BsPrwcKDA==} @@ -1969,6 +2311,11 @@ packages: resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} engines: {node: '>=10'} + yaml@2.9.0: + resolution: {integrity: sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==} + engines: {node: '>= 14.6'} + hasBin: true + yargs-parser@20.2.9: resolution: {integrity: sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==} engines: {node: '>=10'} @@ -2197,12 +2544,43 @@ snapshots: '@esbuild/win32-x64@0.25.12': optional: true + '@fastify/otel@0.18.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/core': 2.7.1(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation': 0.212.0(@opentelemetry/api@1.9.1) + '@opentelemetry/semantic-conventions': 1.41.1 + minimatch: 10.2.5 + transitivePeerDependencies: + - supports-color + + '@hono/mcp@0.2.5(@modelcontextprotocol/sdk@1.29.0(zod@4.4.3))(hono-rate-limiter@0.4.2(hono@4.12.18))(hono@4.12.18)(zod@4.4.3)': + dependencies: + '@modelcontextprotocol/sdk': 1.29.0(zod@4.4.3) + hono: 4.12.18 + hono-rate-limiter: 0.4.2(hono@4.12.18) + pkce-challenge: 5.0.1 + zod: 4.4.3 + '@hono/node-server@1.19.14(hono@4.12.18)': dependencies: hono: 4.12.18 + '@hono/node-server@2.0.2(hono@4.12.18)': + dependencies: + hono: 4.12.18 + '@isaacs/ttlcache@2.1.4': {} + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/sourcemap-codec@1.5.5': {} + + '@jridgewell/trace-mapping@0.3.31': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.5 + '@lukeed/csprng@1.1.0': {} '@lukeed/uuid@2.0.1': @@ -2306,17 +2684,238 @@ snapshots: pkce-challenge: 5.0.1 raw-body: 3.0.2 zod: 3.25.76 - zod-to-json-schema: 3.25.2(zod@3.25.76) + zod-to-json-schema: 3.25.2(zod@4.4.3) transitivePeerDependencies: - supports-color + '@modelcontextprotocol/sdk@1.29.0(zod@4.4.3)': + dependencies: + '@hono/node-server': 1.19.14(hono@4.12.18) + ajv: 8.20.0 + ajv-formats: 3.0.1(ajv@8.20.0) + content-type: 1.0.5 + cors: 2.8.6 + cross-spawn: 7.0.6 + eventsource: 3.0.7 + eventsource-parser: 3.0.8 + express: 5.2.1 + express-rate-limit: 8.5.2(express@5.2.1) + hono: 4.12.18 + jose: 6.2.3 + json-schema-typed: 8.0.2 + pkce-challenge: 5.0.1 + raw-body: 3.0.2 + zod: 4.4.3 + zod-to-json-schema: 3.25.2(zod@4.4.3) + transitivePeerDependencies: + - supports-color + + '@opentelemetry/api-logs@0.207.0': + dependencies: + '@opentelemetry/api': 1.9.1 + + '@opentelemetry/api-logs@0.212.0': + dependencies: + '@opentelemetry/api': 1.9.1 + + '@opentelemetry/api-logs@0.214.0': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/api@1.9.1': {} + '@opentelemetry/core@2.6.1(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/semantic-conventions': 1.41.1 + '@opentelemetry/core@2.7.1(@opentelemetry/api@1.9.1)': dependencies: '@opentelemetry/api': 1.9.1 '@opentelemetry/semantic-conventions': 1.41.1 + '@opentelemetry/instrumentation-amqplib@0.61.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/core': 2.7.1(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation': 0.214.0(@opentelemetry/api@1.9.1) + '@opentelemetry/semantic-conventions': 1.41.1 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-connect@0.57.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/core': 2.7.1(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation': 0.214.0(@opentelemetry/api@1.9.1) + '@opentelemetry/semantic-conventions': 1.41.1 + '@types/connect': 3.4.38 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-dataloader@0.31.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/instrumentation': 0.214.0(@opentelemetry/api@1.9.1) + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-fs@0.33.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/core': 2.7.1(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation': 0.214.0(@opentelemetry/api@1.9.1) + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-generic-pool@0.57.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/instrumentation': 0.214.0(@opentelemetry/api@1.9.1) + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-graphql@0.62.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/instrumentation': 0.214.0(@opentelemetry/api@1.9.1) + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-hapi@0.60.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/core': 2.7.1(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation': 0.214.0(@opentelemetry/api@1.9.1) + '@opentelemetry/semantic-conventions': 1.41.1 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-http@0.214.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/core': 2.6.1(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation': 0.214.0(@opentelemetry/api@1.9.1) + '@opentelemetry/semantic-conventions': 1.41.1 + forwarded-parse: 2.1.2 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-kafkajs@0.23.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/instrumentation': 0.214.0(@opentelemetry/api@1.9.1) + '@opentelemetry/semantic-conventions': 1.41.1 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-knex@0.58.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/instrumentation': 0.214.0(@opentelemetry/api@1.9.1) + '@opentelemetry/semantic-conventions': 1.41.1 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-koa@0.62.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/core': 2.7.1(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation': 0.214.0(@opentelemetry/api@1.9.1) + '@opentelemetry/semantic-conventions': 1.41.1 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-lru-memoizer@0.58.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/instrumentation': 0.214.0(@opentelemetry/api@1.9.1) + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-mongodb@0.67.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/instrumentation': 0.214.0(@opentelemetry/api@1.9.1) + '@opentelemetry/semantic-conventions': 1.41.1 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-mongoose@0.60.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/core': 2.7.1(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation': 0.214.0(@opentelemetry/api@1.9.1) + '@opentelemetry/semantic-conventions': 1.41.1 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-mysql2@0.60.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/instrumentation': 0.214.0(@opentelemetry/api@1.9.1) + '@opentelemetry/semantic-conventions': 1.41.1 + '@opentelemetry/sql-common': 0.41.2(@opentelemetry/api@1.9.1) + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-mysql@0.60.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/instrumentation': 0.214.0(@opentelemetry/api@1.9.1) + '@opentelemetry/semantic-conventions': 1.41.1 + '@types/mysql': 2.15.27 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-pg@0.66.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/core': 2.7.1(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation': 0.214.0(@opentelemetry/api@1.9.1) + '@opentelemetry/semantic-conventions': 1.41.1 + '@opentelemetry/sql-common': 0.41.2(@opentelemetry/api@1.9.1) + '@types/pg': 8.15.6 + '@types/pg-pool': 2.0.7 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-tedious@0.33.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/instrumentation': 0.214.0(@opentelemetry/api@1.9.1) + '@opentelemetry/semantic-conventions': 1.41.1 + '@types/tedious': 4.0.14 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation@0.207.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/api-logs': 0.207.0 + import-in-the-middle: 2.0.6 + require-in-the-middle: 8.0.1 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation@0.212.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/api-logs': 0.212.0 + import-in-the-middle: 2.0.6 + require-in-the-middle: 8.0.1 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation@0.214.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/api-logs': 0.214.0 + import-in-the-middle: 3.0.1 + require-in-the-middle: 8.0.1 + transitivePeerDependencies: + - supports-color + '@opentelemetry/resources@2.7.1(@opentelemetry/api@1.9.1)': dependencies: '@opentelemetry/api': 1.9.1 @@ -2332,10 +2931,22 @@ snapshots: '@opentelemetry/semantic-conventions@1.41.1': {} + '@opentelemetry/sql-common@0.41.2(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/core': 2.7.1(@opentelemetry/api@1.9.1) + '@peggyjs/from-mem@3.1.3': dependencies: semver: 7.7.4 + '@prisma/instrumentation@7.6.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/instrumentation': 0.207.0(@opentelemetry/api@1.9.1) + transitivePeerDependencies: + - supports-color + '@sec-ant/readable-stream@0.4.1': {} '@sentry/api@0.141.0(zod@3.25.76)': @@ -2344,7 +2955,9 @@ snapshots: '@sentry/core@10.50.0(patch_hash=2351f28c53bf19ae9eb2f6d741b6cb880bc9c6e60bf7ddd14fde6a0e25bde762)': {} - '@sentry/node-core@10.50.0(patch_hash=70711b63f77a0c4b4fe10c70350be7126dc05b220ea303cda74090bce733f467)(@opentelemetry/api@1.9.1)(@opentelemetry/core@2.7.1(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.7.1(@opentelemetry/api@1.9.1))(@opentelemetry/semantic-conventions@1.41.1)': + '@sentry/core@10.53.1(patch_hash=2351f28c53bf19ae9eb2f6d741b6cb880bc9c6e60bf7ddd14fde6a0e25bde762)': {} + + '@sentry/node-core@10.50.0(patch_hash=70711b63f77a0c4b4fe10c70350be7126dc05b220ea303cda74090bce733f467)(@opentelemetry/api@1.9.1)(@opentelemetry/core@2.7.1(@opentelemetry/api@1.9.1))(@opentelemetry/instrumentation@0.214.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.7.1(@opentelemetry/api@1.9.1))(@opentelemetry/semantic-conventions@1.41.1)': dependencies: '@sentry/core': 10.50.0(patch_hash=2351f28c53bf19ae9eb2f6d741b6cb880bc9c6e60bf7ddd14fde6a0e25bde762) '@sentry/opentelemetry': 10.50.0(@opentelemetry/api@1.9.1)(@opentelemetry/core@2.7.1(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.7.1(@opentelemetry/api@1.9.1))(@opentelemetry/semantic-conventions@1.41.1) @@ -2352,9 +2965,57 @@ snapshots: optionalDependencies: '@opentelemetry/api': 1.9.1 '@opentelemetry/core': 2.7.1(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation': 0.214.0(@opentelemetry/api@1.9.1) '@opentelemetry/sdk-trace-base': 2.7.1(@opentelemetry/api@1.9.1) '@opentelemetry/semantic-conventions': 1.41.1 + '@sentry/node-core@10.53.1(patch_hash=70711b63f77a0c4b4fe10c70350be7126dc05b220ea303cda74090bce733f467)(@opentelemetry/api@1.9.1)(@opentelemetry/core@2.7.1(@opentelemetry/api@1.9.1))(@opentelemetry/instrumentation@0.214.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.7.1(@opentelemetry/api@1.9.1))(@opentelemetry/semantic-conventions@1.41.1)': + dependencies: + '@sentry/core': 10.53.1(patch_hash=2351f28c53bf19ae9eb2f6d741b6cb880bc9c6e60bf7ddd14fde6a0e25bde762) + '@sentry/opentelemetry': 10.53.1(@opentelemetry/api@1.9.1)(@opentelemetry/core@2.7.1(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.7.1(@opentelemetry/api@1.9.1))(@opentelemetry/semantic-conventions@1.41.1) + import-in-the-middle: 3.0.1 + optionalDependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/core': 2.7.1(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation': 0.214.0(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-trace-base': 2.7.1(@opentelemetry/api@1.9.1) + '@opentelemetry/semantic-conventions': 1.41.1 + + '@sentry/node@10.53.1': + dependencies: + '@fastify/otel': 0.18.0(@opentelemetry/api@1.9.1) + '@opentelemetry/api': 1.9.1 + '@opentelemetry/core': 2.7.1(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation': 0.214.0(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation-amqplib': 0.61.0(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation-connect': 0.57.0(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation-dataloader': 0.31.0(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation-fs': 0.33.0(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation-generic-pool': 0.57.0(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation-graphql': 0.62.0(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation-hapi': 0.60.0(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation-http': 0.214.0(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation-kafkajs': 0.23.0(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation-knex': 0.58.0(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation-koa': 0.62.0(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation-lru-memoizer': 0.58.0(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation-mongodb': 0.67.0(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation-mongoose': 0.60.0(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation-mysql': 0.60.0(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation-mysql2': 0.60.0(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation-pg': 0.66.0(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation-tedious': 0.33.0(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-trace-base': 2.7.1(@opentelemetry/api@1.9.1) + '@opentelemetry/semantic-conventions': 1.41.1 + '@prisma/instrumentation': 7.6.0(@opentelemetry/api@1.9.1) + '@sentry/core': 10.53.1(patch_hash=2351f28c53bf19ae9eb2f6d741b6cb880bc9c6e60bf7ddd14fde6a0e25bde762) + '@sentry/node-core': 10.53.1(patch_hash=70711b63f77a0c4b4fe10c70350be7126dc05b220ea303cda74090bce733f467)(@opentelemetry/api@1.9.1)(@opentelemetry/core@2.7.1(@opentelemetry/api@1.9.1))(@opentelemetry/instrumentation@0.214.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.7.1(@opentelemetry/api@1.9.1))(@opentelemetry/semantic-conventions@1.41.1) + '@sentry/opentelemetry': 10.53.1(@opentelemetry/api@1.9.1)(@opentelemetry/core@2.7.1(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.7.1(@opentelemetry/api@1.9.1))(@opentelemetry/semantic-conventions@1.41.1) + import-in-the-middle: 3.0.1 + transitivePeerDependencies: + - '@opentelemetry/exporter-trace-otlp-http' + - supports-color + '@sentry/opentelemetry@10.50.0(@opentelemetry/api@1.9.1)(@opentelemetry/core@2.7.1(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.7.1(@opentelemetry/api@1.9.1))(@opentelemetry/semantic-conventions@1.41.1)': dependencies: '@opentelemetry/api': 1.9.1 @@ -2363,6 +3024,14 @@ snapshots: '@opentelemetry/semantic-conventions': 1.41.1 '@sentry/core': 10.50.0(patch_hash=2351f28c53bf19ae9eb2f6d741b6cb880bc9c6e60bf7ddd14fde6a0e25bde762) + '@sentry/opentelemetry@10.53.1(@opentelemetry/api@1.9.1)(@opentelemetry/core@2.7.1(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.7.1(@opentelemetry/api@1.9.1))(@opentelemetry/semantic-conventions@1.41.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/core': 2.7.1(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-trace-base': 2.7.1(@opentelemetry/api@1.9.1) + '@opentelemetry/semantic-conventions': 1.41.1 + '@sentry/core': 10.53.1(patch_hash=2351f28c53bf19ae9eb2f6d741b6cb880bc9c6e60bf7ddd14fde6a0e25bde762) + '@sentry/sqlish@1.0.1(react@19.2.6)': optionalDependencies: react: 19.2.6 @@ -2378,6 +3047,32 @@ snapshots: dependencies: escape-string-regexp: 5.0.0 + '@spotlightjs/spotlight@4.11.4(hono-rate-limiter@0.4.2(hono@4.12.18))': + dependencies: + '@hono/mcp': 0.2.5(@modelcontextprotocol/sdk@1.29.0(zod@4.4.3))(hono-rate-limiter@0.4.2(hono@4.12.18))(hono@4.12.18)(zod@4.4.3) + '@hono/node-server': 1.19.14(hono@4.12.18) + '@jridgewell/trace-mapping': 0.3.31 + '@modelcontextprotocol/sdk': 1.29.0(zod@4.4.3) + '@sentry/core': 10.50.0(patch_hash=2351f28c53bf19ae9eb2f6d741b6cb880bc9c6e60bf7ddd14fde6a0e25bde762) + '@sentry/node': 10.53.1 + anser: 2.3.5 + chalk: 5.6.2 + eventsource: 4.1.0 + fast-fuzzy: 1.12.0 + hono: 4.12.18 + launch-editor: 2.13.2 + logfmt: 1.4.0 + mcp-proxy: 5.12.5 + semver: 7.8.0 + uuidv7: 1.2.1 + yaml: 2.9.0 + zod: 4.4.3 + transitivePeerDependencies: + - '@cfworker/json-schema' + - '@opentelemetry/exporter-trace-otlp-http' + - hono-rate-limiter + - supports-color + '@standard-community/standard-json@0.3.5(@standard-schema/spec@1.1.0)(@types/json-schema@7.0.15)(quansync@0.2.11)(zod-to-json-schema@3.25.2(zod@3.25.76))(zod@3.25.76)': dependencies: '@standard-schema/spec': 1.1.0 @@ -2411,6 +3106,10 @@ snapshots: dependencies: bun-types: 1.3.14 + '@types/connect@3.4.38': + dependencies: + '@types/node': 22.19.19 + '@types/debug@4.1.13': dependencies: '@types/ms': 2.1.0 @@ -2425,6 +3124,10 @@ snapshots: '@types/ms@2.1.0': {} + '@types/mysql@2.15.27': + dependencies: + '@types/node': 22.19.19 + '@types/node-fetch@2.6.13': dependencies: '@types/node': 22.19.19 @@ -2438,6 +3141,16 @@ snapshots: dependencies: undici-types: 6.21.0 + '@types/pg-pool@2.0.7': + dependencies: + '@types/pg': 8.15.6 + + '@types/pg@8.15.6': + dependencies: + '@types/node': 22.19.19 + pg-protocol: 1.13.0 + pg-types: 2.2.0 + '@types/picomatch@4.0.3': {} '@types/qrcode-terminal@0.12.2': {} @@ -2448,6 +3161,10 @@ snapshots: '@types/semver@7.7.1': {} + '@types/tedious@4.0.14': + dependencies: + '@types/node': 22.19.19 + '@types/unist@3.0.3': {} '@workflow/serde@4.1.0-beta.2': {} @@ -2482,6 +3199,8 @@ snapshots: json-schema-traverse: 1.0.0 require-from-string: 2.0.2 + anser@2.3.5: {} + ansi-escapes@7.3.0: dependencies: environment: 1.1.0 @@ -2750,6 +3469,10 @@ snapshots: dependencies: eventsource-parser: 3.0.8 + eventsource@4.1.0: + dependencies: + eventsource-parser: 3.0.8 + execa@9.6.1: dependencies: '@sindresorhus/merge-streams': 4.0.0 @@ -2815,6 +3538,10 @@ snapshots: fast-deep-equal@3.1.3: {} + fast-fuzzy@1.12.0: + dependencies: + graphemesplit: 2.6.0 + fast-uri@3.1.2: {} fastq@1.20.1: @@ -2855,6 +3582,8 @@ snapshots: node-domexception: 1.0.0 web-streams-polyfill: 4.0.0-beta.3 + forwarded-parse@2.1.2: {} + forwarded@0.2.0: {} fresh@2.0.0: {} @@ -2896,6 +3625,11 @@ snapshots: gopd@1.2.0: {} + graphemesplit@2.6.0: + dependencies: + js-base64: 3.7.8 + unicode-trie: 2.0.0 + gray-matter@4.0.3: dependencies: js-yaml: 3.14.2 @@ -2926,6 +3660,10 @@ snapshots: optionalDependencies: hono: 4.12.18 + hono-rate-limiter@0.4.2(hono@4.12.18): + dependencies: + hono: 4.12.18 + hono@4.12.18: {} http-cache-semantics@4.2.0: {} @@ -2950,6 +3688,13 @@ snapshots: ignore@7.0.5: {} + import-in-the-middle@2.0.6: + dependencies: + acorn: 8.16.0 + acorn-import-attributes: 1.9.5(acorn@8.16.0) + cjs-module-lexer: 2.2.0 + module-details-from-path: 1.0.4 + import-in-the-middle@3.0.1: dependencies: acorn: 8.16.0 @@ -3030,6 +3775,8 @@ snapshots: jose@6.2.3: {} + js-base64@3.7.8: {} + js-yaml@3.14.2: dependencies: argparse: 1.0.10 @@ -3047,6 +3794,16 @@ snapshots: kind-of@6.0.3: {} + launch-editor@2.13.2: + dependencies: + picocolors: 1.1.1 + shell-quote: 1.8.3 + + logfmt@1.4.0: + dependencies: + split: 0.2.10 + through: 2.3.8 + longest-streak@3.1.0: {} lru-cache@11.3.6: {} @@ -3057,6 +3814,8 @@ snapshots: math-intrinsics@1.1.0: {} + mcp-proxy@5.12.5: {} + mdast-util-find-and-replace@3.0.2: dependencies: '@types/mdast': 4.0.4 @@ -3433,6 +4192,8 @@ snapshots: dependencies: is-network-error: 1.3.2 + pako@0.2.9: {} + parse-ms@4.0.0: {} parse5-htmlparser2-tree-adapter@6.0.1: @@ -3466,12 +4227,34 @@ snapshots: commander: 14.0.3 source-map-generator: 2.0.6 + pg-int8@1.0.1: {} + + pg-protocol@1.13.0: {} + + pg-types@2.2.0: + dependencies: + pg-int8: 1.0.1 + postgres-array: 2.0.0 + postgres-bytea: 1.0.1 + postgres-date: 1.0.7 + postgres-interval: 1.2.0 + picocolors@1.1.1: {} picomatch@4.0.4: {} pkce-challenge@5.0.1: {} + postgres-array@2.0.0: {} + + postgres-bytea@1.0.1: {} + + postgres-date@1.0.7: {} + + postgres-interval@1.2.0: + dependencies: + xtend: 4.0.2 + pretty-ms@9.3.0: dependencies: parse-ms: 4.0.0 @@ -3547,6 +4330,13 @@ snapshots: require-from-string@2.0.2: {} + require-in-the-middle@8.0.1: + dependencies: + debug: 4.4.3 + module-details-from-path: 1.0.4 + transitivePeerDependencies: + - supports-color + restore-cursor@4.0.0: dependencies: onetime: 5.1.2 @@ -3655,6 +4445,10 @@ snapshots: source-map-generator@2.0.6: {} + split@0.2.10: + dependencies: + through: 2.3.8 + sprintf-js@1.0.3: {} stack-utils@2.0.6: @@ -3702,6 +4496,10 @@ snapshots: dependencies: any-promise: 1.3.0 + through@2.3.8: {} + + tiny-inflate@1.0.3: {} + tinyexec@1.1.2: {} tinyglobby@0.2.16: @@ -3757,6 +4555,11 @@ snapshots: undici-types@6.21.0: {} + unicode-trie@2.0.0: + dependencies: + pako: 0.2.9 + tiny-inflate: 1.0.3 + unicorn-magic@0.3.0: {} unified@11.0.5: @@ -3841,10 +4644,14 @@ snapshots: ws@8.20.1: {} + xtend@4.0.2: {} + xxhash-wasm@1.1.0: {} y18n@5.0.8: {} + yaml@2.9.0: {} + yargs-parser@20.2.9: {} yargs@16.2.0: @@ -3875,6 +4682,10 @@ snapshots: dependencies: zod: 3.25.76 + zod-to-json-schema@3.25.2(zod@4.4.3): + dependencies: + zod: 4.4.3 + zod@3.25.76: {} zod@4.4.3: {} From 26869ed97e80d5c78dfeaa7e668a22bc65255124 Mon Sep 17 00:00:00 2001 From: Aditya Mathur <57684218+MathurAditya724@users.noreply.github.com> Date: Fri, 15 May 2026 22:43:22 +0000 Subject: [PATCH 21/46] fix(local): detect upstream disconnect in --quiet attach mode Previously, --quiet mode waited only for SIGINT/SIGTERM and would hang indefinitely if the upstream server shut down. Now it consumes the SSE stream (draining chunks without parsing) so the for-await loop exits naturally on disconnect. --- src/commands/local.ts | 36 +++++++++++++++++++----------------- 1 file changed, 19 insertions(+), 17 deletions(-) diff --git a/src/commands/local.ts b/src/commands/local.ts index e536766d4..93897d363 100644 --- a/src/commands/local.ts +++ b/src/commands/local.ts @@ -713,7 +713,8 @@ function feedSSELine( async function consumeSSE( url: string, activeFilters: ReadonlySet, - signal: AbortSignal + signal: AbortSignal, + quiet = false ): Promise { const res = await fetch(`${url}/stream`, { headers: { Accept: "text/event-stream" }, @@ -723,6 +724,15 @@ async function consumeSSE( return; } + // In quiet mode we still consume the stream to detect disconnection, + // but skip parsing/formatting entirely. + if (quiet) { + for await (const _chunk of res.body) { + // drain + } + return; + } + const decoder = new TextDecoder(); const state: SSEParserState = { eventType: "", dataLines: [] }; const onEvent = (type: string, data: string) => { @@ -842,23 +852,15 @@ export const localCommand = buildCommand({ process.once("SIGINT", stop); process.once("SIGTERM", stop); - if (flags.quiet) { - await new Promise((resolve) => { - if (ac.signal.aborted) { - resolve(); - return; + // Connect to the SSE stream even in quiet mode so we detect when + // the upstream server disconnects (the for-await loop exits). + await consumeSSE(url, activeFilters, ac.signal, flags.quiet).catch( + (err: unknown) => { + if (!(err instanceof DOMException && err.name === "AbortError")) { + throw err; } - ac.signal.addEventListener("abort", () => resolve()); - }); - } else { - await consumeSSE(url, activeFilters, ac.signal).catch( - (err: unknown) => { - if (!(err instanceof DOMException && err.name === "AbortError")) { - throw err; - } - } - ); - } + } + ); logger.log("Disconnected."); return; } From 281202512b2b7feb77d5b69a8383a2ac78c33c25 Mon Sep 17 00:00:00 2001 From: Aditya Mathur <57684218+MathurAditya724@users.noreply.github.com> Date: Wed, 20 May 2026 07:43:13 +0000 Subject: [PATCH 22/46] =?UTF-8?q?feat(local):=20restructure=20into=20subco?= =?UTF-8?q?mmands=20=E2=80=94=20server=20(default)=20and=20run?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Move local.ts → local/server.ts as the default subcommand - Add local/run.ts: spawns child process with SENTRY_SPOTLIGHT, NEXT_PUBLIC_SENTRY_SPOTLIGHT, and SENTRY_TRACES_SAMPLE_RATE=1 injected - Add local/index.ts route map: server (default), run - Fix patchedDependencies: add bun-compatible top-level format alongside pnpm format so Stricli patch applies correctly in both package managers - Update docs fragment with run command examples and env var table --- bun.lock | 11 +- docs/src/content/docs/contributing.md | 2 +- docs/src/fragments/commands/local.md | 28 +++-- package.json | 7 +- plugins/sentry-cli/skills/sentry-cli/SKILL.md | 5 +- .../skills/sentry-cli/references/local.md | 28 +++-- src/app.ts | 4 +- src/commands/local/index.ts | 20 ++++ src/commands/local/run.ts | 108 ++++++++++++++++++ src/commands/{local.ts => local/server.ts} | 16 +-- 10 files changed, 197 insertions(+), 32 deletions(-) create mode 100644 src/commands/local/index.ts create mode 100644 src/commands/local/run.ts rename src/commands/{local.ts => local/server.ts} (98%) diff --git a/bun.lock b/bun.lock index ec3babb29..3af37a4cf 100644 --- a/bun.lock +++ b/bun.lock @@ -54,6 +54,11 @@ }, }, }, + "patchedDependencies": { + "@sentry/core@10.50.0": "patches/@sentry%2Fcore@10.50.0.patch", + "@sentry/node-core@10.50.0": "patches/@sentry%2Fnode-core@10.50.0.patch", + "@stricli/core@1.2.5": "patches/@stricli%2Fcore@1.2.5.patch", + }, "packages": { "@a2a-js/sdk": ["@a2a-js/sdk@0.2.5", "", { "dependencies": { "@types/cors": "^2.8.17", "@types/express": "^4.17.23", "body-parser": "^2.2.0", "cors": "^2.8.5", "express": "^4.21.2", "uuid": "^11.1.0" } }, "sha512-VTDuRS5V0ATbJ/LkaQlisMnTAeYKXAK6scMguVBstf+KIBQ7HIuKhiXLv+G/hvejkV+THoXzoNifInAkU81P1g=="], @@ -153,7 +158,7 @@ "@hono/mcp": ["@hono/mcp@0.2.5", "", { "dependencies": { "pkce-challenge": "^5.0.0" }, "peerDependencies": { "@modelcontextprotocol/sdk": "^1.25.1", "hono": "*", "hono-rate-limiter": "^0.4.2", "zod": "^3.25.0 || ^4.0.0" } }, "sha512-JsaJes7VlNvUrUQ9j2b9C13xjFLvyKQY515aWtsdJ9cwhBmWz5od2yUCbDu7cX38GeADmlLmpu4BKNNAV6G27w=="], - "@hono/node-server": ["@hono/node-server@2.0.2", "", { "peerDependencies": { "hono": "^4" } }, "sha512-tXlTi1h/4V7sDe7i97IVP+9re9ZU7wXZZggnR5ucCRclf1+AX6YhGStrR5w8bLj+3Mlyl0pKfBh9gqTqqnGKfQ=="], + "@hono/node-server": ["@hono/node-server@2.0.3", "", { "peerDependencies": { "hono": "^4" } }, "sha512-a0jV+/HRe3G5zjFID3zObAQFdkl6zpxTuqktdDDXS3MJKcrZIkB8OkLpNBlY/WXFqv2HF4a0takPej+aNFczWA=="], "@isaacs/balanced-match": ["@isaacs/balanced-match@4.0.1", "", {}, "sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ=="], @@ -517,7 +522,7 @@ "highlight.js": ["highlight.js@10.7.3", "", {}, "sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A=="], - "hono": ["hono@4.12.18", "", {}, "sha512-RWzP96k/yv0PQfyXnWjs6zot20TqfpfsNXhOnev8d1InAxubW93L11/oNUc3tQqn2G0bSdAOBpX+2uDFHV7kdQ=="], + "hono": ["hono@4.12.21", "", {}, "sha512-uV63apnb0kyPtAUwoWgaGh9HyIFcv8lgmzPZSiTBQAFOFGIzka5EZ1dZocmGnn0XdX0+XTqJ6Tqv7selMuGLRQ=="], "hono-openapi": ["hono-openapi@1.3.0", "", { "peerDependencies": { "@hono/standard-validator": "^0.2.0", "@standard-community/standard-json": "^0.3.5", "@standard-community/standard-openapi": "^0.2.9", "@types/json-schema": "^7.0.15", "hono": "^4.8.3", "openapi-types": "^12.1.3" }, "optionalPeers": ["@hono/standard-validator", "hono"] }, "sha512-xDvCWpWEIv0weEmnl3EjRQzqbHIO8LnfzMuYOCmbuyE5aes6aXxLg4vM3ybnoZD5TiTUkA6PuRQPJs3R7WRBig=="], @@ -667,7 +672,7 @@ "pg-int8": ["pg-int8@1.0.1", "", {}, "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw=="], - "pg-protocol": ["pg-protocol@1.13.0", "", {}, "sha512-zzdvXfS6v89r6v7OcFCHfHlyG/wvry1ALxZo4LqgUoy7W9xhBDMaqOuMiF3qEV45VqsN6rdlcehHrfDtlCPc8w=="], + "pg-protocol": ["pg-protocol@1.14.0", "", {}, "sha512-n5taZ1kO3s9ngDTVxsEznOqCyToTgz0FLuPq0B33COy5pPpuWJpY3/2oRBVETuOgzdqRXfWpM9HIhp2LBBT1BA=="], "pg-types": ["pg-types@2.2.0", "", { "dependencies": { "pg-int8": "1.0.1", "postgres-array": "~2.0.0", "postgres-bytea": "~1.0.0", "postgres-date": "~1.0.4", "postgres-interval": "^1.1.0" } }, "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA=="], diff --git a/docs/src/content/docs/contributing.md b/docs/src/content/docs/contributing.md index 0e2c2f5b5..a53fcd481 100644 --- a/docs/src/content/docs/contributing.md +++ b/docs/src/content/docs/contributing.md @@ -56,6 +56,7 @@ cli/ │ │ ├── dashboard/ # list, view, create, add, edit, delete, revisions, restore │ │ ├── event/ # view, list │ │ ├── issue/ # list, events, explain, plan, view, resolve, unresolve, archive, merge +│ │ ├── local/ # server, run │ │ ├── log/ # list, view │ │ ├── org/ # list, view │ │ ├── project/ # create, delete, list, view @@ -71,7 +72,6 @@ cli/ │ │ ├── explore.ts # Query aggregate event data (Explore) │ │ ├── help.ts # Help command │ │ ├── init.ts # Initialize Sentry in your project (experimental) -│ │ ├── local.ts # Run a local Spotlight server to capture dev SDK events │ │ └── schema.ts # Browse the Sentry API schema │ ├── lib/ # Shared utilities │ └── types/ # TypeScript types and Zod schemas diff --git a/docs/src/fragments/commands/local.md b/docs/src/fragments/commands/local.md index e56b4b104..9051257aa 100644 --- a/docs/src/fragments/commands/local.md +++ b/docs/src/fragments/commands/local.md @@ -4,24 +4,38 @@ No authentication is required — the server binds to `localhost` by default and is purely a development tool. -Learn more about Spotlight at [spotlightjs.com/docs/getting-started](https://spotlightjs.com/docs/getting-started/). - ## Examples ```bash -# Start the server on the default port (8969) +# Start the server and tail events (default) sentry local -# Use a custom port and bind to all interfaces -sentry local --port 9000 --host 0.0.0.0 +# Run your app with Spotlight auto-enabled +sentry local run -- npm run dev +sentry local run -- python manage.py runserver -# Run quietly (suppress per-envelope tail output) -sentry local --quiet +# Use a custom port +sentry local --port 9000 # Only show errors and logs (filter out transactions) sentry local -f error -f log + +# Run quietly (suppress per-envelope tail output) +sentry local --quiet ``` +## `sentry local run` + +Runs a command with `SENTRY_SPOTLIGHT` injected into the environment. The Sentry SDK automatically detects this variable and sends envelopes to the local server. No code changes needed. + +Env vars injected into the child process: + +| Variable | Value | +|----------|-------| +| `SENTRY_SPOTLIGHT` | `http://localhost:/stream` | +| `NEXT_PUBLIC_SENTRY_SPOTLIGHT` | `http://localhost:/stream` | +| `SENTRY_TRACES_SAMPLE_RATE` | `1` | + ## Endpoints | Method | Path | Description | diff --git a/package.json b/package.json index 141af0eda..e1b3273c3 100644 --- a/package.json +++ b/package.json @@ -118,5 +118,10 @@ "check:docs-sections": "bun run script/generate-docs-sections.ts --check" }, "type": "module", - "types": "./dist/index.d.cts" + "types": "./dist/index.d.cts", + "patchedDependencies": { + "@stricli/core@1.2.5": "patches/@stricli%2Fcore@1.2.5.patch", + "@sentry/node-core@10.50.0": "patches/@sentry%2Fnode-core@10.50.0.patch", + "@sentry/core@10.50.0": "patches/@sentry%2Fcore@10.50.0.patch" + } } diff --git a/plugins/sentry-cli/skills/sentry-cli/SKILL.md b/plugins/sentry-cli/skills/sentry-cli/SKILL.md index 8166ba091..1843adb7c 100644 --- a/plugins/sentry-cli/skills/sentry-cli/SKILL.md +++ b/plugins/sentry-cli/skills/sentry-cli/SKILL.md @@ -470,9 +470,10 @@ Initialize Sentry in your project (experimental) ### Local -Run a local Spotlight server to capture dev SDK events +Run a local Spotlight server for development -- `sentry local` — Run a local Spotlight server to capture dev SDK events +- `sentry local server` — Run a local Spotlight server to capture dev SDK events +- `sentry local run ` — Run a command with Spotlight enabled → Full flags and examples: `references/local.md` diff --git a/plugins/sentry-cli/skills/sentry-cli/references/local.md b/plugins/sentry-cli/skills/sentry-cli/references/local.md index b349a52db..42aebb583 100644 --- a/plugins/sentry-cli/skills/sentry-cli/references/local.md +++ b/plugins/sentry-cli/skills/sentry-cli/references/local.md @@ -1,7 +1,7 @@ --- name: sentry-cli-local version: 0.35.0-dev.0 -description: Run a local Spotlight server to capture dev SDK events +description: Run a local Spotlight server for development requires: bins: ["sentry"] auth: true @@ -9,9 +9,9 @@ requires: # Local Commands -Run a local Spotlight server to capture dev SDK events +Run a local Spotlight server for development -### `sentry local` +### `sentry local server` Run a local Spotlight server to capture dev SDK events @@ -21,21 +21,33 @@ Run a local Spotlight server to capture dev SDK events - `-q, --quiet - Suppress per-envelope tail output` - `-f, --filter ... - Only show items of this type (repeatable: error, transaction, log)` +### `sentry local run ` + +Run a command with Spotlight enabled + +**Flags:** +- `-p, --port - Port for the Spotlight server (default 8969) - (default: "8969")` +- `--host - Hostname for the Spotlight server (default localhost) - (default: "localhost")` + **Examples:** ```bash -# Start the server on the default port (8969) +# Start the server and tail events (default) sentry local -# Use a custom port and bind to all interfaces -sentry local --port 9000 --host 0.0.0.0 +# Run your app with Spotlight auto-enabled +sentry local run -- npm run dev +sentry local run -- python manage.py runserver -# Run quietly (suppress per-envelope tail output) -sentry local --quiet +# Use a custom port +sentry local --port 9000 # Only show errors and logs (filter out transactions) sentry local -f error -f log +# Run quietly (suppress per-envelope tail output) +sentry local --quiet + sentry local -f error -f log # only errors and logs ``` diff --git a/src/app.ts b/src/app.ts index 53c40932d..426b4fdc7 100644 --- a/src/app.ts +++ b/src/app.ts @@ -18,7 +18,7 @@ import { helpCommand } from "./commands/help.js"; import { initCommand } from "./commands/init.js"; import { issueRoute } from "./commands/issue/index.js"; import { listCommand as issueListCommand } from "./commands/issue/list.js"; -import { localCommand } from "./commands/local.js"; +import { localRoute } from "./commands/local/index.js"; import { logRoute } from "./commands/log/index.js"; import { listCommand as logListCommand } from "./commands/log/list.js"; import { orgRoute } from "./commands/org/index.js"; @@ -104,7 +104,7 @@ export const routes = buildRouteMap({ trace: traceRoute, trial: trialRoute, init: initCommand, - local: localCommand, + local: localRoute, api: apiCommand, schema: schemaCommand, dashboards: dashboardListCommand, diff --git a/src/commands/local/index.ts b/src/commands/local/index.ts new file mode 100644 index 000000000..9ce0a31b7 --- /dev/null +++ b/src/commands/local/index.ts @@ -0,0 +1,20 @@ +import { buildRouteMap } from "../../lib/route-map.js"; +import { runCommand } from "./run.js"; +import { serverCommand } from "./server.js"; + +export const localRoute = buildRouteMap({ + routes: { + server: serverCommand, + run: runCommand, + }, + defaultCommand: "server", + docs: { + brief: "Run a local Spotlight server for development", + fullDescription: + "Run a local Spotlight-compatible server to capture Sentry SDK\n" + + "events from your dev stack.\n\n" + + "Commands:\n" + + " server Start the server and tail events (default)\n" + + " run Run a command with SENTRY_SPOTLIGHT auto-injected", + }, +}); diff --git a/src/commands/local/run.ts b/src/commands/local/run.ts new file mode 100644 index 000000000..fb6284e64 --- /dev/null +++ b/src/commands/local/run.ts @@ -0,0 +1,108 @@ +/** + * sentry local run + * + * Run a command with Sentry Spotlight enabled. Starts the local Spotlight + * server (or connects to an existing one), injects `SENTRY_SPOTLIGHT` into + * the child process environment so the Sentry SDK auto-sends envelopes to + * the local server, then tails events to the terminal. + */ + +import type { SentryContext } from "../../context.js"; +import { buildCommand } from "../../lib/command.js"; +import { CliError, ValidationError } from "../../lib/errors.js"; +import { bold } from "../../lib/formatters/colors.js"; +import { logger } from "../../lib/logger.js"; +import { DEFAULT_PORT } from "./server.js"; + +type RunFlags = { + readonly port: number; + readonly host: string; +}; + +/** Parse and validate a port number. */ +function parsePort(value: string): number { + const port = Number(value); + if (!Number.isInteger(port) || port < 0 || port > 65_535) { + throw new ValidationError( + `Invalid port: ${value}. Must be an integer between 0 and 65535.`, + "port" + ); + } + return port; +} + +export const runCommand = buildCommand({ + docs: { + brief: "Run a command with Spotlight enabled", + fullDescription: + "Run a command with the SENTRY_SPOTLIGHT environment variable\n" + + "injected so the Sentry SDK automatically sends envelopes to the\n" + + "local Spotlight server.\n\n" + + "If no server is running on the port, one is started automatically\n" + + "in the background. The child process inherits all current env vars\n" + + "plus SENTRY_SPOTLIGHT and SENTRY_TRACES_SAMPLE_RATE=1.\n\n" + + "Example:\n" + + " sentry local run -- npm run dev\n" + + " sentry local run -- python manage.py runserver", + }, + parameters: { + positional: { + kind: "array", + parameter: { + brief: "Command to run", + parse: String, + placeholder: "command", + }, + }, + flags: { + port: { + kind: "parsed", + parse: parsePort, + brief: `Port for the Spotlight server (default ${DEFAULT_PORT})`, + default: String(DEFAULT_PORT), + }, + host: { + kind: "parsed", + parse: String, + brief: "Hostname for the Spotlight server (default localhost)", + default: "localhost", + }, + }, + aliases: { + p: "port", + }, + }, + auth: false, + // biome-ignore lint/correctness/useYield: child process wrapper, no structured output + async *func(this: SentryContext, flags: RunFlags, args: string[]) { + if (args.length === 0) { + throw new ValidationError( + "No command provided. Usage: sentry local run -- ", + "command" + ); + } + + const spotlightUrl = `http://${flags.host}:${flags.port}/stream`; + + logger.info(`Starting: ${bold(args.join(" "))}`); + logger.info(`SENTRY_SPOTLIGHT=${spotlightUrl}`); + + const child = Bun.spawn(args, { + env: { + ...process.env, + SENTRY_SPOTLIGHT: spotlightUrl, + NEXT_PUBLIC_SENTRY_SPOTLIGHT: spotlightUrl, + SENTRY_TRACES_SAMPLE_RATE: "1", + }, + stdout: "inherit", + stderr: "inherit", + stdin: "inherit", + }); + + const exitCode = await child.exited; + + if (exitCode !== 0) { + throw new CliError(`Process exited with code ${exitCode}`, exitCode); + } + }, +}); diff --git a/src/commands/local.ts b/src/commands/local/server.ts similarity index 98% rename from src/commands/local.ts rename to src/commands/local/server.ts index 93897d363..94826fbc6 100644 --- a/src/commands/local.ts +++ b/src/commands/local/server.ts @@ -22,9 +22,9 @@ import { import { Hono } from "hono"; import { cors } from "hono/cors"; import { streamSSE } from "hono/streaming"; -import type { SentryContext } from "../context.js"; -import { buildCommand, numberParser } from "../lib/command.js"; -import { ValidationError } from "../lib/errors.js"; +import type { SentryContext } from "../../context.js"; +import { buildCommand, numberParser } from "../../lib/command.js"; +import { ValidationError } from "../../lib/errors.js"; import { blue, bold, @@ -33,12 +33,12 @@ import { muted, red, yellow, -} from "../lib/formatters/colors.js"; -import { stripAnsi } from "../lib/formatters/plain-detect.js"; -import { logger } from "../lib/logger.js"; +} from "../../lib/formatters/colors.js"; +import { stripAnsi } from "../../lib/formatters/plain-detect.js"; +import { logger } from "../../lib/logger.js"; /** Default port matches Spotlight's `DEFAULT_PORT`. */ -const DEFAULT_PORT = 8969; +export const DEFAULT_PORT = 8969; /** Buffer size: how many recent envelopes to retain for late subscribers. */ const BUFFER_SIZE = 500; @@ -787,7 +787,7 @@ function processSSEEvent( } } -export const localCommand = buildCommand({ +export const serverCommand = buildCommand({ docs: { brief: "Run a local Spotlight server to capture dev SDK events", fullDescription: From 478e9a5b035b3f250b4b05f798e8d0dcaabcac38 Mon Sep 17 00:00:00 2001 From: Aditya Mathur <57684218+MathurAditya724@users.noreply.github.com> Date: Wed, 20 May 2026 07:45:26 +0000 Subject: [PATCH 23/46] fix(local): rename subcommand 'server' to 'serve' --- src/commands/local/index.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/commands/local/index.ts b/src/commands/local/index.ts index 9ce0a31b7..aa41a7b77 100644 --- a/src/commands/local/index.ts +++ b/src/commands/local/index.ts @@ -4,17 +4,17 @@ import { serverCommand } from "./server.js"; export const localRoute = buildRouteMap({ routes: { - server: serverCommand, + serve: serverCommand, run: runCommand, }, - defaultCommand: "server", + defaultCommand: "serve", docs: { brief: "Run a local Spotlight server for development", fullDescription: "Run a local Spotlight-compatible server to capture Sentry SDK\n" + "events from your dev stack.\n\n" + "Commands:\n" + - " server Start the server and tail events (default)\n" + + " serve Start the server and tail events (default)\n" + " run Run a command with SENTRY_SPOTLIGHT auto-injected", }, }); From 266f80492fa2af06f92d9ef8013d6360474ae922 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 20 May 2026 07:45:56 +0000 Subject: [PATCH 24/46] chore: regenerate docs --- docs/src/content/docs/contributing.md | 2 +- plugins/sentry-cli/skills/sentry-cli/SKILL.md | 2 +- plugins/sentry-cli/skills/sentry-cli/references/local.md | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/src/content/docs/contributing.md b/docs/src/content/docs/contributing.md index a53fcd481..119f2c6e5 100644 --- a/docs/src/content/docs/contributing.md +++ b/docs/src/content/docs/contributing.md @@ -56,7 +56,7 @@ cli/ │ │ ├── dashboard/ # list, view, create, add, edit, delete, revisions, restore │ │ ├── event/ # view, list │ │ ├── issue/ # list, events, explain, plan, view, resolve, unresolve, archive, merge -│ │ ├── local/ # server, run +│ │ ├── local/ # serve, run │ │ ├── log/ # list, view │ │ ├── org/ # list, view │ │ ├── project/ # create, delete, list, view diff --git a/plugins/sentry-cli/skills/sentry-cli/SKILL.md b/plugins/sentry-cli/skills/sentry-cli/SKILL.md index 1843adb7c..98acff102 100644 --- a/plugins/sentry-cli/skills/sentry-cli/SKILL.md +++ b/plugins/sentry-cli/skills/sentry-cli/SKILL.md @@ -472,7 +472,7 @@ Initialize Sentry in your project (experimental) Run a local Spotlight server for development -- `sentry local server` — Run a local Spotlight server to capture dev SDK events +- `sentry local serve` — Run a local Spotlight server to capture dev SDK events - `sentry local run ` — Run a command with Spotlight enabled → Full flags and examples: `references/local.md` diff --git a/plugins/sentry-cli/skills/sentry-cli/references/local.md b/plugins/sentry-cli/skills/sentry-cli/references/local.md index 42aebb583..2869b2e0a 100644 --- a/plugins/sentry-cli/skills/sentry-cli/references/local.md +++ b/plugins/sentry-cli/skills/sentry-cli/references/local.md @@ -11,7 +11,7 @@ requires: Run a local Spotlight server for development -### `sentry local server` +### `sentry local serve` Run a local Spotlight server to capture dev SDK events From 59daf6a79d9242c90d99cf380f49822b2a65b3a5 Mon Sep 17 00:00:00 2001 From: Aditya Mathur <57684218+MathurAditya724@users.noreply.github.com> Date: Wed, 20 May 2026 07:54:03 +0000 Subject: [PATCH 25/46] fix(local): correct misleading docs in run subcommand MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Remove the false claim that `sentry local run` auto-starts a server. The run subcommand is an env-var wrapper — users start the server separately via `sentry local serve`. Also adds `local` to the completions property test. --- docs/src/fragments/commands/local.md | 10 +++++----- plugins/sentry-cli/skills/sentry-cli/SKILL.md | 4 ++-- .../skills/sentry-cli/references/local.md | 12 +++++------ src/commands/local/index.ts | 6 +++--- src/commands/local/run.ts | 20 +++++++++---------- src/commands/local/server.ts | 2 +- test/lib/completions.property.test.ts | 1 + 7 files changed, 27 insertions(+), 28 deletions(-) diff --git a/docs/src/fragments/commands/local.md b/docs/src/fragments/commands/local.md index 9051257aa..2dee714e0 100644 --- a/docs/src/fragments/commands/local.md +++ b/docs/src/fragments/commands/local.md @@ -1,8 +1,8 @@ -[Spotlight](https://spotlightjs.com) is "Sentry for Development" — a lightweight local proxy that ingests Sentry envelopes from SDKs running in your dev stack and surfaces them in real time. `sentry local` runs a minimal [Hono](https://hono.dev/) HTTP server that's wire-compatible with Spotlight's protocol, so your existing SDKs and the [Spotlight overlay](https://spotlightjs.com/about/) work without any changes. +`sentry local` runs a local development server that captures Sentry SDK envelopes from your dev stack and surfaces errors, traces, and logs in real time — right in your terminal. No authentication required. -No authentication is required — the server binds to `localhost` by default and is purely a development tool. +If a server is already running on the port, the command attaches as an SSE consumer instead of starting a duplicate. ## Examples @@ -10,7 +10,7 @@ No authentication is required — the server binds to `localhost` by default and # Start the server and tail events (default) sentry local -# Run your app with Spotlight auto-enabled +# Run your app with the local server auto-enabled sentry local run -- npm run dev sentry local run -- python manage.py runserver @@ -40,7 +40,7 @@ Env vars injected into the child process: | Method | Path | Description | |--------|---------------------------------|----------------------------------------------------| -| `POST` | `/stream` | Spotlight-compatible envelope ingest | +| `POST` | `/stream` | Envelope ingest | | `POST` | `/api/{projectId}/envelope/` | Sentry SDK ingest path | | `GET` | `/stream` | Server-Sent Events feed of incoming envelopes | | `GET` | `/health` | Liveness check (returns `OK`) | @@ -63,4 +63,4 @@ Use `--filter` / `-f` to narrow the output to specific event types (repeatable): sentry local -f error -f log # only errors and logs ``` -Use `--quiet` to suppress tail output entirely if you only need the SSE stream for the Spotlight overlay. +Use `--quiet` to suppress tail output entirely if you only need the SSE stream. diff --git a/plugins/sentry-cli/skills/sentry-cli/SKILL.md b/plugins/sentry-cli/skills/sentry-cli/SKILL.md index 98acff102..2f4fed1be 100644 --- a/plugins/sentry-cli/skills/sentry-cli/SKILL.md +++ b/plugins/sentry-cli/skills/sentry-cli/SKILL.md @@ -470,10 +470,10 @@ Initialize Sentry in your project (experimental) ### Local -Run a local Spotlight server for development +Sentry for local development - `sentry local serve` — Run a local Spotlight server to capture dev SDK events -- `sentry local run ` — Run a command with Spotlight enabled +- `sentry local run ` — Run a command with the local dev server enabled → Full flags and examples: `references/local.md` diff --git a/plugins/sentry-cli/skills/sentry-cli/references/local.md b/plugins/sentry-cli/skills/sentry-cli/references/local.md index 2869b2e0a..ffe10bf8e 100644 --- a/plugins/sentry-cli/skills/sentry-cli/references/local.md +++ b/plugins/sentry-cli/skills/sentry-cli/references/local.md @@ -1,7 +1,7 @@ --- name: sentry-cli-local version: 0.35.0-dev.0 -description: Run a local Spotlight server for development +description: Sentry for local development requires: bins: ["sentry"] auth: true @@ -9,7 +9,7 @@ requires: # Local Commands -Run a local Spotlight server for development +Sentry for local development ### `sentry local serve` @@ -23,11 +23,11 @@ Run a local Spotlight server to capture dev SDK events ### `sentry local run ` -Run a command with Spotlight enabled +Run a command with the local dev server enabled **Flags:** -- `-p, --port - Port for the Spotlight server (default 8969) - (default: "8969")` -- `--host - Hostname for the Spotlight server (default localhost) - (default: "localhost")` +- `-p, --port - Port for the local server (default 8969) - (default: "8969")` +- `--host - Hostname for the local server (default localhost) - (default: "localhost")` **Examples:** @@ -35,7 +35,7 @@ Run a command with Spotlight enabled # Start the server and tail events (default) sentry local -# Run your app with Spotlight auto-enabled +# Run your app with the local server auto-enabled sentry local run -- npm run dev sentry local run -- python manage.py runserver diff --git a/src/commands/local/index.ts b/src/commands/local/index.ts index aa41a7b77..3255cc597 100644 --- a/src/commands/local/index.ts +++ b/src/commands/local/index.ts @@ -9,10 +9,10 @@ export const localRoute = buildRouteMap({ }, defaultCommand: "serve", docs: { - brief: "Run a local Spotlight server for development", + brief: "Sentry for local development", fullDescription: - "Run a local Spotlight-compatible server to capture Sentry SDK\n" + - "events from your dev stack.\n\n" + + "Run a local development server to capture Sentry SDK events\n" + + "from your dev stack.\n\n" + "Commands:\n" + " serve Start the server and tail events (default)\n" + " run Run a command with SENTRY_SPOTLIGHT auto-injected", diff --git a/src/commands/local/run.ts b/src/commands/local/run.ts index fb6284e64..e54adcc35 100644 --- a/src/commands/local/run.ts +++ b/src/commands/local/run.ts @@ -1,10 +1,9 @@ /** * sentry local run * - * Run a command with Sentry Spotlight enabled. Starts the local Spotlight - * server (or connects to an existing one), injects `SENTRY_SPOTLIGHT` into - * the child process environment so the Sentry SDK auto-sends envelopes to - * the local server, then tails events to the terminal. + * Run a command with the local dev server enabled. Injects + * `SENTRY_SPOTLIGHT` into the child process environment so the Sentry SDK + * auto-sends envelopes to the local server. */ import type { SentryContext } from "../../context.js"; @@ -33,14 +32,13 @@ function parsePort(value: string): number { export const runCommand = buildCommand({ docs: { - brief: "Run a command with Spotlight enabled", + brief: "Run a command with the local dev server enabled", fullDescription: "Run a command with the SENTRY_SPOTLIGHT environment variable\n" + "injected so the Sentry SDK automatically sends envelopes to the\n" + - "local Spotlight server.\n\n" + - "If no server is running on the port, one is started automatically\n" + - "in the background. The child process inherits all current env vars\n" + - "plus SENTRY_SPOTLIGHT and SENTRY_TRACES_SAMPLE_RATE=1.\n\n" + + "local server.\n\n" + + "The child process inherits all current env vars plus\n" + + "SENTRY_SPOTLIGHT and SENTRY_TRACES_SAMPLE_RATE=1.\n\n" + "Example:\n" + " sentry local run -- npm run dev\n" + " sentry local run -- python manage.py runserver", @@ -58,13 +56,13 @@ export const runCommand = buildCommand({ port: { kind: "parsed", parse: parsePort, - brief: `Port for the Spotlight server (default ${DEFAULT_PORT})`, + brief: `Port for the local server (default ${DEFAULT_PORT})`, default: String(DEFAULT_PORT), }, host: { kind: "parsed", parse: String, - brief: "Hostname for the Spotlight server (default localhost)", + brief: "Hostname for the local server (default localhost)", default: "localhost", }, }, diff --git a/src/commands/local/server.ts b/src/commands/local/server.ts index 94826fbc6..06d6f6889 100644 --- a/src/commands/local/server.ts +++ b/src/commands/local/server.ts @@ -705,7 +705,7 @@ function feedSSELine( } /** - * Consume SSE events from an upstream Spotlight server and print them. + * Consume SSE events from an upstream server and print them. * * Bun doesn't have a global `EventSource`, so we use `fetch` with a * streaming body and parse the SSE wire format manually. diff --git a/test/lib/completions.property.test.ts b/test/lib/completions.property.test.ts index 7c46c133c..b1c2aa8d9 100644 --- a/test/lib/completions.property.test.ts +++ b/test/lib/completions.property.test.ts @@ -192,6 +192,7 @@ describe("proposeCompletions: Stricli integration", () => { "trace", "span", "log", + "local", ]); test("subcommands match extractCommandTree for each group without defaultCommand", async () => { From 3d749286ed517654e0a0c158ab4e93a8875b7f5e Mon Sep 17 00:00:00 2001 From: Aditya Mathur <57684218+MathurAditya724@users.noreply.github.com> Date: Wed, 20 May 2026 08:04:09 +0000 Subject: [PATCH 26/46] fix(local): guard formatters against non-string envelope fields Use typeof check and String() coercion in formatTransactionItem and formatErrorItem so malformed envelopes with non-string values don't crash stripAnsi. --- src/commands/local/server.ts | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/commands/local/server.ts b/src/commands/local/server.ts index 06d6f6889..97c94c481 100644 --- a/src/commands/local/server.ts +++ b/src/commands/local/server.ts @@ -345,9 +345,9 @@ function formatErrorItem( | undefined; // values is ordered oldest→newest; show the outermost (last) exception const first = exception?.values?.at(-1); - const errorType = stripAnsi(first?.type ?? "Error"); + const errorType = stripAnsi(String(first?.type ?? "Error")); const errorValue = stripAnsi( - first?.value ?? (event.message as string | undefined) ?? "Unknown error" + String(first?.value ?? event.message ?? "Unknown error") ); let msg = `${errorType}: ${errorValue}`; @@ -374,9 +374,11 @@ function formatTransactionItem( ?.trace as | { op?: string; status?: string; description?: string } | undefined; - let msg = stripAnsi( - (event.transaction as string) ?? trace?.description ?? "Transaction" - ); + const txnName = + typeof event.transaction === "string" + ? event.transaction + : (trace?.description ?? "Transaction"); + let msg = stripAnsi(txnName); const op = trace?.op; if (op && op !== "default" && op !== "unknown") { From 135d19e1fc36273405fc8035d91539a529cb5545 Mon Sep 17 00:00:00 2001 From: Aditya Mathur <57684218+MathurAditya724@users.noreply.github.com> Date: Wed, 20 May 2026 08:07:30 +0000 Subject: [PATCH 27/46] fix(local): use EXIT.GENERAL for child exit code, sanitize newlines in envelope fields - run: use EXIT.GENERAL instead of forwarding the child's raw exit code, which could collide with the CLI's semantic exit code schema (10-69). - server: add sanitize() that strips ANSI escapes and collapses newlines so envelope fields can't inject fake log lines in the terminal. --- src/commands/local/run.ts | 4 ++-- src/commands/local/server.ts | 27 +++++++++++++++------------ 2 files changed, 17 insertions(+), 14 deletions(-) diff --git a/src/commands/local/run.ts b/src/commands/local/run.ts index e54adcc35..a95a92e44 100644 --- a/src/commands/local/run.ts +++ b/src/commands/local/run.ts @@ -8,7 +8,7 @@ import type { SentryContext } from "../../context.js"; import { buildCommand } from "../../lib/command.js"; -import { CliError, ValidationError } from "../../lib/errors.js"; +import { CliError, EXIT, ValidationError } from "../../lib/errors.js"; import { bold } from "../../lib/formatters/colors.js"; import { logger } from "../../lib/logger.js"; import { DEFAULT_PORT } from "./server.js"; @@ -100,7 +100,7 @@ export const runCommand = buildCommand({ const exitCode = await child.exited; if (exitCode !== 0) { - throw new CliError(`Process exited with code ${exitCode}`, exitCode); + throw new CliError(`Process exited with code ${exitCode}`, EXIT.GENERAL); } }, }); diff --git a/src/commands/local/server.ts b/src/commands/local/server.ts index 97c94c481..31528e1c9 100644 --- a/src/commands/local/server.ts +++ b/src/commands/local/server.ts @@ -37,6 +37,11 @@ import { import { stripAnsi } from "../../lib/formatters/plain-detect.js"; import { logger } from "../../lib/logger.js"; +/** Strip ANSI escapes and collapse newlines so envelope fields can't inject fake log lines. */ +function sanitize(text: string): string { + return stripAnsi(text).replace(/[\r\n]+/g, " "); +} + /** Default port matches Spotlight's `DEFAULT_PORT`. */ export const DEFAULT_PORT = 8969; @@ -315,12 +320,12 @@ function formatFrameHint(frames: StackFrame[]): string { let hint = ""; if (frame.filename && frame.lineno) { const loc = frame.colno - ? `${stripAnsi(frame.filename)}:${frame.lineno}:${frame.colno}` - : `${stripAnsi(frame.filename)}:${frame.lineno}`; + ? `${sanitize(frame.filename)}:${frame.lineno}:${frame.colno}` + : `${sanitize(frame.filename)}:${frame.lineno}`; hint += ` ${muted(`[${loc}]`)}`; } if (frame.function) { - hint += ` ${muted(`[${stripAnsi(frame.function)}]`)}`; + hint += ` ${muted(`[${sanitize(frame.function)}]`)}`; } return hint; } @@ -345,8 +350,8 @@ function formatErrorItem( | undefined; // values is ordered oldest→newest; show the outermost (last) exception const first = exception?.values?.at(-1); - const errorType = stripAnsi(String(first?.type ?? "Error")); - const errorValue = stripAnsi( + const errorType = sanitize(String(first?.type ?? "Error")); + const errorValue = sanitize( String(first?.value ?? event.message ?? "Unknown error") ); @@ -378,11 +383,11 @@ function formatTransactionItem( typeof event.transaction === "string" ? event.transaction : (trace?.description ?? "Transaction"); - let msg = stripAnsi(txnName); + let msg = sanitize(txnName); const op = trace?.op; if (op && op !== "default" && op !== "unknown") { - msg = `[${stripAnsi(op)}] ${msg}`; + msg = `[${sanitize(op)}] ${msg}`; } const start = event.start_timestamp as number | undefined; @@ -394,7 +399,7 @@ function formatTransactionItem( const status = trace?.status; if (status && status !== "ok") { - msg += ` ${muted(`[${stripAnsi(status)}]`)}`; + msg += ` ${muted(`[${sanitize(status)}]`)}`; } const spans = event.spans as unknown[] | undefined; @@ -417,7 +422,7 @@ type LogEntry = { /** Format one log entry into a colored tail line. */ function formatSingleLog(logEntry: LogEntry, source: string): string { const level = logEntry.level ?? "log"; - let msg = stripAnsi(logEntry.body ?? ""); + let msg = sanitize(logEntry.body ?? ""); if (logEntry.attributes) { const attrs = Object.entries(logEntry.attributes) @@ -425,9 +430,7 @@ function formatSingleLog(logEntry: LogEntry, source: string): string { ([k, v]) => !k.startsWith("sentry.") && v.value !== null && v.value !== undefined ) - .map(([k, v]) => - muted(`[${stripAnsi(k)}=${stripAnsi(String(v.value))}]`) - ); + .map(([k, v]) => muted(`[${sanitize(k)}=${sanitize(String(v.value))}]`)); if (attrs.length > 0) { msg += ` ${attrs.join(" ")}`; } From 662dc7d1347f19693e6d20dde81daad5a9838c02 Mon Sep 17 00:00:00 2001 From: Aditya Mathur <57684218+MathurAditya724@users.noreply.github.com> Date: Wed, 20 May 2026 08:15:41 +0000 Subject: [PATCH 28/46] fix(local): wrap Bun.spawn in try/catch for missing executables Bun.spawn throws synchronously when the command isn't found. Catch it and surface a clean CliError instead of an unhandled exception. --- src/commands/local/run.ts | 30 +++++++++++++++++++----------- 1 file changed, 19 insertions(+), 11 deletions(-) diff --git a/src/commands/local/run.ts b/src/commands/local/run.ts index a95a92e44..e4396f8d6 100644 --- a/src/commands/local/run.ts +++ b/src/commands/local/run.ts @@ -85,17 +85,25 @@ export const runCommand = buildCommand({ logger.info(`Starting: ${bold(args.join(" "))}`); logger.info(`SENTRY_SPOTLIGHT=${spotlightUrl}`); - const child = Bun.spawn(args, { - env: { - ...process.env, - SENTRY_SPOTLIGHT: spotlightUrl, - NEXT_PUBLIC_SENTRY_SPOTLIGHT: spotlightUrl, - SENTRY_TRACES_SAMPLE_RATE: "1", - }, - stdout: "inherit", - stderr: "inherit", - stdin: "inherit", - }); + let child: ReturnType; + try { + child = Bun.spawn(args, { + env: { + ...process.env, + SENTRY_SPOTLIGHT: spotlightUrl, + NEXT_PUBLIC_SENTRY_SPOTLIGHT: spotlightUrl, + SENTRY_TRACES_SAMPLE_RATE: "1", + }, + stdout: "inherit", + stderr: "inherit", + stdin: "inherit", + }); + } catch (err) { + throw new CliError( + `Failed to start "${args[0]}": ${err instanceof Error ? err.message : String(err)}`, + EXIT.GENERAL + ); + } const exitCode = await child.exited; From a71dd3a3f27914644f67c8e08c5c1194038eaf91 Mon Sep 17 00:00:00 2001 From: Aditya Mathur <57684218+MathurAditya724@users.noreply.github.com> Date: Wed, 20 May 2026 08:34:48 +0000 Subject: [PATCH 29/46] fix(local): sanitize lineno/colno and level in formatter output Wrap the full location string through sanitize() instead of only sanitizing filename, and sanitize the level label in formatType(). --- src/commands/local/server.ts | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/commands/local/server.ts b/src/commands/local/server.ts index 31528e1c9..d17b17e2a 100644 --- a/src/commands/local/server.ts +++ b/src/commands/local/server.ts @@ -253,7 +253,7 @@ const SOURCE_WIDTH = 9; /** Format a type/level label as `[TYPE]` padded to fixed width. */ function formatType(level: string): string { - const tag = `[${level.toUpperCase()}]`; + const tag = `[${sanitize(level).toUpperCase()}]`; const colorFn = LEVEL_COLORS[level]; const colored = colorFn ? colorFn(tag) : tag; return colored + " ".repeat(Math.max(0, TYPE_WIDTH - tag.length)); @@ -319,9 +319,11 @@ function formatFrameHint(frames: StackFrame[]): string { } let hint = ""; if (frame.filename && frame.lineno) { - const loc = frame.colno - ? `${sanitize(frame.filename)}:${frame.lineno}:${frame.colno}` - : `${sanitize(frame.filename)}:${frame.lineno}`; + const loc = sanitize( + frame.colno + ? `${frame.filename}:${frame.lineno}:${frame.colno}` + : `${frame.filename}:${frame.lineno}` + ); hint += ` ${muted(`[${loc}]`)}`; } if (frame.function) { From c89ee86c95b8397181ddc7a4fcde3d83299ec78b Mon Sep 17 00:00:00 2001 From: Aditya Mathur <57684218+MathurAditya724@users.noreply.github.com> Date: Wed, 20 May 2026 08:40:39 +0000 Subject: [PATCH 30/46] fix(local): check SSE response status, guard subscriber against format errors - consumeSSE: check res.ok before parsing the stream body so non-200 responses log a warning instead of silently disconnecting. - buffer subscriber: wrap formatEnvelopeLines in try/catch so a malformed envelope doesn't crash the server. --- src/commands/local/server.ts | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/src/commands/local/server.ts b/src/commands/local/server.ts index d17b17e2a..f09d0359c 100644 --- a/src/commands/local/server.ts +++ b/src/commands/local/server.ts @@ -727,6 +727,10 @@ async function consumeSSE( headers: { Accept: "text/event-stream" }, signal, }); + if (!res.ok) { + logger.warn(`SSE stream returned HTTP ${res.status}`); + return; + } if (!res.body) { return; } @@ -876,8 +880,14 @@ export const serverCommand = buildCommand({ if (!flags.quiet) { buffer.subscribe((container) => { - for (const line of formatEnvelopeLines(container, activeFilters)) { - logger.log(line); + try { + for (const line of formatEnvelopeLines(container, activeFilters)) { + logger.log(line); + } + } catch (err) { + logger.debug( + `Failed to format envelope: ${err instanceof Error ? err.message : String(err)}` + ); } }); } From 658bb826e77f73e97e6830bfbbc01fab6ed11ebc Mon Sep 17 00:00:00 2001 From: Aditya Mathur <57684218+MathurAditya724@users.noreply.github.com> Date: Wed, 20 May 2026 08:56:21 +0000 Subject: [PATCH 31/46] fix(local): sanitize fallback label in formatFallbackLine --- src/commands/local/server.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/commands/local/server.ts b/src/commands/local/server.ts index f09d0359c..a8b4b0fdb 100644 --- a/src/commands/local/server.ts +++ b/src/commands/local/server.ts @@ -485,7 +485,7 @@ function itemTypeToFilterCategory( /** Produce a fallback one-liner for unparseable or unsupported items. */ function formatFallbackLine(label: string): string { const ts = new Date().toISOString().slice(11, 23); - return `${muted(ts)} ${cyan("•")} ${bold(label)}`; + return `${muted(ts)} ${cyan("•")} ${bold(sanitize(label))}`; } /** Resolve a human label for a completely unparseable envelope. */ From 12803fbf84e0681330d2815e1e113d5f89d08253 Mon Sep 17 00:00:00 2001 From: Aditya Mathur <57684218+MathurAditya724@users.noreply.github.com> Date: Wed, 20 May 2026 09:03:29 +0000 Subject: [PATCH 32/46] fix(local): guard SSE subscriber against serialization errors Extract the SSE subscriber into buildSSEHandler and wrap the JSON.stringify + writeSSE path in try/catch so non-serializable envelopes don't silently kill the SSE stream. --- src/commands/local/server.ts | 66 ++++++++++++++++++++++++------------ 1 file changed, 45 insertions(+), 21 deletions(-) diff --git a/src/commands/local/server.ts b/src/commands/local/server.ts index a8b4b0fdb..0fde01ca0 100644 --- a/src/commands/local/server.ts +++ b/src/commands/local/server.ts @@ -110,6 +110,49 @@ const LOCALHOST_ORIGIN_RE = /^https?:\/\/(localhost|127\.0\.0\.1)(:\d+)?$/; * `localhost:*` ports (Vite, Next, Astro, etc.) but we must not allow * arbitrary remote origins to read the SSE envelope stream. */ + +/** Build a subscriber callback that serializes envelopes to an SSE stream. */ +function buildSSEHandler(stream: { + writeSSE: (event: { + id?: string; + event?: string; + data: string; + }) => Promise; +}) { + return (container: { + getParsedEnvelope: () => { + envelope: [Record, unknown[]]; + } | null; + getContentType: () => string; + }) => { + try { + const parsed = container.getParsedEnvelope(); + if (!parsed) { + return; + } + const header = parsed.envelope[0]; + const envelopeId = header.__spotlight_envelope_id; + stream + .writeSSE({ + id: envelopeId ? String(envelopeId) : undefined, + event: container.getContentType(), + data: JSON.stringify(parsed.envelope), + }) + .catch((err: unknown) => { + logger.debug( + `SSE write failed (client likely disconnected): ${ + err instanceof Error ? err.message : String(err) + }` + ); + }); + } catch (err) { + logger.debug( + `SSE serialize failed: ${err instanceof Error ? err.message : String(err)}` + ); + } + }; +} + function buildApp( spotlightBuffer: ReturnType ): Hono { @@ -185,27 +228,8 @@ function buildApp( app.get("/stream", (c) => streamSSE(c, async (stream) => { const lastEventId = c.req.header("Last-Event-ID"); - const readerId = spotlightBuffer.subscribe((container) => { - const parsed = container.getParsedEnvelope(); - if (!parsed) { - return; - } - const header = parsed.envelope[0] as Record; - const envelopeId = header.__spotlight_envelope_id; - stream - .writeSSE({ - id: envelopeId ? String(envelopeId) : undefined, - event: container.getContentType(), - data: JSON.stringify(parsed.envelope), - }) - .catch((err: unknown) => { - logger.debug( - `SSE write failed (client likely disconnected): ${ - err instanceof Error ? err.message : String(err) - }` - ); - }); - }, lastEventId); + const onEnvelope = buildSSEHandler(stream); + const readerId = spotlightBuffer.subscribe(onEnvelope, lastEventId); await new Promise((resolve) => { stream.onAbort(() => { From 30eaf878b34b0f7ad8f68a4c1559a6488c31042c Mon Sep 17 00:00:00 2001 From: Aditya Mathur <57684218+MathurAditya724@users.noreply.github.com> Date: Wed, 20 May 2026 14:51:23 +0000 Subject: [PATCH 33/46] chore(local): remove Spotlight branding from user-facing text Keep internal references to the Spotlight SDK/protocol in code comments, but remove from briefs, descriptions, JSDoc headers, and docs. --- plugins/sentry-cli/skills/sentry-cli/SKILL.md | 2 +- .../skills/sentry-cli/references/local.md | 2 +- src/commands/local/server.ts | 36 +++++++++---------- 3 files changed, 18 insertions(+), 22 deletions(-) diff --git a/plugins/sentry-cli/skills/sentry-cli/SKILL.md b/plugins/sentry-cli/skills/sentry-cli/SKILL.md index 2f4fed1be..ecc1e1bd6 100644 --- a/plugins/sentry-cli/skills/sentry-cli/SKILL.md +++ b/plugins/sentry-cli/skills/sentry-cli/SKILL.md @@ -472,7 +472,7 @@ Initialize Sentry in your project (experimental) Sentry for local development -- `sentry local serve` — Run a local Spotlight server to capture dev SDK events +- `sentry local serve` — Start the local dev server and tail events - `sentry local run ` — Run a command with the local dev server enabled → Full flags and examples: `references/local.md` diff --git a/plugins/sentry-cli/skills/sentry-cli/references/local.md b/plugins/sentry-cli/skills/sentry-cli/references/local.md index ffe10bf8e..bd5f2a194 100644 --- a/plugins/sentry-cli/skills/sentry-cli/references/local.md +++ b/plugins/sentry-cli/skills/sentry-cli/references/local.md @@ -13,7 +13,7 @@ Sentry for local development ### `sentry local serve` -Run a local Spotlight server to capture dev SDK events +Start the local dev server and tail events **Flags:** - `-p, --port - Port to listen on (default 8969) - (default: "8969")` diff --git a/src/commands/local/server.ts b/src/commands/local/server.ts index 0fde01ca0..b5c450523 100644 --- a/src/commands/local/server.ts +++ b/src/commands/local/server.ts @@ -1,14 +1,12 @@ /** - * sentry local + * sentry local serve * - * Run a local Spotlight-compatible server, or attach to one already running. + * Start a local development server that captures Sentry SDK envelopes, + * or attach to one already running on the same port. * * On startup the command probes `http://:/health`. If a server - * is already listening (e.g. a Spotlight sidecar or another `sentry local`), - * the command attaches as an SSE consumer and tails events from it. Otherwise - * it starts its own Hono HTTP server. - * - * Learn more: https://spotlightjs.com/docs/getting-started/ + * is already listening, the command attaches as an SSE consumer and tails + * events from it. Otherwise it starts its own Hono HTTP server. * * The command runs until interrupted (Ctrl-C / SIGTERM). */ @@ -42,7 +40,7 @@ function sanitize(text: string): string { return stripAnsi(text).replace(/[\r\n]+/g, " "); } -/** Default port matches Spotlight's `DEFAULT_PORT`. */ +/** Default port for the local dev server. */ export const DEFAULT_PORT = 8969; /** Buffer size: how many recent envelopes to retain for late subscribers. */ @@ -219,10 +217,10 @@ function buildApp( app.post("/api/:projectId/envelope", ingest); /** - * SSE stream — Spotlight overlay / UI clients connect here to receive a - * live feed of envelopes. The event format matches Spotlight's protocol: + * SSE stream — overlay / UI clients connect here to receive a + * live feed of envelopes. The SSE event format: * - `event` is the content type (e.g., "application/x-sentry-envelope") - * - `id` is the Spotlight-assigned envelope UUID (enables reconnection) + * - `id` is the envelope UUID (enables reconnection) * - `data` is the parsed envelope JSON ([header, items]) */ app.get("/stream", (c) => @@ -259,7 +257,7 @@ function formatTime(timestamp?: number | string): string { return date.toLocaleTimeString("en-US", { hour12: false }); } -/** Level → color map for tail output, matching Spotlight's Sentinel theme. */ +/** Level → color map for tail output. */ const LEVEL_COLORS: Record string> = { error: (s) => red(bold(s)), fatal: (s) => red(bold(s)), @@ -298,7 +296,7 @@ const SERVER_JS_MARKERS = [ "sveltekit", ]; -/** Source color map matching Spotlight's Sentinel theme. */ +/** Source → color map for tail output. */ const SOURCE_COLORS: Record string> = { browser: yellow, mobile: blue, @@ -646,7 +644,7 @@ const PORT_RETRY_DELAY_MS = 5000; * Try to start the HTTP server, retrying with backoff on EADDRINUSE. * * Retries up to {@link MAX_PORT_RETRIES} times with a {@link PORT_RETRY_DELAY_MS} - * delay between attempts, matching Spotlight's retry strategy. + * delay between attempts. */ function tryListen( app: Hono, @@ -692,7 +690,7 @@ function tryListen( } /** - * Check whether a Spotlight server is already running on the given URL. + * Check whether a server is already running on the given URL. * Returns `true` if the health endpoint responds successfully. */ async function isServerRunning(url: string): Promise { @@ -824,12 +822,10 @@ function processSSEEvent( export const serverCommand = buildCommand({ docs: { - brief: "Run a local Spotlight server to capture dev SDK events", + brief: "Start the local dev server and tail events", fullDescription: - "Start a local Spotlight-compatible server, or attach to one\n" + - "already running on the same port.\n\n" + - "Spotlight is Sentry for Development — it gives you a live view of\n" + - "errors, traces, and logs emitted by Sentry SDKs in your dev stack.\n\n" + + "Start a local development server that captures envelopes from\n" + + "Sentry SDKs in your dev stack and tails them to the terminal.\n\n" + "If a server is already listening on the port, the command connects\n" + "as an SSE consumer and tails events from it. Otherwise it starts\n" + "its own server.\n\n" + From 3d607733f6e90a5aca31d34bb3b94fd24f64daf8 Mon Sep 17 00:00:00 2001 From: Aditya Mathur <57684218+MathurAditya724@users.noreply.github.com> Date: Wed, 20 May 2026 15:04:03 +0000 Subject: [PATCH 34/46] refactor(local): extract formatters, auto-start server in run, add tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Extract ~380 lines of formatting code from server.ts into src/lib/formatters/local.ts (server.ts: 935 → 557 lines) - Fix run command: auto-starts a background server if none is running, shuts it down when the child process exits - Add 62 tests across 3 files: - test/lib/formatters/local.test.ts: unit tests for all formatters - test/lib/formatters/local.property.test.ts: property tests for parseFilter, formatTime, sanitize, isItemIncluded - test/commands/local/run.test.ts: env var injection, exit code propagation --- src/commands/local/run.ts | 53 ++- src/commands/local/server.ts | 398 +------------------- src/lib/formatters/local.ts | 379 +++++++++++++++++++ test/commands/local/run.test.ts | 68 ++++ test/lib/formatters/local.property.test.ts | 187 +++++++++ test/lib/formatters/local.test.ts | 417 +++++++++++++++++++++ 6 files changed, 1113 insertions(+), 389 deletions(-) create mode 100644 src/lib/formatters/local.ts create mode 100644 test/commands/local/run.test.ts create mode 100644 test/lib/formatters/local.property.test.ts create mode 100644 test/lib/formatters/local.test.ts diff --git a/src/commands/local/run.ts b/src/commands/local/run.ts index e4396f8d6..b99ad4705 100644 --- a/src/commands/local/run.ts +++ b/src/commands/local/run.ts @@ -4,14 +4,24 @@ * Run a command with the local dev server enabled. Injects * `SENTRY_SPOTLIGHT` into the child process environment so the Sentry SDK * auto-sends envelopes to the local server. + * + * If no server is already running on the target port, one is started + * automatically in the background and shut down when the child exits. */ +import type { Server } from "node:http"; +import { createSpotlightBuffer } from "@spotlightjs/spotlight/sdk"; import type { SentryContext } from "../../context.js"; import { buildCommand } from "../../lib/command.js"; import { CliError, EXIT, ValidationError } from "../../lib/errors.js"; import { bold } from "../../lib/formatters/colors.js"; import { logger } from "../../lib/logger.js"; -import { DEFAULT_PORT } from "./server.js"; +import { + buildApp, + DEFAULT_PORT, + isServerRunning, + tryListen, +} from "./server.js"; type RunFlags = { readonly port: number; @@ -30,6 +40,22 @@ function parsePort(value: string): number { return port; } +/** Buffer size for the auto-started background server. */ +const BUFFER_SIZE = 500; + +/** + * Shut down a background server, closing all connections so keep-alive + * sockets (e.g. SSE subscribers) don't block exit. + */ +function shutdownServer(server: Server): Promise { + return new Promise((resolve) => { + if (typeof server.closeAllConnections === "function") { + server.closeAllConnections(); + } + server.close(() => resolve()); + }); +} + export const runCommand = buildCommand({ docs: { brief: "Run a command with the local dev server enabled", @@ -37,6 +63,8 @@ export const runCommand = buildCommand({ "Run a command with the SENTRY_SPOTLIGHT environment variable\n" + "injected so the Sentry SDK automatically sends envelopes to the\n" + "local server.\n\n" + + "If no server is already listening on the port, one is started\n" + + "automatically and shut down when the child process exits.\n\n" + "The child process inherits all current env vars plus\n" + "SENTRY_SPOTLIGHT and SENTRY_TRACES_SAMPLE_RATE=1.\n\n" + "Example:\n" + @@ -80,7 +108,20 @@ export const runCommand = buildCommand({ ); } - const spotlightUrl = `http://${flags.host}:${flags.port}/stream`; + const url = `http://${flags.host}:${flags.port}`; + const spotlightUrl = `${url}/stream`; + + let bgServer: Server | undefined; + + const alreadyRunning = await isServerRunning(url); + if (!alreadyRunning) { + logger.info("No server detected, starting one in the background..."); + const buffer = createSpotlightBuffer(BUFFER_SIZE); + const app = buildApp(buffer); + const { server } = await tryListen(app, flags.port, flags.host); + bgServer = server; + logger.info(`Background server listening on ${bold(url)}`); + } logger.info(`Starting: ${bold(args.join(" "))}`); logger.info(`SENTRY_SPOTLIGHT=${spotlightUrl}`); @@ -99,6 +140,9 @@ export const runCommand = buildCommand({ stdin: "inherit", }); } catch (err) { + if (bgServer) { + await shutdownServer(bgServer); + } throw new CliError( `Failed to start "${args[0]}": ${err instanceof Error ? err.message : String(err)}`, EXIT.GENERAL @@ -107,6 +151,11 @@ export const runCommand = buildCommand({ const exitCode = await child.exited; + if (bgServer) { + logger.info("Stopping background server..."); + await shutdownServer(bgServer); + } + if (exitCode !== 0) { throw new CliError(`Process exited with code ${exitCode}`, EXIT.GENERAL); } diff --git a/src/commands/local/server.ts b/src/commands/local/server.ts index b5c450523..b49b6913f 100644 --- a/src/commands/local/server.ts +++ b/src/commands/local/server.ts @@ -23,42 +23,29 @@ import { streamSSE } from "hono/streaming"; import type { SentryContext } from "../../context.js"; import { buildCommand, numberParser } from "../../lib/command.js"; import { ValidationError } from "../../lib/errors.js"; +import { bold } from "../../lib/formatters/colors.js"; +import type { FilterValue } from "../../lib/formatters/local.js"; import { - blue, - bold, - cyan, - green, - muted, - red, - yellow, -} from "../../lib/formatters/colors.js"; -import { stripAnsi } from "../../lib/formatters/plain-detect.js"; + FILTER_VALUES, + formatEnvelopeLines, + formatItem, + isItemIncluded, + SENTRY_CONTENT_TYPE, +} from "../../lib/formatters/local.js"; import { logger } from "../../lib/logger.js"; -/** Strip ANSI escapes and collapse newlines so envelope fields can't inject fake log lines. */ -function sanitize(text: string): string { - return stripAnsi(text).replace(/[\r\n]+/g, " "); -} - /** Default port for the local dev server. */ export const DEFAULT_PORT = 8969; /** Buffer size: how many recent envelopes to retain for late subscribers. */ const BUFFER_SIZE = 500; -/** Canonical content type for Sentry envelopes. */ -const SENTRY_CONTENT_TYPE = "application/x-sentry-envelope"; - /** Trailing carriage return — stripped from SSE lines. */ const CR_RE = /\r$/; /** Maximum ingest body size (10 MB). Rejects oversized payloads early. */ const MAX_BODY_BYTES = 10 * 1024 * 1024; -/** Envelope item categories that can be filtered via `--filter`. */ -const FILTER_VALUES = ["error", "transaction", "log"] as const; -type FilterValue = (typeof FILTER_VALUES)[number]; - /** * Parse and validate a `--filter` value. * Accepts the canonical names: error, transaction, logger. @@ -151,7 +138,7 @@ function buildSSEHandler(stream: { }; } -function buildApp( +export function buildApp( spotlightBuffer: ReturnType ): Hono { const app = new Hono(); @@ -241,369 +228,6 @@ function buildApp( return app; } -/** Format a local timestamp as HH:MM:SS from a Sentry timestamp. */ -function formatTime(timestamp?: number | string): string { - let date: Date; - if (!timestamp) { - date = new Date(); - } else if (typeof timestamp === "string") { - date = new Date(timestamp); - } else { - date = new Date(timestamp * 1000); - } - if (Number.isNaN(date.getTime())) { - return "??:??:??"; - } - return date.toLocaleTimeString("en-US", { hour12: false }); -} - -/** Level → color map for tail output. */ -const LEVEL_COLORS: Record string> = { - error: (s) => red(bold(s)), - fatal: (s) => red(bold(s)), - warning: yellow, - info: cyan, - trace: green, - debug: muted, -}; - -/** Longest bracketed type label: `[WARNING]` = 9 chars. */ -const TYPE_WIDTH = 9; - -/** Longest bracketed source label: `[BROWSER]` = 9 chars. */ -const SOURCE_WIDTH = 9; - -/** Format a type/level label as `[TYPE]` padded to fixed width. */ -function formatType(level: string): string { - const tag = `[${sanitize(level).toUpperCase()}]`; - const colorFn = LEVEL_COLORS[level]; - const colored = colorFn ? colorFn(tag) : tag; - return colored + " ".repeat(Math.max(0, TYPE_WIDTH - tag.length)); -} - -/** Mobile SDK name substrings. */ -const MOBILE_MARKERS = ["cocoa", "android", "react-native", "flutter"]; - -/** Server-side JS SDK name substrings — exclude from browser detection. */ -const SERVER_JS_MARKERS = [ - "node", - "bun", - "deno", - "nextjs", - "remix", - "astro", - "nuxt", - "sveltekit", -]; - -/** Source → color map for tail output. */ -const SOURCE_COLORS: Record string> = { - browser: yellow, - mobile: blue, - server: cyan, -}; - -/** - * Infer the source platform from the envelope header's `sdk.name` field. - * Returns a colored, bracketed, padded label like `[SERVER] `. - */ -function inferSource(header: Record): string { - const sdk = header.sdk as { name?: string } | undefined; - const name = sdk?.name ?? ""; - let source = "server"; - if (MOBILE_MARKERS.some((m) => name.includes(m))) { - source = "mobile"; - } else if ( - name.startsWith("sentry.javascript.") && - !SERVER_JS_MARKERS.some((m) => name.includes(m)) - ) { - source = "browser"; - } - const tag = `[${source.toUpperCase()}]`; - const colorFn = SOURCE_COLORS[source] ?? cyan; - return colorFn(tag) + " ".repeat(Math.max(0, SOURCE_WIDTH - tag.length)); -} - -/** Shape of a single stack frame in the exception value. */ -type StackFrame = { - filename?: string; - lineno?: number; - colno?: number; - function?: string; - in_app?: boolean; -}; - -/** Build the `[file:line:col] [func]` suffix for the best stack frame. */ -function formatFrameHint(frames: StackFrame[]): string { - const frame = frames.find((f) => f.in_app) ?? frames.at(-1); - if (!frame) { - return ""; - } - let hint = ""; - if (frame.filename && frame.lineno) { - const loc = sanitize( - frame.colno - ? `${frame.filename}:${frame.lineno}:${frame.colno}` - : `${frame.filename}:${frame.lineno}` - ); - hint += ` ${muted(`[${loc}]`)}`; - } - if (frame.function) { - hint += ` ${muted(`[${sanitize(frame.function)}]`)}`; - } - return hint; -} - -/** - * Format an error event item into a colored one-liner. - * - * Output: `HH:MM:SS [ERROR] [SERVER] TypeError: x is not a function [file.ts:42:5] [handleRequest]` - */ -function formatErrorItem( - event: Record, - header: Record -): string { - const exception = event.exception as - | { - values?: { - type?: string; - value?: string; - stacktrace?: { frames?: StackFrame[] }; - }[]; - } - | undefined; - // values is ordered oldest→newest; show the outermost (last) exception - const first = exception?.values?.at(-1); - const errorType = sanitize(String(first?.type ?? "Error")); - const errorValue = sanitize( - String(first?.value ?? event.message ?? "Unknown error") - ); - - let msg = `${errorType}: ${errorValue}`; - - const frames = first?.stacktrace?.frames; - if (frames?.length) { - msg += formatFrameHint(frames); - } - - const ts = formatTime(event.timestamp as number | undefined); - return `${muted(ts)} ${formatType("error")} ${inferSource(header)} ${msg}`; -} - -/** - * Format a transaction event item into a colored one-liner. - * - * Output: `HH:MM:SS [TRACE] [BROWSER] [http.client] GET /api/users [245ms] [3 spans]` - */ -function formatTransactionItem( - event: Record, - header: Record -): string { - const trace = (event.contexts as Record | undefined) - ?.trace as - | { op?: string; status?: string; description?: string } - | undefined; - const txnName = - typeof event.transaction === "string" - ? event.transaction - : (trace?.description ?? "Transaction"); - let msg = sanitize(txnName); - - const op = trace?.op; - if (op && op !== "default" && op !== "unknown") { - msg = `[${sanitize(op)}] ${msg}`; - } - - const start = event.start_timestamp as number | undefined; - const end = event.timestamp as number | undefined; - if (start !== undefined && end !== undefined) { - const durationMs = Math.round((end - start) * 1000); - msg += ` ${muted(`[${durationMs}ms]`)}`; - } - - const status = trace?.status; - if (status && status !== "ok") { - msg += ` ${muted(`[${sanitize(status)}]`)}`; - } - - const spans = event.spans as unknown[] | undefined; - if (spans?.length) { - msg += ` ${muted(`[${spans.length} span${spans.length === 1 ? "" : "s"}]`)}`; - } - - const ts = formatTime(event.timestamp as number | undefined); - return `${muted(ts)} ${formatType("trace")} ${inferSource(header)} ${msg}`; -} - -/** Shape of a single log entry inside a log envelope item. */ -type LogEntry = { - level?: string; - body?: string; - timestamp?: number; - attributes?: Record; -}; - -/** Format one log entry into a colored tail line. */ -function formatSingleLog(logEntry: LogEntry, source: string): string { - const level = logEntry.level ?? "log"; - let msg = sanitize(logEntry.body ?? ""); - - if (logEntry.attributes) { - const attrs = Object.entries(logEntry.attributes) - .filter( - ([k, v]) => - !k.startsWith("sentry.") && v.value !== null && v.value !== undefined - ) - .map(([k, v]) => muted(`[${sanitize(k)}=${sanitize(String(v.value))}]`)); - if (attrs.length > 0) { - msg += ` ${attrs.join(" ")}`; - } - } - - const ts = formatTime(logEntry.timestamp); - return `${muted(ts)} ${formatType(level)} ${source} ${msg}`; -} - -/** - * Format a log event item. A log envelope item contains an `items` array - * of individual log entries; each gets its own line. - * - * Output: `HH:MM:SS [INFO] [SERVER] User logged in [user_id=1234]` - */ -function formatLogItem( - event: Record, - header: Record -): string[] { - const items = event.items as LogEntry[] | undefined; - if (!items?.length) { - return []; - } - - const source = inferSource(header); - return items.map((logEntry) => formatSingleLog(logEntry, source)); -} - -/** Item types that map to the error formatter. */ -const ERROR_TYPES = new Set(["event", "error"]); - -/** - * Map envelope item `type` to the corresponding `FilterValue`. - * Returns undefined for item types that don't map to a filter category. - */ -function itemTypeToFilterCategory( - itemType: string | undefined -): FilterValue | undefined { - if (!itemType) { - return; - } - if (ERROR_TYPES.has(itemType)) { - return "error"; - } - if (itemType === "transaction" || itemType === "log") { - return itemType; - } -} - -/** Produce a fallback one-liner for unparseable or unsupported items. */ -function formatFallbackLine(label: string): string { - const ts = new Date().toISOString().slice(11, 23); - return `${muted(ts)} ${cyan("•")} ${bold(sanitize(label))}`; -} - -/** Resolve a human label for a completely unparseable envelope. */ -function resolveUnparseableLabel(container: { - getContentType: () => string; - getEventTypes: () => string[] | null; -}): string { - const types = container.getEventTypes(); - if (types && types.length > 0) { - return types.join("+"); - } - const ct = container.getContentType(); - return ct === "application/x-sentry-envelope" ? "envelope" : ct; -} - -/** Format a single envelope item into one or more output lines. */ -function formatItem( - itemType: string | undefined, - payload: Record, - header: Record, - fallbackLabel: string -): string[] { - if (itemType && ERROR_TYPES.has(itemType)) { - return [formatErrorItem(payload, header)]; - } - if (itemType === "transaction") { - return [formatTransactionItem(payload, header)]; - } - if (itemType === "log") { - return formatLogItem(payload, header); - } - return [formatFallbackLine(fallbackLabel)]; -} - -/** Check whether an item should be shown given active filters. */ -function isItemIncluded( - itemType: string | undefined, - activeFilters: ReadonlySet -): boolean { - if (activeFilters.size === 0) { - return true; - } - const category = itemTypeToFilterCategory(itemType); - return category !== undefined && activeFilters.has(category); -} - -/** - * Format a freshly received envelope for terminal output. - * - * When `activeFilters` is non-empty, only items whose category matches - * one of the filter values are rendered; non-matching items are silently - * dropped. When empty, all items are shown. - */ -function formatEnvelopeLines( - container: { - getParsedEnvelope: () => { - envelope: [Record, [{ type?: string }, unknown][]]; - } | null; - getContentType: () => string; - getEventTypes: () => string[] | null; - }, - activeFilters: ReadonlySet -): string[] { - const parsed = container.getParsedEnvelope(); - if (!parsed) { - if (activeFilters.size > 0) { - return []; - } - return [formatFallbackLine(resolveUnparseableLabel(container))]; - } - - const [header, items] = parsed.envelope; - const lines: string[] = []; - for (const [itemHeader, itemPayload] of items) { - if (!isItemIncluded(itemHeader.type, activeFilters)) { - continue; - } - lines.push( - ...formatItem( - itemHeader.type, - itemPayload as Record, - header, - itemHeader.type ?? container.getContentType() - ) - ); - } - - if (lines.length > 0) { - return lines; - } - if (activeFilters.size > 0) { - return []; - } - return [formatFallbackLine(resolveUnparseableLabel(container))]; -} - /** * Install signal handlers that stop the HTTP server on Ctrl-C / SIGTERM. * @@ -646,7 +270,7 @@ const PORT_RETRY_DELAY_MS = 5000; * Retries up to {@link MAX_PORT_RETRIES} times with a {@link PORT_RETRY_DELAY_MS} * delay between attempts. */ -function tryListen( +export function tryListen( app: Hono, port: number, hostname: string @@ -693,7 +317,7 @@ function tryListen( * Check whether a server is already running on the given URL. * Returns `true` if the health endpoint responds successfully. */ -async function isServerRunning(url: string): Promise { +export async function isServerRunning(url: string): Promise { try { const res = await fetch(`${url}/health`, { signal: AbortSignal.timeout(2000), diff --git a/src/lib/formatters/local.ts b/src/lib/formatters/local.ts new file mode 100644 index 000000000..42193715f --- /dev/null +++ b/src/lib/formatters/local.ts @@ -0,0 +1,379 @@ +/** Tail formatters for the local dev server. */ + +import { blue, bold, cyan, green, muted, red, yellow } from "./colors.js"; +import { stripAnsi } from "./plain-detect.js"; + +/** Strip ANSI escapes and collapse newlines so envelope fields can't inject fake log lines. */ +export function sanitize(text: string): string { + return stripAnsi(text).replace(/[\r\n]+/g, " "); +} + +/** Canonical content type for Sentry envelopes. */ +export const SENTRY_CONTENT_TYPE = "application/x-sentry-envelope"; + +/** Envelope item categories that can be filtered via `--filter`. */ +export const FILTER_VALUES = ["error", "transaction", "log"] as const; +export type FilterValue = (typeof FILTER_VALUES)[number]; + +/** Format a local timestamp as HH:MM:SS from a Sentry timestamp. */ +export function formatTime(timestamp?: number | string): string { + let date: Date; + if (!timestamp) { + date = new Date(); + } else if (typeof timestamp === "string") { + date = new Date(timestamp); + } else { + date = new Date(timestamp * 1000); + } + if (Number.isNaN(date.getTime())) { + return "??:??:??"; + } + return date.toLocaleTimeString("en-US", { hour12: false }); +} + +/** Level → color map for tail output. */ +const LEVEL_COLORS: Record string> = { + error: (s) => red(bold(s)), + fatal: (s) => red(bold(s)), + warning: yellow, + info: cyan, + trace: green, + debug: muted, +}; + +/** Longest bracketed type label: `[WARNING]` = 9 chars. */ +const TYPE_WIDTH = 9; + +/** Longest bracketed source label: `[BROWSER]` = 9 chars. */ +const SOURCE_WIDTH = 9; + +/** Format a type/level label as `[TYPE]` padded to fixed width. */ +export function formatType(level: string): string { + const tag = `[${sanitize(level).toUpperCase()}]`; + const colorFn = LEVEL_COLORS[level]; + const colored = colorFn ? colorFn(tag) : tag; + return colored + " ".repeat(Math.max(0, TYPE_WIDTH - tag.length)); +} + +/** Mobile SDK name substrings. */ +const MOBILE_MARKERS = ["cocoa", "android", "react-native", "flutter"]; + +/** Server-side JS SDK name substrings — exclude from browser detection. */ +const SERVER_JS_MARKERS = [ + "node", + "bun", + "deno", + "nextjs", + "remix", + "astro", + "nuxt", + "sveltekit", +]; + +/** Source → color map for tail output. */ +const SOURCE_COLORS: Record string> = { + browser: yellow, + mobile: blue, + server: cyan, +}; + +/** + * Infer the source platform from the envelope header's `sdk.name` field. + * Returns a colored, bracketed, padded label like `[SERVER] `. + */ +export function inferSource(header: Record): string { + const sdk = header.sdk as { name?: string } | undefined; + const name = sdk?.name ?? ""; + let source = "server"; + if (MOBILE_MARKERS.some((m) => name.includes(m))) { + source = "mobile"; + } else if ( + name.startsWith("sentry.javascript.") && + !SERVER_JS_MARKERS.some((m) => name.includes(m)) + ) { + source = "browser"; + } + const tag = `[${source.toUpperCase()}]`; + const colorFn = SOURCE_COLORS[source] ?? cyan; + return colorFn(tag) + " ".repeat(Math.max(0, SOURCE_WIDTH - tag.length)); +} + +/** Shape of a single stack frame in the exception value. */ +export type StackFrame = { + filename?: string; + lineno?: number; + colno?: number; + function?: string; + in_app?: boolean; +}; + +/** Build the `[file:line:col] [func]` suffix for the best stack frame. */ +export function formatFrameHint(frames: StackFrame[]): string { + const frame = frames.find((f) => f.in_app) ?? frames.at(-1); + if (!frame) { + return ""; + } + let hint = ""; + if (frame.filename && frame.lineno) { + const loc = sanitize( + frame.colno + ? `${frame.filename}:${frame.lineno}:${frame.colno}` + : `${frame.filename}:${frame.lineno}` + ); + hint += ` ${muted(`[${loc}]`)}`; + } + if (frame.function) { + hint += ` ${muted(`[${sanitize(frame.function)}]`)}`; + } + return hint; +} + +/** + * Format an error event item into a colored one-liner. + * + * Output: `HH:MM:SS [ERROR] [SERVER] TypeError: x is not a function [file.ts:42:5] [handleRequest]` + */ +export function formatErrorItem( + event: Record, + header: Record +): string { + const exception = event.exception as + | { + values?: { + type?: string; + value?: string; + stacktrace?: { frames?: StackFrame[] }; + }[]; + } + | undefined; + // values is ordered oldest→newest; show the outermost (last) exception + const first = exception?.values?.at(-1); + const errorType = sanitize(String(first?.type ?? "Error")); + const errorValue = sanitize( + String(first?.value ?? event.message ?? "Unknown error") + ); + + let msg = `${errorType}: ${errorValue}`; + + const frames = first?.stacktrace?.frames; + if (frames?.length) { + msg += formatFrameHint(frames); + } + + const ts = formatTime(event.timestamp as number | undefined); + return `${muted(ts)} ${formatType("error")} ${inferSource(header)} ${msg}`; +} + +/** + * Format a transaction event item into a colored one-liner. + * + * Output: `HH:MM:SS [TRACE] [BROWSER] [http.client] GET /api/users [245ms] [3 spans]` + */ +export function formatTransactionItem( + event: Record, + header: Record +): string { + const trace = (event.contexts as Record | undefined) + ?.trace as + | { op?: string; status?: string; description?: string } + | undefined; + const txnName = + typeof event.transaction === "string" + ? event.transaction + : (trace?.description ?? "Transaction"); + let msg = sanitize(txnName); + + const op = trace?.op; + if (op && op !== "default" && op !== "unknown") { + msg = `[${sanitize(op)}] ${msg}`; + } + + const start = event.start_timestamp as number | undefined; + const end = event.timestamp as number | undefined; + if (start !== undefined && end !== undefined) { + const durationMs = Math.round((end - start) * 1000); + msg += ` ${muted(`[${durationMs}ms]`)}`; + } + + const status = trace?.status; + if (status && status !== "ok") { + msg += ` ${muted(`[${sanitize(status)}]`)}`; + } + + const spans = event.spans as unknown[] | undefined; + if (spans?.length) { + msg += ` ${muted(`[${spans.length} span${spans.length === 1 ? "" : "s"}]`)}`; + } + + const ts = formatTime(event.timestamp as number | undefined); + return `${muted(ts)} ${formatType("trace")} ${inferSource(header)} ${msg}`; +} + +/** Shape of a single log entry inside a log envelope item. */ +export type LogEntry = { + level?: string; + body?: string; + timestamp?: number; + attributes?: Record; +}; + +/** Format one log entry into a colored tail line. */ +export function formatSingleLog(logEntry: LogEntry, source: string): string { + const level = logEntry.level ?? "log"; + let msg = sanitize(logEntry.body ?? ""); + + if (logEntry.attributes) { + const attrs = Object.entries(logEntry.attributes) + .filter( + ([k, v]) => + !k.startsWith("sentry.") && v.value !== null && v.value !== undefined + ) + .map(([k, v]) => muted(`[${sanitize(k)}=${sanitize(String(v.value))}]`)); + if (attrs.length > 0) { + msg += ` ${attrs.join(" ")}`; + } + } + + const ts = formatTime(logEntry.timestamp); + return `${muted(ts)} ${formatType(level)} ${source} ${msg}`; +} + +/** + * Format a log event item. A log envelope item contains an `items` array + * of individual log entries; each gets its own line. + * + * Output: `HH:MM:SS [INFO] [SERVER] User logged in [user_id=1234]` + */ +export function formatLogItem( + event: Record, + header: Record +): string[] { + const items = event.items as LogEntry[] | undefined; + if (!items?.length) { + return []; + } + + const source = inferSource(header); + return items.map((logEntry) => formatSingleLog(logEntry, source)); +} + +/** Item types that map to the error formatter. */ +export const ERROR_TYPES = new Set(["event", "error"]); + +/** + * Map envelope item `type` to the corresponding `FilterValue`. + * Returns undefined for item types that don't map to a filter category. + */ +export function itemTypeToFilterCategory( + itemType: string | undefined +): FilterValue | undefined { + if (!itemType) { + return; + } + if (ERROR_TYPES.has(itemType)) { + return "error"; + } + if (itemType === "transaction" || itemType === "log") { + return itemType; + } +} + +/** Produce a fallback one-liner for unparseable or unsupported items. */ +export function formatFallbackLine(label: string): string { + const ts = new Date().toISOString().slice(11, 23); + return `${muted(ts)} ${cyan("•")} ${bold(sanitize(label))}`; +} + +/** Resolve a human label for a completely unparseable envelope. */ +export function resolveUnparseableLabel(container: { + getContentType: () => string; + getEventTypes: () => string[] | null; +}): string { + const types = container.getEventTypes(); + if (types && types.length > 0) { + return types.join("+"); + } + const ct = container.getContentType(); + return ct === "application/x-sentry-envelope" ? "envelope" : ct; +} + +/** Format a single envelope item into one or more output lines. */ +export function formatItem( + itemType: string | undefined, + payload: Record, + header: Record, + fallbackLabel: string +): string[] { + if (itemType && ERROR_TYPES.has(itemType)) { + return [formatErrorItem(payload, header)]; + } + if (itemType === "transaction") { + return [formatTransactionItem(payload, header)]; + } + if (itemType === "log") { + return formatLogItem(payload, header); + } + return [formatFallbackLine(fallbackLabel)]; +} + +/** Check whether an item should be shown given active filters. */ +export function isItemIncluded( + itemType: string | undefined, + activeFilters: ReadonlySet +): boolean { + if (activeFilters.size === 0) { + return true; + } + const category = itemTypeToFilterCategory(itemType); + return category !== undefined && activeFilters.has(category); +} + +/** + * Format a freshly received envelope for terminal output. + * + * When `activeFilters` is non-empty, only items whose category matches + * one of the filter values are rendered; non-matching items are silently + * dropped. When empty, all items are shown. + */ +export function formatEnvelopeLines( + container: { + getParsedEnvelope: () => { + envelope: [Record, [{ type?: string }, unknown][]]; + } | null; + getContentType: () => string; + getEventTypes: () => string[] | null; + }, + activeFilters: ReadonlySet +): string[] { + const parsed = container.getParsedEnvelope(); + if (!parsed) { + if (activeFilters.size > 0) { + return []; + } + return [formatFallbackLine(resolveUnparseableLabel(container))]; + } + + const [header, items] = parsed.envelope; + const lines: string[] = []; + for (const [itemHeader, itemPayload] of items) { + if (!isItemIncluded(itemHeader.type, activeFilters)) { + continue; + } + lines.push( + ...formatItem( + itemHeader.type, + itemPayload as Record, + header, + itemHeader.type ?? container.getContentType() + ) + ); + } + + if (lines.length > 0) { + return lines; + } + if (activeFilters.size > 0) { + return []; + } + return [formatFallbackLine(resolveUnparseableLabel(container))]; +} diff --git a/test/commands/local/run.test.ts b/test/commands/local/run.test.ts new file mode 100644 index 000000000..f38809bab --- /dev/null +++ b/test/commands/local/run.test.ts @@ -0,0 +1,68 @@ +/** + * Tests for the `sentry local run` command. + * + * Exercises the command's func() body directly to verify env var injection + * and exit code propagation. + */ + +import { describe, expect, mock, test } from "bun:test"; +import { runCommand } from "../../../src/commands/local/run.js"; +import { CliError, ValidationError } from "../../../src/lib/errors.js"; + +type RunFunc = ( + this: unknown, + flags: { port: number; host: string }, + args: string[] +) => Promise; + +function makeContext() { + return { + stdout: { write: mock(() => true) }, + stderr: { write: mock(() => true) }, + cwd: "/tmp", + }; +} + +describe("sentry local run", () => { + test("throws ValidationError when no command provided", async () => { + const func = (await runCommand.loader()) as unknown as RunFunc; + const ctx = makeContext(); + try { + await func.call(ctx, { port: 0, host: "localhost" }, []); + expect.unreachable("should have thrown"); + } catch (err) { + expect(err).toBeInstanceOf(ValidationError); + expect((err as ValidationError).message).toContain("No command provided"); + } + }); + + test("injects SENTRY_SPOTLIGHT env var into child process", async () => { + const func = (await runCommand.loader()) as unknown as RunFunc; + const ctx = makeContext(); + + // Use a high ephemeral port unlikely to conflict. + // The command auto-starts a background server since none is running. + // `printenv SENTRY_SPOTLIGHT` prints the var and exits 0. + const port = 19_876; + await func.call(ctx, { port, host: "127.0.0.1" }, [ + "printenv", + "SENTRY_SPOTLIGHT", + ]); + // If we got here without error, the child exited 0 and env vars were set. + }); + + test("propagates non-zero exit code as CliError", async () => { + const func = (await runCommand.loader()) as unknown as RunFunc; + const ctx = makeContext(); + + // `false` is a POSIX command that always exits with code 1. + const port = 19_877; + try { + await func.call(ctx, { port, host: "127.0.0.1" }, ["false"]); + expect.unreachable("should have thrown"); + } catch (err) { + expect(err).toBeInstanceOf(CliError); + expect((err as CliError).message).toContain("exited with code"); + } + }); +}); diff --git a/test/lib/formatters/local.property.test.ts b/test/lib/formatters/local.property.test.ts new file mode 100644 index 000000000..60d1cc706 --- /dev/null +++ b/test/lib/formatters/local.property.test.ts @@ -0,0 +1,187 @@ +/** + * Property-Based Tests for Local Dev Server Formatters + * + * Uses fast-check to verify invariants that should hold for any valid input. + */ + +import { describe, expect, test } from "bun:test"; +import { + constantFrom, + double, + assert as fcAssert, + integer, + oneof, + option, + property, + string, + stringMatching, +} from "fast-check"; +import type { FilterValue } from "../../../src/lib/formatters/local.js"; +import { + FILTER_VALUES, + formatTime, + isItemIncluded, + itemTypeToFilterCategory, + sanitize, +} from "../../../src/lib/formatters/local.js"; +import { DEFAULT_NUM_RUNS } from "../../model-based/helpers.js"; + +/** ANSI escape pattern — should not appear in sanitize output. */ +// biome-ignore lint/suspicious/noControlCharactersInRegex: ANSI control chars +const ANSI_RE = /\x1b\[[0-9;]*m/; + +describe("property: sanitize", () => { + test("output never contains ANSI escapes", () => { + fcAssert( + property(string(), (input) => { + const result = sanitize(input); + expect(ANSI_RE.test(result)).toBe(false); + }), + { numRuns: DEFAULT_NUM_RUNS } + ); + }); + + test("output never contains newlines", () => { + fcAssert( + property(string(), (input) => { + const result = sanitize(input); + expect(result).not.toContain("\n"); + expect(result).not.toContain("\r"); + }), + { numRuns: DEFAULT_NUM_RUNS } + ); + }); + + test("idempotent: sanitize(sanitize(x)) === sanitize(x)", () => { + fcAssert( + property(string(), (input) => { + const once = sanitize(input); + const twice = sanitize(once); + expect(twice).toBe(once); + }), + { numRuns: DEFAULT_NUM_RUNS } + ); + }); +}); + +describe("property: formatTime", () => { + test("never throws for any number input", () => { + fcAssert( + property(double({ noNaN: false }), (n) => { + const result = formatTime(n); + expect(typeof result).toBe("string"); + expect(result.length).toBeGreaterThan(0); + }), + { numRuns: DEFAULT_NUM_RUNS } + ); + }); + + test("never throws for any string input", () => { + fcAssert( + property(string(), (s) => { + const result = formatTime(s); + expect(typeof result).toBe("string"); + expect(result.length).toBeGreaterThan(0); + }), + { numRuns: DEFAULT_NUM_RUNS } + ); + }); + + test("falsy number inputs (0, NaN) fall through to current time", () => { + // NaN and 0 are falsy, so !timestamp is true → uses new Date() + const result = formatTime(Number.NaN); + expect(result).toMatch(/^\d{2}:\d{2}:\d{2}$/); + const result0 = formatTime(0); + expect(result0).toMatch(/^\d{2}:\d{2}:\d{2}$/); + }); + + test("valid epoch seconds produce HH:MM:SS format", () => { + fcAssert( + property(integer({ min: 0, max: 4_102_444_800 }), (epoch) => { + const result = formatTime(epoch); + expect(result).toMatch(/^\d{2}:\d{2}:\d{2}$/); + }), + { numRuns: DEFAULT_NUM_RUNS } + ); + }); +}); + +describe("property: isItemIncluded", () => { + test("empty filter set always returns true", () => { + const itemTypes = oneof( + constantFrom( + "error", + "event", + "transaction", + "log", + "attachment", + "session" + ), + option(string(), { nil: undefined }) + ); + fcAssert( + property(itemTypes, (itemType) => { + const empty = new Set(); + expect(isItemIncluded(itemType as string | undefined, empty)).toBe( + true + ); + }), + { numRuns: DEFAULT_NUM_RUNS } + ); + }); + + test("known filter categories are self-consistent with itemTypeToFilterCategory", () => { + const knownTypes = constantFrom("error", "event", "transaction", "log"); + const filterArb = constantFrom(...FILTER_VALUES); + fcAssert( + property(knownTypes, filterArb, (itemType, filter) => { + const filters = new Set([filter]); + const category = itemTypeToFilterCategory(itemType); + const included = isItemIncluded(itemType, filters); + if (category === filter) { + expect(included).toBe(true); + } else { + expect(included).toBe(false); + } + }), + { numRuns: DEFAULT_NUM_RUNS } + ); + }); +}); + +describe("property: itemTypeToFilterCategory", () => { + test("error types map to error", () => { + const errorTypes = constantFrom("error", "event"); + fcAssert( + property(errorTypes, (itemType) => { + expect(itemTypeToFilterCategory(itemType)).toBe("error"); + }), + { numRuns: 10 } + ); + }); + + test("transaction maps to transaction", () => { + expect(itemTypeToFilterCategory("transaction")).toBe("transaction"); + }); + + test("log maps to log", () => { + expect(itemTypeToFilterCategory("log")).toBe("log"); + }); + + test("random non-matching strings return undefined", () => { + const nonMatching = stringMatching(/^[a-z]{3,10}$/).filter( + (s) => + s !== "error" && s !== "event" && s !== "transaction" && s !== "log" + ); + fcAssert( + property(nonMatching, (itemType) => { + expect(itemTypeToFilterCategory(itemType)).toBeUndefined(); + }), + { numRuns: DEFAULT_NUM_RUNS } + ); + }); + + test("undefined input returns undefined", () => { + expect(itemTypeToFilterCategory(undefined)).toBeUndefined(); + }); +}); diff --git a/test/lib/formatters/local.test.ts b/test/lib/formatters/local.test.ts new file mode 100644 index 000000000..ca482adf0 --- /dev/null +++ b/test/lib/formatters/local.test.ts @@ -0,0 +1,417 @@ +/** + * Unit tests for local dev server formatters. + * + * Note: Core invariants (never-throw, sanitization round-trips, filter + * emptiness) are tested via property-based tests in local.property.test.ts. + * These tests focus on specific output formatting and edge cases. + */ + +import { describe, expect, test } from "bun:test"; +import type { FilterValue } from "../../../src/lib/formatters/local.js"; +import { + formatErrorItem, + formatItem, + formatSingleLog, + formatTime, + formatTransactionItem, + inferSource, + isItemIncluded, + itemTypeToFilterCategory, +} from "../../../src/lib/formatters/local.js"; +import { stripAnsi } from "../../../src/lib/formatters/plain-detect.js"; + +describe("formatTime", () => { + test("formats epoch seconds", () => { + const result = formatTime(1_700_000_000); + expect(result).toMatch(/^\d{2}:\d{2}:\d{2}$/); + }); + + test("formats ISO string", () => { + const result = formatTime("2024-01-15T12:30:45Z"); + expect(result).toMatch(/^\d{2}:\d{2}:\d{2}$/); + }); + + test("returns current time for NaN (NaN is falsy)", () => { + // NaN is falsy, so !timestamp is true → uses new Date() + const result = formatTime(Number.NaN); + expect(result).toMatch(/^\d{2}:\d{2}:\d{2}$/); + }); + + test("returns ??:??:?? for unparseable string", () => { + expect(formatTime("not-a-date")).toBe("??:??:??"); + }); + + test("returns current time when missing", () => { + const result = formatTime(); + expect(result).toMatch(/^\d{2}:\d{2}:\d{2}$/); + }); + + test("returns current time for undefined", () => { + const result = formatTime(undefined); + expect(result).toMatch(/^\d{2}:\d{2}:\d{2}$/); + }); +}); + +describe("formatErrorItem", () => { + const serverHeader = { sdk: { name: "sentry.node" } }; + + test("formats error with exception values", () => { + const event = { + timestamp: 1_700_000_000, + exception: { + values: [{ type: "TypeError", value: "x is not a function" }], + }, + }; + const result = stripAnsi(formatErrorItem(event, serverHeader)); + expect(result).toContain("[ERROR]"); + expect(result).toContain("TypeError: x is not a function"); + }); + + test("falls back to message when no exception", () => { + const event = { + timestamp: 1_700_000_000, + message: "Something went wrong", + }; + const result = stripAnsi(formatErrorItem(event, serverHeader)); + expect(result).toContain("Error: Something went wrong"); + }); + + test("falls back to Unknown error when no exception or message", () => { + const event = { timestamp: 1_700_000_000 }; + const result = stripAnsi(formatErrorItem(event, serverHeader)); + expect(result).toContain("Error: Unknown error"); + }); + + test("includes stack frame hint for in_app frame", () => { + const event = { + timestamp: 1_700_000_000, + exception: { + values: [ + { + type: "Error", + value: "boom", + stacktrace: { + frames: [ + { filename: "node_modules/lib.js", lineno: 10, in_app: false }, + { + filename: "src/handler.ts", + lineno: 42, + colno: 5, + function: "handleRequest", + in_app: true, + }, + ], + }, + }, + ], + }, + }; + const result = stripAnsi(formatErrorItem(event, serverHeader)); + expect(result).toContain("[src/handler.ts:42:5]"); + expect(result).toContain("[handleRequest]"); + }); + + test("prefers in_app frame over last frame", () => { + const event = { + timestamp: 1_700_000_000, + exception: { + values: [ + { + type: "Error", + value: "boom", + stacktrace: { + frames: [ + { filename: "src/app.ts", lineno: 10, in_app: true }, + { filename: "node_modules/lib.js", lineno: 99, in_app: false }, + ], + }, + }, + ], + }, + }; + const result = stripAnsi(formatErrorItem(event, serverHeader)); + expect(result).toContain("[src/app.ts:10]"); + }); +}); + +describe("formatTransactionItem", () => { + const browserHeader = { sdk: { name: "sentry.javascript.browser" } }; + + test("formats transaction with op", () => { + const event = { + timestamp: 1_700_000_001, + start_timestamp: 1_700_000_000, + transaction: "GET /api/users", + contexts: { trace: { op: "http.client", status: "ok" } }, + }; + const result = stripAnsi(formatTransactionItem(event, browserHeader)); + expect(result).toContain("[TRACE]"); + expect(result).toContain("[http.client]"); + expect(result).toContain("GET /api/users"); + expect(result).toContain("[1000ms]"); + }); + + test("omits op when default", () => { + const event = { + timestamp: 1_700_000_001, + start_timestamp: 1_700_000_000, + transaction: "my-txn", + contexts: { trace: { op: "default" } }, + }; + const result = stripAnsi(formatTransactionItem(event, browserHeader)); + expect(result).not.toContain("[default]"); + expect(result).toContain("my-txn"); + }); + + test("shows non-ok status", () => { + const event = { + timestamp: 1_700_000_001, + start_timestamp: 1_700_000_000, + transaction: "POST /api", + contexts: { trace: { op: "http.server", status: "internal_error" } }, + }; + const result = stripAnsi(formatTransactionItem(event, browserHeader)); + expect(result).toContain("[internal_error]"); + }); + + test("shows span count", () => { + const event = { + timestamp: 1_700_000_001, + start_timestamp: 1_700_000_000, + transaction: "my-txn", + contexts: { trace: { op: "http.server" } }, + spans: [{}, {}, {}], + }; + const result = stripAnsi(formatTransactionItem(event, browserHeader)); + expect(result).toContain("[3 spans]"); + }); + + test("uses singular span for count of 1", () => { + const event = { + timestamp: 1_700_000_001, + start_timestamp: 1_700_000_000, + transaction: "my-txn", + contexts: { trace: { op: "http.server" } }, + spans: [{}], + }; + const result = stripAnsi(formatTransactionItem(event, browserHeader)); + expect(result).toContain("[1 span]"); + }); + + test("falls back to Transaction when no transaction name", () => { + const event = { + timestamp: 1_700_000_001, + start_timestamp: 1_700_000_000, + contexts: { trace: {} }, + }; + const result = stripAnsi(formatTransactionItem(event, browserHeader)); + expect(result).toContain("Transaction"); + }); +}); + +describe("formatSingleLog", () => { + test("formats log with body", () => { + const result = stripAnsi( + formatSingleLog( + { level: "info", body: "User logged in", timestamp: 1_700_000_000 }, + "[SERVER] " + ) + ); + expect(result).toContain("[INFO]"); + expect(result).toContain("User logged in"); + }); + + test("filters sentry.* attributes", () => { + const result = stripAnsi( + formatSingleLog( + { + level: "info", + body: "hello", + attributes: { + "sentry.sdk.name": { value: "node" }, + user_id: { value: 42 }, + }, + }, + "[SERVER] " + ) + ); + expect(result).toContain("[user_id=42]"); + expect(result).not.toContain("sentry.sdk.name"); + }); + + test("formats without body", () => { + const result = stripAnsi(formatSingleLog({ level: "debug" }, "[SERVER] ")); + expect(result).toContain("[DEBUG]"); + }); + + test("defaults level to log when missing", () => { + const result = stripAnsi(formatSingleLog({}, "[SERVER] ")); + expect(result).toContain("[LOG]"); + }); +}); + +describe("formatItem", () => { + const header = { sdk: { name: "sentry.node" } }; + + test("dispatches error type to error formatter", () => { + const event = { timestamp: 1_700_000_000, message: "boom" }; + const lines = formatItem("error", event, header, "fallback"); + expect(lines).toHaveLength(1); + expect(stripAnsi(lines[0])).toContain("[ERROR]"); + }); + + test("dispatches event type to error formatter", () => { + const event = { timestamp: 1_700_000_000, message: "boom" }; + const lines = formatItem("event", event, header, "fallback"); + expect(lines).toHaveLength(1); + expect(stripAnsi(lines[0])).toContain("[ERROR]"); + }); + + test("dispatches transaction type to transaction formatter", () => { + const event = { + timestamp: 1_700_000_001, + start_timestamp: 1_700_000_000, + transaction: "GET /", + contexts: { trace: { op: "http.server" } }, + }; + const lines = formatItem("transaction", event, header, "fallback"); + expect(lines).toHaveLength(1); + expect(stripAnsi(lines[0])).toContain("[TRACE]"); + }); + + test("dispatches log type to log formatter", () => { + const event = { + items: [{ level: "info", body: "hello" }], + }; + const lines = formatItem("log", event, header, "fallback"); + expect(lines).toHaveLength(1); + expect(stripAnsi(lines[0])).toContain("[INFO]"); + }); + + test("falls back for unknown types", () => { + const lines = formatItem("attachment", {}, header, "attachment"); + expect(lines).toHaveLength(1); + expect(stripAnsi(lines[0])).toContain("attachment"); + }); + + test("falls back for undefined type", () => { + const lines = formatItem(undefined, {}, header, "unknown"); + expect(lines).toHaveLength(1); + expect(stripAnsi(lines[0])).toContain("unknown"); + }); +}); + +describe("isItemIncluded", () => { + test("empty filters includes everything", () => { + const empty = new Set(); + expect(isItemIncluded("error", empty)).toBe(true); + expect(isItemIncluded("transaction", empty)).toBe(true); + expect(isItemIncluded("log", empty)).toBe(true); + expect(isItemIncluded("attachment", empty)).toBe(true); + expect(isItemIncluded(undefined, empty)).toBe(true); + }); + + test("error filter matches error and event types", () => { + const filters = new Set(["error"]); + expect(isItemIncluded("error", filters)).toBe(true); + expect(isItemIncluded("event", filters)).toBe(true); + expect(isItemIncluded("transaction", filters)).toBe(false); + expect(isItemIncluded("log", filters)).toBe(false); + }); + + test("transaction filter matches only transaction", () => { + const filters = new Set(["transaction"]); + expect(isItemIncluded("transaction", filters)).toBe(true); + expect(isItemIncluded("error", filters)).toBe(false); + }); + + test("log filter matches only log", () => { + const filters = new Set(["log"]); + expect(isItemIncluded("log", filters)).toBe(true); + expect(isItemIncluded("error", filters)).toBe(false); + }); + + test("non-matching item type excluded with active filters", () => { + const filters = new Set(["error"]); + expect(isItemIncluded("attachment", filters)).toBe(false); + expect(isItemIncluded(undefined, filters)).toBe(false); + }); +}); + +describe("itemTypeToFilterCategory", () => { + test("maps error types to error", () => { + expect(itemTypeToFilterCategory("error")).toBe("error"); + expect(itemTypeToFilterCategory("event")).toBe("error"); + }); + + test("maps transaction to transaction", () => { + expect(itemTypeToFilterCategory("transaction")).toBe("transaction"); + }); + + test("maps log to log", () => { + expect(itemTypeToFilterCategory("log")).toBe("log"); + }); + + test("returns undefined for unknown types", () => { + expect(itemTypeToFilterCategory("attachment")).toBeUndefined(); + expect(itemTypeToFilterCategory("session")).toBeUndefined(); + expect(itemTypeToFilterCategory(undefined)).toBeUndefined(); + }); +}); + +describe("inferSource", () => { + test("detects mobile SDK (cocoa)", () => { + const result = stripAnsi(inferSource({ sdk: { name: "sentry.cocoa" } })); + expect(result).toContain("[MOBILE]"); + }); + + test("detects mobile SDK (android)", () => { + const result = stripAnsi(inferSource({ sdk: { name: "sentry.android" } })); + expect(result).toContain("[MOBILE]"); + }); + + test("detects mobile SDK (react-native)", () => { + const result = stripAnsi( + inferSource({ sdk: { name: "sentry.javascript.react-native" } }) + ); + expect(result).toContain("[MOBILE]"); + }); + + test("detects mobile SDK (flutter)", () => { + const result = stripAnsi( + inferSource({ sdk: { name: "sentry.dart.flutter" } }) + ); + expect(result).toContain("[MOBILE]"); + }); + + test("detects browser SDK", () => { + const result = stripAnsi( + inferSource({ sdk: { name: "sentry.javascript.browser" } }) + ); + expect(result).toContain("[BROWSER]"); + }); + + test("detects server JS SDK (node)", () => { + const result = stripAnsi( + inferSource({ sdk: { name: "sentry.javascript.node" } }) + ); + expect(result).toContain("[SERVER]"); + }); + + test("detects server JS SDK (nextjs)", () => { + const result = stripAnsi( + inferSource({ sdk: { name: "sentry.javascript.nextjs" } }) + ); + expect(result).toContain("[SERVER]"); + }); + + test("defaults to server for unknown SDK", () => { + const result = stripAnsi(inferSource({ sdk: { name: "sentry.python" } })); + expect(result).toContain("[SERVER]"); + }); + + test("defaults to server when no SDK", () => { + const result = stripAnsi(inferSource({})); + expect(result).toContain("[SERVER]"); + }); +}); From ca3356ee1c1ba9b9735f9a6cbe5e08c7384cd3d1 Mon Sep 17 00:00:00 2001 From: Aditya Mathur <57684218+MathurAditya724@users.noreply.github.com> Date: Wed, 20 May 2026 15:17:22 +0000 Subject: [PATCH 35/46] fix(local): forward child exit code, validate content-encoding, strip control chars - run: set process.exitCode directly instead of EXIT.GENERAL so callers can distinguish the child's error type. - server: validate content-encoding header against known values before passing to pushToSpotlightBuffer. - sanitize: strip C0 control characters (BEL, BS, etc.) in addition to ANSI escapes and newlines. --- src/commands/local/run.ts | 6 +++++- src/commands/local/server.ts | 13 ++++++++----- src/lib/formatters/local.ts | 9 +++++++-- 3 files changed, 20 insertions(+), 8 deletions(-) diff --git a/src/commands/local/run.ts b/src/commands/local/run.ts index b99ad4705..c132ce1eb 100644 --- a/src/commands/local/run.ts +++ b/src/commands/local/run.ts @@ -157,7 +157,11 @@ export const runCommand = buildCommand({ } if (exitCode !== 0) { - throw new CliError(`Process exited with code ${exitCode}`, EXIT.GENERAL); + // Forward the child's exit code directly so callers (CI, scripts) + // can distinguish error types. We set process.exitCode instead of + // throwing CliError to avoid mapping to the CLI's semantic exit + // code schema. + process.exitCode = exitCode; } }, }); diff --git a/src/commands/local/server.ts b/src/commands/local/server.ts index b49b6913f..da7a95bbb 100644 --- a/src/commands/local/server.ts +++ b/src/commands/local/server.ts @@ -181,11 +181,14 @@ export function buildApp( ) { contentType = SENTRY_CONTENT_TYPE; } - const contentEncoding = c.req.header("content-encoding") as - | "gzip" - | "deflate" - | "br" - | undefined; + const rawEncoding = c.req.header("content-encoding"); + const contentEncoding = ( + rawEncoding === "gzip" || + rawEncoding === "deflate" || + rawEncoding === "br" + ? rawEncoding + : undefined + ) as "gzip" | "deflate" | "br" | undefined; const userAgent = c.req.header("user-agent"); pushToSpotlightBuffer({ diff --git a/src/lib/formatters/local.ts b/src/lib/formatters/local.ts index 42193715f..686469297 100644 --- a/src/lib/formatters/local.ts +++ b/src/lib/formatters/local.ts @@ -3,9 +3,14 @@ import { blue, bold, cyan, green, muted, red, yellow } from "./colors.js"; import { stripAnsi } from "./plain-detect.js"; -/** Strip ANSI escapes and collapse newlines so envelope fields can't inject fake log lines. */ +/** + * Strip ANSI escapes, collapse newlines, and remove C0 control characters + * so envelope fields can't inject fake log lines or terminal commands. + */ export function sanitize(text: string): string { - return stripAnsi(text).replace(/[\r\n]+/g, " "); + const stripped = stripAnsi(text).replace(/[\r\n]+/g, " "); + // biome-ignore lint/suspicious/noControlCharactersInRegex: stripping C0 control chars from untrusted envelope data + return stripped.replace(/[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/g, ""); } /** Canonical content type for Sentry envelopes. */ From 3c6d4e0892e70cd3f7bcf9596c4ca8feee73fefa Mon Sep 17 00:00:00 2001 From: Aditya Mathur <57684218+MathurAditya724@users.noreply.github.com> Date: Wed, 20 May 2026 15:18:49 +0000 Subject: [PATCH 36/46] fix(local): clean up signal handlers after shutdown - waitForShutdown: remove SIGINT/SIGTERM listeners after the first signal fires, preventing handler accumulation. - consumer mode: wrap consumeSSE in try/finally to remove signal handlers on both normal exit and error paths. --- src/commands/local/server.ts | 34 ++++++++++++++++++---------------- 1 file changed, 18 insertions(+), 16 deletions(-) diff --git a/src/commands/local/server.ts b/src/commands/local/server.ts index da7a95bbb..4b9e8317d 100644 --- a/src/commands/local/server.ts +++ b/src/commands/local/server.ts @@ -240,24 +240,25 @@ export function buildApp( function waitForShutdown(server: Server): Promise { return new Promise((resolve) => { let shuttingDown = false; - const shutdown = (signal: NodeJS.Signals) => { + const onSigint = () => shutdown("SIGINT"); + const onSigterm = () => shutdown("SIGTERM"); + + function shutdown(signal: NodeJS.Signals) { if (shuttingDown) { - // Second signal — force exit. Bypasses the `process.exit` hook so - // we don't dangle on stuck connections. process.exit(0); } shuttingDown = true; logger.log(`Received ${signal}, shutting down...`); + process.removeListener("SIGINT", onSigint); + process.removeListener("SIGTERM", onSigterm); server.close(() => resolve()); - // Force-close keep-alive connections so we don't wait on long-lived - // SSE subscribers. if (typeof server.closeAllConnections === "function") { server.closeAllConnections(); } - }; + } - process.on("SIGINT", () => shutdown("SIGINT")); - process.on("SIGTERM", () => shutdown("SIGTERM")); + process.on("SIGINT", onSigint); + process.on("SIGTERM", onSigterm); }); } @@ -510,15 +511,16 @@ export const serverCommand = buildCommand({ process.once("SIGINT", stop); process.once("SIGTERM", stop); - // Connect to the SSE stream even in quiet mode so we detect when - // the upstream server disconnects (the for-await loop exits). - await consumeSSE(url, activeFilters, ac.signal, flags.quiet).catch( - (err: unknown) => { - if (!(err instanceof DOMException && err.name === "AbortError")) { - throw err; - } + try { + await consumeSSE(url, activeFilters, ac.signal, flags.quiet); + } catch (err: unknown) { + if (!(err instanceof DOMException && err.name === "AbortError")) { + throw err; } - ); + } finally { + process.removeListener("SIGINT", stop); + process.removeListener("SIGTERM", stop); + } logger.log("Disconnected."); return; } From 37e4630073f0231c6e1d825df16a754a1f55dd44 Mon Sep 17 00:00:00 2001 From: Aditya Mathur <57684218+MathurAditya724@users.noreply.github.com> Date: Wed, 20 May 2026 15:25:29 +0000 Subject: [PATCH 37/46] fix(local): use actual bound port when --port 0 is passed tryListen now reads the OS-assigned port from server.address() instead of returning the original argument. run.ts builds spotlightUrl after the server starts so the child process gets the correct URL. --- src/commands/local/run.ts | 12 ++++++++---- src/commands/local/server.ts | 6 +++++- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/src/commands/local/run.ts b/src/commands/local/run.ts index c132ce1eb..6a897faca 100644 --- a/src/commands/local/run.ts +++ b/src/commands/local/run.ts @@ -108,9 +108,7 @@ export const runCommand = buildCommand({ ); } - const url = `http://${flags.host}:${flags.port}`; - const spotlightUrl = `${url}/stream`; - + let url = `http://${flags.host}:${flags.port}`; let bgServer: Server | undefined; const alreadyRunning = await isServerRunning(url); @@ -118,11 +116,17 @@ export const runCommand = buildCommand({ logger.info("No server detected, starting one in the background..."); const buffer = createSpotlightBuffer(BUFFER_SIZE); const app = buildApp(buffer); - const { server } = await tryListen(app, flags.port, flags.host); + const { server, port: boundPort } = await tryListen( + app, + flags.port, + flags.host + ); bgServer = server; + url = `http://${flags.host}:${boundPort}`; logger.info(`Background server listening on ${bold(url)}`); } + const spotlightUrl = `${url}/stream`; logger.info(`Starting: ${bold(args.join(" "))}`); logger.info(`SENTRY_SPOTLIGHT=${spotlightUrl}`); diff --git a/src/commands/local/server.ts b/src/commands/local/server.ts index 4b9e8317d..32f1cf9b4 100644 --- a/src/commands/local/server.ts +++ b/src/commands/local/server.ts @@ -289,7 +289,11 @@ export function tryListen( hostname, }) as unknown as Server; - server.once("listening", () => resolve({ server, port })); + server.once("listening", () => { + const addr = server.address(); + const boundPort = typeof addr === "object" && addr ? addr.port : port; + resolve({ server, port: boundPort }); + }); server.once("error", async (err: NodeJS.ErrnoException) => { server.close(); if (err.code === "EADDRINUSE") { From 39171999ad7f1d88747ac4f0d34d09ed5547268e Mon Sep 17 00:00:00 2001 From: Aditya Mathur <57684218+MathurAditya724@users.noreply.github.com> Date: Wed, 20 May 2026 15:29:03 +0000 Subject: [PATCH 38/46] fix(local): throw CliError with child exit code, case-insensitive level colors - run: revert to throwing CliError with the child's actual exit code instead of setting process.exitCode, matching test expectations. - formatType: use lowercase level for LEVEL_COLORS lookup so uppercase levels like 'INFO' still get colored output. --- src/commands/local/run.ts | 6 +----- src/lib/formatters/local.ts | 2 +- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/src/commands/local/run.ts b/src/commands/local/run.ts index 6a897faca..a5612c9d3 100644 --- a/src/commands/local/run.ts +++ b/src/commands/local/run.ts @@ -161,11 +161,7 @@ export const runCommand = buildCommand({ } if (exitCode !== 0) { - // Forward the child's exit code directly so callers (CI, scripts) - // can distinguish error types. We set process.exitCode instead of - // throwing CliError to avoid mapping to the CLI's semantic exit - // code schema. - process.exitCode = exitCode; + throw new CliError(`Process exited with code ${exitCode}`, exitCode); } }, }); diff --git a/src/lib/formatters/local.ts b/src/lib/formatters/local.ts index 686469297..0e5234467 100644 --- a/src/lib/formatters/local.ts +++ b/src/lib/formatters/local.ts @@ -55,7 +55,7 @@ const SOURCE_WIDTH = 9; /** Format a type/level label as `[TYPE]` padded to fixed width. */ export function formatType(level: string): string { const tag = `[${sanitize(level).toUpperCase()}]`; - const colorFn = LEVEL_COLORS[level]; + const colorFn = LEVEL_COLORS[level.toLowerCase()]; const colored = colorFn ? colorFn(tag) : tag; return colored + " ".repeat(Math.max(0, TYPE_WIDTH - tag.length)); } From 0bc38d7a77bd482963ce5d356727c8a52fdb3b90 Mon Sep 17 00:00:00 2001 From: Aditya Mathur <57684218+MathurAditya724@users.noreply.github.com> Date: Wed, 20 May 2026 15:31:05 +0000 Subject: [PATCH 39/46] fix(local): use formatTime() in formatFallbackLine for consistent timestamps --- src/lib/formatters/local.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/lib/formatters/local.ts b/src/lib/formatters/local.ts index 0e5234467..892d29bd3 100644 --- a/src/lib/formatters/local.ts +++ b/src/lib/formatters/local.ts @@ -285,8 +285,7 @@ export function itemTypeToFilterCategory( /** Produce a fallback one-liner for unparseable or unsupported items. */ export function formatFallbackLine(label: string): string { - const ts = new Date().toISOString().slice(11, 23); - return `${muted(ts)} ${cyan("•")} ${bold(sanitize(label))}`; + return `${muted(formatTime())} ${cyan("•")} ${bold(sanitize(label))}`; } /** Resolve a human label for a completely unparseable envelope. */ From 6d1b2a3322ac10910740c832a4e4e7a3b1150805 Mon Sep 17 00:00:00 2001 From: Aditya Mathur <57684218+MathurAditya724@users.noreply.github.com> Date: Thu, 21 May 2026 10:43:12 +0000 Subject: [PATCH 40/46] =?UTF-8?q?fix(local):=20fix=20run=20command=20arg?= =?UTF-8?q?=20passing=20=E2=80=94=20use=20rest=20params,=20strip=20'--'=20?= =?UTF-8?q?separator?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Use ...args rest parameter (Stricli passes variadic positionals as individual params, not an array) - Strip leading '--' separator that Stricli passes through --- src/commands/local/run.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/commands/local/run.ts b/src/commands/local/run.ts index a5612c9d3..a7f4e34cb 100644 --- a/src/commands/local/run.ts +++ b/src/commands/local/run.ts @@ -100,7 +100,9 @@ export const runCommand = buildCommand({ }, auth: false, // biome-ignore lint/correctness/useYield: child process wrapper, no structured output - async *func(this: SentryContext, flags: RunFlags, args: string[]) { + async *func(this: SentryContext, flags: RunFlags, ...rawArgs: string[]) { + // Strip leading "--" separator that Stricli passes through + const args = rawArgs[0] === "--" ? rawArgs.slice(1) : rawArgs; if (args.length === 0) { throw new ValidationError( "No command provided. Usage: sentry local run -- ", From 53b85f0cb60718ec1b8b4f04efced0cc28b6646d Mon Sep 17 00:00:00 2001 From: Aditya Mathur <57684218+MathurAditya724@users.noreply.github.com> Date: Thu, 21 May 2026 11:03:58 +0000 Subject: [PATCH 41/46] fix(local): forward SIGINT/SIGTERM to child process in run command Without signal forwarding, SIGTERM sent to the parent doesn't reach the grandchild process (e.g. bun run app.ts), causing the run command to hang on shutdown. --- src/commands/local/run.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/commands/local/run.ts b/src/commands/local/run.ts index a7f4e34cb..2c4b7d765 100644 --- a/src/commands/local/run.ts +++ b/src/commands/local/run.ts @@ -155,6 +155,13 @@ export const runCommand = buildCommand({ ); } + // Forward signals to the child so the whole process tree shuts down. + const forwardSignal = (signal: NodeJS.Signals) => { + child.kill(signal); + }; + process.once("SIGINT", () => forwardSignal("SIGINT")); + process.once("SIGTERM", () => forwardSignal("SIGTERM")); + const exitCode = await child.exited; if (bgServer) { From ff3c8b8cd230ff19ec3e292697d7ce7953cb0331 Mon Sep 17 00:00:00 2001 From: Aditya Mathur <57684218+MathurAditya724@users.noreply.github.com> Date: Thu, 21 May 2026 11:40:04 +0000 Subject: [PATCH 42/46] fix(local): fix run command test to use rest params and Promise return type The buildCommand wrapper consumes the async generator internally, so loader() returns a plain async function. Tests must spread args as individual parameters (matching Stricli's variadic positional convention) and await the Promise directly. --- test/commands/local/run.test.ts | 21 +++++++++------------ 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/test/commands/local/run.test.ts b/test/commands/local/run.test.ts index f38809bab..ed93da58b 100644 --- a/test/commands/local/run.test.ts +++ b/test/commands/local/run.test.ts @@ -12,8 +12,8 @@ import { CliError, ValidationError } from "../../../src/lib/errors.js"; type RunFunc = ( this: unknown, flags: { port: number; host: string }, - args: string[] -) => Promise; + ...args: string[] +) => Promise; function makeContext() { return { @@ -28,7 +28,7 @@ describe("sentry local run", () => { const func = (await runCommand.loader()) as unknown as RunFunc; const ctx = makeContext(); try { - await func.call(ctx, { port: 0, host: "localhost" }, []); + await func.call(ctx, { port: 0, host: "localhost" }); expect.unreachable("should have thrown"); } catch (err) { expect(err).toBeInstanceOf(ValidationError); @@ -40,25 +40,22 @@ describe("sentry local run", () => { const func = (await runCommand.loader()) as unknown as RunFunc; const ctx = makeContext(); - // Use a high ephemeral port unlikely to conflict. - // The command auto-starts a background server since none is running. - // `printenv SENTRY_SPOTLIGHT` prints the var and exits 0. const port = 19_876; - await func.call(ctx, { port, host: "127.0.0.1" }, [ + await func.call( + ctx, + { port, host: "127.0.0.1" }, "printenv", - "SENTRY_SPOTLIGHT", - ]); - // If we got here without error, the child exited 0 and env vars were set. + "SENTRY_SPOTLIGHT" + ); }); test("propagates non-zero exit code as CliError", async () => { const func = (await runCommand.loader()) as unknown as RunFunc; const ctx = makeContext(); - // `false` is a POSIX command that always exits with code 1. const port = 19_877; try { - await func.call(ctx, { port, host: "127.0.0.1" }, ["false"]); + await func.call(ctx, { port, host: "127.0.0.1" }, "false"); expect.unreachable("should have thrown"); } catch (err) { expect(err).toBeInstanceOf(CliError); From 7a9f14b587e9780d01ede4fd6dbe5ff2b9e35013 Mon Sep 17 00:00:00 2001 From: Aditya Mathur <57684218+MathurAditya724@users.noreply.github.com> Date: Thu, 21 May 2026 11:45:26 +0000 Subject: [PATCH 43/46] fix(local): call server.close() before closeAllConnections() The reverse order can prevent the close callback from firing when active SSE connections exist, causing the shutdown promise to hang. --- src/commands/local/run.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/commands/local/run.ts b/src/commands/local/run.ts index 2c4b7d765..c64780d8a 100644 --- a/src/commands/local/run.ts +++ b/src/commands/local/run.ts @@ -49,10 +49,10 @@ const BUFFER_SIZE = 500; */ function shutdownServer(server: Server): Promise { return new Promise((resolve) => { + server.close(() => resolve()); if (typeof server.closeAllConnections === "function") { server.closeAllConnections(); } - server.close(() => resolve()); }); } From afb54166541fd34d78705385e60886b3c6cb1c43 Mon Sep 17 00:00:00 2001 From: Aditya Mathur <57684218+MathurAditya724@users.noreply.github.com> Date: Thu, 21 May 2026 11:54:01 +0000 Subject: [PATCH 44/46] fix(local): add IPv6 localhost to CORS, guard null attribute values - CORS: add [::1] to LOCALHOST_ORIGIN_RE so IPv6 dev stacks pass preflight. - formatSingleLog: guard against null attribute entries before accessing v.value. - run.ts: remove unused biome-ignore suppression. --- src/commands/local/run.ts | 1 - src/commands/local/server.ts | 5 +++-- src/lib/formatters/local.ts | 6 +++++- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/src/commands/local/run.ts b/src/commands/local/run.ts index c64780d8a..f1d6bc81e 100644 --- a/src/commands/local/run.ts +++ b/src/commands/local/run.ts @@ -99,7 +99,6 @@ export const runCommand = buildCommand({ }, }, auth: false, - // biome-ignore lint/correctness/useYield: child process wrapper, no structured output async *func(this: SentryContext, flags: RunFlags, ...rawArgs: string[]) { // Strip leading "--" separator that Stricli passes through const args = rawArgs[0] === "--" ? rawArgs.slice(1) : rawArgs; diff --git a/src/commands/local/server.ts b/src/commands/local/server.ts index 32f1cf9b4..b02fe310b 100644 --- a/src/commands/local/server.ts +++ b/src/commands/local/server.ts @@ -85,8 +85,9 @@ function parsePort(value: string): number { return port; } -/** Match localhost origins on any port (http or https). */ -const LOCALHOST_ORIGIN_RE = /^https?:\/\/(localhost|127\.0\.0\.1)(:\d+)?$/; +/** Match localhost origins on any port (http or https), including IPv6. */ +const LOCALHOST_ORIGIN_RE = + /^https?:\/\/(localhost|127\.0\.0\.1|\[::1\])(:\d+)?$/; /** * Build the Hono application. diff --git a/src/lib/formatters/local.ts b/src/lib/formatters/local.ts index 892d29bd3..228958aac 100644 --- a/src/lib/formatters/local.ts +++ b/src/lib/formatters/local.ts @@ -231,7 +231,11 @@ export function formatSingleLog(logEntry: LogEntry, source: string): string { const attrs = Object.entries(logEntry.attributes) .filter( ([k, v]) => - !k.startsWith("sentry.") && v.value !== null && v.value !== undefined + !k.startsWith("sentry.") && + v !== null && + v !== undefined && + v.value !== null && + v.value !== undefined ) .map(([k, v]) => muted(`[${sanitize(k)}=${sanitize(String(v.value))}]`)); if (attrs.length > 0) { From 24f6f1aff419e63644892628055f13525bee4319 Mon Sep 17 00:00:00 2001 From: Aditya Mathur <57684218+MathurAditya724@users.noreply.github.com> Date: Thu, 21 May 2026 11:56:52 +0000 Subject: [PATCH 45/46] fix(local): strip C1 control characters and NEL in sanitize() Extend sanitize() to cover the C1 range (0x80-0x9F) which includes raw 8-bit CSI/OSC/DCS introducers, and collapse NEL (U+0085) as a line break. --- src/lib/formatters/local.ts | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/lib/formatters/local.ts b/src/lib/formatters/local.ts index 228958aac..f62866880 100644 --- a/src/lib/formatters/local.ts +++ b/src/lib/formatters/local.ts @@ -4,13 +4,16 @@ import { blue, bold, cyan, green, muted, red, yellow } from "./colors.js"; import { stripAnsi } from "./plain-detect.js"; /** - * Strip ANSI escapes, collapse newlines, and remove C0 control characters + * Strip ANSI escapes, collapse newlines, and remove C0/C1 control characters * so envelope fields can't inject fake log lines or terminal commands. */ export function sanitize(text: string): string { - const stripped = stripAnsi(text).replace(/[\r\n]+/g, " "); - // biome-ignore lint/suspicious/noControlCharactersInRegex: stripping C0 control chars from untrusted envelope data - return stripped.replace(/[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/g, ""); + // Collapse CR, LF, and NEL (U+0085) which terminals treat as line breaks. + const stripped = stripAnsi(text).replace(/[\r\n\x85]+/g, " "); + // Strip C0 (0x00-0x1F, 0x7F) and C1 (0x80-0x9F) control characters. + // C1 includes raw 8-bit CSI (0x9B), OSC (0x9D), and DCS (0x90) introducers. + // biome-ignore lint/suspicious/noControlCharactersInRegex: stripping control chars from untrusted envelope data + return stripped.replace(/[\x00-\x08\x0b\x0c\x0e-\x1f\x7f\x80-\x9f]/g, ""); } /** Canonical content type for Sentry envelopes. */ From d5e46ee632b46253447295aa1421871544bddd20 Mon Sep 17 00:00:00 2001 From: Aditya Mathur <57684218+MathurAditya724@users.noreply.github.com> Date: Thu, 21 May 2026 12:12:00 +0000 Subject: [PATCH 46/46] fix(local): strip Unicode bidirectional override characters in sanitize() Add a pass to remove bidi marks (U+200E-200F, U+202A-202E, U+2066-2069) that can reorder terminal output rendering. --- src/lib/formatters/local.ts | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/lib/formatters/local.ts b/src/lib/formatters/local.ts index f62866880..78965dcef 100644 --- a/src/lib/formatters/local.ts +++ b/src/lib/formatters/local.ts @@ -11,9 +11,13 @@ export function sanitize(text: string): string { // Collapse CR, LF, and NEL (U+0085) which terminals treat as line breaks. const stripped = stripAnsi(text).replace(/[\r\n\x85]+/g, " "); // Strip C0 (0x00-0x1F, 0x7F) and C1 (0x80-0x9F) control characters. - // C1 includes raw 8-bit CSI (0x9B), OSC (0x9D), and DCS (0x90) introducers. - // biome-ignore lint/suspicious/noControlCharactersInRegex: stripping control chars from untrusted envelope data - return stripped.replace(/[\x00-\x08\x0b\x0c\x0e-\x1f\x7f\x80-\x9f]/g, ""); + const noCtrl = stripped.replace( + // biome-ignore lint/suspicious/noControlCharactersInRegex: stripping control chars from untrusted envelope data + /[\x00-\x08\x0b\x0c\x0e-\x1f\x7f\x80-\x9f]/g, + "" + ); + // Strip Unicode bidirectional override/isolate characters that can reorder terminal output. + return noCtrl.replace(/[\u200e\u200f\u202a-\u202e\u2066-\u2069]/g, ""); } /** Canonical content type for Sentry envelopes. */