From 36b975af7e701d6b0d84287933c6f150665c1f14 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Beteg=C3=B3n?= Date: Fri, 17 Apr 2026 15:56:48 +0200 Subject: [PATCH 1/2] rewrite init cli transport for workflows --- bun.lock | 349 +-------------- package.json | 1 - src/commands/init.ts | 10 +- src/lib/init/constants.ts | 14 +- src/lib/init/formatters.ts | 18 +- src/lib/init/transport.ts | 260 +++++++++++ src/lib/init/types.ts | 98 +++- src/lib/init/wizard-runner.ts | 643 ++++++++++++++------------- test/init-eval/helpers/run-wizard.ts | 10 +- test/lib/init/formatters.test.ts | 76 ++-- test/lib/init/transport.test.ts | 155 +++++++ test/lib/init/wizard-runner.test.ts | 403 +++++++++++------ 12 files changed, 1181 insertions(+), 856 deletions(-) create mode 100644 src/lib/init/transport.ts create mode 100644 test/lib/init/transport.test.ts diff --git a/bun.lock b/bun.lock index 406cc30ea..1bd3e2025 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.94.0", "@sentry/node-core": "10.47.0", "@sentry/sqlish": "^1.0.0", @@ -50,22 +49,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=="], @@ -142,26 +125,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/context-async-hooks": ["@opentelemetry/context-async-hooks@2.5.0", "", { "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-uOXpVX0ZjO7heSVjhheW2XEPrhQAWr2BScDPoZ9UDycl5iuHG+Usyc3AIfG6kZeC1GyLpMInpQ6X5+9n69yOFw=="], @@ -186,98 +153,46 @@ "@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.6", "", { "dependencies": { "bun-types": "1.3.6" } }, "sha512-uWCv6FO/8LcpREhenN1d1b6fcspAB+cefwD7uti8C8VffIv0Um08TKMn98FynpTiU38+y2dUO55T11NgDt8VAA=="], - "@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=="], "@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.6", "", { "dependencies": { "@types/node": "*" } }, "sha512-OlFwHcnNV99r//9v5IIOgQ9Uk37gZqrNMCcqEaExdkVq3Avwqok1bJFmvGMCkCE0FqzdY8VMOZpfpR3lwI+CsQ=="], - "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=="], @@ -298,38 +213,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=="], @@ -342,46 +233,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=="], @@ -396,8 +259,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=="], @@ -408,70 +269,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=="], @@ -486,10 +301,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=="], @@ -498,34 +309,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=="], @@ -534,74 +327,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=="], @@ -612,44 +355,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=="], @@ -660,24 +385,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=="], "bun-types/@types/node": ["@types/node@20.19.30", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-WJtwWJu7UdlvzEAUm484QNg5eAoq5QR08KDNx7g45Usrs2NtOPiX8ugDqmKdXkyL03rBqU5dYNYVQetEpBHq2g=="], @@ -690,80 +399,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/package.json b/package.json index de121753b..952ca3c5d 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.94.0", "@sentry/node-core": "10.47.0", "@sentry/sqlish": "^1.0.0", diff --git a/src/commands/init.ts b/src/commands/init.ts index 7012850df..f9df2058e 100644 --- a/src/commands/init.ts +++ b/src/commands/init.ts @@ -2,8 +2,8 @@ * 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. + * Streams progress from the init API and performs any requested + * local filesystem operations or interactive prompts on the CLI side. * * Supports two optional positionals with smart disambiguation: * sentry init — auto-detect everything, dir = cwd @@ -262,9 +262,9 @@ export const initCommand = buildCommand< await resolveTarget(targetArg); // 5. Start background org detection when org is not yet known. - // The prefetch runs concurrently with the preamble, the wizard startup, - // and all early suspend/resume rounds — by the time the wizard needs the - // org (inside createSentryProject), the result is already cached. + // The prefetch runs concurrently with the preamble, wizard startup, + // and early streamed action round-trips — by the time the wizard needs + // the org (inside createSentryProject), the result is already cached. if (!explicitOrg) { warmOrgDetection(targetDir); } diff --git a/src/lib/init/constants.ts b/src/lib/init/constants.ts index 5f7349cb3..aadab5943 100644 --- a/src/lib/init/constants.ts +++ b/src/lib/init/constants.ts @@ -1,23 +1,19 @@ -export const MASTRA_API_URL = - process.env.MASTRA_API_URL ?? - "https://sentry-init-agent.getsentry.workers.dev"; - -export const WORKFLOW_ID = "sentry-wizard"; +export const INIT_API_URL = + process.env.INIT_API_URL ?? "https://sentry-init-agent.getsentry.workers.dev"; 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 +export const API_TIMEOUT_MS = 120_000; // 2 minutes timeout for init API calls +export const STREAM_CONNECT_TIMEOUT_MS = 30_000; // 30 seconds to establish/re-establish the stream +export const MAX_STREAM_RECONNECTS = 8; // 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"; - // The feature that is always included in every setup export const REQUIRED_FEATURE = "errorMonitoring"; diff --git a/src/lib/init/formatters.ts b/src/lib/init/formatters.ts index a7dda1a4a..4eb440f86 100644 --- a/src/lib/init/formatters.ts +++ b/src/lib/init/formatters.ts @@ -13,7 +13,7 @@ import { 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 +160,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,11 +181,10 @@ 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 { + const output = error.output; + const message = error.message || "Wizard failed with an unknown error"; + const exitCode = error.exitCode ?? output?.exitCode ?? 1; log.error(String(message)); @@ -195,7 +193,7 @@ export function formatError(result: WorkflowRunResult): void { "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; + const commands = error.commands ?? output?.commands; if (commands?.length) { log.warn( `You can install dependencies manually:\n${commands.map((cmd) => ` $ ${cmd}`).join("\n")}` @@ -205,7 +203,7 @@ export function formatError(result: WorkflowRunResult): void { log.warn("Hint: Fix the verification issues and run 'sentry init' again."); } - const docsUrl = inner?.docsUrl; + const docsUrl = error.docsUrl ?? output?.docsUrl; if (docsUrl) { log.info(`Docs: ${terminalLink(docsUrl)}`); } diff --git a/src/lib/init/transport.ts b/src/lib/init/transport.ts new file mode 100644 index 000000000..86ce63a29 --- /dev/null +++ b/src/lib/init/transport.ts @@ -0,0 +1,260 @@ +import { getTraceData } from "@sentry/node-core/light"; +import { + API_TIMEOUT_MS, + INIT_API_URL, + STREAM_CONNECT_TIMEOUT_MS, +} from "./constants.js"; +import type { InitActionResumeBody, InitEvent, InitStartInput } from "./types.js"; + +type InitTransportOptions = { + baseUrl?: string; + fetchImpl?: typeof fetch; + requestTimeoutMs?: number; + streamConnectTimeoutMs?: number; +}; + +export type InitStreamConnection = { + response: Response; + runId?: string; +}; + +type FetchHeaders = Record; + +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 isRecord(value: unknown): value is Record { + return typeof value === "object" && value !== null; +} + +function isWizardOutput(value: unknown): boolean { + return isRecord(value); +} + +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; + default: + throw new Error(`Unknown init event type: ${String(raw.type)}`); + } +} + +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; +} + +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 }), + }; +} + +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 throwIfNotOk(response: Response, label: string): Promise { + if (response.ok) { + return; + } + + const text = await response.text(); + throw new Error( + `${label} failed (${response.status}): ${text || response.statusText}` + ); +} + +export async function startInitStream( + input: InitStartInput, + options: InitTransportOptions = {} +): Promise { + const baseUrl = options.baseUrl ?? INIT_API_URL; + const fetchImpl = options.fetchImpl ?? fetch; + const response = await fetchWithTimeout( + fetchImpl, + buildApiUrl(baseUrl, "/api/init"), + { + method: "POST", + headers: createFetchHeaders(true), + body: JSON.stringify(input), + }, + options.requestTimeoutMs ?? API_TIMEOUT_MS, + "Init start" + ); + + await throwIfNotOk(response, "Init start"); + return { response, runId: readRunId(response) }; +} + +export async function reconnectInitStream( + runId: string, + startIndex: number, + options: InitTransportOptions = {} +): Promise { + const baseUrl = options.baseUrl ?? INIT_API_URL; + const fetchImpl = options.fetchImpl ?? fetch; + const response = await fetchWithTimeout( + fetchImpl, + buildApiUrl( + baseUrl, + `/api/init/${encodeURIComponent(runId)}/stream?startIndex=${startIndex}` + ), + { + method: "GET", + headers: createFetchHeaders(), + }, + options.streamConnectTimeoutMs ?? STREAM_CONNECT_TIMEOUT_MS, + "Init stream connection" + ); + + await throwIfNotOk(response, "Init stream"); + return response; +} + +export async function resumeInitAction( + actionId: string, + body: InitActionResumeBody, + options: InitTransportOptions = {} +): Promise { + const baseUrl = options.baseUrl ?? INIT_API_URL; + const fetchImpl = options.fetchImpl ?? fetch; + const response = await fetchWithTimeout( + fetchImpl, + buildApiUrl(baseUrl, `/api/init/actions/${encodeURIComponent(actionId)}`), + { + method: "POST", + headers: createFetchHeaders(true), + body: JSON.stringify(body), + }, + options.requestTimeoutMs ?? API_TIMEOUT_MS, + `Resume action ${actionId}` + ); + + await throwIfNotOk(response, `Resume action ${actionId}`); +} + +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/types.ts b/src/lib/init/types.ts index 62cfbe43d..5b5aa69dc 100644 --- a/src/lib/init/types.ts +++ b/src/lib/init/types.ts @@ -225,11 +225,95 @@ export type ConfirmPayload = { export type SuspendPayload = ToolPayload | InteractivePayload; -export type WorkflowRunResult = { - status: "suspended" | "success" | "failed"; - suspended?: string[][]; - steps?: Record; - suspendPayload?: unknown; - result?: WizardOutput; - error?: string; +export type InitStartInput = { + directory: string; + yes: boolean; + dryRun: boolean; + features?: string[]; + org?: string; + team?: string; + project?: string; + existingProject?: ExistingProjectData; + existingSentry?: { + status: "installed" | "partial" | "none"; + signals: string[]; + } | null; + cliVersion: string; +}; + +export type InitActionResumeBody = + | { ok: true; output: Record } + | { + ok: false; + error: { + message: string; + code?: string; + details?: unknown; + }; + }; + +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; +}; + +export type InitEvent = + | InitStatusEvent + | InitActionRequestEvent + | InitActionResultEvent + | InitWarningEvent + | InitSummaryEvent + | InitErrorEvent + | InitDoneEvent; + +export type InitStatusResponse = { + status: "queued" | "running" | "waiting_for_action" | "completed" | "failed" | "cancelled"; + output?: WizardOutput; + error?: { + message: string; + exitCode?: number; + docsUrl?: string; + commands?: string[]; + }; }; diff --git a/src/lib/init/wizard-runner.ts b/src/lib/init/wizard-runner.ts index 9f8dc34c7..d03917b40 100644 --- a/src/lib/init/wizard-runner.ts +++ b/src/lib/init/wizard-runner.ts @@ -1,16 +1,17 @@ /** * 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. + * Drives the remote init workflow by: + * 1. Starting a durable run + * 2. Streaming NDJSON progress events + * 3. Executing local tools/prompts when requested + * 4. Resuming the workflow with local results + * 5. Reconnecting to the stream when needed */ -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 { captureException } from "@sentry/node-core/light"; import { formatBanner } from "../banner.js"; import { CLI_VERSION } from "../constants.js"; import { WizardError } from "../errors.js"; @@ -21,48 +22,61 @@ import { 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 { abortIfCancelled, WizardCancelledError } from "./clack-utils.js"; +import { INIT_API_URL, MAX_STREAM_RECONNECTS, SENTRY_DOCS_URL } 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 { + readNdjsonStream, + reconnectInitStream, + resumeInitAction, + startInitStream, +} from "./transport.js"; import { describeTool, executeTool } from "./tools/registry.js"; import type { + InitActionRequestEvent, + InitActionResumeBody, + InitDoneEvent, + InitErrorEvent, + InitEvent, + InitStartInput, + InteractivePayload, ResolvedInitContext, - SuspendPayload, + ToolPayload, + WizardOutput, WizardOptions, - WorkflowRunResult, } from "./types.js"; -import { - precomputeDirListing, - precomputeSentryDetection, - preReadCommonFiles, -} from "./workflow-inputs.js"; +import { precomputeSentryDetection } from "./workflow-inputs.js"; + +const VERIFY_CHANGES_STEP = "verify-changes"; type Spinner = ReturnType; type SpinState = { running: boolean }; type StepContext = { - payload: SuspendPayload; - stepId: string; + event: InitActionRequestEvent; spin: Spinner; spinState: SpinState; context: ResolvedInitContext; }; +type ReadFilesDisplay = { + paths: string[]; + phase: "reading" | "analyzing"; +}; + +type StreamState = { + nextStartIndex: number; + finalOutput?: WizardOutput; + finalError?: InitErrorEvent; + done?: InitDoneEvent; + completedActionIds: Set; +}; + function nextPhase( stepPhases: Map, stepId: string, @@ -73,6 +87,40 @@ function nextPhase( return names[Math.min(phase - 1, names.length - 1)] ?? "done"; } +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; +} + +function nextReconnectDelay(attempt: number): number { + return Math.min(250 * 2 ** attempt, 4_000); +} + +function sleepMs(ms: number): Promise { + return new Promise((resolve) => setTimeout(resolve, ms)); +} + /** * Truncate a spinner message to fit within the terminal width. * Leaves room for the spinner character and padding. @@ -97,33 +145,6 @@ function truncateLineForTerminal(line: string): string { 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", "✓") @@ -142,15 +163,31 @@ function compactDisplayPaths(paths: string[]): string[] { }); } -/** - * 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; +function formatReadFilesSummary(progress: ReadFilesDisplay): string { + const { paths, phase } = progress; + if (paths.length === 0) { + return phase === "analyzing" ? "Analyzing files..." : "Reading files..."; } + const header = + phase === "analyzing" + ? paths.length === 1 + ? "Analyzing file..." + : "Analyzing files..." + : 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 describePostTool(payload: ToolPayload): string | undefined { switch (payload.operation) { case "read-files": return formatReadFilesSummary({ @@ -166,138 +203,181 @@ function describePostTool(payload: SuspendPayload): string | undefined { } } -// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: suspend handling needs to branch across tool and interactive payload kinds -async function handleSuspendedStep( +function toResumeError(error: unknown): InitActionResumeBody { + const message = errorMessage(error); + return { + ok: false, + error: { + message, + details: error, + }, + }; +} + +async function performActionRequest( ctx: StepContext, stepPhases: Map, stepHistory: Map[]> -): Promise> { - const { payload, stepId, spin, spinState, context } = ctx; - const label = STEP_LABELS[stepId] ?? stepId; +): Promise { + const { event, context, spin, spinState } = ctx; + const stepId = event.name; - if (payload.type === "tool") { + if (event.kind === "tool") { + const payload = assertToolPayload(event.payload); const message = - ("detail" in payload && typeof payload.detail === "string" - ? payload.detail - : undefined) ?? + event.description ?? (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.ok === false) { + return { + ok: false, + error: { + message: toolResult.error ?? "Local tool failed", + details: toolResult.data, + }, + }; + } if (toolResult.message) { spin.stop(renderInlineMarkdown(toolResult.message)); spin.start("Processing..."); + spinState.running = true; } else { - const followUpMessage = - toolResult.ok === false ? undefined : describePostTool(payload); - if (followUpMessage) { - spin.message( - renderInlineMarkdown(truncateForTerminal(followUpMessage)) - ); + const followUp = describePostTool(payload); + if (followUp) { + spin.message(renderInlineMarkdown(truncateForTerminal(followUp))); } } const history = stepHistory.get(stepId) ?? []; - history.push(toolResult); + history.push(toolResult as Record); stepHistory.set(stepId, history); return { - ...toolResult, - _phase: nextPhase(stepPhases, stepId, ["read-files", "analyze", "done"]), - _prevPhases: history.slice(0, -1), + ok: true, + output: { + ...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 { + const payload = assertInteractivePayload(event.payload); + if (context.dryRun && event.name === VERIFY_CHANGES_STEP) { + return { + ok: true, + output: { action: "continue", _phase: nextPhase(stepPhases, stepId, ["apply"]), - }; - } + }, + }; + } - spin.stop(label); + if (spinState.running) { + spin.stop(event.description ?? payload.prompt); spinState.running = false; + } - const interactiveResult = await handleInteractive(payload, context); - + try { + const promptResult = await handleInteractive(payload, context); spin.start("Processing..."); spinState.running = true; - return { - ...interactiveResult, - _phase: nextPhase(stepPhases, stepId, ["apply"]), + ok: true, + output: { + ...promptResult, + _phase: nextPhase(stepPhases, stepId, ["apply"]), + }, }; + } catch (error) { + if (!(error instanceof WizardCancelledError)) { + spin.start("Processing..."); + spinState.running = true; + } + throw error; } - - 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; -} +async function handleEvent( + event: InitEvent, + context: ResolvedInitContext, + spin: Spinner, + spinState: SpinState, + state: StreamState, + stepPhases: Map, + stepHistory: Map[]> +): Promise { + state.nextStartIndex += 1; -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; -} + 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 "action_result": + if (event.summary) { + spin.message(renderInlineMarkdown(truncateForTerminal(event.summary))); + } + return; + case "action_request": + if (state.completedActionIds.has(event.actionId)) { + return; + } -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); + try { + const resumeBody = await performActionRequest( + { + event, + context, + spin, + spinState, + }, + stepPhases, + stepHistory + ); + await resumeInitAction(event.actionId, resumeBody, { + baseUrl: INIT_API_URL, + }); + state.completedActionIds.add(event.actionId); + } 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 { @@ -330,14 +410,15 @@ async function preamble( let confirmed: boolean; try { confirmed = await confirmExperimental(yes || dryRun); - } catch (err) { - if (err instanceof WizardCancelledError) { - captureException(err); + } catch (error) { + if (error instanceof WizardCancelledError) { + captureException(error); process.exitCode = 0; return false; } - throw err; + throw error; } + if (!confirmed) { cancel("Setup cancelled."); process.exitCode = 0; @@ -358,7 +439,29 @@ async function preamble( return true; } -// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: sequential wizard orchestration with error handling branches +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, + }; +} + +// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: sequential wizard orchestration with stream reconnect handling export async function runWizard(initialOptions: WizardOptions): Promise { const { directory, yes, dryRun, features } = initialOptions; @@ -379,188 +482,130 @@ export async function runWizard(initialOptions: WizardOptions): Promise { 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; - - const client = new MastraClient({ - baseUrl: MASTRA_API_URL, - headers: token ? { Authorization: `Bearer ${token}` } : {}, - fetch: ((url, init) => { - const traceData = getTraceData(); - 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 }; + const state: StreamState = { + nextStartIndex: 0, + completedActionIds: new Set(), + }; + const stepPhases = new Map(); + const stepHistory = new Map[]>(); spin.start("Scanning project..."); spinState.running = true; - let run: Awaited>; - let result: WorkflowRunResult; + let runId: string | undefined; 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(); - result = assertWorkflowResult( - await withTimeout( - run.startAsync({ - inputData: { - directory, - yes, - dryRun, - features, - dirListing, - fileCache, - existingSentry: existingSentry?.data, - }, - tracingOptions, - }), - API_TIMEOUT_MS, - "Workflow start" - ) + const existingSentry = await precomputeSentryDetection(directory).catch( + () => null ); - } catch (err) { + const startInput: InitStartInput = { + directory, + yes, + dryRun, + features, + org: context.org, + team: context.team, + project: context.project, + existingProject: context.existingProject, + existingSentry: + existingSentry?.ok === true + ? (existingSentry.data as InitStartInput["existingSentry"]) + : null, + cliVersion: CLI_VERSION, + }; + + spin.message("Connecting to wizard..."); + const started = await startInitStream(startInput, { + baseUrl: INIT_API_URL, + }); + runId = started.runId; + } catch (error) { spin.stop("Connection failed", 1); spinState.running = false; - log.error(errorMessage(err)); + log.error(errorMessage(error)); cancel("Setup failed"); - throw new WizardError(errorMessage(err)); + throw new WizardError(errorMessage(error)); } - 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}"`); - } + if (!runId) { + throw new Error("Init start succeeded but no workflow runId was returned."); + } + + let reconnectAttempt = 0; + let currentResponse = await reconnectInitStream(runId, state.nextStartIndex, { + baseUrl: INIT_API_URL, + }); - const resumeData = await handleSuspendedStep( - { - payload: extracted.payload, - stepId: extracted.stepId, + while (!(state.done || state.finalError)) { + const eventCount = await readNdjsonStream(currentResponse, async (event) => { + await handleEvent( + event, + context, spin, spinState, - context, - }, - stepPhases, - stepHistory - ); + state, + stepPhases, + stepHistory + ); + }); - result = assertWorkflowResult( - await withTimeout( - run.resumeAsync({ - step: extracted.stepId, - resumeData, - tracingOptions, - }), - API_TIMEOUT_MS, - "Workflow resume" + if (state.done || state.finalError) { + break; + } + + reconnectAttempt = eventCount === 0 ? reconnectAttempt + 1 : 0; + if (reconnectAttempt > MAX_STREAM_RECONNECTS) { + throw new Error( + `Init stream disconnected too many times (${MAX_STREAM_RECONNECTS})` + ); + } + + spin.message( + renderInlineMarkdown( + truncateForTerminal("Connection interrupted. Reconnecting...") ) ); + await sleepMs(nextReconnectDelay(reconnectAttempt)); + currentResponse = await reconnectInitStream(runId, state.nextStartIndex, { + baseUrl: INIT_API_URL, + }); } - } catch (err) { - if (err instanceof WizardCancelledError) { - captureException(err); + } catch (error) { + if (error instanceof WizardCancelledError) { + captureException(error); process.exitCode = 0; return; } - if (err instanceof WizardError) { - throw err; - } if (spinState.running) { spin.stop("Error", 1); spinState.running = false; } - log.error(errorMessage(err)); + log.error(errorMessage(error)); cancel("Setup failed"); - throw new WizardError(errorMessage(err)); + throw new WizardError(errorMessage(error)); } - handleFinalResult(result, spin, spinState); -} - -function handleFinalResult( - result: WorkflowRunResult, - spin: Spinner, - spinState: SpinState -): void { - const hasError = result.status !== "success" || result.result?.exitCode; - - if (hasError) { + if (state.done?.ok) { if (spinState.running) { - spin.stop("Failed", 1); + spin.stop("Done"); spinState.running = false; } - formatError(result); - throw new WizardError("Workflow returned an error"); + formatResult(state.finalOutput ?? {}); + return; } + const finalError = buildFinalError( + state.finalError, + state.finalOutput, + state.done + ); + if (spinState.running) { - spin.stop("Done"); + spin.stop("Failed", 1); 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; + formatError(finalError); + throw new WizardError(finalError.message); } diff --git a/test/init-eval/helpers/run-wizard.ts b/test/init-eval/helpers/run-wizard.ts index 214614556..7bd8830e0 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 initApiUrl = process.env.INIT_API_URL ?? process.env.MASTRA_API_URL; + if (!initApiUrl) { + throw new Error("INIT_API_URL env var is required to run init evals"); } // Install dependencies first so the wizard sees a realistic project @@ -68,8 +68,8 @@ export async function runWizard( stderr: "pipe", env: { ...process.env, - // Override the hardcoded Mastra URL to point at local/test server - MASTRA_API_URL: mastraUrl, + // Override the default init API URL to point at the test server + INIT_API_URL: initApiUrl, // Disable telemetry SENTRY_CLI_NO_TELEMETRY: "1", }, diff --git a/test/lib/init/formatters.test.ts b/test/lib/init/formatters.test.ts index a306eeaba..1ac3ab1cb 100644 --- a/test/lib/init/formatters.test.ts +++ b/test/lib/init/formatters.test.ts @@ -44,21 +44,18 @@ afterEach(() => { describe("formatResult", () => { test("displays summary with all fields and a nested changed-files tree", () => { formatResult({ - status: "success", - result: { - platform: "Next.js", - projectDir: "/app", - features: ["errorMonitoring", "performanceMonitoring"], - commands: ["npm install @sentry/nextjs"], - sentryProjectUrl: "https://sentry.io/project", - docsUrl: "https://docs.sentry.io", - changedFiles: [ - { action: "modify", path: "next.config.js" }, - { action: "create", path: "src/app/instrumentation-client.ts" }, - { action: "modify", path: "src/app/layout.tsx" }, - { action: "delete", path: "src/old-sentry.js" }, - ], - }, + platform: "Next.js", + projectDir: "/app", + features: ["errorMonitoring", "performanceMonitoring"], + commands: ["npm install @sentry/nextjs"], + sentryProjectUrl: "https://sentry.io/project", + docsUrl: "https://docs.sentry.io", + changedFiles: [ + { action: "modify", path: "next.config.js" }, + { action: "create", path: "src/app/instrumentation-client.ts" }, + { action: "modify", path: "src/app/layout.tsx" }, + { action: "delete", path: "src/old-sentry.js" }, + ], }); expect(logMessageSpy).toHaveBeenCalledTimes(1); @@ -88,7 +85,7 @@ describe("formatResult", () => { }); test("skips summary when result has no summary fields", () => { - formatResult({ status: "success" }); + formatResult({}); expect(logMessageSpy).not.toHaveBeenCalled(); expect(outroSpy).toHaveBeenCalled(); @@ -96,10 +93,7 @@ describe("formatResult", () => { test("displays warnings when present", () => { formatResult({ - status: "success", - result: { - warnings: ["Source maps not configured", "Missing DSN"], - }, + warnings: ["Source maps not configured", "Missing DSN"], }); expect(logWarnSpy).toHaveBeenCalledTimes(2); @@ -107,8 +101,8 @@ describe("formatResult", () => { expect(logWarnSpy.mock.calls[1][0]).toBe("Missing DSN"); }); - test("unwraps nested result property", () => { - formatResult({ status: "success", result: { platform: "React" } }); + test("renders a minimal summary", () => { + formatResult({ platform: "React" }); const content: string = logMessageSpy.mock.calls[0][0]; expect(content).toContain("React"); @@ -117,28 +111,24 @@ describe("formatResult", () => { describe("formatError", () => { test("logs the error message", () => { - formatError({ status: "failed", error: "Connection timed out" }); + formatError({ type: "error", message: "Connection timed out" }); expect(logErrorSpy).toHaveBeenCalledWith("Connection timed out"); expect(cancelSpy).toHaveBeenCalledWith("Setup failed"); }); - test("extracts message from nested result.message", () => { - formatError({ status: "failed", result: { message: "Inner failure" } }); - - expect(logErrorSpy).toHaveBeenCalledWith("Inner failure"); - }); - - test("falls back to unknown error when no message available", () => { - formatError({ status: "failed" }); + test("falls back to unknown error when the message is empty", () => { + formatError({ type: "error", message: "" }); - expect(logErrorSpy).toHaveBeenCalledWith( - "Wizard failed with an unknown error" - ); + expect(logErrorSpy).toHaveBeenCalledWith("Wizard failed with an unknown error"); }); test("shows platform hint for detection failure exit code (20)", () => { - formatError({ status: "failed", result: { exitCode: 20 } }); + formatError({ + type: "error", + message: "Could not detect platform", + exitCode: 20, + }); const warnMsg: string = logWarnSpy.mock.calls[0][0]; expect(warnMsg).toContain("platform"); @@ -146,8 +136,9 @@ describe("formatError", () => { test("shows manual install commands for dependency failure (30)", () => { formatError({ - status: "failed", - result: { + type: "error", + message: "Install failed", + output: { exitCode: 30, commands: ["npm install @sentry/node"], }, @@ -158,7 +149,11 @@ describe("formatError", () => { }); test("shows verification hint for exit code 50", () => { - formatError({ status: "failed", result: { exitCode: 50 } }); + formatError({ + type: "error", + message: "Verification failed", + exitCode: 50, + }); const warnMsg: string = logWarnSpy.mock.calls[0][0]; expect(warnMsg).toContain("verification"); @@ -166,8 +161,9 @@ describe("formatError", () => { test("shows docs URL when present", () => { formatError({ - status: "failed", - result: { docsUrl: "https://docs.sentry.io/platforms/react/" }, + type: "error", + message: "See docs", + output: { docsUrl: "https://docs.sentry.io/platforms/react/" }, }); const infoCalls = logInfoSpy.mock.calls.map((c) => String(c[0])); diff --git a/test/lib/init/transport.test.ts b/test/lib/init/transport.test.ts new file mode 100644 index 000000000..0bc56218d --- /dev/null +++ b/test/lib/init/transport.test.ts @@ -0,0 +1,155 @@ +import { afterEach, beforeEach, describe, expect, test } from "bun:test"; +import { + readNdjsonStream, + reconnectInitStream, + resumeInitAction, + startInitStream, +} from "../../../src/lib/init/transport.js"; +import type { InitEvent, InitStartInput } from "../../../src/lib/init/types.js"; +import { mockFetch } from "../../helpers.js"; + +function streamResponse( + chunks: string[], + headers?: HeadersInit, + status = 200 +): Response { + const encoder = new TextEncoder(); + const stream = new ReadableStream({ + start(controller) { + for (const chunk of chunks) { + controller.enqueue(encoder.encode(chunk)); + } + controller.close(); + }, + }); + + return new Response(stream, { + status, + headers: { + "content-type": "application/x-ndjson", + ...headers, + }, + }); +} + +async function collectEvents(response: Response): Promise { + const events: InitEvent[] = []; + await readNdjsonStream(response, async (event) => { + events.push(event); + }); + return events; +} + +type FetchCall = { + url: string; + init?: RequestInit; +}; + +let originalFetch: typeof globalThis.fetch; +let calls: FetchCall[]; +let responses: Response[]; + +beforeEach(() => { + calls = []; + responses = []; + originalFetch = globalThis.fetch; + globalThis.fetch = mockFetch(async (input, init) => { + calls.push({ url: String(input), init }); + return ( + responses.shift() ?? + new Response("Unexpected fetch", { + status: 500, + }) + ); + }); +}); + +afterEach(() => { + globalThis.fetch = originalFetch; +}); + +describe("init transport", () => { + test("starts with POST /api/init and returns the runId", async () => { + responses = [ + new Response(JSON.stringify({ runId: "run-123" }), { + headers: { + "content-type": "application/json", + "x-workflow-run-id": "run-123", + }, + status: 202, + }), + ]; + + const input: InitStartInput = { + directory: "/tmp/test", + yes: true, + dryRun: false, + org: "acme", + cliVersion: "0.29.0-dev.0", + }; + + const started = await startInitStream(input, { + baseUrl: "https://example.test", + }); + + expect(started.runId).toBe("run-123"); + expect(calls[0]?.url).toBe("https://example.test/api/init"); + expect(calls[0]?.init?.method).toBe("POST"); + expect(JSON.parse(String(calls[0]?.init?.body))).toEqual(input); + }); + + test("reconnects the stream with startIndex", async () => { + responses = [streamResponse(['{"type":"done","ok":true}\n'])]; + + const response = await reconnectInitStream("run-123", 7, { + baseUrl: "https://example.test", + }); + const events = await collectEvents(response); + + expect(events).toEqual([{ type: "done", ok: true }]); + expect(calls[0]?.url).toBe( + "https://example.test/api/init/run-123/stream?startIndex=7" + ); + expect(calls[0]?.init?.method).toBe("GET"); + }); + + test("posts wrapped action results to the action endpoint", async () => { + responses = [new Response(null, { status: 204 })]; + + await resumeInitAction( + "action-1", + { + ok: true, + output: { + action: "continue", + _phase: "apply", + }, + }, + { baseUrl: "https://example.test" } + ); + + expect(calls[0]?.url).toBe("https://example.test/api/init/actions/action-1"); + expect(calls[0]?.init?.method).toBe("POST"); + expect(JSON.parse(String(calls[0]?.init?.body))).toEqual({ + ok: true, + output: { + action: "continue", + _phase: "apply", + }, + }); + }); + + test("parses split NDJSON chunks across line boundaries", async () => { + const events = await collectEvents( + streamResponse([ + '{"type":"summary","output":{"platform":"No', + 'de"}}\n{"type":"done","ok":true}\n', + ]) + ); + + expect(events).toEqual([ + { type: "summary", output: { platform: "Node" } }, + { type: "done", ok: true }, + ]); + }); +}); diff --git a/test/lib/init/wizard-runner.test.ts b/test/lib/init/wizard-runner.test.ts index 9c1d29fc0..c94b1b623 100644 --- a/test/lib/init/wizard-runner.test.ts +++ b/test/lib/init/wizard-runner.test.ts @@ -9,7 +9,6 @@ import { } 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"; @@ -24,12 +23,15 @@ 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 initTransport from "../../../src/lib/init/transport.js"; +// biome-ignore lint/performance/noNamespaceImport: spyOn requires object reference import * as registry from "../../../src/lib/init/tools/registry.js"; import type { + InitActionResumeBody, + InitEvent, 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 @@ -63,15 +65,12 @@ function makeContext( dryRun: false, org: "acme", team: "platform", + project: "my-app", authToken: "test-token", ...overrides, }; } -let mockStartResult: WorkflowRunResult; -let mockResumeResults: WorkflowRunResult[]; -let resumeCallCount = 0; - let introSpy: ReturnType; let confirmSpy: ReturnType; let cancelSpy: ReturnType; @@ -86,19 +85,22 @@ let formatErrorSpy: ReturnType; let checkGitStatusSpy: ReturnType; let handleInteractiveSpy: ReturnType; let resolveInitContextSpy: ReturnType; +let startInitStreamSpy: ReturnType; +let reconnectInitStreamSpy: ReturnType; +let resumeInitActionSpy: ReturnType; +let readNdjsonStreamSpy: ReturnType; let describeToolSpy: ReturnType; let executeToolSpy: ReturnType; -let precomputeDirListingSpy: ReturnType; -let preReadCommonFilesSpy: ReturnType; let precomputeSentryDetectionSpy: ReturnType; -let getWorkflowSpy: ReturnType; let stderrSpy: ReturnType; +let streamBatches: InitEvent[][]; +let resumeBodies: InitActionResumeBody[]; + beforeEach(() => { - mockStartResult = { status: "success", result: { platform: "React" } }; - mockResumeResults = []; - resumeCallCount = 0; process.exitCode = 0; + streamBatches = [[{ type: "summary", output: { platform: "React" } }, { type: "done", ok: true }]]; + resumeBodies = []; introSpy = spyOn(clack, "intro").mockImplementation(noop); confirmSpy = spyOn(clack, "confirm").mockResolvedValue(true); @@ -107,7 +109,7 @@ beforeEach(() => { logWarnSpy = spyOn(clack.log, "warn").mockImplementation(noop); logErrorSpy = spyOn(clack.log, "error").mockImplementation(noop); spinnerSpy = spyOn(initSpinner, "createWizardSpinner").mockReturnValue( - spinnerMock as any + spinnerMock as never ); spinnerMock.start.mockClear(); @@ -132,14 +134,6 @@ beforeEach(() => { ok: true, data: { results: [] }, }); - precomputeDirListingSpy = spyOn( - workflowInputs, - "precomputeDirListing" - ).mockResolvedValue([]); - preReadCommonFilesSpy = spyOn( - workflowInputs, - "preReadCommonFiles" - ).mockResolvedValue({}); precomputeSentryDetectionSpy = spyOn( workflowInputs, "precomputeSentryDetection" @@ -148,25 +142,36 @@ beforeEach(() => { data: { status: "none", signals: [] }, }); stderrSpy = spyOn(process.stderr, "write").mockImplementation( - () => true as any + () => true as never ); - const run = { - startAsync: mock(() => Promise.resolve(mockStartResult)), - resumeAsync: mock(() => { - const result = mockResumeResults[resumeCallCount] ?? { - status: "success", - }; - resumeCallCount += 1; - return Promise.resolve(result); - }), - }; - const workflow = { - createRun: mock(() => Promise.resolve(run)), - }; - getWorkflowSpy = spyOn(MastraClient.prototype, "getWorkflow").mockReturnValue( - workflow as any - ); + startInitStreamSpy = spyOn( + initTransport, + "startInitStream" + ).mockResolvedValue({ + response: {} as Response, + runId: "run-123", + }); + reconnectInitStreamSpy = spyOn( + initTransport, + "reconnectInitStream" + ).mockResolvedValue({} as Response); + resumeInitActionSpy = spyOn( + initTransport, + "resumeInitAction" + ).mockImplementation(async (_actionId, body) => { + resumeBodies.push(body); + }); + readNdjsonStreamSpy = spyOn( + initTransport, + "readNdjsonStream" + ).mockImplementation(async (_response, onEvent) => { + const batch = streamBatches.shift() ?? []; + for (const event of batch) { + await onEvent(event); + } + return batch.length; + }); }); afterEach(() => { @@ -184,43 +189,44 @@ afterEach(() => { checkGitStatusSpy.mockRestore(); handleInteractiveSpy.mockRestore(); resolveInitContextSpy.mockRestore(); + startInitStreamSpy.mockRestore(); + reconnectInitStreamSpy.mockRestore(); + resumeInitActionSpy.mockRestore(); + readNdjsonStreamSpy.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 () => { + test("starts the workflow without authToken, dirListing, or fileCache in the payload", async () => { await runWizard(makeOptions()); - expect(formatResultSpy).toHaveBeenCalled(); + const startArg = startInitStreamSpy.mock.calls[0]?.[0] as Record< + string, + unknown + >; + expect(startArg).toMatchObject({ + directory: "/tmp/test", + yes: true, + dryRun: false, + org: "acme", + team: "platform", + project: "my-app", + existingSentry: { status: "none", signals: [] }, + cliVersion: expect.any(String), + }); + expect(startArg.authToken).toBeUndefined(); + expect(startArg.dirListing).toBeUndefined(); + expect(startArg.fileCache).toBeUndefined(); + expect(formatResultSpy).toHaveBeenCalledWith({ platform: "React" }); 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 })); @@ -230,12 +236,12 @@ describe("runWizard", () => { expect(logWarnSpy).toHaveBeenCalled(); }); - test("stops before workflow creation when preflight returns null", async () => { + test("stops before transport creation when preflight returns null", async () => { resolveInitContextSpy.mockResolvedValue(null); await runWizard(makeOptions()); - expect(getWorkflowSpy).not.toHaveBeenCalled(); + expect(startInitStreamSpy).not.toHaveBeenCalled(); expect(formatResultSpy).not.toHaveBeenCalled(); }); @@ -245,47 +251,69 @@ describe("runWizard", () => { await runWizard(makeOptions()); expect(cancelSpy).toHaveBeenCalledWith("Setup cancelled."); - expect(getWorkflowSpy).not.toHaveBeenCalled(); + expect(startInitStreamSpy).not.toHaveBeenCalled(); }); - test("dispatches tool payloads through the registry", async () => { + test("dispatches tool action requests through the local 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" }]; + streamBatches = [ + [ + { + type: "action_request", + actionId: "tool-1", + kind: "tool", + name: "install-deps", + payload, + }, + ], + [{ type: "done", ok: true }], + ]; await runWizard(makeOptions()); expect(describeToolSpy).toHaveBeenCalledWith(payload); expect(executeToolSpy).toHaveBeenCalledWith(payload, makeContext()); + expect(resumeInitActionSpy).toHaveBeenCalledWith( + "tool-1", + expect.objectContaining({ + ok: true, + output: expect.objectContaining({ + ok: true, + data: { results: [] }, + }), + }), + expect.objectContaining({ baseUrl: expect.any(String) }) + ); + expect(reconnectInitStreamSpy).toHaveBeenCalledWith( + "run-123", + 1, + expect.objectContaining({ baseUrl: expect.any(String) }) + ); 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: { + test("dispatches prompt action requests through interactive handlers", async () => { + streamBatches = [ + [ + { + type: "action_request", + actionId: "prompt-1", + kind: "prompt", + name: "select-features", + payload: { type: "interactive", kind: "confirm", prompt: "Continue?", }, }, - }, - }; - mockResumeResults = [{ status: "success" }]; + ], + [{ type: "done", ok: true }], + ]; await runWizard(makeOptions()); @@ -297,68 +325,177 @@ describe("runWizard", () => { }, makeContext() ); + expect(resumeBodies[0]).toEqual({ + ok: true, + output: { + action: "continue", + _phase: "apply", + }, + }); + }); + + test("deduplicates replayed action requests by actionId", async () => { + const payload: ToolPayload = { + type: "tool", + operation: "list-dir", + cwd: "/tmp/test", + params: { path: ".", recursive: true }, + }; + streamBatches = [ + [ + { + type: "action_request", + actionId: "dup-1", + kind: "tool", + name: "detect-platform", + payload, + }, + ], + [ + { + type: "action_request", + actionId: "dup-1", + kind: "tool", + name: "detect-platform", + payload, + }, + { type: "summary", output: { platform: "node" } }, + { type: "done", ok: true }, + ], + ]; + + await runWizard(makeOptions()); + + expect(executeToolSpy).toHaveBeenCalledTimes(1); + expect(resumeInitActionSpy).toHaveBeenCalledTimes(1); + expect(reconnectInitStreamSpy).toHaveBeenCalledWith( + "run-123", + 1, + expect.objectContaining({ baseUrl: expect.any(String) }) + ); }); - test("skips verify-changes interactive prompts during dry-run", async () => { + test("skips verify-changes prompt actions during dry-run", async () => { resolveInitContextSpy.mockResolvedValue(makeContext({ dryRun: true })); - mockStartResult = { - status: "suspended", - suspended: [["verify-changes"]], - steps: { - "verify-changes": { - suspendPayload: { + streamBatches = [ + [ + { + type: "action_request", + actionId: "verify-1", + kind: "prompt", + name: "verify-changes", + payload: { type: "interactive", kind: "confirm", - prompt: "Verify changes?", + prompt: "Verification found issues. Continue anyway?", }, }, - }, - }; - mockResumeResults = [{ status: "success" }]; + ], + [{ type: "done", ok: true }], + ]; await runWizard(makeOptions({ dryRun: true })); expect(handleInteractiveSpy).not.toHaveBeenCalled(); + expect(resumeBodies[0]).toEqual({ + ok: true, + output: { + action: "continue", + _phase: "apply", + }, + }); }); - 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: "." }, + test("renders final summaries from summary and done events", async () => { + streamBatches = [ + [ + { + type: "summary", + output: { + platform: "Node", + projectDir: "/tmp/test", + warnings: ["Heads up"], }, }, - }, - }; + { type: "done", ok: true }, + ], + ]; - await expect(runWizard(makeOptions())).rejects.toThrow(WizardError); + await runWizard(makeOptions()); + + expect(formatResultSpy).toHaveBeenCalledWith({ + platform: "Node", + projectDir: "/tmp/test", + warnings: ["Heads up"], + }); }); - test("fails when a suspended step has no payload", async () => { - mockStartResult = { - status: "suspended", - suspended: [["detect-platform"]], - steps: { - "detect-platform": {}, + test("renders final errors from summary and error events", async () => { + streamBatches = [ + [ + { + type: "summary", + output: { platform: "Node", commands: ["npm install"] }, + }, + { + type: "error", + message: "Could not determine project platform", + exitCode: 20, + }, + ], + ]; + + await expect(runWizard(makeOptions())).rejects.toThrow(WizardError); + + expect(formatErrorSpy).toHaveBeenCalledWith({ + type: "error", + message: "Could not determine project platform", + exitCode: 20, + output: { + platform: "Node", + commands: ["npm install"], }, - }; + }); + expect(formatResultSpy).not.toHaveBeenCalled(); + }); + + test("surfaces malformed action payload types", async () => { + streamBatches = [ + [ + { + type: "action_request", + actionId: "bad-1", + kind: "tool", + name: "detect-platform", + payload: { + type: "unknown", + operation: "list-dir", + cwd: "/tmp/test", + params: { path: "." }, + }, + }, + ], + [{ type: "error", message: "Bad action payload" }], + ]; await expect(runWizard(makeOptions())).rejects.toThrow(WizardError); + expect(resumeBodies[0]).toEqual({ + ok: false, + error: expect.objectContaining({ + message: "Invalid tool action payload", + }), + }); }); test("shows a multiline tree while reading files and then analyzing them", async () => { - mockStartResult = { - status: "suspended", - suspended: [["detect-platform"]], - steps: { - "detect-platform": { - suspendPayload: { + streamBatches = [ + [ + { + type: "action_request", + actionId: "read-1", + kind: "tool", + name: "detect-platform", + payload: { type: "tool", operation: "read-files", cwd: "/tmp/test", @@ -367,9 +504,9 @@ describe("runWizard", () => { }, }, }, - }, - }; - mockResumeResults = [{ status: "success" }]; + ], + [{ type: "done", ok: true }], + ]; await runWizard(makeOptions()); @@ -389,26 +526,28 @@ describe("runWizard", () => { }); test("renders tool result messages via the spinner stop state", async () => { - mockStartResult = { - status: "suspended", - suspended: [["ensure-sentry-project"]], - steps: { - "ensure-sentry-project": { - suspendPayload: { + streamBatches = [ + [ + { + type: "action_request", + actionId: "ensure-1", + kind: "tool", + name: "ensure-sentry-project", + payload: { type: "tool", operation: "create-sentry-project", cwd: "/tmp/test", params: { name: "my-app", platform: "javascript-react" }, }, }, - }, - }; + ], + [{ type: "done", ok: true }], + ]; executeToolSpy.mockResolvedValue({ ok: true, message: "Using existing project", data: {}, }); - mockResumeResults = [{ status: "success" }]; await runWizard(makeOptions()); From 7d15af49fde1450af0174f3f7c7c605e451fca27 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Beteg=C3=B3n?= Date: Wed, 22 Apr 2026 17:24:35 +0200 Subject: [PATCH 2/2] clean up workflow init transport and runner --- src/lib/init/stream-parser.ts | 112 ++++++ src/lib/init/transport.ts | 254 ++++++-------- src/lib/init/wizard-runner.ts | 218 ++++++++---- test/lib/init/transport.test.ts | 238 ++++++------- test/lib/init/wizard-runner.test.ts | 511 +++++++++------------------- 5 files changed, 654 insertions(+), 679 deletions(-) create mode 100644 src/lib/init/stream-parser.ts diff --git a/src/lib/init/stream-parser.ts b/src/lib/init/stream-parser.ts new file mode 100644 index 000000000..8f55a0a0b --- /dev/null +++ b/src/lib/init/stream-parser.ts @@ -0,0 +1,112 @@ +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; + 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. + */ +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 index 86ce63a29..4c0ca248e 100644 --- a/src/lib/init/transport.ts +++ b/src/lib/init/transport.ts @@ -5,6 +5,7 @@ import { STREAM_CONNECT_TIMEOUT_MS, } from "./constants.js"; import type { InitActionResumeBody, InitEvent, InitStartInput } from "./types.js"; +import { readNdjsonStream } from "./stream-parser.js"; type InitTransportOptions = { baseUrl?: string; @@ -19,6 +20,16 @@ export type InitStreamConnection = { }; 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", @@ -30,64 +41,6 @@ function buildApiUrl(baseUrl: string, pathname: string): string { return new URL(pathname, baseUrl).toString(); } -function isRecord(value: unknown): value is Record { - return typeof value === "object" && value !== null; -} - -function isWizardOutput(value: unknown): boolean { - return isRecord(value); -} - -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; - default: - throw new Error(`Unknown init event type: ${String(raw.type)}`); - } -} - function readRunId(response: Response): string | undefined { for (const headerName of RUN_ID_HEADERS) { const runId = response.headers.get(headerName)?.trim(); @@ -110,6 +63,18 @@ function createFetchHeaders(contentType = false): FetchHeaders { }; } +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, @@ -132,36 +97,79 @@ async function fetchWithTimeout( } } +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 text = await response.text(); + const error = await readErrorPayload(response); throw new Error( - `${label} failed (${response.status}): ${text || response.statusText}` + `${label} failed (${response.status}): ${error.message}${error.retryable ? " [retryable]" : ""}` ); } -export async function startInitStream( - input: InitStartInput, - options: InitTransportOptions = {} -): Promise { - const baseUrl = options.baseUrl ?? INIT_API_URL; - const fetchImpl = options.fetchImpl ?? fetch; +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( - fetchImpl, - buildApiUrl(baseUrl, "/api/init"), + resolved.fetchImpl, + buildApiUrl(resolved.baseUrl, args.path), { - method: "POST", - headers: createFetchHeaders(true), - body: JSON.stringify(input), + method: args.method, + headers: createFetchHeaders(args.contentType), + ...(args.body ? { body: args.body } : {}), }, - options.requestTimeoutMs ?? API_TIMEOUT_MS, - "Init start" + args.timeoutMs, + args.label ); - await throwIfNotOk(response, "Init start"); + await throwIfNotOk(response, args.label); + return response; +} + +export async function startInitStream( + input: InitStartInput, + options: InitTransportOptions = {} +): Promise { + 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, + }); return { response, runId: readRunId(response) }; } @@ -170,24 +178,14 @@ export async function reconnectInitStream( startIndex: number, options: InitTransportOptions = {} ): Promise { - const baseUrl = options.baseUrl ?? INIT_API_URL; - const fetchImpl = options.fetchImpl ?? fetch; - const response = await fetchWithTimeout( - fetchImpl, - buildApiUrl( - baseUrl, - `/api/init/${encodeURIComponent(runId)}/stream?startIndex=${startIndex}` - ), - { - method: "GET", - headers: createFetchHeaders(), - }, - options.streamConnectTimeoutMs ?? STREAM_CONNECT_TIMEOUT_MS, - "Init stream connection" - ); - - await throwIfNotOk(response, "Init stream"); - return response; + const resolved = resolveTransportOptions(options); + return requestInitApi({ + label: "Init stream", + method: "GET", + options, + path: `/api/init/${encodeURIComponent(runId)}/stream?startIndex=${startIndex}`, + timeoutMs: resolved.streamConnectTimeoutMs, + }); } export async function resumeInitAction( @@ -195,66 +193,16 @@ export async function resumeInitAction( body: InitActionResumeBody, options: InitTransportOptions = {} ): Promise { - const baseUrl = options.baseUrl ?? INIT_API_URL; - const fetchImpl = options.fetchImpl ?? fetch; - const response = await fetchWithTimeout( - fetchImpl, - buildApiUrl(baseUrl, `/api/init/actions/${encodeURIComponent(actionId)}`), - { - method: "POST", - headers: createFetchHeaders(true), - body: JSON.stringify(body), - }, - options.requestTimeoutMs ?? API_TIMEOUT_MS, - `Resume action ${actionId}` - ); - - await throwIfNotOk(response, `Resume action ${actionId}`); + 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, + }); } -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; -} +export { readNdjsonStream }; diff --git a/src/lib/init/wizard-runner.ts b/src/lib/init/wizard-runner.ts index d03917b40..6907c6a12 100644 --- a/src/lib/init/wizard-runner.ts +++ b/src/lib/init/wizard-runner.ts @@ -214,6 +214,70 @@ function toResumeError(error: unknown): InitActionResumeBody { }; } +function stopSpinner( + spin: Spinner, + spinState: SpinState, + message: string, + code?: number +): void { + if (!spinState.running) { + return; + } + + spin.stop(message, code); + spinState.running = false; +} + +function failWizard(args: { + message: string; + spin: Spinner; + spinState: SpinState; + stopCode?: number; + stopMessage: string; +}): never { + stopSpinner(args.spin, args.spinState, args.stopMessage, args.stopCode); + log.error(args.message); + cancel("Setup failed"); + throw new WizardError(args.message); +} + +async function resumeActionRequest( + event: InitActionRequestEvent, + body: InitActionResumeBody +): Promise { + try { + await resumeInitAction(event.actionId, body, { + baseUrl: INIT_API_URL, + }); + } catch (error) { + throw new Error( + `Failed to resume ${event.kind} action ${safeCodeSpan(event.name)}: ${errorMessage(error)}` + ); + } +} + +function describeStreamReadError(error: unknown): string { + const message = errorMessage(error); + if ( + message.includes("Invalid ") || + message.includes("Unknown init event type") || + message.includes("JSON") + ) { + return `Malformed init stream event: ${message}`; + } + + return `Init stream read failed: ${message}`; +} + +async function connectInitStream( + runId: string, + startIndex: number +): Promise { + return reconnectInitStream(runId, startIndex, { + baseUrl: INIT_API_URL, + }); +} + async function performActionRequest( ctx: StepContext, stepPhases: Map, @@ -359,17 +423,13 @@ async function handleEvent( stepPhases, stepHistory ); - await resumeInitAction(event.actionId, resumeBody, { - baseUrl: INIT_API_URL, - }); + await resumeActionRequest(event, resumeBody); state.completedActionIds.add(event.actionId); } catch (error) { if (error instanceof WizardCancelledError) { throw error; } - await resumeInitAction(event.actionId, toResumeError(error), { - baseUrl: INIT_API_URL, - }); + await resumeActionRequest(event, toResumeError(error)); state.completedActionIds.add(event.actionId); } return; @@ -380,6 +440,70 @@ async function handleEvent( } } +async function consumeStreamUntilTerminal(args: { + context: ResolvedInitContext; + runId: string; + spin: Spinner; + spinState: SpinState; + state: StreamState; + stepHistory: Map[]>; + stepPhases: Map; +}): Promise { + let reconnectAttempt = 0; + let currentResponse = await connectInitStream( + args.runId, + args.state.nextStartIndex + ); + + while (!(args.state.done || args.state.finalError)) { + let eventCount: number; + try { + eventCount = await readNdjsonStream(currentResponse, async (event) => { + await handleEvent( + event, + args.context, + args.spin, + args.spinState, + args.state, + args.stepPhases, + args.stepHistory + ); + }); + } catch (error) { + throw new Error(describeStreamReadError(error)); + } + + if (args.state.done || args.state.finalError) { + return; + } + + reconnectAttempt = eventCount === 0 ? reconnectAttempt + 1 : 0; + if (reconnectAttempt > MAX_STREAM_RECONNECTS) { + throw new Error( + `Init stream disconnected too many times without new events (${MAX_STREAM_RECONNECTS})` + ); + } + + args.spin.message( + renderInlineMarkdown( + truncateForTerminal("Connection interrupted. Reconnecting...") + ) + ); + await sleepMs(nextReconnectDelay(reconnectAttempt)); + + try { + currentResponse = await connectInitStream( + args.runId, + args.state.nextStartIndex + ); + } catch (error) { + throw new Error( + `Failed to reconnect to the init stream: ${errorMessage(error)}` + ); + } + } +} + async function confirmExperimental(yes: boolean): Promise { if (yes) { return true; @@ -521,11 +645,13 @@ export async function runWizard(initialOptions: WizardOptions): Promise { }); runId = started.runId; } catch (error) { - spin.stop("Connection failed", 1); - spinState.running = false; - log.error(errorMessage(error)); - cancel("Setup failed"); - throw new WizardError(errorMessage(error)); + failWizard({ + message: errorMessage(error), + spin, + spinState, + stopCode: 1, + stopMessage: "Connection failed", + }); } try { @@ -533,65 +659,32 @@ export async function runWizard(initialOptions: WizardOptions): Promise { throw new Error("Init start succeeded but no workflow runId was returned."); } - let reconnectAttempt = 0; - let currentResponse = await reconnectInitStream(runId, state.nextStartIndex, { - baseUrl: INIT_API_URL, + await consumeStreamUntilTerminal({ + context, + runId, + spin, + spinState, + state, + stepHistory, + stepPhases, }); - - while (!(state.done || state.finalError)) { - const eventCount = await readNdjsonStream(currentResponse, async (event) => { - await handleEvent( - event, - context, - spin, - spinState, - state, - stepPhases, - stepHistory - ); - }); - - if (state.done || state.finalError) { - break; - } - - reconnectAttempt = eventCount === 0 ? reconnectAttempt + 1 : 0; - if (reconnectAttempt > MAX_STREAM_RECONNECTS) { - throw new Error( - `Init stream disconnected too many times (${MAX_STREAM_RECONNECTS})` - ); - } - - spin.message( - renderInlineMarkdown( - truncateForTerminal("Connection interrupted. Reconnecting...") - ) - ); - await sleepMs(nextReconnectDelay(reconnectAttempt)); - currentResponse = await reconnectInitStream(runId, state.nextStartIndex, { - baseUrl: INIT_API_URL, - }); - } } catch (error) { if (error instanceof WizardCancelledError) { captureException(error); process.exitCode = 0; return; } - if (spinState.running) { - spin.stop("Error", 1); - spinState.running = false; - } - log.error(errorMessage(error)); - cancel("Setup failed"); - throw new WizardError(errorMessage(error)); + failWizard({ + message: errorMessage(error), + spin, + spinState, + stopCode: 1, + stopMessage: "Error", + }); } if (state.done?.ok) { - if (spinState.running) { - spin.stop("Done"); - spinState.running = false; - } + stopSpinner(spin, spinState, "Done"); formatResult(state.finalOutput ?? {}); return; } @@ -602,10 +695,7 @@ export async function runWizard(initialOptions: WizardOptions): Promise { state.done ); - if (spinState.running) { - spin.stop("Failed", 1); - spinState.running = false; - } + stopSpinner(spin, spinState, "Failed", 1); formatError(finalError); throw new WizardError(finalError.message); } diff --git a/test/lib/init/transport.test.ts b/test/lib/init/transport.test.ts index 0bc56218d..5dcb460a4 100644 --- a/test/lib/init/transport.test.ts +++ b/test/lib/init/transport.test.ts @@ -1,155 +1,165 @@ -import { afterEach, beforeEach, describe, expect, test } from "bun:test"; +import { describe, expect, mock, test } from "bun:test"; import { readNdjsonStream, reconnectInitStream, resumeInitAction, startInitStream, } from "../../../src/lib/init/transport.js"; -import type { InitEvent, InitStartInput } from "../../../src/lib/init/types.js"; -import { mockFetch } from "../../helpers.js"; -function streamResponse( - chunks: string[], - headers?: HeadersInit, - status = 200 +function createJsonResponse( + body: unknown, + init: ResponseInit = {} ): Response { - const encoder = new TextEncoder(); - const stream = new ReadableStream({ - start(controller) { - for (const chunk of chunks) { - controller.enqueue(encoder.encode(chunk)); - } - controller.close(); - }, - }); - - return new Response(stream, { - status, + return new Response(JSON.stringify(body), { headers: { - "content-type": "application/x-ndjson", - ...headers, + "content-type": "application/json", + ...(init.headers ?? {}), }, + status: init.status ?? 200, }); } -async function collectEvents(response: Response): Promise { - const events: InitEvent[] = []; - await readNdjsonStream(response, async (event) => { - events.push(event); +describe("init transport", () => { + test("starts a workflow run and reads the run id header", async () => { + const fetchImpl = mock(async () => + createJsonResponse( + { runId: "run_123" }, + { + headers: { + "content-type": "application/json", + "x-workflow-run-id": "run_123", + }, + status: 202, + } + ) + ) as typeof fetch; + + const started = await startInitStream( + { + cliVersion: "0.29.0-dev.0", + directory: "/tmp/project", + dryRun: false, + yes: true, + }, + { + baseUrl: "http://localhost:3000", + fetchImpl, + } + ); + + expect(started.runId).toBe("run_123"); + expect(fetchImpl).toHaveBeenCalledTimes(1); }); - return events; -} -type FetchCall = { - url: string; - init?: RequestInit; -}; - -let originalFetch: typeof globalThis.fetch; -let calls: FetchCall[]; -let responses: Response[]; - -beforeEach(() => { - calls = []; - responses = []; - originalFetch = globalThis.fetch; - globalThis.fetch = mockFetch(async (input, init) => { - calls.push({ url: String(input), init }); - return ( - responses.shift() ?? - new Response("Unexpected fetch", { - status: 500, - }) + test("surfaces retryable start failures from the backend", async () => { + const fetchImpl = mock(async () => + createJsonResponse( + { + error: "Runner did not become ready in time", + retryable: true, + }, + { status: 503 } + ) + ) as typeof fetch; + + await expect( + startInitStream( + { + cliVersion: "0.29.0-dev.0", + directory: "/tmp/project", + dryRun: false, + yes: true, + }, + { + baseUrl: "http://localhost:3000", + fetchImpl, + } + ) + ).rejects.toThrow( + "Init start failed (503): Runner did not become ready in time [retryable]" ); }); -}); - -afterEach(() => { - globalThis.fetch = originalFetch; -}); -describe("init transport", () => { - test("starts with POST /api/init and returns the runId", async () => { - responses = [ - new Response(JSON.stringify({ runId: "run-123" }), { + test("reconnects the NDJSON stream from a start index", async () => { + const fetchImpl = mock(async () => + new Response("", { headers: { - "content-type": "application/json", - "x-workflow-run-id": "run-123", + "content-type": "application/x-ndjson; charset=utf-8", }, - status: 202, - }), - ]; - - const input: InitStartInput = { - directory: "/tmp/test", - yes: true, - dryRun: false, - org: "acme", - cliVersion: "0.29.0-dev.0", - }; - - const started = await startInitStream(input, { - baseUrl: "https://example.test", - }); - - expect(started.runId).toBe("run-123"); - expect(calls[0]?.url).toBe("https://example.test/api/init"); - expect(calls[0]?.init?.method).toBe("POST"); - expect(JSON.parse(String(calls[0]?.init?.body))).toEqual(input); - }); - - test("reconnects the stream with startIndex", async () => { - responses = [streamResponse(['{"type":"done","ok":true}\n'])]; + }) + ) as typeof fetch; - const response = await reconnectInitStream("run-123", 7, { - baseUrl: "https://example.test", + await reconnectInitStream("run_123", 7, { + baseUrl: "http://localhost:3000", + fetchImpl, }); - const events = await collectEvents(response); - expect(events).toEqual([{ type: "done", ok: true }]); - expect(calls[0]?.url).toBe( - "https://example.test/api/init/run-123/stream?startIndex=7" + expect(fetchImpl).toHaveBeenCalledWith( + "http://localhost:3000/api/init/run_123/stream?startIndex=7", + expect.objectContaining({ + method: "GET", + }) ); - expect(calls[0]?.init?.method).toBe("GET"); }); - test("posts wrapped action results to the action endpoint", async () => { - responses = [new Response(null, { status: 204 })]; + test("posts action resume payloads", async () => { + const fetchImpl = mock(async () => createJsonResponse({ ok: true })) as typeof fetch; await resumeInitAction( - "action-1", + "run_123:action:001:read-files", { ok: true, output: { - action: "continue", - _phase: "apply", + files: {}, }, }, - { baseUrl: "https://example.test" } + { + baseUrl: "http://localhost:3000", + fetchImpl, + } ); - expect(calls[0]?.url).toBe("https://example.test/api/init/actions/action-1"); - expect(calls[0]?.init?.method).toBe("POST"); - expect(JSON.parse(String(calls[0]?.init?.body))).toEqual({ - ok: true, - output: { - action: "continue", - _phase: "apply", - }, - }); + expect(fetchImpl).toHaveBeenCalledTimes(1); }); - test("parses split NDJSON chunks across line boundaries", async () => { - const events = await collectEvents( - streamResponse([ - '{"type":"summary","output":{"platform":"No', - 'de"}}\n{"type":"done","ok":true}\n', - ]) + test("reads NDJSON events and validates them", async () => { + const response = new Response( + [ + JSON.stringify({ + message: "Inspecting project…", + phase: "bootstrap", + type: "status", + }), + JSON.stringify({ + ok: true, + type: "done", + }), + ].join("\n"), + { + headers: { + "content-type": "application/x-ndjson; charset=utf-8", + }, + } ); - expect(events).toEqual([ - { type: "summary", output: { platform: "Node" } }, - { type: "done", ok: true }, - ]); + const seen: string[] = []; + const count = await readNdjsonStream(response, async (event) => { + seen.push(event.type); + }); + + expect(count).toBe(2); + expect(seen).toEqual(["status", "done"]); + }); + + test("fails on malformed NDJSON events", async () => { + const response = new Response('{"type":"status"}\n', { + headers: { + "content-type": "application/x-ndjson; charset=utf-8", + }, + }); + + await expect( + readNdjsonStream(response, async () => {}) + ).rejects.toThrow("Invalid status event"); }); }); diff --git a/test/lib/init/wizard-runner.test.ts b/test/lib/init/wizard-runner.test.ts index c94b1b623..ec80df5db 100644 --- a/test/lib/init/wizard-runner.test.ts +++ b/test/lib/init/wizard-runner.test.ts @@ -7,6 +7,7 @@ import { spyOn, test, } from "bun:test"; +import { cancel, log } from "@clack/prompts"; // biome-ignore lint/performance/noNamespaceImport: spyOn requires object reference import * as clack from "@clack/prompts"; // biome-ignore lint/performance/noNamespaceImport: spyOn requires object reference @@ -23,16 +24,15 @@ 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 initTransport from "../../../src/lib/init/transport.js"; -// biome-ignore lint/performance/noNamespaceImport: spyOn requires object reference import * as registry from "../../../src/lib/init/tools/registry.js"; import type { - InitActionResumeBody, - InitEvent, + InitActionRequestEvent, ResolvedInitContext, ToolPayload, WizardOptions, } from "../../../src/lib/init/types.js"; +// biome-ignore lint/performance/noNamespaceImport: spyOn requires object reference +import * as transport from "../../../src/lib/init/transport.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"; @@ -65,12 +65,19 @@ function makeContext( dryRun: false, org: "acme", team: "platform", - project: "my-app", authToken: "test-token", ...overrides, }; } +function makeResponse(): Response { + return new Response("", { + headers: { + "content-type": "application/x-ndjson; charset=utf-8", + }, + }); +} + let introSpy: ReturnType; let confirmSpy: ReturnType; let cancelSpy: ReturnType; @@ -85,31 +92,27 @@ let formatErrorSpy: ReturnType; let checkGitStatusSpy: ReturnType; let handleInteractiveSpy: ReturnType; let resolveInitContextSpy: ReturnType; -let startInitStreamSpy: ReturnType; -let reconnectInitStreamSpy: ReturnType; -let resumeInitActionSpy: ReturnType; -let readNdjsonStreamSpy: ReturnType; let describeToolSpy: ReturnType; let executeToolSpy: ReturnType; let precomputeSentryDetectionSpy: ReturnType; let stderrSpy: ReturnType; -let streamBatches: InitEvent[][]; -let resumeBodies: InitActionResumeBody[]; +let startInitStreamSpy: ReturnType; +let reconnectInitStreamSpy: ReturnType; +let resumeInitActionSpy: ReturnType; +let readNdjsonStreamSpy: ReturnType; beforeEach(() => { process.exitCode = 0; - streamBatches = [[{ type: "summary", output: { platform: "React" } }, { type: "done", ok: true }]]; - resumeBodies = []; 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); + logInfoSpy = spyOn(log, "info").mockImplementation(noop); + logWarnSpy = spyOn(log, "warn").mockImplementation(noop); + logErrorSpy = spyOn(log, "error").mockImplementation(noop); spinnerSpy = spyOn(initSpinner, "createWizardSpinner").mockReturnValue( - spinnerMock as never + spinnerMock as any ); spinnerMock.start.mockClear(); @@ -142,36 +145,44 @@ beforeEach(() => { data: { status: "none", signals: [] }, }); stderrSpy = spyOn(process.stderr, "write").mockImplementation( - () => true as never + () => true as any ); - startInitStreamSpy = spyOn( - initTransport, - "startInitStream" - ).mockResolvedValue({ - response: {} as Response, - runId: "run-123", + startInitStreamSpy = spyOn(transport, "startInitStream").mockResolvedValue({ + response: makeResponse(), + runId: "run_123", }); reconnectInitStreamSpy = spyOn( - initTransport, + transport, "reconnectInitStream" - ).mockResolvedValue({} as Response); + ).mockResolvedValue(makeResponse()); resumeInitActionSpy = spyOn( - initTransport, + transport, "resumeInitAction" - ).mockImplementation(async (_actionId, body) => { - resumeBodies.push(body); - }); - readNdjsonStreamSpy = spyOn( - initTransport, - "readNdjsonStream" - ).mockImplementation(async (_response, onEvent) => { - const batch = streamBatches.shift() ?? []; - for (const event of batch) { - await onEvent(event); + ).mockResolvedValue(); + readNdjsonStreamSpy = spyOn(transport, "readNdjsonStream").mockImplementation( + async (_response, onEvent) => { + await onEvent({ + output: { + changedFiles: [], + commands: [], + docsUrl: "https://docs.sentry.io/platforms/javascript/guides/nextjs/", + features: ["errorMonitoring"], + message: "Done", + platform: "Next.js", + projectDir: "/tmp/test", + sentryProjectUrl: "https://sentry.io/settings/test/projects/app/", + warnings: [], + }, + type: "summary", + }); + await onEvent({ + ok: true, + type: "done", + }); + return 2; } - return batch.length; - }); + ); }); afterEach(() => { @@ -189,368 +200,172 @@ afterEach(() => { checkGitStatusSpy.mockRestore(); handleInteractiveSpy.mockRestore(); resolveInitContextSpy.mockRestore(); - startInitStreamSpy.mockRestore(); - reconnectInitStreamSpy.mockRestore(); - resumeInitActionSpy.mockRestore(); - readNdjsonStreamSpy.mockRestore(); describeToolSpy.mockRestore(); executeToolSpy.mockRestore(); precomputeSentryDetectionSpy.mockRestore(); stderrSpy.mockRestore(); + startInitStreamSpy.mockRestore(); + reconnectInitStreamSpy.mockRestore(); + resumeInitActionSpy.mockRestore(); + readNdjsonStreamSpy.mockRestore(); + process.exitCode = 0; }); describe("runWizard", () => { - test("starts the workflow without authToken, dirListing, or fileCache in the payload", async () => { + test("formats successful results", async () => { await runWizard(makeOptions()); - const startArg = startInitStreamSpy.mock.calls[0]?.[0] as Record< - string, - unknown - >; - expect(startArg).toMatchObject({ - directory: "/tmp/test", - yes: true, - dryRun: false, - org: "acme", - team: "platform", - project: "my-app", - existingSentry: { status: "none", signals: [] }, - cliVersion: expect.any(String), - }); - expect(startArg.authToken).toBeUndefined(); - expect(startArg.dirListing).toBeUndefined(); - expect(startArg.fileCache).toBeUndefined(); - expect(formatResultSpy).toHaveBeenCalledWith({ platform: "React" }); + expect(formatResultSpy).toHaveBeenCalled(); expect(formatErrorSpy).not.toHaveBeenCalled(); - expect(spinnerMock.stop).toHaveBeenCalledWith("Done"); - }); - - 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 transport creation when preflight returns null", async () => { - resolveInitContextSpy.mockResolvedValue(null); - - await runWizard(makeOptions()); - - expect(startInitStreamSpy).not.toHaveBeenCalled(); - expect(formatResultSpy).not.toHaveBeenCalled(); + expect(spinnerMock.stop).toHaveBeenCalledWith("Done", undefined); }); - test("aborts cleanly when git safety check fails", async () => { - checkGitStatusSpy.mockResolvedValue(false); - - await runWizard(makeOptions()); - - expect(cancelSpy).toHaveBeenCalledWith("Setup cancelled."); - expect(startInitStreamSpy).not.toHaveBeenCalled(); - }); - - test("dispatches tool action requests through the local registry", async () => { + test("dispatches tool payloads through the registry and resumes the action", async () => { const payload: ToolPayload = { - type: "tool", - operation: "run-commands", cwd: "/tmp/test", + operation: "run-commands", params: { commands: ["npm install @sentry/node"] }, + type: "tool", }; - streamBatches = [ - [ - { - type: "action_request", - actionId: "tool-1", - kind: "tool", - name: "install-deps", - payload, + + readNdjsonStreamSpy.mockImplementationOnce(async (_response, onEvent) => { + await onEvent({ + actionId: "run_123:action:001:run-commands", + kind: "tool", + name: "run-commands", + payload, + type: "action_request", + }); + await onEvent({ + output: { + changedFiles: [], + commands: [], + features: ["errorMonitoring"], + message: "Done", + warnings: [], }, - ], - [{ type: "done", ok: true }], - ]; + type: "summary", + }); + await onEvent({ + ok: true, + type: "done", + }); + return 3; + }); await runWizard(makeOptions()); expect(describeToolSpy).toHaveBeenCalledWith(payload); expect(executeToolSpy).toHaveBeenCalledWith(payload, makeContext()); expect(resumeInitActionSpy).toHaveBeenCalledWith( - "tool-1", + "run_123:action:001:run-commands", expect.objectContaining({ ok: true, - output: expect.objectContaining({ - ok: true, - data: { results: [] }, - }), }), - expect.objectContaining({ baseUrl: expect.any(String) }) - ); - expect(reconnectInitStreamSpy).toHaveBeenCalledWith( - "run-123", - 1, - expect.objectContaining({ baseUrl: expect.any(String) }) + expect.objectContaining({ + baseUrl: expect.any(String), + }) ); - expect(spinnerMock.message).toHaveBeenCalledWith("Running tool..."); }); - test("dispatches prompt action requests through interactive handlers", async () => { - streamBatches = [ - [ - { - type: "action_request", - actionId: "prompt-1", - kind: "prompt", - name: "select-features", - payload: { - type: "interactive", - kind: "confirm", - prompt: "Continue?", - }, - }, - ], - [{ type: "done", ok: true }], - ]; - - await runWizard(makeOptions()); - - expect(handleInteractiveSpy).toHaveBeenCalledWith( - { - type: "interactive", - kind: "confirm", - prompt: "Continue?", + test("dedupes replayed action requests after reconnect", async () => { + const event: InitActionRequestEvent = { + actionId: "run_123:action:001:run-commands", + description: "Installing dependencies", + kind: "tool", + name: "run-commands", + payload: { + cwd: "/tmp/test", + operation: "run-commands", + params: { commands: ["npm install"] }, + type: "tool", }, - makeContext() - ); - expect(resumeBodies[0]).toEqual({ - ok: true, - output: { - action: "continue", - _phase: "apply", - }, - }); - }); - - test("deduplicates replayed action requests by actionId", async () => { - const payload: ToolPayload = { - type: "tool", - operation: "list-dir", - cwd: "/tmp/test", - params: { path: ".", recursive: true }, + type: "action_request", }; - streamBatches = [ - [ - { - type: "action_request", - actionId: "dup-1", - kind: "tool", - name: "detect-platform", - payload, - }, - ], - [ - { - type: "action_request", - actionId: "dup-1", - kind: "tool", - name: "detect-platform", - payload, - }, - { type: "summary", output: { platform: "node" } }, - { type: "done", ok: true }, - ], - ]; + + readNdjsonStreamSpy + .mockImplementationOnce(async (_response, onEvent) => { + await onEvent(event); + return 1; + }) + .mockImplementationOnce(async (_response, onEvent) => { + await onEvent(event); + await onEvent({ + output: { + changedFiles: [], + commands: [], + features: ["errorMonitoring"], + message: "Done", + warnings: [], + }, + type: "summary", + }); + await onEvent({ + ok: true, + type: "done", + }); + return 3; + }); await runWizard(makeOptions()); - expect(executeToolSpy).toHaveBeenCalledTimes(1); expect(resumeInitActionSpy).toHaveBeenCalledTimes(1); expect(reconnectInitStreamSpy).toHaveBeenCalledWith( - "run-123", + "run_123", 1, - expect.objectContaining({ baseUrl: expect.any(String) }) + expect.objectContaining({ + baseUrl: expect.any(String), + }) ); }); - test("skips verify-changes prompt actions during dry-run", async () => { - resolveInitContextSpy.mockResolvedValue(makeContext({ dryRun: true })); - streamBatches = [ - [ - { - type: "action_request", - actionId: "verify-1", - kind: "prompt", - name: "verify-changes", - payload: { - type: "interactive", - kind: "confirm", - prompt: "Verification found issues. Continue anyway?", - }, - }, - ], - [{ type: "done", ok: true }], - ]; - - await runWizard(makeOptions({ dryRun: true })); - - expect(handleInteractiveSpy).not.toHaveBeenCalled(); - expect(resumeBodies[0]).toEqual({ - ok: true, - output: { - action: "continue", - _phase: "apply", - }, - }); - }); - - test("renders final summaries from summary and done events", async () => { - streamBatches = [ - [ - { - type: "summary", - output: { - platform: "Node", - projectDir: "/tmp/test", - warnings: ["Heads up"], - }, - }, - { type: "done", ok: true }, - ], - ]; - - await runWizard(makeOptions()); - - expect(formatResultSpy).toHaveBeenCalledWith({ - platform: "Node", - projectDir: "/tmp/test", - warnings: ["Heads up"], - }); - }); - - test("renders final errors from summary and error events", async () => { - streamBatches = [ - [ - { - type: "summary", - output: { platform: "Node", commands: ["npm install"] }, - }, - { - type: "error", - message: "Could not determine project platform", - exitCode: 20, - }, - ], - ]; + test("surfaces malformed stream events clearly", async () => { + readNdjsonStreamSpy.mockRejectedValueOnce(new Error("Invalid status event")); await expect(runWizard(makeOptions())).rejects.toThrow(WizardError); - - expect(formatErrorSpy).toHaveBeenCalledWith({ - type: "error", - message: "Could not determine project platform", - exitCode: 20, - output: { - platform: "Node", - commands: ["npm install"], - }, - }); - expect(formatResultSpy).not.toHaveBeenCalled(); + expect(logErrorSpy).toHaveBeenCalledWith( + "Malformed init stream event: Invalid status event" + ); }); - test("surfaces malformed action payload types", async () => { - streamBatches = [ - [ - { - type: "action_request", - actionId: "bad-1", - kind: "tool", - name: "detect-platform", - payload: { - type: "unknown", - operation: "list-dir", - cwd: "/tmp/test", - params: { path: "." }, - }, + test("surfaces action resume failures clearly", async () => { + resumeInitActionSpy.mockRejectedValue(new Error("connection dropped")); + readNdjsonStreamSpy.mockImplementationOnce(async (_response, onEvent) => { + await onEvent({ + actionId: "run_123:action:001:run-commands", + description: "Installing dependencies", + kind: "tool", + name: "run-commands", + payload: { + cwd: "/tmp/test", + operation: "run-commands", + params: { commands: ["npm install"] }, + type: "tool", }, - ], - [{ type: "error", message: "Bad action payload" }], - ]; + type: "action_request", + }); + return 1; + }); await expect(runWizard(makeOptions())).rejects.toThrow(WizardError); - expect(resumeBodies[0]).toEqual({ - ok: false, - error: expect.objectContaining({ - message: "Invalid tool action payload", - }), - }); + expect(logErrorSpy).toHaveBeenCalledWith( + expect.stringContaining("Failed to resume tool action") + ); }); - test("shows a multiline tree while reading files and then analyzing them", async () => { - streamBatches = [ - [ - { - type: "action_request", - actionId: "read-1", - kind: "tool", - name: "detect-platform", - payload: { - type: "tool", - operation: "read-files", - cwd: "/tmp/test", - params: { - paths: ["src/settings.py", "src/urls.py"], - }, - }, - }, - ], - [{ type: "done", ok: true }], - ]; - - 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, - "" + test("surfaces retryable runner startup failures from the backend", async () => { + startInitStreamSpy.mockRejectedValueOnce( + new Error( + "Init start failed (503): Runner did not become ready in time [retryable]" ) ); - expect(messages).toContain( - "Reading files...\n├─ ● settings.py\n└─ ● urls.py" - ); - expect(messages).toContain( - "Analyzing files...\n├─ ✓ settings.py\n└─ ✓ urls.py" - ); - }); - test("renders tool result messages via the spinner stop state", async () => { - streamBatches = [ - [ - { - type: "action_request", - actionId: "ensure-1", - kind: "tool", - name: "ensure-sentry-project", - payload: { - type: "tool", - operation: "create-sentry-project", - cwd: "/tmp/test", - params: { name: "my-app", platform: "javascript-react" }, - }, - }, - ], - [{ type: "done", ok: true }], - ]; - executeToolSpy.mockResolvedValue({ - ok: true, - message: "Using existing project", - data: {}, - }); - - await runWizard(makeOptions()); - - expect(spinnerMock.stop).toHaveBeenCalledWith("Using existing project"); + await expect(runWizard(makeOptions())).rejects.toThrow(WizardError); + expect(logErrorSpy).toHaveBeenCalledWith( + "Init start failed (503): Runner did not become ready in time [retryable]" + ); + expect(cancel).toHaveBeenCalledWith("Setup failed"); }); });