diff --git a/bun.lock b/bun.lock index ead0d2419..ff9fef9dd 100644 --- a/bun.lock +++ b/bun.lock @@ -8,7 +8,6 @@ "@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.113.0", "@sentry/node-core": "10.50.0", "@sentry/sqlish": "^1.0.0", @@ -51,22 +50,6 @@ "@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=="], - "@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=="], "@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=="], @@ -143,26 +126,10 @@ "@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.12", "", { "os": "win32", "cpu": "x64" }, "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA=="], - "@hono/node-server": ["@hono/node-server@1.19.9", "", { "peerDependencies": { "hono": "^4" } }, "sha512-vHL6w3ecZsky+8P5MD+eFfaGTyCeOHUIFYMGpQGbrBTSmNNoxv0if69rEZ5giu36weC5saFuznL411gRX7bJDw=="], - "@isaacs/balanced-match": ["@isaacs/balanced-match@4.0.1", "", {}, "sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ=="], "@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=="], - - "@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.7.1", "", { "dependencies": { "@lukeed/uuid": "^2.0.1", "@mastra/core": "1.8.0", "@mastra/schema-compat": "1.1.3", "json-schema": "^0.4.0" }, "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" } }, "sha512-+MIIjhNr61WKWcEZgdNzBhYdNSVOQMRTXYO749kNXAuwPN8yeh4ESHesOPHPOs+3o8wFB8Cg82bz5Gl2FZyvJg=="], - - "@mastra/core": ["@mastra/core@1.8.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.3", "@modelcontextprotocol/sdk": "^1.17.5", "@sindresorhus/slugify": "^2.2.1", "dotenv": "^17.2.3", "gray-matter": "^4.0.3", "hono": "^4.11.9", "hono-openapi": "^1.1.1", "js-tiktoken": "^1.0.21", "json-schema": "^0.4.0", "lru-cache": "^11.2.6", "p-map": "^7.0.3", "p-retry": "^7.1.0", "picomatch": "^4.0.3", "radash": "^12.1.1", "xxhash-wasm": "^1.1.0" }, "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" } }, "sha512-AK6Isj21mWlwX1zIZNUxgAQvRfjJmdjsPsKoh1cOvaM+h748S4U48TJ5DsmundSj/8NBeKHmYXqH2RYqwN35nw=="], - - "@mastra/schema-compat": ["@mastra/schema-compat@1.1.3", "", { "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-szLMJhqfnEn4VctFLKRZ2NIpfg+3UTghQWgy8Fcdchj2HvHxB2uilJxRybM9ugMmvyE+W48tVdz4Xi2Z1P3pFA=="], - - "@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.27.1", "", { "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-sr6GbP+4edBwFndLbM60gf07z0FQ79gaExpnsjMGePXqFcSSb7t6iscpjk9DhFhwd+mTEQrzNafGP8/iGGFYaA=="], - "@opentelemetry/api": ["@opentelemetry/api@1.9.0", "", {}, "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg=="], "@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=="], @@ -185,42 +152,16 @@ "@sentry/sqlish": ["@sentry/sqlish@1.0.0", "", { "peerDependencies": { "react": ">=17" }, "optionalPeers": ["react"] }, "sha512-L/ZZ6AKNaINqshjcSbAPz+lghbyPThn/gQB9LuPkKqq2ftuHC5m8sv7VpY2ZckQ2XIiUa3/0C2guKIReWCfOJg=="], - "@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=="], - "@types/body-parser": ["@types/body-parser@1.19.6", "", { "dependencies": { "@types/connect": "*", "@types/node": "*" } }, "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g=="], - "@types/bun": ["@types/bun@1.3.13", "", { "dependencies": { "bun-types": "1.3.13" } }, "sha512-9fqXWk5YIHGGnUau9TEi+qdlTYDAnOj+xLCmSTwXfAIqXr2x4tytJb43E9uCvt09zJURKXwAtkoH4nLQfzeTXw=="], - "@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-cache-semantics": ["@types/http-cache-semantics@4.2.0", "", {}, "sha512-L3LgimLHXtGkWikKnsPg0/VFx9OGZaC+eN1u4r+OB1XRqH3meBIAVC2zr1WdMH+RHmnRkqliQAOHNJ/E0j/e0Q=="], - "@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/node": ["@types/node@22.19.7", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-MciR4AKGHWl7xwxkBa6xUGxQJ4VBOmPTF7sL+iGzuahOFaO0jHCsuEfS80pan1ef4gWId1oWOweIhrDEYLuaOw=="], "@types/node-fetch": ["@types/node-fetch@2.6.13", "", { "dependencies": { "@types/node": "*", "form-data": "^4.0.4" } }, "sha512-QGpRVpzSaUs30JBSGPjOg4Uveu384erbHBoT1zeONvyCfwQxIkUshLAOqN/k9EjGviPRmWTTe6aH2qySWKTVSw=="], @@ -229,56 +170,30 @@ "@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=="], - "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=="], "acorn-import-attributes": ["acorn-import-attributes@1.9.5", "", { "peerDependencies": { "acorn": "^8" } }, "sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ=="], "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=="], - "ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="], "ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="], "any-promise": ["any-promise@1.3.0", "", {}, "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A=="], - "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=="], - "asynckit": ["asynckit@0.4.0", "", {}, "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="], - "base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="], - "binpunch": ["binpunch@1.0.0", "", { "bin": { "binpunch": "dist/cli.js" } }, "sha512-ghxdoerLN3WN64kteDJuL4d9dy7gbvcqoADNRWBk6aQ5FrYH1EmPmREAdcdIdTNAA3uW3V38Env5OqH2lj+i+g=="], - "body-parser": ["body-parser@2.2.2", "", { "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", "debug": "^4.4.3", "http-errors": "^2.0.0", "iconv-lite": "^0.7.0", "on-finished": "^2.4.1", "qs": "^6.14.1", "raw-body": "^3.0.1", "type-is": "^2.0.1" } }, "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA=="], - "bun-types": ["bun-types@1.3.13", "", { "dependencies": { "@types/node": "*" } }, "sha512-QXKeHLlOLqQX9LgYaHJfzdBaV21T63HhFJnvuRCcjZiaUDpbs5ED1MgxbMra71CsryN/1dAoXuJJJwIv/2drVA=="], - "bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="], - "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=="], - "chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="], "citty": ["citty@0.2.0", "", {}, "sha512-8csy5IBFI2ex2hTVpaHN2j+LNE199AgiI7y4dMintrr8i0lQiFn+0AWMZrWdHKIgMOer65f8IThysYhoReqjWA=="], @@ -299,38 +214,14 @@ "consola": ["consola@3.4.2", "", {}, "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA=="], - "content-disposition": ["content-disposition@0.5.4", "", { "dependencies": { "safe-buffer": "5.2.1" } }, "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ=="], - - "content-type": ["content-type@1.0.5", "", {}, "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA=="], - - "cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="], - - "cookie-signature": ["cookie-signature@1.0.7", "", {}, "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA=="], - - "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=="], "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=="], - - "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=="], - "emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], - "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=="], @@ -343,46 +234,18 @@ "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=="], - "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=="], - - "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=="], - "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=="], - "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=="], - - "fresh": ["fresh@0.5.2", "", {}, "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q=="], - "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="], "get-caller-file": ["get-caller-file@2.0.5", "", {}, "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="], @@ -397,8 +260,6 @@ "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-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], "has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="], @@ -409,70 +270,24 @@ "highlight.js": ["highlight.js@10.7.3", "", {}, "sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A=="], - "hono": ["hono@4.12.3", "", {}, "sha512-SFsVSjp8sj5UumXOOFlkZOG6XS9SJDKw0TbwFeV+AJ8xlST8kxK5Z/5EYa111UY8732lK2S/xB653ceuaoGwpg=="], - - "hono-openapi": ["hono-openapi@1.3.0", "", { "peerDependencies": { "@hono/standard-validator": "^0.2.0", "@standard-community/standard-json": "^0.3.5", "@standard-community/standard-openapi": "^0.2.9", "@types/json-schema": "^7.0.15", "hono": "^4.8.3", "openapi-types": "^12.1.3" }, "optionalPeers": ["@hono/standard-validator", "hono"] }, "sha512-xDvCWpWEIv0weEmnl3EjRQzqbHIO8LnfzMuYOCmbuyE5aes6aXxLg4vM3ybnoZD5TiTUkA6PuRQPJs3R7WRBig=="], - "http-cache-semantics": ["http-cache-semantics@4.2.0", "", {}, "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ=="], - "http-errors": ["http-errors@2.0.1", "", { "dependencies": { "depd": "~2.0.0", "inherits": "~2.0.4", "setprototypeof": "~1.2.0", "statuses": "~2.0.2", "toidentifier": "~1.0.1" } }, "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ=="], - "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=="], "import-in-the-middle": ["import-in-the-middle@3.0.0", "", { "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-OnGy+eYT7wVejH2XWgLRgbmzujhhVIATQH0ztIeRilwHBjTeG3pD+XnH3PKX0r9gJ0BuJmJ68q/oh9qgXnNDQg=="], - "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-extendable": ["is-extendable@0.1.1", "", {}, "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw=="], - "is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="], - "is-network-error": ["is-network-error@1.3.1", "", {}, "sha512-6QCxa49rQbmUWLfk0nuGqzql9U8uaV2H6279bRErPBHe/109hCzsLUBUHfbEtvLIHBd6hyXbgedBSHevm43Edw=="], - - "is-promise": ["is-promise@4.0.0", "", {}, "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ=="], - - "isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], - - "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-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=="], - - "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=="], - "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=="], - - "lru-cache": ["lru-cache@11.2.6", "", {}, "sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ=="], + "lru-cache": ["lru-cache@11.2.4", "", {}, "sha512-B5Y16Jr9LB9dHVkh6ZevG+vAbOsNOYCX+sXvFWFu7B3Iz5mijW3zdbMyhsh8ANd2mSWBYdJgnqi+mL7/LrOPYg=="], "marked": ["marked@15.0.12", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-8dD6FusOQSrpv9Z1rdNMdlSgQOIP880DHqnohobOmYLElGEqAL/JvxvuxZO16r4HtjTlfPRDC1hbvxC9dPN2nA=="], "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.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], "mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], @@ -487,10 +302,6 @@ "mz": ["mz@2.7.0", "", { "dependencies": { "any-promise": "^1.0.0", "object-assign": "^4.0.1", "thenify-all": "^1.0.0" } }, "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q=="], - "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-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=="], @@ -499,34 +310,16 @@ "object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="], - "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.2.0", "", { "dependencies": { "yocto-queue": "^1.2.1" } }, "sha512-ATHLtwoTNDloHRFFxFJdHnG6n2WUeFjaR8XQMFdKIv0xkXjrER8/iG9iu265jOM95zXHAfv9oTkqhrfbIzosrQ=="], - "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=="], "parse5": ["parse5@5.1.1", "", {}, "sha512-ugq4DFI0Ptb+WWjAdOK16+u/nHfiIrcE+sh8kZMaM0WllQKLI9rOUq6c2b7cwPkXdzfQESqvoqK6ug7U/Yyzug=="], "parse5-htmlparser2-tree-adapter": ["parse5-htmlparser2-tree-adapter@6.0.1", "", { "dependencies": { "parse5": "^6.0.1" } }, "sha512-qPuWvbLgvDGilKc5BoicRovlT4MtYT6JfJyBOMDsKoiT+GiuP5qyrPCnR9HcPECIJJmZh5jRndyNThnhhb/vlA=="], - "parseurl": ["parseurl@1.3.3", "", {}, "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="], - - "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=="], "peggy": ["peggy@5.1.0", "", { "dependencies": { "@peggyjs/from-mem": "3.1.3", "commander": "^14.0.3", "source-map-generator": "2.0.6" }, "bin": { "peggy": "bin/peggy.js" } }, "sha512-IEo5aYRZ2kXH4Qby06cjtL114PZnwLoTiA41vUmg2vPZgANn+c87m5BUurhuDr5/cu758ZlpgsAfBVx+hhO5+w=="], @@ -535,74 +328,24 @@ "picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="], - "pkce-challenge": ["pkce-challenge@5.0.1", "", {}, "sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ=="], - "pretty-ms": ["pretty-ms@9.3.0", "", { "dependencies": { "parse-ms": "^4.0.0" } }, "sha512-gjVS5hOP+M3wMm5nmNOucbIrqudzs9v/57bWRHQWLYklXqoXKrVfYW2W9+glfGsqtPgpiz5WwyEEB+ksXIx3gQ=="], - "proxy-addr": ["proxy-addr@2.0.7", "", { "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" } }, "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg=="], - "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=="], - "require-directory": ["require-directory@2.1.1", "", {}, "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q=="], - "require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="], - - "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.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="], - "send": ["send@0.19.2", "", { "dependencies": { "debug": "2.6.9", "depd": "2.0.0", "destroy": "1.2.0", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", "fresh": "~0.5.2", "http-errors": "~2.0.1", "mime": "1.6.0", "ms": "2.1.3", "on-finished": "~2.4.1", "range-parser": "~1.2.1", "statuses": "~2.0.2" } }, "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg=="], - - "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=="], "source-map-generator": ["source-map-generator@2.0.6", "", {}, "sha512-IlassDs1Ve8nV6uyQZXF9kdkJpVKnMte2JZQXu13M0A5zwc+vu6+LNHfmxsHBMDtoZE21RHiKI0/xvpecZRCNg=="], - "sprintf-js": ["sprintf-js@1.0.3", "", {}, "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g=="], - - "statuses": ["statuses@2.0.2", "", {}, "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw=="], - "string-width": ["string-width@8.2.0", "", { "dependencies": { "get-east-asian-width": "^1.5.0", "strip-ansi": "^7.1.2" } }, "sha512-6hJPQ8N0V0P3SNmP6h2J99RLuzrWz2gvT7VnK5tKvrNqJoyS9W4/Fb8mo31UiPvy00z7DQXkP2hnKBVav76thw=="], "strip-ansi": ["strip-ansi@7.2.0", "", { "dependencies": { "ansi-regex": "^6.2.2" } }, "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w=="], - "strip-bom-string": ["strip-bom-string@1.0.0", "", {}, "sha512-uCC2VHvQRYu+lMh4My/sFNmF2klFymLX1wHJeXnbEJERpV/ZsVuonzerjfrGpIGF7LBVa1O7i9kjiWvJiFck8g=="], - "supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], "thenify": ["thenify@3.3.1", "", { "dependencies": { "any-promise": "^1.0.0" } }, "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw=="], @@ -613,44 +356,26 @@ "tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="], - "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=="], - - "utils-merge": ["utils-merge@1.0.1", "", {}, "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA=="], - - "uuid": ["uuid@11.1.0", "", { "bin": { "uuid": "dist/esm/bin/uuid" } }, "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A=="], - "uuidv7": ["uuidv7@1.1.0", "", { "bin": { "uuidv7": "cli.js" } }, "sha512-2VNnOC0+XQlwogChUDzy6pe8GQEys9QFZBGOh54l6qVfwoCUwwRvk7rDTgaIsRgsF5GFa5oiNg8LqXE3jofBBg=="], - "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=="], "whatwg-url": ["whatwg-url@5.0.0", "", { "dependencies": { "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" } }, "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw=="], - "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], - "wrap-ansi": ["wrap-ansi@10.0.0", "", { "dependencies": { "ansi-styles": "^6.2.3", "string-width": "^8.2.0", "strip-ansi": "^7.1.2" } }, "sha512-SGcvg80f0wUy2/fXES19feHMz8E0JoXv2uNgHOu4Dgi2OrCy1lqwFYEJz1BLbDI0exjPMe/ZdzZ/YpGECBG/aQ=="], - "wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="], - - "xxhash-wasm": ["xxhash-wasm@1.1.0", "", {}, "sha512-147y/6YNh+tlp6nd/2pWq38i9h6mz/EuQ6njIrmW8D1BS5nCqs0P6DG+m6zTGnNz5I+uhZ0SHxBs9BsPrwcKDA=="], - "y18n": ["y18n@5.0.8", "", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="], "yargs": ["yargs@16.2.0", "", { "dependencies": { "cliui": "^7.0.2", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.0", "y18n": "^5.0.5", "yargs-parser": "^20.2.2" } }, "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw=="], @@ -661,24 +386,8 @@ "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=="], - - "@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=="], - "@anthropic-ai/sdk/@types/node": ["@types/node@18.19.130", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg=="], - "@modelcontextprotocol/sdk/express": ["express@5.2.1", "", { "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.1", "content-disposition": "^1.0.0", "content-type": "^1.0.5", "cookie": "^0.7.1", "cookie-signature": "^1.2.1", "debug": "^4.4.0", "depd": "^2.0.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "finalhandler": "^2.1.0", "fresh": "^2.0.0", "http-errors": "^2.0.0", "merge-descriptors": "^2.0.0", "mime-types": "^3.0.0", "on-finished": "^2.4.1", "once": "^1.4.0", "parseurl": "^1.3.3", "proxy-addr": "^2.0.7", "qs": "^6.14.0", "range-parser": "^1.2.1", "router": "^2.2.0", "send": "^1.1.0", "serve-static": "^2.2.0", "statuses": "^2.0.1", "type-is": "^2.0.1", "vary": "^1.1.2" } }, "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw=="], - - "@modelcontextprotocol/sdk/zod": ["zod@4.3.5", "", {}, "sha512-k7Nwx6vuWx1IJ9Bjuf4Zt1PEllcwe7cls3VNzm4CQ1/hgtFUK2bRNG3rvnpPUhFjmqJKAKtjV576KnUkHocg/g=="], - "@peggyjs/from-mem/semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="], "cli-highlight/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], @@ -689,80 +398,24 @@ "cliui/wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="], - "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=="], - - "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=="], - "parse5-htmlparser2-tree-adapter/parse5": ["parse5@6.0.1", "", {}, "sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw=="], - "path-scurry/lru-cache": ["lru-cache@11.2.4", "", {}, "sha512-B5Y16Jr9LB9dHVkh6ZevG+vAbOsNOYCX+sXvFWFu7B3Iz5mijW3zdbMyhsh8ANd2mSWBYdJgnqi+mL7/LrOPYg=="], - - "router/path-to-regexp": ["path-to-regexp@8.3.0", "", {}, "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA=="], - - "send/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], - "trpc-cli/commander": ["commander@14.0.2", "", {}, "sha512-TywoWNNRbhoD0BXs1P3ZEScW8W5iKrnbithIl0YH+uCmBd0QpPOA8yc82DS3BIE5Ma6FnBVUsJ7wVUDz4dvOWQ=="], - "type-is/mime-types": ["mime-types@3.0.2", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A=="], - "ultracite/zod": ["zod@4.3.5", "", {}, "sha512-k7Nwx6vuWx1IJ9Bjuf4Zt1PEllcwe7cls3VNzm4CQ1/hgtFUK2bRNG3rvnpPUhFjmqJKAKtjV576KnUkHocg/g=="], "yargs/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], - "zod-from-json-schema/zod": ["zod@4.3.5", "", {}, "sha512-k7Nwx6vuWx1IJ9Bjuf4Zt1PEllcwe7cls3VNzm4CQ1/hgtFUK2bRNG3rvnpPUhFjmqJKAKtjV576KnUkHocg/g=="], - "@anthropic-ai/sdk/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="], - "@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/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=="], - "cli-highlight/chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], "cliui/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], "cliui/wrap-ansi/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], - "express/body-parser/iconv-lite": ["iconv-lite@0.4.24", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3" } }, "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA=="], - - "express/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=="], - - "finalhandler/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], - - "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=="], - "yargs/string-width/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], - "@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=="], - "yargs/string-width/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], } } diff --git a/docs/src/fragments/commands/init.md b/docs/src/fragments/commands/init.md index 226084c32..f4904361b 100644 --- a/docs/src/fragments/commands/init.md +++ b/docs/src/fragments/commands/init.md @@ -28,8 +28,8 @@ sentry init acme/my-app # Assign a team when creating a new project sentry init acme/ --team backend -# Enable specific features -sentry init --features profiling,replay +# Skip the agent-driven feature picker and use a fixed list (CI / non-interactive) +sentry init --features tracing,replay,sourcemaps ``` ## Target Syntax @@ -60,9 +60,13 @@ Path-like arguments (starting with `.`, `/`, or `~`) are always treated as the d ## 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 +1. **Detects your framework** — scans your project files to identify the platform, framework, runtime, and relevant libraries +2. **Researches the docs** — walks the Sentry docs to figure out which features are supported and useful for *your* project +3. **Asks which features to enable** — proposes only the features that fit your stack (e.g. no Session Replay on a server-only Node app), with a short reason next to each one. Error monitoring is always on +4. **Installs the SDK** — adds the appropriate Sentry SDK package to your project +5. **Instruments your code** — configures error monitoring and the features you picked + +Use `--features ` to skip the analyze-and-pick step (handy for `--yes` / CI). When provided, the wizard treats it as canonical and the agent goes straight to instrumentation. ### Supported Platforms diff --git a/package.json b/package.json index 451a02c16..7ff31858f 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,6 @@ "@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.113.0", "@sentry/node-core": "10.50.0", "@sentry/sqlish": "^1.0.0", diff --git a/plugins/sentry-cli/skills/sentry-cli/references/init.md b/plugins/sentry-cli/skills/sentry-cli/references/init.md index a6ad7a0a0..de0b6f77b 100644 --- a/plugins/sentry-cli/skills/sentry-cli/references/init.md +++ b/plugins/sentry-cli/skills/sentry-cli/references/init.md @@ -45,8 +45,8 @@ sentry init acme/my-app # Assign a team when creating a new project sentry init acme/ --team backend -# Enable specific features -sentry init --features profiling,replay +# Skip the agent-driven feature picker and use a fixed list (CI / non-interactive) +sentry init --features tracing,replay,sourcemaps ``` All commands also support `--json`, `--fields`, `--help`, `--log-level`, and `--verbose` flags. diff --git a/src/commands/init.ts b/src/commands/init.ts index f1f7dad14..476efb746 100644 --- a/src/commands/init.ts +++ b/src/commands/init.ts @@ -1,9 +1,9 @@ /** * 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. + * Initialize Sentry in a project using the remote wizard. + * Starts a Vercel Workflow that runs a Claude Agent SDK session in a + * sandbox and bridges file/command operations back to this CLI. * * Supports two optional positionals with smart disambiguation: * sentry init — auto-detect everything, dir = cwd @@ -24,7 +24,7 @@ import { looksLikePath, parseOrgProjectArg } from "../lib/arg-parsing.js"; import { buildCommand } from "../lib/command.js"; import { ContextError, ValidationError } from "../lib/errors.js"; import { warmOrgDetection } from "../lib/init/org-prefetch.js"; -import { runWizard } from "../lib/init/wizard-runner.js"; +import { runInit } from "../lib/init/init-runner.js"; import { validateResourceId } from "../lib/input-validation.js"; import { logger } from "../lib/logger.js"; import { @@ -277,7 +277,7 @@ export const initCommand = buildCommand< // would skip the timer and the process would hang on the error // display — exactly what Cursor Bugbot flagged on an earlier revision. try { - await runWizard({ + await runInit({ directory: targetDir, yes: flags.yes, dryRun: flags["dry-run"], @@ -289,11 +289,11 @@ export const initCommand = buildCommand< } finally { // 7. macOS-only force-exit safety net. // - // On Darwin, `runWizard` installs the `/dev/tty` forwarding + // On Darwin, `runInit` installs the `/dev/tty` forwarding // workaround from stdin-reopen.ts to get keystrokes through to // clack. That workaround opens a second `tty.ReadStream` which // leaks a libuv handle on Bun 1.3.11 — no userland cleanup - // releases it (upstream oven-sh/bun#29126). After `runWizard` + // releases it (upstream oven-sh/bun#29126). After `runInit` // returns (or throws), the event loop stays ref'd and the process // hangs until the user presses a key. // diff --git a/src/lib/init/clack-utils.ts b/src/lib/init/clack-utils.ts index d796fd003..19f5322b7 100644 --- a/src/lib/init/clack-utils.ts +++ b/src/lib/init/clack-utils.ts @@ -107,7 +107,7 @@ export const STEP_LABELS: Record = { "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", + "propose-features": "Selecting features", "install-deps": "Installing dependencies", "plan-codemods": "Planning code modifications", "apply-codemods": "Applying code modifications", diff --git a/src/lib/init/constants.ts b/src/lib/init/constants.ts index 5f7349cb3..c202633e5 100644 --- a/src/lib/init/constants.ts +++ b/src/lib/init/constants.ts @@ -1,23 +1,39 @@ -export const MASTRA_API_URL = - process.env.MASTRA_API_URL ?? - "https://sentry-init-agent.getsentry.workers.dev"; +export const INIT_API_URL = + process.env.SENTRY_INIT_API_URL ?? + process.env.INIT_API_URL ?? + "https://sentry-init-agent.vercel.app"; -export const WORKFLOW_ID = "sentry-wizard"; +/** + * Initial-handshake timeout for `GET /api/init/:runId/stream` and + * `GET /api/init/:runId` (status). The stream body is a long-lived + * NDJSON pipe that idles for minutes between events; the runner + * handles that via the `handleStreamClosure` -> status check -> + * `resumeRun` loop, so this only protects the request *connect* phase. + */ +export const STREAM_CONNECT_TIMEOUT_MS = 30_000; + +/** + * Maximum consecutive failures of `GET /api/init/:runId` (the status + * endpoint) before we give up. Stream drops themselves are normal and + * NOT counted: they flow into `handleStreamClosure` which fetches + * status, branches on running/completed/failed/cancelled, and + * reconnects when appropriate. Mirrors birthday-card-generator's + * `maxConsecutiveErrors: 5` on `WorkflowChatTransport`. + */ +export const MAX_STATUS_FAILURES = 5; + +/** + * Cap exponential backoff between status-failure reconnect attempts so + * we don't sleep for minutes after a flake. + */ +export const MAX_RECONNECT_DELAY_MS = 30_000; export const SENTRY_DOCS_URL = "https://docs.sentry.io/platforms/"; export const MAX_FILE_BYTES = 262_144; // 256KB per file export const MAX_OUTPUT_BYTES = 65_536; // 64KB stdout/stderr truncation export const DEFAULT_COMMAND_TIMEOUT_MS = 120_000; // 2 minutes -export const API_TIMEOUT_MS = 120_000; // 2 minutes timeout for Mastra API calls - -// Exit codes returned by the remote workflow -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"; +export const API_TIMEOUT_MS = 120_000; // 2 minutes timeout for API calls // The feature that is always included in every setup export const REQUIRED_FEATURE = "errorMonitoring"; diff --git a/src/lib/init/ensure-project.ts b/src/lib/init/ensure-project.ts new file mode 100644 index 000000000..9591f2788 --- /dev/null +++ b/src/lib/init/ensure-project.ts @@ -0,0 +1,135 @@ +/** + * Ensure a Sentry project + DSN exist before the workflow starts. + * + * Mirrors the legacy server-side `create-sentry-project` tool, but + * runs entirely in CLI preflight so the workflow input is complete + * by the time we POST `/api/init`. The agent never has to ask the + * user to pick an org/team/project mid-run. + */ + +import { log } from "@clack/prompts"; +import { createProjectWithDsn } from "../api-client.js"; +import { ApiError, WizardError } from "../errors.js"; +import { resolveOrCreateTeam } from "../resolve-team.js"; +import { slugify } from "../utils.js"; +import { tryGetExistingProjectData } from "./existing-project.js"; +import type { ExistingProjectData, ResolvedInitContext } from "./types.js"; + +/** Default platform slug used to create new projects from `sentry init`. */ +const DEFAULT_CREATE_PLATFORM = "javascript"; + +export type EnsuredProject = { + orgSlug: string; + teamSlug?: string; + projectSlug: string; + projectId: string; + dsn: string; + url: string; + /** True if the project existed before this run. */ + preExisting: boolean; +}; + +export async function ensureSentryProject( + ctx: ResolvedInitContext +): Promise { + const explicit = ctx.existingProject; + if (explicit) { + return projectFromExisting(explicit, ctx.team, true); + } + + const projectName = ctx.project ?? deriveProjectName(ctx.directory); + const slug = slugify(projectName); + if (!slug) { + throw new WizardError( + `Cannot create project: "${projectName}" produces an empty slug.` + ); + } + + // First check if it already exists under the resolved org. + try { + const existing = await tryGetExistingProjectData(ctx.org, slug); + if (existing) { + return projectFromExisting(existing, ctx.team, true); + } + } catch (err) { + if (!(err instanceof ApiError && err.status === 404)) { + throw err; + } + } + + if (ctx.dryRun) { + return { + orgSlug: ctx.org, + teamSlug: ctx.team, + projectSlug: slug, + projectId: "(dry-run)", + dsn: "https://key@o0.ingest.sentry.io/0", + url: "https://sentry.io/dry-run", + preExisting: false, + }; + } + + // Create the project. Resolve the team if it wasn't already. + const teamSlug = ctx.team + ? ctx.team + : ( + await resolveOrCreateTeam(ctx.org, { + autoCreateSlug: slug, + usageHint: "sentry init", + dryRun: ctx.dryRun, + }) + ).slug; + + log.info(`Creating Sentry project '${slug}' in ${ctx.org}/${teamSlug}...`); + + const { project, dsn, url } = await createProjectWithDsn(ctx.org, teamSlug, { + name: projectName, + platform: DEFAULT_CREATE_PLATFORM, + }); + + if (!dsn) { + throw new WizardError( + `Project '${project.slug}' created in ${ctx.org} but no DSN was issued.` + ); + } + + return { + orgSlug: ctx.org, + teamSlug, + projectSlug: project.slug, + projectId: project.id, + dsn, + url, + preExisting: false, + }; +} + +function projectFromExisting( + existing: ExistingProjectData, + team: string | undefined, + preExisting: boolean +): EnsuredProject { + if (!existing.dsn) { + throw new WizardError( + `Existing project '${existing.projectSlug}' has no DSN configured.` + ); + } + return { + orgSlug: existing.orgSlug, + teamSlug: team, + projectSlug: existing.projectSlug, + projectId: existing.projectId, + dsn: existing.dsn, + url: existing.url, + preExisting, + }; +} + +function deriveProjectName(directory: string): string { + // Last non-empty path segment. `path.basename` works on Posix and Windows. + const parts = directory + .replaceAll("\\", "/") + .split("/") + .filter((p) => p.length > 0); + return parts.at(-1) ?? "sentry-project"; +} diff --git a/src/lib/init/formatters.ts b/src/lib/init/formatters.ts index a7dda1a4a..d2e837485 100644 --- a/src/lib/init/formatters.ts +++ b/src/lib/init/formatters.ts @@ -8,12 +8,7 @@ import { cancel, log, outro } from "@clack/prompts"; import { terminalLink } from "../formatters/colors.js"; import { colorTag, mdKvTable, renderMarkdown } from "../formatters/markdown.js"; import { featureLabel } from "./clack-utils.js"; -import { - EXIT_DEPENDENCY_INSTALL_FAILED, - EXIT_PLATFORM_NOT_DETECTED, - EXIT_VERIFICATION_FAILED, -} from "./constants.js"; -import type { WizardOutput, WorkflowRunResult } from "./types.js"; +import type { InitErrorEvent, WizardOutput } from "./types.js"; type ChangedFile = NonNullable[number]; @@ -160,8 +155,7 @@ function buildSummary(output: WizardOutput): string { return sections.join("\n\n"); } -export function formatResult(result: WorkflowRunResult): void { - const output: WizardOutput = result.result ?? {}; +export function formatResult(output: WizardOutput): void { const md = buildSummary(output); if (md.length > 0) { @@ -182,32 +176,17 @@ export function formatResult(result: WorkflowRunResult): void { outro("Sentry SDK installed successfully!"); } -export function formatError(result: WorkflowRunResult): void { - const inner = result.result; - const message = - result.error ?? inner?.message ?? "Wizard failed with an unknown error"; - const exitCode = inner?.exitCode ?? 1; +export function formatError(error: InitErrorEvent): void { + log.error(error.message); - log.error(String(message)); - - 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 === EXIT_DEPENDENCY_INSTALL_FAILED) { - const commands = inner?.commands; - if (commands?.length) { - log.warn( - `You can install dependencies manually:\n${commands.map((cmd) => ` $ ${cmd}`).join("\n")}` - ); + if (error.output?.warnings?.length) { + for (const w of error.output.warnings) { + log.warn(w); } - } else if (exitCode === EXIT_VERIFICATION_FAILED) { - log.warn("Hint: Fix the verification issues and run 'sentry init' again."); } - const docsUrl = inner?.docsUrl; - if (docsUrl) { - log.info(`Docs: ${terminalLink(docsUrl)}`); + if (error.docsUrl) { + log.info(`Docs: ${terminalLink(error.docsUrl)}`); } cancel("Setup failed"); diff --git a/src/lib/init/init-runner.ts b/src/lib/init/init-runner.ts new file mode 100644 index 000000000..c283d3ed8 --- /dev/null +++ b/src/lib/init/init-runner.ts @@ -0,0 +1,614 @@ +/** + * sentry init — local driver. + * + * Talks to the Nitro+Hono server in `cli-init-api/apps/server`: + * 1. Preflight (banner, git, org/team/project, features) on the local box. + * 2. POST /api/init -> {runId} + * 3. GET /api/init/:runId/stream (NDJSON of `InitEvent`s) + * 4. For each `action_request` event: run the local tool / prompt, then + * POST /api/init/actions/:actionId with the result. + * 5. Render `summary` / `error`; the stream ends on `done`. + */ + +import { cancel, confirm, intro, log } from "@clack/prompts"; +import { captureException } from "@sentry/node-core/light"; +import { formatBanner } from "../banner.js"; +import { CLI_VERSION } from "../constants.js"; +import { WizardError } from "../errors.js"; +import { terminalLink } from "../formatters/colors.js"; +import { + renderInlineMarkdown, + stripColorTags, +} from "../formatters/markdown.js"; +import { + abortIfCancelled, + WizardCancelledError, +} from "./clack-utils.js"; +import { + INIT_API_URL, + MAX_RECONNECT_DELAY_MS, + MAX_STATUS_FAILURES, + SENTRY_DOCS_URL, +} from "./constants.js"; +import { ensureSentryProject } from "./ensure-project.js"; +import { formatError, formatResult } from "./formatters.js"; +import { checkGitStatus } from "./git.js"; +import { handleInteractive } from "./interactive.js"; +import { resolveInitContext } from "./preflight.js"; +import { normaliseFromFlag } from "./select-features.js"; +import { precomputeProjectContext } from "./workflow-inputs.js"; +import { createWizardSpinner } from "./spinner.js"; +import { forwardFreshTtyToStdin } from "./stdin-reopen.js"; +import { + fetchRunStatus, + openInitStream, + readNdjsonStream, + resumeInitAction, + startInit, +} from "./transport.js"; +import { describeTool, executeTool } from "./tools/registry.js"; +import type { + InitActionRequestEvent, + InitActionResumeBody, + InitDoneEvent, + InitErrorEvent, + InitEvent, + InitStartInput, + InitStatusResponse, + InteractivePayload, + ResolvedInitContext, + ToolPayload, + WizardOptions, + WizardOutput, +} from "./types.js"; + +type Spinner = ReturnType; +type SpinState = { running: boolean }; + +type StreamState = { + /** + * Index of the next un-handled event in the workflow's NDJSON stream. + * When the stream drops (idle body timeout, network blip, etc.) we + * reopen with `?startIndex=nextStartIndex` so the workflow replays + * everything we haven't seen yet. + */ + nextStartIndex: number; + finalOutput?: WizardOutput; + finalError?: InitErrorEvent; + done?: InitDoneEvent; + completedActionIds: Set; +}; + +function sleepMs(ms: number): Promise { + return new Promise((resolve) => setTimeout(resolve, ms)); +} + +function nextReconnectDelay(attempt: number): number { + return Math.min(250 * 2 ** attempt, MAX_RECONNECT_DELAY_MS); +} + +function errorMessage(err: unknown): string { + return err instanceof Error ? err.message : String(err); +} + +function isRecord(value: unknown): value is Record { + return typeof value === "object" && value !== null; +} + +function assertToolPayload(raw: unknown): ToolPayload { + if ( + !isRecord(raw) || + raw.type !== "tool" || + typeof raw.operation !== "string" + ) { + throw new Error("Invalid tool action payload"); + } + return raw as ToolPayload; +} + +function assertInteractivePayload(raw: unknown): InteractivePayload { + if ( + !isRecord(raw) || + raw.type !== "interactive" || + typeof raw.kind !== "string" + ) { + throw new Error("Invalid prompt action payload"); + } + return raw as InteractivePayload; +} + +/** Truncate a spinner message so it doesn't wrap. */ +function truncateForTerminal(message: string): string { + return message.split("\n").map(truncateLineForTerminal).join("\n"); +} + +function truncateLineForTerminal(line: string): string { + const maxWidth = (process.stdout.columns || 80) - 4; + const visibleLine = stripColorTags(line).replace(/`/g, ""); + if (visibleLine.length <= maxWidth) return line; + let truncated = line.slice(0, maxWidth - 1); + const backtickCount = truncated.split("`").length - 1; + if (backtickCount % 2 !== 0) { + const lastBacktick = truncated.lastIndexOf("`"); + truncated = + truncated.slice(0, lastBacktick) + truncated.slice(lastBacktick + 1); + } + return `${truncated}…`; +} + +function toResumeError(error: unknown): InitActionResumeBody { + return { + ok: false, + error: { message: errorMessage(error), details: error }, + }; +} + +function stopSpinner( + spin: Spinner, + spinState: SpinState, + message: string, + code?: number +): void { + if (!spinState.running) return; + spin.stop(message, code); + spinState.running = false; +} + +async function performActionRequest( + event: InitActionRequestEvent, + context: ResolvedInitContext, + spin: Spinner, + spinState: SpinState +): Promise { + if (event.kind === "tool") { + const payload = assertToolPayload(event.payload); + const message = event.description ?? describeTool(payload); + spin.message(renderInlineMarkdown(truncateForTerminal(message))); + + const toolResult = await executeTool(payload, context); + if (!toolResult.ok) { + return { + ok: false, + error: { + message: toolResult.error ?? "Local tool failed", + details: toolResult.data, + }, + }; + } + + if (toolResult.message) { + spin.message(renderInlineMarkdown(toolResult.message)); + } + + return { ok: true, output: toolResult as Record }; + } + + const payload = assertInteractivePayload(event.payload); + + if (spinState.running) { + spin.stop(event.description ?? payload.prompt); + spinState.running = false; + } + + try { + const promptResult = await handleInteractive(payload, context); + spin.start("Processing..."); + spinState.running = true; + return { ok: true, output: promptResult }; + } catch (error) { + if (!(error instanceof WizardCancelledError)) { + spin.start("Processing..."); + spinState.running = true; + } + throw error; + } +} + +async function handleEvent( + event: InitEvent, + context: ResolvedInitContext, + spin: Spinner, + spinState: SpinState, + state: StreamState +): Promise { + // Bump BEFORE handling. If the handler throws or the stream then + // drops, the reconnect uses this updated index to skip events we've + // already processed and ones currently being handled. + state.nextStartIndex += 1; + + switch (event.type) { + case "status": + spin.message(renderInlineMarkdown(truncateForTerminal(event.message))); + return; + case "warning": + log.warn(event.message); + return; + case "summary": + state.finalOutput = event.output; + return; + case "error": + state.finalError = event; + return; + case "done": + state.done = event; + return; + case "heartbeat": + // Server-side keepalive. `nextStartIndex` was already bumped at + // the top of this function, so reconnects skip the heartbeat + // chunk on replay; nothing else to do. + return; + case "action_result": + if (event.summary) { + spin.message(renderInlineMarkdown(truncateForTerminal(event.summary))); + } + return; + case "action_request": { + if (state.completedActionIds.has(event.actionId)) return; + try { + const resumeBody = await performActionRequest( + event, + context, + spin, + spinState + ); + await resumeInitAction(event.actionId, resumeBody, { + baseUrl: INIT_API_URL, + }); + } catch (error) { + if (error instanceof WizardCancelledError) throw error; + await resumeInitAction(event.actionId, toResumeError(error), { + baseUrl: INIT_API_URL, + }); + } + state.completedActionIds.add(event.actionId); + return; + } + default: { + const _exhaustive: never = event; + throw new Error(`Unhandled init event: ${String(_exhaustive)}`); + } + } +} + +async function confirmExperimental(yes: boolean): Promise { + if (yes) return true; + const proceed = await confirm({ + message: + "EXPERIMENTAL: This feature is experimental and may modify your code. Continue?", + initialValue: true, + }); + abortIfCancelled(proceed); + return !!proceed; +} + +async function preamble( + directory: string, + yes: boolean, + dryRun: boolean +): Promise { + if (!(yes || dryRun || process.stdin.isTTY)) { + throw new WizardError( + "Interactive mode requires a terminal. Use --yes for non-interactive mode.", + { rendered: false } + ); + } + + process.stderr.write(`\n${formatBanner()}\n\n`); + intro(`sentry init ${CLI_VERSION} (experimental)`); + + let confirmed: boolean; + try { + confirmed = await confirmExperimental(yes || dryRun); + } catch (error) { + if (error instanceof WizardCancelledError) { + captureException(error); + process.exitCode = 0; + return false; + } + throw error; + } + + if (!confirmed) { + cancel("Setup cancelled."); + process.exitCode = 0; + return false; + } + + if (dryRun) log.warn("Dry-run mode: no files will be modified."); + + const gitOk = await checkGitStatus({ cwd: directory, yes: yes || dryRun }); + if (!gitOk) { + cancel("Setup cancelled."); + process.exitCode = 0; + return false; + } + + return true; +} + +/** + * Per-stream context plumbed through the consume / closure / resume + * helpers. The shape mirrors `birthday-card-generator/components/form.tsx` + * (refs + setters in React, plain values for us). `state` is the same + * `StreamState` we mutate from `handleEvent`; `statusFailures` counts + * consecutive `fetchRunStatus` failures so we can escalate sensibly + * when the SERVER is unreachable (vs the stream just going idle). + */ +type StreamCtx = { + context: ResolvedInitContext; + spin: Spinner; + spinState: SpinState; + state: StreamState; + statusFailures: number; +}; + +/** + * Read the workflow's NDJSON body until it ends (cleanly OR with an + * error), then hand off to `handleStreamClosure` to decide what's + * next. Mirrors `birthday-card-generator/components/form.tsx:398-440` + * `consumeStream`. + * + * Crucially, a thrown read is NOT treated as fatal: long-running + * workflows go idle (Bun/undici fetch-body timeout, transient network + * drops) and the canonical recovery is `fetchRunStatus` + `resumeRun` + * — which is exactly what `handleStreamClosure` does. + */ +async function consumeStream( + response: Response, + runId: string, + ctx: StreamCtx +): Promise { + try { + await readNdjsonStream(response, async (event) => { + await handleEvent( + event, + ctx.context, + ctx.spin, + ctx.spinState, + ctx.state + ); + }); + } catch { + // Stream errored mid-read (idle timeout / network blip). Same + // recovery as a clean close: ask the run for its status and let + // `handleStreamClosure` decide. + } + await handleStreamClosure(runId, ctx); +} + +/** + * The stream just ended. Read the run's status to decide whether to + * reconnect or terminate. Mirrors `birthday-card-generator/components/ + * form.tsx:353-396` `handleStreamClosure`. Status is the source of + * truth for "are we done"; the local stream is just a transport. + */ +async function handleStreamClosure( + runId: string, + ctx: StreamCtx +): Promise { + if (ctx.state.done || ctx.state.finalError) return; + + let payload: InitStatusResponse; + try { + payload = await fetchRunStatus(runId, { baseUrl: INIT_API_URL }); + ctx.statusFailures = 0; + } catch (error) { + ctx.statusFailures += 1; + if (ctx.statusFailures > MAX_STATUS_FAILURES) { + throw new Error( + `Lost contact with the init server: ${errorMessage(error)}` + ); + } + ctx.spin.message("Reconnecting..."); + await sleepMs(nextReconnectDelay(ctx.statusFailures)); + await resumeRun(runId, ctx); + return; + } + + if ( + payload.status === "running" || + payload.status === "queued" || + payload.status === "waiting_for_action" + ) { + ctx.spin.message("Reconnecting..."); + // Match birthday-card-generator's 1s pacing between reconnects. + await sleepMs(1_000); + await resumeRun(runId, ctx); + return; + } + + if (payload.status === "completed") { + if (!ctx.state.finalOutput && payload.output) { + ctx.state.finalOutput = payload.output; + } + ctx.state.done = { type: "done", ok: true }; + return; + } + + // failed | cancelled + ctx.state.finalError = { + type: "error", + message: + payload.error?.message ?? + (payload.status === "cancelled" + ? "The workflow was cancelled before it finished." + : "The workflow stopped before it could finish."), + ...(payload.error?.commands ? { commands: payload.error.commands } : {}), + ...(payload.error?.docsUrl ? { docsUrl: payload.error.docsUrl } : {}), + ...(payload.error?.exitCode !== undefined + ? { exitCode: payload.error.exitCode } + : {}), + ...(payload.error?.output ? { output: payload.error.output } : {}), + }; +} + +/** + * Re-open the NDJSON stream at the current `nextStartIndex` and hand + * back to `consumeStream`. Mirrors `birthday-card-generator/components/ + * form.tsx:442-478` `resumeRun`, with one difference: their resume + * surfaces an open failure to the UI as a terminal error (their + * workflow is short). Ours runs for 30+ minutes, so a transient + * server-side flap (5xx, undici TCP reset) is treated the same as a + * `fetchRunStatus` failure: count against `MAX_STATUS_FAILURES`, + * sleep with backoff, and recurse back into `handleStreamClosure`. + * That helper re-checks status (so we still terminate cleanly if the + * run actually went terminal during the hiccup) and calls us again. + */ +async function resumeRun(runId: string, ctx: StreamCtx): Promise { + let response: Response; + try { + response = await openInitStream(runId, { + baseUrl: INIT_API_URL, + startIndex: ctx.state.nextStartIndex, + }); + ctx.statusFailures = 0; + } catch (error) { + ctx.statusFailures += 1; + if (ctx.statusFailures > MAX_STATUS_FAILURES) { + throw new Error( + `Failed to reconnect to the init stream: ${errorMessage(error)}` + ); + } + ctx.spin.message("Reconnecting..."); + await sleepMs(nextReconnectDelay(ctx.statusFailures)); + return handleStreamClosure(runId, ctx); + } + await consumeStream(response, runId, ctx); +} + +function buildFinalError( + finalError: InitErrorEvent | undefined, + finalOutput: WizardOutput | undefined, + done: InitDoneEvent | undefined +): InitErrorEvent { + if (finalError) { + return { ...finalError, output: finalError.output ?? finalOutput }; + } + return { + type: "error", + message: + done?.ok === false + ? "Workflow completed with an error." + : "Workflow completed without a success result.", + output: finalOutput, + }; +} + +export async function runInit(initialOptions: WizardOptions): Promise { + const { directory, yes, dryRun } = initialOptions; + + if (!(await preamble(directory, yes, dryRun))) return; + + log.info( + `This wizard uses AI to analyze your project and configure Sentry.\nFor manual setup: ${terminalLink(SENTRY_DOCS_URL)}` + ); + + const effectiveOptions = dryRun + ? { ...initialOptions, yes: true } + : initialOptions; + + // Local TTY workaround for macOS Bun (see stdin-reopen.ts). + using _tty = + process.platform === "darwin" + ? forwardFreshTtyToStdin() + : { [Symbol.dispose]: () => undefined }; + + const context = await resolveInitContext(effectiveOptions); + if (!context) return; + + // Resolve / create the Sentry project so the workflow has a real DSN. + const project = await ensureSentryProject(context); + // Feature selection is agent-driven: the in-sandbox Claude analyses the + // project + Sentry docs and proposes only the features that apply via + // the `propose_features` MCP tool, which bridges back here as a + // multi-select. The `--features` flag is a non-interactive override: + // when present, the agent skips its own proposal. + const overrideFeatures = normaliseFromFlag(context.features); + + // Pre-compute the project structure + common config files locally so + // the agent has phase-1 context on its very first turn — no bridge + // round-trip needed for the initial list_dir / read_files calls. + const projectContext = await precomputeProjectContext(directory); + + const startInput: InitStartInput = { + directory, + yes, + dryRun, + ...(overrideFeatures.length > 0 ? { features: overrideFeatures } : {}), + org: project.orgSlug, + team: project.teamSlug, + project: project.projectSlug, + existingProject: { + orgSlug: project.orgSlug, + projectSlug: project.projectSlug, + projectId: project.projectId, + dsn: project.dsn, + url: project.url, + }, + sentryAuthToken: context.authToken, + cliVersion: CLI_VERSION, + projectContext, + }; + + const spin = createWizardSpinner(); + const spinState: SpinState = { running: false }; + const state: StreamState = { + nextStartIndex: 0, + completedActionIds: new Set(), + }; + + spin.start("Starting wizard..."); + spinState.running = true; + + let runId: string; + try { + spin.message("Connecting to wizard..."); + const started = await startInit(startInput, { baseUrl: INIT_API_URL }); + runId = started.runId; + } catch (error) { + stopSpinner(spin, spinState, "Connection failed", 1); + log.error(errorMessage(error)); + cancel("Setup failed"); + throw new WizardError(errorMessage(error)); + } + + try { + const ctx: StreamCtx = { + context, + spin, + spinState, + state, + statusFailures: 0, + }; + const initialResponse = await openInitStream(runId, { + baseUrl: INIT_API_URL, + startIndex: state.nextStartIndex, + }); + await consumeStream(initialResponse, runId, ctx); + } catch (error) { + if (error instanceof WizardCancelledError) { + captureException(error); + process.exitCode = 0; + return; + } + stopSpinner(spin, spinState, "Error", 1); + log.error(errorMessage(error)); + cancel("Setup failed"); + throw new WizardError(errorMessage(error)); + } + + if (state.done?.ok) { + stopSpinner(spin, spinState, "Done"); + formatResult(state.finalOutput ?? {}); + return; + } + + const finalError = buildFinalError( + state.finalError, + state.finalOutput, + state.done + ); + stopSpinner(spin, spinState, "Failed", 1); + formatError(finalError); + throw new WizardError(finalError.message); +} diff --git a/src/lib/init/interactive.ts b/src/lib/init/interactive.ts index 52f39cddf..03776b632 100644 --- a/src/lib/init/interactive.ts +++ b/src/lib/init/interactive.ts @@ -1,20 +1,15 @@ /** * Interactive Dispatcher * - * Handles interactive prompts from the remote workflow. - * Supports select, multi-select, and confirm prompts. - * Respects --yes flag for non-interactive mode. + * Handles `prompt_request` actions from the workflow. + * Supports select, multi-select, and confirm prompts. Respects --yes. */ import { confirm, log, multiselect, select } from "@clack/prompts"; import chalk from "chalk"; -import { - abortIfCancelled, - featureHint, - featureLabel, - sortFeatures, -} from "./clack-utils.js"; +import { abortIfCancelled } from "./clack-utils.js"; import { REQUIRED_FEATURE } from "./constants.js"; +import { FEATURE_LABELS, sortFeatures } from "./select-features.js"; import type { ConfirmPayload, InteractiveContext, @@ -63,19 +58,24 @@ async function handleSelect( 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: items.map((item, i) => ({ + value: item, + label: item, + hint: apps[i]?.framework ?? undefined, + })), }); return { selectedApp: abortIfCancelled(selected) }; } +function featureLabel(id: string): string { + return FEATURE_LABELS[id]?.label ?? id; +} + +function featureHint(id: string): string | undefined { + return FEATURE_LABELS[id]?.hint; +} + async function handleMultiSelect( payload: MultiSelectPayload, options: InteractiveContext @@ -87,6 +87,11 @@ async function handleMultiSelect( } const hasRequired = available.includes(REQUIRED_FEATURE); + // The agent calls this prompt as `propose-features` after analysing the + // project + docs. The IDs in `available` are exactly what the agent + // proposed (errorMonitoring is excluded by the tool's contract). We + // treat any other multi-select that happens to include errorMonitoring + // the same way. if (options.yes) { log.info( @@ -95,27 +100,25 @@ async function handleMultiSelect( return { features: available }; } - const optional = sortFeatures( - available.filter((f) => f !== REQUIRED_FEATURE) - ); + const optional = sortFeatures(available.filter((f) => f !== REQUIRED_FEATURE)); if (optional.length === 0) { if (hasRequired) { - log.info(`${featureLabel(REQUIRED_FEATURE)} is always included.`); + log.info("Error monitoring is always included."); } return { features: hasRequired ? [REQUIRED_FEATURE] : [] }; } - const hints: string[] = []; - // Use clack's vertical bar character so hint lines align with the option lines below const bar = chalk.gray("\u2502"); + const hints: string[] = []; if (hasRequired) { - hints.push( - `${bar} ${chalk.dim(`${featureLabel(REQUIRED_FEATURE)} is always included`)}` - ); + hints.push(`${bar} ${chalk.dim("Error monitoring is always included")}`); } hints.push(`${bar} ${chalk.dim("space=toggle, a=all, enter=confirm")}`); + // Show the agent's prompt body verbatim — it already contains the + // "Why these features are relevant: …" reasons block built by the + // sandboxed `propose_features` tool. const selected = await multiselect({ message: `${payload.prompt}\n${hints.join("\n")}`, options: optional.map((feature) => ({ @@ -123,7 +126,6 @@ async function handleMultiSelect( label: featureLabel(feature), hint: featureHint(feature), })), - initialValues: optional.filter((f) => f === "performanceMonitoring"), required: false, }); diff --git a/src/lib/init/select-features.ts b/src/lib/init/select-features.ts new file mode 100644 index 000000000..ad97d916e --- /dev/null +++ b/src/lib/init/select-features.ts @@ -0,0 +1,151 @@ +/** + * Sentry feature catalog (CLI side). + * + * Feature SELECTION is now agent-driven: the sandboxed Claude agent + * analyses the project + docs and calls a `propose_features` MCP tool + * that bridges back to the CLI as a tailored multi-select. This file + * no longer contains an upfront prompt — it owns: + * + * - `FEATURE_LABELS`: the human-readable label/hint per id, used by + * [interactive.ts](./interactive.ts) when rendering the agent's + * multi-select. + * - `SELECTABLE_FEATURE_IDS`: the canonical ID set, kept in lockstep + * with the server's `KNOWN_FEATURES` (minus `errorMonitoring`). + * - `normaliseFromFlag`: parses the `--features` flag into canonical + * IDs (handles aliases like `replay`, `tracing`, etc.). The result + * is sent to the server as `InitStartInput.features` and tells the + * agent to skip its own proposal. + * + * `errorMonitoring` is implicit and is tracked separately in + * [REQUIRED_FEATURE](./constants.ts). + */ + +export const FEATURE_LABELS: Record = { + tracing: { + label: "Performance Monitoring (Tracing)", + hint: "Distributed transaction and span tracing", + }, + performanceMonitoring: { + label: "Performance Monitoring (Tracing)", + hint: "Distributed transaction and span tracing", + }, + logs: { label: "Logging", hint: "Structured log ingestion" }, + sessionReplay: { + label: "Session Replay", + hint: "Visual replay of user sessions (browsers only)", + }, + profiling: { + label: "Profiling", + hint: "Code-level CPU/wall-time profiling", + }, + aiMonitoring: { + label: "AI Agent Monitoring", + hint: "Track LLM/agent calls, tool runs, tokens", + }, + userFeedback: { + label: "User Feedback", + hint: "Collect in-app user feedback (browsers only)", + }, + sourceMaps: { + label: "Source Maps", + hint: "Show original source in production stack traces", + }, + crons: { + label: "Cron Monitoring", + hint: "Monitor scheduled / recurring jobs", + }, + metrics: { + label: "Metrics", + hint: "Custom counters, distributions, gauges", + }, +}; + +const SORT_ORDER = [ + "tracing", + "performanceMonitoring", + "logs", + "sessionReplay", + "profiling", + "aiMonitoring", + "userFeedback", + "sourceMaps", + "crons", + "metrics", +]; + +export function sortFeatures(ids: string[]): string[] { + return [...ids].sort( + (a, b) => + (SORT_ORDER.indexOf(a) === -1 + ? Number.MAX_SAFE_INTEGER + : SORT_ORDER.indexOf(a)) - + (SORT_ORDER.indexOf(b) === -1 + ? Number.MAX_SAFE_INTEGER + : SORT_ORDER.indexOf(b)) + ); +} + +/** + * Feature ids the wizard knows how to install. Kept in lockstep with the + * server-side `KNOWN_FEATURES` (minus `errorMonitoring`, which is + * implicit). `interactive.ts` looks up labels for IDs the agent proposes + * via `FEATURE_LABELS`; unknown IDs fall back to the raw id. + */ +export const SELECTABLE_FEATURE_IDS = [ + "tracing", + "logs", + "sessionReplay", + "profiling", + "aiMonitoring", + "userFeedback", + "sourceMaps", + "crons", + "metrics", + "performanceMonitoring", +] as const; + +export type SelectableFeatureId = (typeof SELECTABLE_FEATURE_IDS)[number]; + +const ALIASES: Record = { + errors: "tracing", // backward-compat + performance: "tracing", + trace: "tracing", + log: "logs", + logging: "logs", + replay: "sessionReplay", + "session-replay": "sessionReplay", + ai: "aiMonitoring", + "ai-monitoring": "aiMonitoring", + feedback: "userFeedback", + "user-feedback": "userFeedback", + sourcemaps: "sourceMaps", + "source-maps": "sourceMaps", + cron: "crons", +}; + +/** + * Normalise the `--features ` flag into canonical IDs. Aliases + * (`replay`, `tracing`, …) are resolved; unknown values are silently + * dropped (the agent only acts on IDs it recognises). + */ +export function normaliseFromFlag(features?: string[]): SelectableFeatureId[] { + if (!features || features.length === 0) { + return []; + } + const out = new Set(); + for (const raw of features) { + const clean = raw.trim(); + if (!clean) { + continue; + } + if ((SELECTABLE_FEATURE_IDS as readonly string[]).includes(clean)) { + out.add(clean as SelectableFeatureId); + continue; + } + const aliased = ALIASES[clean]; + if (aliased) { + out.add(aliased); + } + } + return [...out]; +} diff --git a/src/lib/init/stream-parser.ts b/src/lib/init/stream-parser.ts new file mode 100644 index 000000000..bf533dffa --- /dev/null +++ b/src/lib/init/stream-parser.ts @@ -0,0 +1,116 @@ +import type { InitEvent } from "./types.js"; + +function isRecord(value: unknown): value is Record { + return typeof value === "object" && value !== null; +} + +function isWizardOutput(value: unknown): boolean { + return isRecord(value); +} + +/** + * Validate a streamed init event before the wizard runner consumes it. + */ +export function assertInitEvent(raw: unknown): InitEvent { + if (!isRecord(raw) || typeof raw.type !== "string") { + throw new Error("Invalid init event"); + } + + switch (raw.type) { + case "status": + if (typeof raw.message !== "string") { + throw new Error("Invalid status event"); + } + return raw as InitEvent; + case "action_request": + if ( + typeof raw.actionId !== "string" || + (raw.kind !== "tool" && raw.kind !== "prompt") || + typeof raw.name !== "string" + ) { + throw new Error("Invalid action_request event"); + } + return raw as InitEvent; + case "action_result": + if (typeof raw.actionId !== "string" || typeof raw.ok !== "boolean") { + throw new Error("Invalid action_result event"); + } + return raw as InitEvent; + case "warning": + if (typeof raw.message !== "string") { + throw new Error("Invalid warning event"); + } + return raw as InitEvent; + case "summary": + if (!isWizardOutput(raw.output)) { + throw new Error("Invalid summary event"); + } + return raw as InitEvent; + case "error": + if (typeof raw.message !== "string") { + throw new Error("Invalid error event"); + } + return raw as InitEvent; + case "done": + if (typeof raw.ok !== "boolean") { + throw new Error("Invalid done event"); + } + return raw as InitEvent; + case "heartbeat": + // Server-side stream keepalive (see InitHeartbeatEvent). No + // payload to validate; the runner advances `nextStartIndex` and + // otherwise ignores it. + return raw as InitEvent; + default: + throw new Error(`Unknown init event type: ${String(raw.type)}`); + } +} + +/** + * Read the CLI progress stream as NDJSON and invoke `onEvent` for each + * typed event. Returns the number of events processed. + */ +export async function readNdjsonStream( + response: Response, + onEvent: (event: InitEvent) => Promise +): Promise { + if (!response.body) { + throw new Error("Init stream response had no body"); + } + + const reader = response.body.getReader(); + const decoder = new TextDecoder(); + let buffer = ""; + let eventCount = 0; + + try { + for (;;) { + const { done, value } = await reader.read(); + if (done) break; + + buffer += decoder.decode(value, { stream: true }); + let newlineIndex = buffer.indexOf("\n"); + + while (newlineIndex !== -1) { + const line = buffer.slice(0, newlineIndex).trim(); + buffer = buffer.slice(newlineIndex + 1); + if (line) { + await onEvent(assertInitEvent(JSON.parse(line))); + eventCount += 1; + } + newlineIndex = buffer.indexOf("\n"); + } + } + + buffer += decoder.decode(); + const trailing = buffer.trim(); + if (trailing) { + await onEvent(assertInitEvent(JSON.parse(trailing))); + eventCount += 1; + } + } finally { + reader.releaseLock(); + } + + return eventCount; +} diff --git a/src/lib/init/transport.ts b/src/lib/init/transport.ts new file mode 100644 index 000000000..cd27e5765 --- /dev/null +++ b/src/lib/init/transport.ts @@ -0,0 +1,248 @@ +/** + * HTTP transport for the init wizard. + * + * Three calls: + * - POST /api/init — start a workflow run. + * - GET /api/init/:runId/stream — open the NDJSON event stream. + * - POST /api/init/actions/:actionId — submit a local-action result. + */ + +import { getTraceData } from "@sentry/node-core/light"; +import { + API_TIMEOUT_MS, + INIT_API_URL, + STREAM_CONNECT_TIMEOUT_MS, +} from "./constants.js"; +import { readNdjsonStream } from "./stream-parser.js"; +import type { + InitActionResumeBody, + InitStartInput, + InitStatusResponse, +} from "./types.js"; + +type InitTransportOptions = { + baseUrl?: string; + fetchImpl?: typeof fetch; + requestTimeoutMs?: number; + streamConnectTimeoutMs?: number; +}; + +export type InitStreamConnection = { + response: Response; + runId?: string; +}; + +type FetchHeaders = Record; +type ErrorPayload = { message: string; retryable: boolean }; +type ResolvedTransportOptions = { + baseUrl: string; + fetchImpl: typeof fetch; + requestTimeoutMs: number; + streamConnectTimeoutMs: number; +}; + +const RUN_ID_HEADERS = [ + "x-workflow-run-id", + "x-vercel-workflow-run-id", + "x-init-run-id", +] as const; + +function buildApiUrl(baseUrl: string, pathname: string): string { + return new URL(pathname, baseUrl).toString(); +} + +function readRunId(response: Response): string | undefined { + for (const headerName of RUN_ID_HEADERS) { + const runId = response.headers.get(headerName)?.trim(); + if (runId) return runId; + } + return undefined; +} + +function createFetchHeaders(contentType = false): FetchHeaders { + const traceData = getTraceData(); + return { + ...(contentType ? { "content-type": "application/json" } : {}), + ...(traceData["sentry-trace"] && { + "sentry-trace": traceData["sentry-trace"], + }), + ...(traceData.baggage && { baggage: traceData.baggage }), + }; +} + +function resolveTransportOptions( + options: InitTransportOptions +): ResolvedTransportOptions { + return { + baseUrl: options.baseUrl ?? INIT_API_URL, + fetchImpl: options.fetchImpl ?? fetch, + requestTimeoutMs: options.requestTimeoutMs ?? API_TIMEOUT_MS, + streamConnectTimeoutMs: + options.streamConnectTimeoutMs ?? STREAM_CONNECT_TIMEOUT_MS, + }; +} + +async function fetchWithTimeout( + fetchImpl: typeof fetch, + input: string | URL | Request, + init: RequestInit, + ms: number, + label: string +): Promise { + const controller = new AbortController(); + const timer = setTimeout(() => controller.abort(label), ms); + try { + return await fetchImpl(input, { ...init, signal: controller.signal }); + } catch (error) { + if ((error as Error).name === "AbortError") { + throw new Error(`${label} timed out after ${ms / 1000}s`); + } + throw error; + } finally { + clearTimeout(timer); + } +} + +async function readErrorPayload(response: Response): Promise { + const contentType = response.headers.get("content-type") ?? ""; + if (contentType.includes("application/json")) { + const payload = (await response.json().catch(() => null)) as + | { error?: unknown; retryable?: unknown } + | null; + if (payload && typeof payload.error === "string") { + return { + message: payload.error, + retryable: payload.retryable === true, + }; + } + } + const text = await response.text(); + return { + message: text || response.statusText, + retryable: false, + }; +} + +async function throwIfNotOk(response: Response, label: string): Promise { + if (response.ok) return; + const error = await readErrorPayload(response); + throw new Error( + `${label} failed (${response.status}): ${error.message}${error.retryable ? " [retryable]" : ""}` + ); +} + +async function requestInitApi(args: { + body?: string; + contentType?: boolean; + label: string; + method: string; + options: InitTransportOptions; + path: string; + timeoutMs: number; +}): Promise { + const resolved = resolveTransportOptions(args.options); + const response = await fetchWithTimeout( + resolved.fetchImpl, + buildApiUrl(resolved.baseUrl, args.path), + { + method: args.method, + headers: createFetchHeaders(args.contentType), + ...(args.body ? { body: args.body } : {}), + }, + args.timeoutMs, + args.label + ); + + await throwIfNotOk(response, args.label); + return response; +} + +export async function startInit( + input: InitStartInput, + options: InitTransportOptions = {} +): Promise<{ runId: string }> { + const resolved = resolveTransportOptions(options); + const response = await requestInitApi({ + body: JSON.stringify(input), + contentType: true, + label: "Init start", + method: "POST", + options, + path: "/api/init", + timeoutMs: resolved.requestTimeoutMs, + }); + const fromHeader = readRunId(response); + if (fromHeader) return { runId: fromHeader }; + const body = (await response.json().catch(() => null)) as + | { runId?: string } + | null; + if (!body?.runId) { + throw new Error("Init start succeeded but did not return a runId"); + } + return { runId: body.runId }; +} + +type OpenStreamOptions = InitTransportOptions & { + /** Replay events starting at this index (used for reconnects). */ + startIndex?: number; +}; + +export async function openInitStream( + runId: string, + options: OpenStreamOptions = {} +): Promise { + const resolved = resolveTransportOptions(options); + const search = + typeof options.startIndex === "number" + ? `?startIndex=${options.startIndex}` + : ""; + return requestInitApi({ + label: "Init stream", + method: "GET", + options, + path: `/api/init/${encodeURIComponent(runId)}/stream${search}`, + timeoutMs: resolved.streamConnectTimeoutMs, + }); +} + +export async function resumeInitAction( + actionId: string, + body: InitActionResumeBody, + options: InitTransportOptions = {} +): Promise { + const resolved = resolveTransportOptions(options); + await requestInitApi({ + body: JSON.stringify(body), + contentType: true, + label: `Resume action ${actionId}`, + method: "POST", + options, + path: `/api/init/actions/${encodeURIComponent(actionId)}`, + timeoutMs: resolved.requestTimeoutMs, + }); +} + +/** + * Fetch the workflow run's terminal/intermediate state. Mirrors + * birthday-card-generator/components/form.tsx:338-351 `fetchRunStatus` + * and is used by `handleStreamClosure` to decide whether to reconnect + * to the NDJSON stream or terminate the wizard. + * + * GET /api/init/:runId -> InitStatusResponse + */ +export async function fetchRunStatus( + runId: string, + options: InitTransportOptions = {} +): Promise { + const resolved = resolveTransportOptions(options); + const response = await requestInitApi({ + label: "Init status", + method: "GET", + options, + path: `/api/init/${encodeURIComponent(runId)}`, + timeoutMs: resolved.requestTimeoutMs, + }); + return (await response.json()) as InitStatusResponse; +} + +export { readNdjsonStream }; diff --git a/src/lib/init/types.ts b/src/lib/init/types.ts index 182b3a58b..d96f5ad43 100644 --- a/src/lib/init/types.ts +++ b/src/lib/init/types.ts @@ -40,7 +40,8 @@ export type ResolvedInitContext = { export type InteractiveContext = Pick; -// Tool suspend payloads +// ── Tool payloads (unchanged — used by `lib/init/tools/registry.ts`). ── + export type ToolPayload = | ListDirPayload | ReadFilesPayload @@ -100,22 +101,7 @@ export type GrepSearch = { pattern: string; path?: string; include?: string; - /** - * Case-insensitive match. Default: false (case-sensitive, matching - * `rg`'s default). A leading `(?i)` inline flag in `pattern` has - * the same effect — callers can use either. - * - * No current Mastra server invocation sets this field; reserving - * it here means the server can start sending it without a CLI - * update. The underlying scan engine natively supports it. - */ caseInsensitive?: boolean; - /** - * Multiline mode: when true (default), `^` and `$` match at line - * boundaries within the file — grep/rg semantics. When false, they - * anchor to the buffer start/end — strict JS `RegExp` semantics. - * Rarely needs to be set. - */ multiline?: boolean; }; @@ -162,7 +148,6 @@ export type ApplyPatchsetPayload = { export type CreateSentryProjectPayload = { type: "tool"; operation: "create-sentry-project"; - /** Human-readable spinner hint from the server (≤ 120 chars, sensitive values redacted). */ detail?: string; cwd: string; params: { @@ -174,7 +159,6 @@ export type CreateSentryProjectPayload = { export type EnsureSentryProjectPayload = { type: "tool"; operation: "ensure-sentry-project"; - /** Human-readable spinner hint from the server (≤ 120 chars, sensitive values redacted). */ detail?: string; cwd: string; params: { @@ -198,7 +182,8 @@ export type ToolResult = { data?: unknown; }; -// Wizard output +// ── Wizard output ────────────────────────────────────────────────── + export type WizardOutput = { platform?: string; projectDir?: string; @@ -212,11 +197,7 @@ export type WizardOutput = { message?: string; }; -// Interactive payloads -export type InteractivePayload = - | SelectPayload - | MultiSelectPayload - | ConfirmPayload; +// ── Interactive payloads (the agent asks the user via a prompt). ── export type SelectPayload = { type: "interactive"; @@ -240,13 +221,148 @@ export type ConfirmPayload = { prompt: string; }; -export type SuspendPayload = ToolPayload | InteractivePayload; +export type InteractivePayload = + | SelectPayload + | MultiSelectPayload + | ConfirmPayload; -export type WorkflowRunResult = { - status: "suspended" | "success" | "failed"; - suspended?: string[][]; - steps?: Record; - suspendPayload?: unknown; - result?: WizardOutput; - error?: string; +// ── /api/init request body ──────────────────────────────────────── + +/** + * Snapshot of the user's project captured locally before the workflow + * starts. Embedded in the agent's user prompt so phase 1 doesn't need + * to round-trip through the bridge for `list_dir` / `read_files`. + */ +export type InitProjectContext = { + /** Recursive listing capped at depth 3 / 500 entries (POSIX paths). */ + dirListing: DirEntry[]; + /** path -> content (null when too big / unreadable). */ + configFiles: Record; + /** Heuristic check for an existing Sentry installation. */ + existingSentry: { + status: "none" | "installed"; + signals: string[]; + dsn?: string; + }; +}; + +export type InitStartInput = { + directory: string; + yes: boolean; + dryRun: boolean; + features?: string[]; + org?: string; + team?: string; + project?: string; + existingProject?: ExistingProjectData; + sentryAuthToken?: string; + cliVersion: string; + projectContext?: InitProjectContext; +}; + +// ── Local-action resume body (CLI -> server). ───────────────────── + +export type InitActionResumeBody = + | { ok: true; output: Record } + | { + ok: false; + error: { + message: string; + code?: string; + details?: unknown; + }; + }; + +// ── Stream events emitted by the workflow (server -> CLI). ───────── + +export type InitStatusEvent = { + type: "status"; + message: string; + phase?: string; +}; + +export type InitActionRequestEvent = { + type: "action_request"; + actionId: string; + kind: "tool" | "prompt"; + name: string; + description?: string; + payload: unknown; +}; + +export type InitActionResultEvent = { + type: "action_result"; + actionId: string; + ok: boolean; + summary?: string; +}; + +export type InitWarningEvent = { + type: "warning"; + message: string; +}; + +export type InitSummaryEvent = { + type: "summary"; + output: WizardOutput; +}; + +export type InitErrorEvent = { + type: "error"; + message: string; + exitCode?: number; + docsUrl?: string; + commands?: string[]; + output?: WizardOutput; +}; + +export type InitDoneEvent = { + type: "done"; + ok: boolean; +}; + +/** + * Server-side stream keepalive. Emitted every ~30s by the workflow's + * NDJSON wrapper to prevent Bun/undici's fetch-body idle timer from + * dropping the connection during long agent steps. The CLI advances + * its `nextStartIndex` cursor for the chunk and otherwise ignores it. + */ +export type InitHeartbeatEvent = { + type: "heartbeat"; +}; + +export type InitEvent = + | InitStatusEvent + | InitActionRequestEvent + | InitActionResultEvent + | InitWarningEvent + | InitSummaryEvent + | InitErrorEvent + | InitDoneEvent + | InitHeartbeatEvent; + +// ── Run status response (CLI <- server, GET /api/init/:runId). ──── + +/** + * Mirror of the server's `initStatusResponseSchema`. Returned by + * `GET /api/init/:runId` and consumed by `fetchRunStatus` to decide + * whether to reconnect to the stream or terminate the wizard. + */ +export type InitStatusResponse = { + runId?: string; + status: + | "queued" + | "running" + | "waiting_for_action" + | "completed" + | "failed" + | "cancelled"; + output?: WizardOutput; + error?: { + message: string; + commands?: string[]; + docsUrl?: string; + exitCode?: number; + output?: WizardOutput; + }; }; diff --git a/src/lib/init/wizard-runner.ts b/src/lib/init/wizard-runner.ts deleted file mode 100644 index caa8d7f85..000000000 --- a/src/lib/init/wizard-runner.ts +++ /dev/null @@ -1,623 +0,0 @@ -/** - * Wizard Runner - * - * Main suspend/resume loop that drives the remote Mastra workflow. - * Each iteration: check status → if suspended, perform tool or - * interactive prompt → resume with result → repeat. - */ - -import { randomBytes } from "node:crypto"; -import { basename } from "node:path"; -import { cancel, confirm, intro, log } from "@clack/prompts"; -import { MastraClient } from "@mastra/client-js"; -import { captureException, getTraceData } from "@sentry/node-core/light"; -import { formatBanner } from "../banner.js"; -import { CLI_VERSION } from "../constants.js"; -import { WizardError } from "../errors.js"; -import { terminalLink } from "../formatters/colors.js"; -import { - colorTag, - renderInlineMarkdown, - safeCodeSpan, - stripColorTags, -} from "../formatters/markdown.js"; -import { - abortIfCancelled, - STEP_LABELS, - WizardCancelledError, -} from "./clack-utils.js"; -import { - API_TIMEOUT_MS, - MASTRA_API_URL, - SENTRY_DOCS_URL, - VERIFY_CHANGES_STEP, - WORKFLOW_ID, -} from "./constants.js"; -import { formatError, formatResult } from "./formatters.js"; -import { checkGitStatus } from "./git.js"; -import { handleInteractive } from "./interactive.js"; -import { resolveInitContext } from "./preflight.js"; -import { createWizardSpinner } from "./spinner.js"; -import { forwardFreshTtyToStdin } from "./stdin-reopen.js"; -import { describeTool, executeTool } from "./tools/registry.js"; -import type { - ResolvedInitContext, - SuspendPayload, - WizardOptions, - WorkflowRunResult, -} from "./types.js"; -import { - precomputeDirListing, - precomputeSentryDetection, - preReadCommonFiles, -} from "./workflow-inputs.js"; - -type Spinner = ReturnType; - -type SpinState = { running: boolean }; - -type StepContext = { - payload: SuspendPayload; - stepId: string; - spin: Spinner; - spinState: SpinState; - context: ResolvedInitContext; -}; - -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"; -} - -/** - * Truncate a spinner message to fit within the terminal width. - * Leaves room for the spinner character and padding. - */ -function truncateForTerminal(message: string): string { - return message.split("\n").map(truncateLineForTerminal).join("\n"); -} - -function truncateLineForTerminal(line: string): string { - const maxWidth = (process.stdout.columns || 80) - 4; - const visibleLine = stripColorTags(line).replace(/`/g, ""); - if (visibleLine.length <= maxWidth) { - return line; - } - let truncated = line.slice(0, maxWidth - 1); - const backtickCount = truncated.split("`").length - 1; - if (backtickCount % 2 !== 0) { - const lastBacktick = truncated.lastIndexOf("`"); - truncated = - truncated.slice(0, lastBacktick) + truncated.slice(lastBacktick + 1); - } - return `${truncated}…`; -} - -type ReadFilesDisplay = { - paths: string[]; - phase: "reading" | "analyzing"; -}; - -function formatReadFilesSummary(progress: ReadFilesDisplay): string { - const { paths, phase } = progress; - if (paths.length === 0) { - return phase === "analyzing" ? "Analyzing files..." : "Reading files..."; - } - - let header: string; - if (phase === "analyzing") { - header = paths.length === 1 ? "Analyzing file..." : "Analyzing files..."; - } else { - header = paths.length === 1 ? "Reading file..." : "Reading files..."; - } - - const icon = readFilesStatusIcon(phase); - const displayPaths = compactDisplayPaths(paths); - const items = displayPaths.map((filePath, index) => { - const branch = index === paths.length - 1 ? "└─" : "├─"; - return `${branch} ${icon} ${safeCodeSpan(filePath)}`; - }); - return `${header}\n${items.join("\n")}`; -} - -function readFilesStatusIcon(phase: ReadFilesDisplay["phase"]): string { - return phase === "analyzing" - ? colorTag("green", "✓") - : colorTag("yellow", "●"); -} - -function compactDisplayPaths(paths: string[]): string[] { - const basenameCounts = new Map(); - for (const filePath of paths) { - const name = basename(filePath); - basenameCounts.set(name, (basenameCounts.get(name) ?? 0) + 1); - } - return paths.map((filePath) => { - const name = basename(filePath); - return basenameCounts.get(name) === 1 ? name : filePath; - }); -} - -/** - * Build a follow-up spinner message after a tool succeeds and the CLI is - * waiting for the server to continue processing the returned data. - */ -function describePostTool(payload: SuspendPayload): string | undefined { - if (payload.type !== "tool") { - return; - } - - switch (payload.operation) { - case "read-files": - return formatReadFilesSummary({ - paths: payload.params.paths, - phase: "analyzing", - }); - case "list-dir": - return "Analyzing directory structure..."; - case "file-exists-batch": - return "Analyzing project files..."; - default: - return; - } -} - -// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: suspend handling needs to branch across tool and interactive payload kinds -async function handleSuspendedStep( - ctx: StepContext, - stepPhases: Map, - stepHistory: Map[]> -): Promise> { - const { payload, stepId, spin, spinState, context } = ctx; - const label = STEP_LABELS[stepId] ?? stepId; - - if (payload.type === "tool") { - const message = - ("detail" in payload && typeof payload.detail === "string" - ? payload.detail - : undefined) ?? - (payload.operation === "read-files" - ? formatReadFilesSummary({ - paths: payload.params.paths, - phase: "reading", - }) - : describeTool(payload)); - spin.message(renderInlineMarkdown(truncateForTerminal(message))); - - const toolResult = await executeTool(payload, context); - - if (toolResult.message) { - spin.stop(renderInlineMarkdown(toolResult.message)); - spin.start("Processing..."); - } else { - const followUpMessage = - toolResult.ok === false ? undefined : describePostTool(payload); - if (followUpMessage) { - spin.message( - renderInlineMarkdown(truncateForTerminal(followUpMessage)) - ); - } - } - - const history = stepHistory.get(stepId) ?? []; - history.push(toolResult); - stepHistory.set(stepId, history); - - return { - ...toolResult, - _phase: nextPhase(stepPhases, stepId, ["read-files", "analyze", "done"]), - _prevPhases: history.slice(0, -1), - }; - } - - if (payload.type === "interactive") { - if (context.dryRun && stepId === VERIFY_CHANGES_STEP) { - return { - action: "continue", - _phase: nextPhase(stepPhases, stepId, ["apply"]), - }; - } - - spin.stop(label); - spinState.running = false; - - const interactiveResult = await handleInteractive(payload, context); - - spin.start("Processing..."); - spinState.running = true; - - return { - ...interactiveResult, - _phase: nextPhase(stepPhases, stepId, ["apply"]), - }; - } - - spin.stop("Error", 1); - spinState.running = false; - log.error( - `Unknown suspend payload type "${(payload as { type: string }).type}"` - ); - cancel("Setup failed"); - throw new WizardCancelledError(); -} - -function errorMessage(err: unknown): string { - return err instanceof Error ? err.message : String(err); -} - -function assertWorkflowResult(raw: unknown): WorkflowRunResult { - if (!raw || typeof raw !== "object") { - throw new Error("Invalid workflow response: expected object"); - } - const obj = raw as Record; - if ( - typeof obj.status !== "string" || - !["suspended", "success", "failed"].includes(obj.status) - ) { - throw new Error(`Unexpected workflow status: ${String(obj.status)}`); - } - return obj as WorkflowRunResult; -} - -function assertSuspendPayload(raw: unknown): SuspendPayload { - if (!raw || typeof raw !== "object") { - throw new Error("Invalid suspend payload: expected object"); - } - const obj = raw as Record; - if ( - typeof obj.type !== "string" || - !["tool", "interactive"].includes(obj.type) - ) { - throw new Error(`Unknown suspend payload type: ${String(obj.type)}`); - } - return obj as SuspendPayload; -} - -function withTimeout( - promise: Promise, - ms: number, - label: string -): Promise { - return new Promise((resolve, reject) => { - const timer = setTimeout( - () => reject(new Error(`${label} timed out after ${ms / 1000}s`)), - ms - ); - promise.then( - (val) => { - clearTimeout(timer); - resolve(val); - }, - (err) => { - clearTimeout(timer); - reject(err); - } - ); - }); -} - -async function confirmExperimental(yes: boolean): Promise { - if (yes) { - return true; - } - const proceed = await confirm({ - message: - "EXPERIMENTAL: This feature is experimental and may modify your code. Continue?", - }); - abortIfCancelled(proceed); - return !!proceed; -} - -async function preamble( - directory: string, - yes: boolean, - dryRun: boolean -): Promise { - if (!(yes || dryRun || process.stdin.isTTY)) { - throw new WizardError( - "Interactive mode requires a terminal. Use --yes for non-interactive mode.", - { rendered: false } - ); - } - - process.stderr.write(`\n${formatBanner()}\n\n`); - intro("sentry init"); - - let confirmed: boolean; - try { - confirmed = await confirmExperimental(yes || dryRun); - } catch (err) { - if (err instanceof WizardCancelledError) { - captureException(err); - process.exitCode = 0; - return false; - } - throw err; - } - if (!confirmed) { - cancel("Setup cancelled."); - process.exitCode = 0; - return false; - } - - if (dryRun) { - log.warn("Dry-run mode: no files will be modified."); - } - - const gitOk = await checkGitStatus({ cwd: directory, yes: yes || dryRun }); - if (!gitOk) { - cancel("Setup cancelled."); - process.exitCode = 0; - return false; - } - - return true; -} - -// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: sequential wizard orchestration with error handling branches -export async function runWizard(initialOptions: WizardOptions): Promise { - // macOS-only: Bun's compiled binaries on Darwin don't deliver keystrokes - // through TTY fds inherited via shell redirection (`curl | bash` → - // `exec sentry init { - // intentionally empty — workaround not installed on this platform - }, - }; - - const { directory, yes, dryRun, features } = initialOptions; - - if (!(await preamble(directory, yes, dryRun))) { - return; - } - - log.info( - "This wizard uses AI to analyze your project and configure Sentry." + - `\nFor manual setup: ${terminalLink(SENTRY_DOCS_URL)}` - ); - - const effectiveOptions = dryRun - ? { ...initialOptions, yes: true } - : initialOptions; - const context = await resolveInitContext(effectiveOptions); - if (!context) { - return; - } - - const tracingOptions = { - traceId: randomBytes(16).toString("hex"), - tags: ["sentry-cli", "init-wizard"], - metadata: { - cliVersion: CLI_VERSION, - os: process.platform, - arch: process.arch, - nodeVersion: process.version, - dryRun, - }, - }; - - const token = context.authToken; - - // AbortController bound to the MastraClient lifecycle. Aborting on - // teardown (success OR failure, via `using` below) cancels any in-flight - // fetches — releasing keep-alive sockets so the event loop drains and - // `sentry init` returns to the shell promptly. Without this, a stuck or - // idle socket in Bun's fetch dispatcher can hold the process alive past - // the wizard's natural exit. - const abortController = new AbortController(); - using _mastraCleanup = { - [Symbol.dispose]: (): void => { - // AbortController.abort() is spec-idempotent, so no guard needed. - abortController.abort(); - }, - }; - - const client = new MastraClient({ - baseUrl: MASTRA_API_URL, - headers: token ? { Authorization: `Bearer ${token}` } : {}, - abortSignal: abortController.signal, - fetch: ((url, init) => { - const traceData = getTraceData(); - // Preserve `init.signal` via the spread — MastraClient may pass its - // own per-request signal, and the client-level `abortSignal` is - // forwarded through the same channel. - return fetch(url, { - ...init, - headers: { - ...(init?.headers as Record | undefined), - ...(traceData["sentry-trace"] && { - "sentry-trace": traceData["sentry-trace"], - }), - ...(traceData.baggage && { baggage: traceData.baggage }), - }, - }); - }) as typeof fetch, - }); - const workflow = client.getWorkflow(WORKFLOW_ID); - - const spin = createWizardSpinner(); - const spinState: SpinState = { running: false }; - - spin.start("Scanning project..."); - spinState.running = true; - - let run: Awaited>; - let result: WorkflowRunResult; - try { - const [dirListing, existingSentry] = await Promise.all([ - precomputeDirListing(directory), - precomputeSentryDetection(directory).catch(() => null), - ]); - const fileCache = await preReadCommonFiles(directory, dirListing); - spin.message("Connecting to wizard..."); - run = await workflow.createRun(); - // Large shared context (dirListing, fileCache, existingSentry) - // travels via Mastra's workflow `initialState` instead of `inputData`. - // Keeping it on state means the server stores it exactly once per run - // rather than duplicating it across every step's output in the D1 - // snapshot — which used to overflow the per-row size limit on big - // projects and surface as a cascading "workflow run was not suspended" - // error. See getsentry/cli-init-api#98. - result = assertWorkflowResult( - await withTimeout( - run.startAsync({ - inputData: { - directory, - yes, - dryRun, - features, - }, - initialState: { - dirListing, - fileCache, - existingSentry: existingSentry?.data, - }, - tracingOptions, - }), - API_TIMEOUT_MS, - "Workflow start" - ) - ); - } catch (err) { - spin.stop("Connection failed", 1); - spinState.running = false; - log.error(errorMessage(err)); - cancel("Setup failed"); - throw new WizardError(errorMessage(err)); - } - - const stepPhases = new Map(); - const stepHistory = new Map[]>(); - - try { - while (result.status === "suspended") { - const stepPath = result.suspended?.at(0) ?? []; - const stepId: string = stepPath.at(-1) ?? "unknown"; - - const extracted = extractSuspendPayload(result, stepId); - if (!extracted) { - spin.stop("Error", 1); - spinState.running = false; - log.error(`No suspend payload found for step "${stepId}"`); - cancel("Setup failed"); - throw new WizardError(`No suspend payload found for step "${stepId}"`); - } - - const resumeData = await handleSuspendedStep( - { - payload: extracted.payload, - stepId: extracted.stepId, - spin, - spinState, - context, - }, - stepPhases, - stepHistory - ); - - result = assertWorkflowResult( - await withTimeout( - run.resumeAsync({ - step: extracted.stepId, - resumeData, - tracingOptions, - }), - API_TIMEOUT_MS, - "Workflow resume" - ) - ); - } - } catch (err) { - // A running spinner owns a live interval, so stop it before any early - // return or rethrow to avoid leaving the event loop artificially busy. - if (spinState.running) { - const [label, code] = - err instanceof WizardCancelledError - ? (["Cancelled", 0] as const) - : (["Error", 1] as const); - spin.stop(label, code); - spinState.running = false; - } - if (err instanceof WizardCancelledError) { - captureException(err); - process.exitCode = 0; - return; - } - if (err instanceof WizardError) { - throw err; - } - log.error(errorMessage(err)); - cancel("Setup failed"); - throw new WizardError(errorMessage(err)); - } - - handleFinalResult(result, spin, spinState); -} - -function handleFinalResult( - result: WorkflowRunResult, - spin: Spinner, - spinState: SpinState -): void { - const hasError = result.status !== "success" || result.result?.exitCode; - - if (hasError) { - if (spinState.running) { - spin.stop("Failed", 1); - spinState.running = false; - } - formatError(result); - throw new WizardError("Workflow returned an error"); - } - - if (spinState.running) { - spin.stop("Done"); - spinState.running = false; - } - formatResult(result); -} - -function extractSuspendPayload( - result: WorkflowRunResult, - stepId: string -): { payload: SuspendPayload; stepId: string } | undefined { - const stepPayload = result.steps?.[stepId]?.suspendPayload; - if (stepPayload) { - return { payload: assertSuspendPayload(stepPayload), stepId }; - } - - if (result.suspendPayload) { - return { payload: assertSuspendPayload(result.suspendPayload), stepId }; - } - - for (const key of Object.keys(result.steps ?? {})) { - const step = result.steps?.[key]; - if (step?.suspendPayload) { - return { - payload: assertSuspendPayload(step.suspendPayload), - stepId: key, - }; - } - } - - return; -} diff --git a/src/lib/init/workflow-inputs.ts b/src/lib/init/workflow-inputs.ts index 4af83515a..479b062ae 100644 --- a/src/lib/init/workflow-inputs.ts +++ b/src/lib/init/workflow-inputs.ts @@ -1,3 +1,13 @@ +/** + * Local preflight that captures a snapshot of the user's project so the + * agent has phase-1 context on its very first turn — no bridge round- + * trip needed for the initial `list_dir` / `read_files` calls. Modeled + * on the main-branch `workflow-inputs.ts`. + * + * The output is sent to the server as `InitStartInput.projectContext` + * and embedded directly in the Claude user prompt. + */ + import fs from "node:fs"; import path from "node:path"; import { MAX_FILE_BYTES } from "./constants.js"; @@ -6,7 +16,9 @@ import { listDir } from "./tools/list-dir.js"; import type { DirEntry } from "./types.js"; /** - * Common config files that multiple init steps frequently inspect. + * Common config files that almost every init run needs to inspect. + * Whitelist over all platforms we support — package.json, tsconfig, + * framework configs, manifests for Python/Ruby/Go/etc. */ const COMMON_CONFIG_FILES = [ "package.json", @@ -81,8 +93,22 @@ const COMMON_CONFIG_FILES = [ const MAX_PREREAD_TOTAL_BYTES = 512 * 1024; +export type ExistingSentryDetection = { + status: "none" | "installed"; + signals: string[]; + dsn?: string; +}; + +export type ProjectContext = { + dirListing: DirEntry[]; + configFiles: Record; + existingSentry: ExistingSentryDetection; +}; + /** - * Pre-compute the initial directory listing before the first workflow call. + * Pre-compute a recursive directory listing capped at depth 3 / 500 + * entries. Skips `node_modules` (and other VCS / build noise that + * `listDir` already filters by default). */ export async function precomputeDirListing( directory: string @@ -93,19 +119,23 @@ export async function precomputeDirListing( cwd: directory, params: { path: ".", recursive: true, maxDepth: 3, maxEntries: 500 }, }); - return (result.data as { entries?: DirEntry[] } | undefined)?.entries ?? []; + if (!result.ok) return []; + const data = result.data as { entries?: DirEntry[] } | undefined; + return data?.entries ?? []; } /** - * Pre-read common config files to avoid repeated suspend/resume round-trips. + * Pre-read the subset of `COMMON_CONFIG_FILES` that exist in the + * project. Capped at 512 KB total (per-file cap from MAX_FILE_BYTES); + * unread-able / over-large files come back as `null` so the agent can + * see they exist but didn't fit in the prompt. */ export async function preReadCommonFiles( directory: string, dirListing: DirEntry[] ): Promise> { - // `listDir` emits POSIX-normalized paths regardless of host OS, - // so `COMMON_CONFIG_FILES` (POSIX) membership checks don't need - // any per-path separator translation. + // `listDir` emits POSIX-normalized paths regardless of host OS, so + // membership against the POSIX whitelist works on Windows too. const listingPaths = new Set(dirListing.map((entry) => entry.path)); const toRead = COMMON_CONFIG_FILES.filter((filePath) => listingPaths.has(filePath) @@ -115,26 +145,27 @@ export async function preReadCommonFiles( let totalBytes = 0; for (const filePath of toRead) { - if (totalBytes >= MAX_PREREAD_TOTAL_BYTES) { - break; - } + if (totalBytes >= MAX_PREREAD_TOTAL_BYTES) break; try { const absPath = path.join(directory, filePath); const stat = await fs.promises.stat(absPath); // Guard against FIFOs / sockets / devices — `fs.readFile` on a - // FIFO blocks indefinitely waiting for a writer. `stat` follows - // symlinks, so a symlink → FIFO is also caught here. + // FIFO blocks indefinitely. `stat` follows symlinks, so a + // symlink to a FIFO is also caught here. if (!stat.isFile()) { cache[filePath] = null; continue; } if (stat.size > MAX_FILE_BYTES) { + cache[filePath] = null; continue; } const content = await fs.promises.readFile(absPath, "utf-8"); if (totalBytes + content.length <= MAX_PREREAD_TOTAL_BYTES) { cache[filePath] = content; totalBytes += content.length; + } else { + cache[filePath] = null; } } catch { cache[filePath] = null; @@ -145,8 +176,39 @@ export async function preReadCommonFiles( } /** - * Pre-compute local Sentry detection so the workflow can start with that context. + * Detect existing Sentry signals (DSN env vars, source-code hits) so + * the agent can decide between fresh-install and instrumentation-tuning. */ -export async function precomputeSentryDetection(directory: string) { - return await detectSentry(directory); +export async function precomputeSentryDetection( + directory: string +): Promise { + const result = await detectSentry(directory); + if (!result.ok) { + return { status: "none", signals: [] }; + } + const data = result.data as + | { status?: "none" | "installed"; signals?: string[]; dsn?: string } + | undefined; + return { + status: data?.status ?? "none", + signals: data?.signals ?? [], + ...(data?.dsn ? { dsn: data.dsn } : {}), + }; +} + +/** + * Run all three preflight steps for a project. Failures are non-fatal: + * we return an empty / partial context rather than aborting the wizard. + */ +export async function precomputeProjectContext( + directory: string +): Promise { + const dirListing = await precomputeDirListing(directory).catch(() => []); + const [configFiles, existingSentry] = await Promise.all([ + preReadCommonFiles(directory, dirListing).catch(() => ({})), + precomputeSentryDetection(directory).catch( + () => ({ status: "none", signals: [] }) as ExistingSentryDetection + ), + ]); + return { dirListing, configFiles, existingSentry }; } diff --git a/test/commands/init.test.ts b/test/commands/init.test.ts index 9b113e0e4..30b589f26 100644 --- a/test/commands/init.test.ts +++ b/test/commands/init.test.ts @@ -1,8 +1,8 @@ /** * Tests for the `sentry init` command entry point. * - * Uses spyOn on the wizard-runner and projects API namespaces to - * capture runWizard calls and mock findProjectsBySlug without + * Uses spyOn on the init-runner and projects API namespaces to + * capture runInit calls and mock findProjectsBySlug without * mock.module (which leaks across test files). */ @@ -13,10 +13,10 @@ import { initCommand } from "../../src/commands/init.js"; import * as projectsApi from "../../src/lib/api/projects.js"; import { ContextError, ValidationError } from "../../src/lib/errors.js"; // biome-ignore lint/performance/noNamespaceImport: spyOn requires object reference +import * as initRunner from "../../src/lib/init/init-runner.js"; +// biome-ignore lint/performance/noNamespaceImport: spyOn requires object reference import * as prefetchNs from "../../src/lib/init/org-prefetch.js"; import { resetPrefetch } from "../../src/lib/init/org-prefetch.js"; -// biome-ignore lint/performance/noNamespaceImport: spyOn requires object reference -import * as wizardRunner from "../../src/lib/init/wizard-runner.js"; /** Minimal org shape for mock returns */ const MOCK_ORG = { id: "1", slug: "resolved-org", name: "Resolved Org" }; @@ -27,7 +27,7 @@ function mockProject(slug: string, orgSlug = "resolved-org") { } let capturedArgs: Record | undefined; -let runWizardSpy: ReturnType; +let runInitSpy: ReturnType; let findProjectsSpy: ReturnType; let warmSpy: ReturnType; @@ -57,7 +57,7 @@ const DEFAULT_FLAGS = { yes: true, "dry-run": false } as const; beforeEach(() => { capturedArgs = undefined; resetPrefetch(); - runWizardSpy = spyOn(wizardRunner, "runWizard").mockImplementation( + runInitSpy = spyOn(initRunner, "runInit").mockImplementation( (args: Record) => { capturedArgs = args; return Promise.resolve(); @@ -79,7 +79,7 @@ beforeEach(() => { }); afterEach(() => { - runWizardSpy.mockRestore(); + runInitSpy.mockRestore(); findProjectsSpy.mockRestore(); warmSpy.mockRestore(); resetPrefetch(); diff --git a/test/init-eval/helpers/run-wizard.ts b/test/init-eval/helpers/run-wizard.ts index 214614556..f0f7944e4 100644 --- a/test/init-eval/helpers/run-wizard.ts +++ b/test/init-eval/helpers/run-wizard.ts @@ -29,9 +29,9 @@ export async function runWizard( 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"); + const apiUrl = process.env.SENTRY_INIT_API_URL; + if (!apiUrl) { + throw new Error("SENTRY_INIT_API_URL env var is required to run init evals"); } // Install dependencies first so the wizard sees a realistic project @@ -68,9 +68,7 @@ export async function runWizard( stderr: "pipe", env: { ...process.env, - // Override the hardcoded Mastra URL to point at local/test server - MASTRA_API_URL: mastraUrl, - // Disable telemetry + SENTRY_INIT_API_URL: apiUrl, SENTRY_CLI_NO_TELEMETRY: "1", }, }); diff --git a/test/lib/init/interactive.test.ts b/test/lib/init/interactive.test.ts index f0ff04851..3964b1014 100644 --- a/test/lib/init/interactive.test.ts +++ b/test/lib/init/interactive.test.ts @@ -287,6 +287,66 @@ describe("handleMultiSelect", () => { expect(values).not.toContain("errorMonitoring"); expect(values).toContain("performanceMonitoring"); }); + + test("renders FEATURE_LABELS for agent-proposed IDs (propose-features flow)", async () => { + multiselectSpy.mockImplementation( + () => Promise.resolve(["tracing", "sessionReplay"]) as any + ); + + await handleInteractive( + { + type: "interactive", + prompt: + "Pick the Sentry features to enable for this project.\n\n" + + "Why these features are relevant:\n" + + "- tracing (recommended): Next.js detected\n" + + "- sessionReplay: browser app", + kind: "multi-select", + availableFeatures: ["tracing", "sessionReplay"], + }, + makeOptions({ yes: false }) + ); + + const call = multiselectSpy.mock.calls[0][0] as { + message: string; + options: Array<{ value: string; label: string; hint?: string }>; + }; + + // Agent's "Why these features are relevant" body is rendered verbatim. + expect(call.message).toContain("Why these features are relevant:"); + expect(call.message).toContain( + "- tracing (recommended): Next.js detected" + ); + + // Labels come from FEATURE_LABELS, not raw IDs. + const tracing = call.options.find((o) => o.value === "tracing"); + const replay = call.options.find((o) => o.value === "sessionReplay"); + expect(tracing?.label).toBe("Performance Monitoring (Tracing)"); + expect(replay?.label).toBe("Session Replay"); + expect(replay?.hint).toContain("browsers only"); + }); + + test("sorts agent-proposed IDs into the canonical display order", async () => { + multiselectSpy.mockImplementation(() => Promise.resolve([]) as any); + + await handleInteractive( + { + type: "interactive", + prompt: "Pick features", + kind: "multi-select", + // Agent sends in arbitrary order + availableFeatures: ["crons", "tracing", "logs", "sourceMaps"], + }, + makeOptions({ yes: false }) + ); + + const call = multiselectSpy.mock.calls[0][0] as { + options: Array<{ value: string }>; + }; + const values = call.options.map((o: { value: string }) => o.value); + // tracing < logs < sourceMaps < crons in SORT_ORDER + expect(values).toEqual(["tracing", "logs", "sourceMaps", "crons"]); + }); }); describe("handleConfirm", () => { diff --git a/test/lib/init/select-features.test.ts b/test/lib/init/select-features.test.ts new file mode 100644 index 000000000..81808aa26 --- /dev/null +++ b/test/lib/init/select-features.test.ts @@ -0,0 +1,76 @@ +import { describe, expect, test } from "bun:test"; +import { + FEATURE_LABELS, + normaliseFromFlag, + sortFeatures, +} from "../../../src/lib/init/select-features.js"; + +describe("normaliseFromFlag", () => { + test("returns empty array for undefined / empty input", () => { + expect(normaliseFromFlag(undefined)).toEqual([]); + expect(normaliseFromFlag([])).toEqual([]); + }); + + test("passes through canonical IDs unchanged", () => { + expect(normaliseFromFlag(["tracing", "sessionReplay"])).toEqual([ + "tracing", + "sessionReplay", + ]); + }); + + test("resolves aliases", () => { + expect(normaliseFromFlag(["replay"])).toEqual(["sessionReplay"]); + expect(normaliseFromFlag(["sourcemaps"])).toEqual(["sourceMaps"]); + expect(normaliseFromFlag(["cron"])).toEqual(["crons"]); + expect(normaliseFromFlag(["ai"])).toEqual(["aiMonitoring"]); + }); + + test("trims whitespace", () => { + expect(normaliseFromFlag([" tracing ", " logs"])).toEqual([ + "tracing", + "logs", + ]); + }); + + test("dedupes when canonical and alias are both present", () => { + expect(normaliseFromFlag(["sessionReplay", "replay"])).toEqual([ + "sessionReplay", + ]); + }); + + test("silently drops unknown values", () => { + expect(normaliseFromFlag(["tracing", "made-up-feature", ""])).toEqual([ + "tracing", + ]); + }); +}); + +describe("sortFeatures", () => { + test("sorts into canonical display order", () => { + expect( + sortFeatures(["crons", "tracing", "sourceMaps", "logs"]) + ).toEqual(["tracing", "logs", "sourceMaps", "crons"]); + }); + + test("stable for unknown ids (pushed to the end)", () => { + expect(sortFeatures(["zzz", "tracing"])).toEqual(["tracing", "zzz"]); + }); +}); + +describe("FEATURE_LABELS", () => { + test("provides label + hint for every selectable id used by the agent", () => { + for (const id of [ + "tracing", + "logs", + "sessionReplay", + "profiling", + "aiMonitoring", + "userFeedback", + "sourceMaps", + "crons", + ]) { + expect(FEATURE_LABELS[id]?.label).toBeTruthy(); + expect(FEATURE_LABELS[id]?.hint).toBeTruthy(); + } + }); +}); diff --git a/test/lib/init/wizard-runner.test.ts b/test/lib/init/wizard-runner.test.ts deleted file mode 100644 index d3f46a09c..000000000 --- a/test/lib/init/wizard-runner.test.ts +++ /dev/null @@ -1,634 +0,0 @@ -import { - afterEach, - beforeEach, - describe, - expect, - mock, - spyOn, - test, -} from "bun:test"; -// biome-ignore lint/performance/noNamespaceImport: spyOn requires object reference -import * as clack from "@clack/prompts"; -import { MastraClient } from "@mastra/client-js"; -// biome-ignore lint/performance/noNamespaceImport: spyOn requires object reference -import * as banner from "../../../src/lib/banner.js"; -import { WizardError } from "../../../src/lib/errors.js"; -import { WizardCancelledError } from "../../../src/lib/init/clack-utils.js"; -// biome-ignore lint/performance/noNamespaceImport: spyOn requires object reference -import * as fmt from "../../../src/lib/init/formatters.js"; -// biome-ignore lint/performance/noNamespaceImport: spyOn requires object reference -import * as git from "../../../src/lib/init/git.js"; -// biome-ignore lint/performance/noNamespaceImport: spyOn requires object reference -import * as inter from "../../../src/lib/init/interactive.js"; -// biome-ignore lint/performance/noNamespaceImport: spyOn requires object reference -import * as preflight from "../../../src/lib/init/preflight.js"; -// biome-ignore lint/performance/noNamespaceImport: spyOn requires object reference -import * as initSpinner from "../../../src/lib/init/spinner.js"; -// biome-ignore lint/performance/noNamespaceImport: spyOn requires object reference -import * as registry from "../../../src/lib/init/tools/registry.js"; -import type { - ResolvedInitContext, - ToolPayload, - WizardOptions, - WorkflowRunResult, -} from "../../../src/lib/init/types.js"; -import { runWizard } from "../../../src/lib/init/wizard-runner.js"; -// biome-ignore lint/performance/noNamespaceImport: spyOn requires object reference -import * as workflowInputs from "../../../src/lib/init/workflow-inputs.js"; - -const noop = () => { - /* suppress output */ -}; - -const spinnerMock = { - start: mock(), - stop: mock(), - message: mock(), -}; - -function makeOptions(overrides?: Partial): WizardOptions { - return { - directory: "/tmp/test", - yes: true, - dryRun: false, - ...overrides, - }; -} - -function makeContext( - overrides?: Partial -): ResolvedInitContext { - return { - directory: "/tmp/test", - yes: true, - dryRun: false, - org: "acme", - team: "platform", - authToken: "test-token", - ...overrides, - }; -} - -let mockStartResult: WorkflowRunResult; -let mockResumeResults: WorkflowRunResult[]; -let resumeCallCount = 0; -let startAsyncMock: ReturnType; - -let introSpy: ReturnType; -let confirmSpy: ReturnType; -let cancelSpy: ReturnType; -let logInfoSpy: ReturnType; -let logWarnSpy: ReturnType; -let logErrorSpy: ReturnType; -let spinnerSpy: ReturnType; - -let formatBannerSpy: ReturnType; -let formatResultSpy: ReturnType; -let formatErrorSpy: ReturnType; -let checkGitStatusSpy: ReturnType; -let handleInteractiveSpy: ReturnType; -let resolveInitContextSpy: ReturnType; -let describeToolSpy: ReturnType; -let executeToolSpy: ReturnType; -let precomputeDirListingSpy: ReturnType; -let preReadCommonFilesSpy: ReturnType; -let precomputeSentryDetectionSpy: ReturnType; -let getWorkflowSpy: ReturnType; -let stderrSpy: ReturnType; -/** - * ClientOptions captured from each MastraClient instance constructed by - * runWizard. Used by the MastraClient lifecycle suite to assert that the - * `abortSignal` passed at construction time is aborted on teardown. - */ -let capturedClientOptions: { abortSignal?: AbortSignal }[] = []; - -beforeEach(() => { - mockStartResult = { status: "success", result: { platform: "React" } }; - mockResumeResults = []; - resumeCallCount = 0; - process.exitCode = 0; - - introSpy = spyOn(clack, "intro").mockImplementation(noop); - confirmSpy = spyOn(clack, "confirm").mockResolvedValue(true); - 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); - spinnerSpy = spyOn(initSpinner, "createWizardSpinner").mockReturnValue( - spinnerMock as any - ); - - spinnerMock.start.mockClear(); - spinnerMock.stop.mockClear(); - spinnerMock.message.mockClear(); - - formatBannerSpy = spyOn(banner, "formatBanner").mockReturnValue("BANNER"); - formatResultSpy = spyOn(fmt, "formatResult").mockImplementation(noop); - formatErrorSpy = spyOn(fmt, "formatError").mockImplementation(noop); - checkGitStatusSpy = spyOn(git, "checkGitStatus").mockResolvedValue(true); - handleInteractiveSpy = spyOn(inter, "handleInteractive").mockResolvedValue({ - action: "continue", - }); - resolveInitContextSpy = spyOn( - preflight, - "resolveInitContext" - ).mockResolvedValue(makeContext()); - describeToolSpy = spyOn(registry, "describeTool").mockReturnValue( - "Running tool..." - ); - executeToolSpy = spyOn(registry, "executeTool").mockResolvedValue({ - ok: true, - data: { results: [] }, - }); - precomputeDirListingSpy = spyOn( - workflowInputs, - "precomputeDirListing" - ).mockResolvedValue([]); - preReadCommonFilesSpy = spyOn( - workflowInputs, - "preReadCommonFiles" - ).mockResolvedValue({}); - precomputeSentryDetectionSpy = spyOn( - workflowInputs, - "precomputeSentryDetection" - ).mockResolvedValue({ - ok: true, - data: { status: "none", signals: [] }, - }); - stderrSpy = spyOn(process.stderr, "write").mockImplementation( - () => true as any - ); - - startAsyncMock = mock(() => Promise.resolve(mockStartResult)); - const run = { - startAsync: startAsyncMock, - resumeAsync: mock(() => { - const result = mockResumeResults[resumeCallCount] ?? { - status: "success", - }; - resumeCallCount += 1; - return Promise.resolve(result); - }), - }; - const workflow = { - createRun: mock(() => Promise.resolve(run)), - }; - capturedClientOptions = []; - getWorkflowSpy = spyOn( - MastraClient.prototype, - "getWorkflow" - ).mockImplementation(function (this: MastraClient) { - // `this` is the MastraClient instance. `BaseResource.options` holds the - // full ClientOptions passed to the constructor — including abortSignal. - capturedClientOptions.push( - (this as unknown as { options: { abortSignal?: AbortSignal } }).options - ); - return workflow as any; - }); -}); - -afterEach(() => { - introSpy.mockRestore(); - confirmSpy.mockRestore(); - cancelSpy.mockRestore(); - logInfoSpy.mockRestore(); - logWarnSpy.mockRestore(); - logErrorSpy.mockRestore(); - spinnerSpy.mockRestore(); - - formatBannerSpy.mockRestore(); - formatResultSpy.mockRestore(); - formatErrorSpy.mockRestore(); - checkGitStatusSpy.mockRestore(); - handleInteractiveSpy.mockRestore(); - resolveInitContextSpy.mockRestore(); - describeToolSpy.mockRestore(); - executeToolSpy.mockRestore(); - precomputeDirListingSpy.mockRestore(); - preReadCommonFilesSpy.mockRestore(); - precomputeSentryDetectionSpy.mockRestore(); - getWorkflowSpy.mockRestore(); - stderrSpy.mockRestore(); - - process.exitCode = 0; -}); - -describe("runWizard", () => { - test("formats successful results", async () => { - await runWizard(makeOptions()); - - expect(formatResultSpy).toHaveBeenCalled(); - expect(formatErrorSpy).not.toHaveBeenCalled(); - expect(spinnerMock.stop).toHaveBeenCalledWith("Done"); - }); - - test("throws when stdin is not a TTY without --yes", async () => { - const originalIsTTY = process.stdin.isTTY; - Object.defineProperty(process.stdin, "isTTY", { - value: false, - configurable: true, - }); - - await expect(runWizard(makeOptions({ yes: false }))).rejects.toThrow( - WizardError - ); - - Object.defineProperty(process.stdin, "isTTY", { - value: originalIsTTY, - configurable: true, - }); - }); - - test("passes dry-run as non-interactive into preflight", async () => { - await runWizard(makeOptions({ dryRun: true, yes: false })); - - expect(resolveInitContextSpy).toHaveBeenCalledWith( - expect.objectContaining({ dryRun: true, yes: true }) - ); - expect(logWarnSpy).toHaveBeenCalled(); - }); - - test("stops before workflow creation when preflight returns null", async () => { - resolveInitContextSpy.mockResolvedValue(null); - - await runWizard(makeOptions()); - - expect(getWorkflowSpy).not.toHaveBeenCalled(); - expect(formatResultSpy).not.toHaveBeenCalled(); - }); - - test("aborts cleanly when git safety check fails", async () => { - checkGitStatusSpy.mockResolvedValue(false); - - await runWizard(makeOptions()); - - expect(cancelSpy).toHaveBeenCalledWith("Setup cancelled."); - expect(getWorkflowSpy).not.toHaveBeenCalled(); - }); - - test("dispatches tool payloads through the registry", async () => { - const payload: ToolPayload = { - type: "tool", - operation: "run-commands", - cwd: "/tmp/test", - params: { commands: ["npm install @sentry/node"] }, - }; - mockStartResult = { - status: "suspended", - suspended: [["install-deps"]], - steps: { - "install-deps": { suspendPayload: payload }, - }, - }; - mockResumeResults = [{ status: "success" }]; - - await runWizard(makeOptions()); - - expect(describeToolSpy).toHaveBeenCalledWith(payload); - expect(executeToolSpy).toHaveBeenCalledWith(payload, makeContext()); - expect(spinnerMock.message).toHaveBeenCalledWith("Running tool..."); - }); - - test("dispatches interactive payloads to the prompt handler", async () => { - mockStartResult = { - status: "suspended", - suspended: [["pick-feature"]], - steps: { - "pick-feature": { - suspendPayload: { - type: "interactive", - kind: "confirm", - prompt: "Continue?", - }, - }, - }, - }; - mockResumeResults = [{ status: "success" }]; - - await runWizard(makeOptions()); - - expect(handleInteractiveSpy).toHaveBeenCalledWith( - { - type: "interactive", - kind: "confirm", - prompt: "Continue?", - }, - makeContext() - ); - }); - - test("skips verify-changes interactive prompts during dry-run", async () => { - resolveInitContextSpy.mockResolvedValue(makeContext({ dryRun: true })); - mockStartResult = { - status: "suspended", - suspended: [["verify-changes"]], - steps: { - "verify-changes": { - suspendPayload: { - type: "interactive", - kind: "confirm", - prompt: "Verify changes?", - }, - }, - }, - }; - mockResumeResults = [{ status: "success" }]; - - await runWizard(makeOptions({ dryRun: true })); - - expect(handleInteractiveSpy).not.toHaveBeenCalled(); - }); - - test("surfaces malformed suspend payload types", async () => { - mockStartResult = { - status: "suspended", - suspended: [["detect-platform"]], - steps: { - "detect-platform": { - suspendPayload: { - type: "unknown", - operation: "list-dir", - cwd: "/tmp/test", - params: { path: "." }, - }, - }, - }, - }; - - await expect(runWizard(makeOptions())).rejects.toThrow(WizardError); - }); - - test("fails when a suspended step has no payload", async () => { - mockStartResult = { - status: "suspended", - suspended: [["detect-platform"]], - steps: { - "detect-platform": {}, - }, - }; - - await expect(runWizard(makeOptions())).rejects.toThrow(WizardError); - }); - - test("tears down forwarding and stops the spinner on tool errors", async () => { - const payload: ToolPayload = { - type: "tool", - operation: "run-commands", - cwd: "/tmp/test", - params: { commands: ["npm install @sentry/node"] }, - }; - mockStartResult = { - status: "suspended", - suspended: [["install-deps"]], - steps: { - "install-deps": { suspendPayload: payload }, - }, - }; - executeToolSpy.mockRejectedValue(new Error("boom")); - - await expect(runWizard(makeOptions())).rejects.toThrow(WizardError); - - expect(spinnerMock.stop).toHaveBeenCalledWith("Error", 1); - expect(cancelSpy).toHaveBeenCalledWith("Setup failed"); - }); - - test("tears down forwarding and stops the spinner on cancellation", async () => { - const payload: ToolPayload = { - type: "tool", - operation: "run-commands", - cwd: "/tmp/test", - params: { commands: ["npm install @sentry/node"] }, - }; - mockStartResult = { - status: "suspended", - suspended: [["install-deps"]], - steps: { - "install-deps": { suspendPayload: payload }, - }, - }; - executeToolSpy.mockRejectedValue(new WizardCancelledError()); - - await runWizard(makeOptions()); - - expect(process.exitCode).toBe(0); - expect(spinnerMock.stop).toHaveBeenCalledWith("Cancelled", 0); - }); - - test("tears down forwarding when a WizardError is rethrown from a tool", async () => { - // The reordered catch block stops the spinner BEFORE the WizardError - // rethrow branch, so any WizardError thrown from handleSuspendedStep - // (e.g. tool handlers, malformed payloads) must still release the TTY - // handle via `using` teardown. - const payload: ToolPayload = { - type: "tool", - operation: "run-commands", - cwd: "/tmp/test", - params: { commands: ["npm install @sentry/node"] }, - }; - mockStartResult = { - status: "suspended", - suspended: [["install-deps"]], - steps: { - "install-deps": { suspendPayload: payload }, - }, - }; - executeToolSpy.mockRejectedValue( - new WizardError("tool rejected by server") - ); - - await expect(runWizard(makeOptions())).rejects.toThrow(WizardError); - - expect(spinnerMock.stop).toHaveBeenCalledWith("Error", 1); - }); - - test("shows a multiline tree while reading files and then analyzing them", async () => { - mockStartResult = { - status: "suspended", - suspended: [["detect-platform"]], - steps: { - "detect-platform": { - suspendPayload: { - type: "tool", - operation: "read-files", - cwd: "/tmp/test", - params: { - paths: ["src/settings.py", "src/urls.py"], - }, - }, - }, - }, - }; - mockResumeResults = [{ status: "success" }]; - - await runWizard(makeOptions()); - - const messages = spinnerMock.message.mock.calls.map((call: string[]) => - call[0]?.replace( - // biome-ignore lint/suspicious/noControlCharactersInRegex: stripping ANSI escape sequences - /\x1b\[[^m]*m/g, - "" - ) - ); - expect(messages).toContain( - "Reading files...\n├─ ● settings.py\n└─ ● urls.py" - ); - expect(messages).toContain( - "Analyzing files...\n├─ ✓ settings.py\n└─ ✓ urls.py" - ); - }); - - test("passes precomputed dirListing/fileCache/existingSentry via initialState, not inputData", async () => { - const dirListing = [ - { name: "package.json", path: "package.json", type: "file" as const }, - ]; - const fileCache = { "package.json": '{"name":"app"}' }; - const detectedSentry = { status: "none" as const, signals: [] }; - - precomputeDirListingSpy.mockResolvedValue(dirListing); - preReadCommonFilesSpy.mockResolvedValue(fileCache); - precomputeSentryDetectionSpy.mockResolvedValue({ - ok: true, - data: detectedSentry, - }); - - await runWizard(makeOptions()); - - expect(startAsyncMock).toHaveBeenCalledTimes(1); - const call = startAsyncMock.mock.calls[0] as - | [ - { - inputData?: Record; - initialState?: Record; - }, - ] - | undefined; - expect(call).toBeDefined(); - const args = call?.[0] ?? {}; - - // Large shared context lives on state, not on inputData. - expect(args.inputData).not.toHaveProperty("dirListing"); - expect(args.inputData).not.toHaveProperty("fileCache"); - expect(args.inputData).not.toHaveProperty("existingSentry"); - expect(args.initialState?.dirListing).toEqual(dirListing); - expect(args.initialState?.fileCache).toEqual(fileCache); - expect(args.initialState?.existingSentry).toEqual(detectedSentry); - }); - - test("renders tool result messages via the spinner stop state", async () => { - mockStartResult = { - status: "suspended", - suspended: [["ensure-sentry-project"]], - steps: { - "ensure-sentry-project": { - suspendPayload: { - type: "tool", - operation: "create-sentry-project", - cwd: "/tmp/test", - params: { name: "my-app", platform: "javascript-react" }, - }, - }, - }, - }; - executeToolSpy.mockResolvedValue({ - ok: true, - message: "Using existing project", - data: {}, - }); - mockResumeResults = [{ status: "success" }]; - - await runWizard(makeOptions()); - - expect(spinnerMock.stop).toHaveBeenCalledWith("Using existing project"); - }); -}); - -describe("runWizard — MastraClient lifecycle", () => { - test("aborts the MastraClient signal after a successful run", async () => { - await runWizard(makeOptions()); - - expect(capturedClientOptions).toHaveLength(1); - const signal = capturedClientOptions[0]?.abortSignal; - expect(signal).toBeInstanceOf(AbortSignal); - // Using the non-null assertion safely — we asserted toBeInstanceOf above. - expect((signal as AbortSignal).aborted).toBe(true); - }); - - test("aborts the MastraClient signal when a tool throws", async () => { - const payload: ToolPayload = { - type: "tool", - operation: "run-commands", - cwd: "/tmp/test", - params: { commands: ["npm install @sentry/node"] }, - }; - mockStartResult = { - status: "suspended", - suspended: [["install-deps"]], - steps: { - "install-deps": { suspendPayload: payload }, - }, - }; - executeToolSpy.mockRejectedValue(new Error("tool blew up")); - - await expect(runWizard(makeOptions())).rejects.toThrow(WizardError); - - expect(capturedClientOptions).toHaveLength(1); - const signal = capturedClientOptions[0]?.abortSignal; - expect(signal).toBeInstanceOf(AbortSignal); - expect((signal as AbortSignal).aborted).toBe(true); - }); - - test("aborts the MastraClient signal on cancellation", async () => { - const payload: ToolPayload = { - type: "tool", - operation: "run-commands", - cwd: "/tmp/test", - params: { commands: ["npm install @sentry/node"] }, - }; - mockStartResult = { - status: "suspended", - suspended: [["install-deps"]], - steps: { - "install-deps": { suspendPayload: payload }, - }, - }; - executeToolSpy.mockRejectedValue(new WizardCancelledError()); - - await runWizard(makeOptions()); - - expect(capturedClientOptions).toHaveLength(1); - const signal = capturedClientOptions[0]?.abortSignal; - expect(signal).toBeInstanceOf(AbortSignal); - expect((signal as AbortSignal).aborted).toBe(true); - }); - - test("signal is live (not pre-aborted) while the wizard is running", async () => { - // `getWorkflow` runs BEFORE `startAsync` (client.getWorkflow is called - // synchronously right after `new MastraClient(...)`), so the signal - // observed at that time is the same instance that in-flight fetches - // would see during the wizard. If the signal were somehow pre-aborted - // at construction, it would be aborted here too. This proves the - // `using _mastraCleanup` disposable does NOT fire until teardown. - let abortedAtConstruction: boolean | undefined; - getWorkflowSpy.mockImplementation(function (this: MastraClient) { - const opts = ( - this as unknown as { options: { abortSignal?: AbortSignal } } - ).options; - capturedClientOptions.push(opts); - abortedAtConstruction = opts.abortSignal?.aborted; - return { - createRun: mock(() => - Promise.resolve({ - startAsync: startAsyncMock, - resumeAsync: mock(() => Promise.resolve({ status: "success" })), - }) - ), - } as any; - }); - - await runWizard(makeOptions()); - - expect(abortedAtConstruction).toBe(false); - // And teardown aborted it by the time the wizard returned. - expect(capturedClientOptions[0]?.abortSignal?.aborted).toBe(true); - }); -}); diff --git a/test/lib/init/workflow-inputs.test.ts b/test/lib/init/workflow-inputs.test.ts new file mode 100644 index 000000000..aeadfa2b3 --- /dev/null +++ b/test/lib/init/workflow-inputs.test.ts @@ -0,0 +1,66 @@ +import { describe, expect, test } from "bun:test"; +import path from "node:path"; +import { + precomputeDirListing, + precomputeProjectContext, + preReadCommonFiles, +} from "../../../src/lib/init/workflow-inputs.js"; + +const CLI_ROOT = path.resolve(__dirname, "../../.."); + +describe("precomputeDirListing", () => { + test("returns POSIX-normalized entries from a real directory", async () => { + const entries = await precomputeDirListing(CLI_ROOT); + expect(Array.isArray(entries)).toBe(true); + expect(entries.length).toBeGreaterThan(0); + for (const entry of entries.slice(0, 50)) { + expect(typeof entry.path).toBe("string"); + expect(entry.path).not.toContain("\\"); + expect(["file", "directory"]).toContain(entry.type); + } + }); + + test("respects the depth-3 / 500-entry caps", async () => { + const entries = await precomputeDirListing(CLI_ROOT); + expect(entries.length).toBeLessThanOrEqual(500); + // `path.split('/').length` yields segment count: depth 3 means up + // to 4 segments (root + 3 nested levels), e.g. a/b/c/d. + const maxSegments = Math.max( + ...entries.map((e) => e.path.split("/").length) + ); + expect(maxSegments).toBeLessThanOrEqual(4); + }); +}); + +describe("preReadCommonFiles", () => { + test("reads package.json when present in the listing", async () => { + const entries = await precomputeDirListing(CLI_ROOT); + const cache = await preReadCommonFiles(CLI_ROOT, entries); + expect(cache["package.json"]).toBeTruthy(); + expect(cache["package.json"]).toContain("\"name\""); + }); + + test("ignores files that aren't on the listing", async () => { + const cache = await preReadCommonFiles(CLI_ROOT, []); + expect(Object.keys(cache).length).toBe(0); + }); +}); + +describe("precomputeProjectContext", () => { + test("returns a populated context for the cli repo", async () => { + const context = await precomputeProjectContext(CLI_ROOT); + expect(context.dirListing.length).toBeGreaterThan(0); + expect(context.configFiles["package.json"]).toBeTruthy(); + expect(["none", "installed"]).toContain(context.existingSentry.status); + expect(Array.isArray(context.existingSentry.signals)).toBe(true); + }); + + test("returns empty defaults for a missing directory", async () => { + const context = await precomputeProjectContext( + "/this/path/definitely/does/not/exist/" + Math.random() + ); + expect(context.dirListing).toEqual([]); + expect(context.configFiles).toEqual({}); + expect(context.existingSentry.status).toBe("none"); + }); +});