diff --git a/README.md b/README.md index bc496a5..7a8d508 100644 --- a/README.md +++ b/README.md @@ -7,13 +7,47 @@ The AI powered Seam setup wizard. ## Description -TODO +An interactive terminal wizard that takes a project from zero to a working +Seam integration. Run inside a project, it will: + +1. **Detect the project** — JavaScript/TypeScript or Python, + and the package manager or installer in use. +2. **Connect a Seam account** — an existing `SEAM_API_KEY` + in the environment or a `.env*` file is verified and reused. + Otherwise the developer can continue in the browser, where the + Console mints a fresh key and hands it back over a localhost + callback, or paste a key by hand. + Either way the key is verified and saved to `.env`. +3. **Install the Seam SDK** — `seam` via npm, pnpm, yarn, or bun + for JavaScript, or pip, poetry, or uv for Python. +4. **Install the Seam plugin** — + [`seamapi/seam-plugin`](https://github.com/seamapi/seam-plugin), + which provides the Seam integration skills and the `seam-docs` MCP + for the developer's AI coding assistant. + Claude Code is detected via `.claude` or `CLAUDE.md`, + in which case the wizard prints the slash commands to run, + since an external program cannot drive them. +5. **Write the integration** — optionally. The wizard analyzes the + project, recommends an approach, confirms it with the developer, + records the plan in `.seam/onboarding.json`, and then runs an + embedded agent that writes the integration for review as a diff. + +The wizard never asks for a password: keys are created in the browser +or pasted, never minted here. Inference for the analysis and the +embedded agent is routed through Seam, so no developer Anthropic API +key is needed. + +The wizard is built with [Ink] and takes over the terminal for the +duration of a run, restoring it and reprinting a transcript on the way +out. This package is not a standalone command line program: it deliberately publishes no `bin`. The wizard is distributed as a library and mounted by the [Seam CLI](https://github.com/seamapi/cli) under `seam wizard`. +[Ink]: https://github.com/vadimdemedes/ink + ## Installation Add this as a dependency to your project using [npm] with @@ -43,6 +77,37 @@ The `commandName` option is only used in help output so that the wizard describes itself using the command that was actually run. +The wizard handles `--help` itself and otherwise takes over the terminal +until the developer finishes, then restores it and prints a transcript of +the run. It resolves when the wizard is done, and only rejects if the +wizard could not run at all: a step that fails reports itself to the +developer and does not reject. + +Pass `cwd` to set the project root the wizard sets up. +It defaults to `process.cwd()`, +which is the project the command was run in. +The wizard reads and writes files there, +e.g., `.env` and `.seam/onboarding.json`. + +```ts +await wizard({ + argv: process.argv.slice(3), + commandName: 'seam wizard', + cwd: '/path/to/project', +}) +``` + +### Environment variables + +- `SEAM_API_KEY`: An existing Seam API key. + When set, the wizard verifies and reuses it instead of asking + the developer to connect an account. + The wizard also looks for this in the project's `.env*` files, + and writes the key it obtains back to `.env`. +- `SEAM_CONSOLE_URL`: Points the browser connection flow at a + non-production Console. + Defaults to `https://console.seam.co`. + ## Development and Testing ### Quickstart @@ -55,10 +120,10 @@ $ npm install $ npm run test:watch ``` -Run the wizard locally with +Run the wizard locally against a project with ``` -$ npm run wizard +$ npm run wizard -- --cwd ``` This runs the development CLI in `src/bin/cli.ts`, @@ -66,6 +131,30 @@ which simply calls the wizard with the arguments given. That file exists for local development only: it is excluded from the build and from the published package. +`--cwd` is an option of that CLI, which becomes the wizard's `cwd`. +Every other argument is forwarded to the wizard untouched. + +### Source layout + +The wizard is an [Ink] application under `src/lib`: + +- `wizard.ts` is the entrypoint the package exports. + It parses arguments, handles `--help`, and hands off to the renderer. +- `render.tsx` runs the app full-screen + and reprints its transcript on exit. +- `app.tsx` is the state machine driving the run, + one phase per step, and holds all of the rendering. +- `steps/` holds the logic for each step, with no UI in it. +- `util/` holds the Seam API client, dotenv handling, + and the subprocess runner. + +Modules under `src/lib` are imported through the `lib/*` path alias +rather than parent-relative specifiers. + +Types that mirror the Seam API wire format +or the on-disk `.seam/onboarding.json` record keep their `snake_case` +property names, which the lint configuration allows. + Primary development tasks are defined under `scripts` in `package.json` and available via `npm run`. View them with diff --git a/examples/index.ts b/examples/index.ts deleted file mode 100755 index 7a1cc11..0000000 --- a/examples/index.ts +++ /dev/null @@ -1,9 +0,0 @@ -#!/usr/bin/env tsx - -import landlubber from 'landlubber' - -import * as todo from './todo.js' - -const commands = [todo] - -await landlubber(commands).parse() diff --git a/examples/todo.ts b/examples/todo.ts deleted file mode 100644 index e97cd8a..0000000 --- a/examples/todo.ts +++ /dev/null @@ -1,24 +0,0 @@ -import type { Builder, Command, Describe, Handler } from 'landlubber' - -import { todo } from '@seamapi/wizard' - -interface Options { - x: string -} - -export const command: Command = 'todo x' - -export const describe: Describe = 'TODO' - -export const builder: Builder = { - x: { - type: 'string', - default: 'TODO', - describe: 'TODO', - }, -} - -export const handler: Handler = async ({ x, logger }) => { - await Promise.resolve() - logger.info({ data: todo(x) }, 'TODO') -} diff --git a/package-lock.json b/package-lock.json index 6e4d64d..d955af7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,19 +9,27 @@ "version": "0.0.1", "license": "MIT", "dependencies": { - "minimist": "^1.2.8" + "@anthropic-ai/claude-agent-sdk": "^0.3.216", + "ink": "^5.2.1", + "ink-select-input": "^6.2.0", + "ink-spinner": "^5.0.0", + "ink-text-input": "^6.0.0", + "minimist": "^1.2.8", + "open": "^10.1.0", + "react": "^18.3.1" }, "devDependencies": { "@types/minimist": "^1.2.5", "@types/node": "^24.10.9", + "@types/react": "^18.3.27", "@vitest/coverage-v8": "^4.1.10", "del-cli": "^7.0.0", "eslint": "^9.31.0", "eslint-plugin-import": "^2.32.0", "eslint-plugin-simple-import-sort": "^12.1.1", "eslint-plugin-unused-imports": "^4.1.4", + "ink-testing-library": "^4.0.0", "jiti": "^2.4.2", - "landlubber": "^2.0.0", "neostandard": "^0.13.0", "prettier": "^3.0.0", "tsc-alias": "^1.8.2", @@ -34,6 +42,193 @@ "npm": ">=10.0.0" } }, + "node_modules/@alcalzone/ansi-tokenize": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@alcalzone/ansi-tokenize/-/ansi-tokenize-0.1.3.tgz", + "integrity": "sha512-3yWxPTq3UQ/FY9p1ErPxIyfT64elWaMvM9lIHnaqpyft63tkxodF5aUElYHrdisWve5cETkh1+KBw1yJuW0aRw==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "is-fullwidth-code-point": "^4.0.0" + }, + "engines": { + "node": ">=14.13.1" + } + }, + "node_modules/@alcalzone/ansi-tokenize/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@alcalzone/ansi-tokenize/node_modules/is-fullwidth-code-point": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-4.0.0.tgz", + "integrity": "sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@anthropic-ai/claude-agent-sdk": { + "version": "0.3.220", + "resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk/-/claude-agent-sdk-0.3.220.tgz", + "integrity": "sha512-glc7SdwPkOkLw8oxwLo9PKTdLJGqW/PIR4urWXFoRtX9YllwozsEVc5Tc1+EvLSkfrsxPJqQWqOgpjUOQXf1oA==", + "license": "SEE LICENSE IN README.md", + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "@anthropic-ai/claude-agent-sdk-darwin-arm64": "0.3.220", + "@anthropic-ai/claude-agent-sdk-darwin-x64": "0.3.220", + "@anthropic-ai/claude-agent-sdk-linux-arm64": "0.3.220", + "@anthropic-ai/claude-agent-sdk-linux-arm64-musl": "0.3.220", + "@anthropic-ai/claude-agent-sdk-linux-x64": "0.3.220", + "@anthropic-ai/claude-agent-sdk-linux-x64-musl": "0.3.220", + "@anthropic-ai/claude-agent-sdk-win32-arm64": "0.3.220", + "@anthropic-ai/claude-agent-sdk-win32-x64": "0.3.220" + }, + "peerDependencies": { + "@anthropic-ai/sdk": ">=0.93.0", + "@modelcontextprotocol/sdk": "^1.29.0", + "zod": "^4.0.0" + } + }, + "node_modules/@anthropic-ai/claude-agent-sdk-darwin-arm64": { + "version": "0.3.220", + "resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-darwin-arm64/-/claude-agent-sdk-darwin-arm64-0.3.220.tgz", + "integrity": "sha512-7VxlbEosK7DODiOnsjoVd0DSJzbnaPrM2jelMHI0y8zx1UnLS3WC6EFUXbvy74F2sXqEznh2tzn7EKWInaRN6Q==", + "cpu": [ + "arm64" + ], + "license": "SEE LICENSE IN LICENSE.md", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@anthropic-ai/claude-agent-sdk-darwin-x64": { + "version": "0.3.220", + "resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-darwin-x64/-/claude-agent-sdk-darwin-x64-0.3.220.tgz", + "integrity": "sha512-X9RwDsSmbF6ultKZroaip+DL8WRgC64gHbrAwrRlAFSPNZV7zmJyP2ur8rW7KrxqmtuehdMMkw8+SAC/6hD2PA==", + "cpu": [ + "x64" + ], + "license": "SEE LICENSE IN LICENSE.md", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@anthropic-ai/claude-agent-sdk-linux-arm64": { + "version": "0.3.220", + "resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-linux-arm64/-/claude-agent-sdk-linux-arm64-0.3.220.tgz", + "integrity": "sha512-WkROPwWskqhKR9XgnmseHQ6rLi9zM9qt57IWoToIjL/eXOqDWipp7JXZ1L5ud+LrA42dunHPZfBwD/vXZ+A7LA==", + "cpu": [ + "arm64" + ], + "license": "SEE LICENSE IN LICENSE.md", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@anthropic-ai/claude-agent-sdk-linux-arm64-musl": { + "version": "0.3.220", + "resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-linux-arm64-musl/-/claude-agent-sdk-linux-arm64-musl-0.3.220.tgz", + "integrity": "sha512-OHoZOZ8Cf2TBr6oXIXPwyvUxj9jrq2w8E4poA8dMpacXszcPSPiCQCMuuOh4aWJzfeJE1+TtWxhKMVb2csXyZQ==", + "cpu": [ + "arm64" + ], + "license": "SEE LICENSE IN LICENSE.md", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@anthropic-ai/claude-agent-sdk-linux-x64": { + "version": "0.3.220", + "resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-linux-x64/-/claude-agent-sdk-linux-x64-0.3.220.tgz", + "integrity": "sha512-tkTJFnpR9VifvWX2fmkCAPkT6+8Wk/gVu8B5jsVekKZPiZoWRHmMXO30BnZn+f0TZhgYP+82PSX3S8crH1kn+w==", + "cpu": [ + "x64" + ], + "license": "SEE LICENSE IN LICENSE.md", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@anthropic-ai/claude-agent-sdk-linux-x64-musl": { + "version": "0.3.220", + "resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-linux-x64-musl/-/claude-agent-sdk-linux-x64-musl-0.3.220.tgz", + "integrity": "sha512-K+FWj+LcGhC1Z7wqeWoLxm1iemcba5xKpLLFVwYm4V6HyMx3ruYd/2r2TiQtjT+JWeNFWIys0ScHiItR6vWAiA==", + "cpu": [ + "x64" + ], + "license": "SEE LICENSE IN LICENSE.md", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@anthropic-ai/claude-agent-sdk-win32-arm64": { + "version": "0.3.220", + "resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-win32-arm64/-/claude-agent-sdk-win32-arm64-0.3.220.tgz", + "integrity": "sha512-rIwgq0UwQExWl6KrHUyC4w5KwpL9l6nd95aUTx6RitexaAuEw//xtfTVLnuE4hDDQZFkzEwpdKc3nxDWoGcUbA==", + "cpu": [ + "arm64" + ], + "license": "SEE LICENSE IN LICENSE.md", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@anthropic-ai/claude-agent-sdk-win32-x64": { + "version": "0.3.220", + "resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-win32-x64/-/claude-agent-sdk-win32-x64-0.3.220.tgz", + "integrity": "sha512-MuOuXhbr66HlGaWXD2f3w0k2PsvmnbkwcUZ0dAe2poFLdl72GC2dapwwOBefxm9QmoNqk9+jmv/dSKGOVWyvLw==", + "cpu": [ + "x64" + ], + "license": "SEE LICENSE IN LICENSE.md", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@anthropic-ai/sdk": { + "version": "0.115.0", + "resolved": "https://registry.npmjs.org/@anthropic-ai/sdk/-/sdk-0.115.0.tgz", + "integrity": "sha512-BJrFIVyjNuU8lfDyIJTvlRYzgQg+zEl78BxE7fq8esULsGz9IRQvGtW5spq3tydmtjQb/GFdooKGdGsetpx+lQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "json-schema-to-ts": "^3.1.1", + "standardwebhooks": "^1.0.0" + }, + "bin": { + "anthropic-ai-sdk": "bin/cli" + }, + "peerDependencies": { + "zod": "^3.25.0 || ^4.0.0" + }, + "peerDependenciesMeta": { + "zod": { + "optional": true + } + } + }, "node_modules/@babel/helper-string-parser": { "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", @@ -70,6 +265,16 @@ "node": ">=6.0.0" } }, + "node_modules/@babel/runtime": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.7.tgz", + "integrity": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/types": { "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz", @@ -717,6 +922,19 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, + "node_modules/@hono/node-server": { + "version": "2.0.12", + "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-2.0.12.tgz", + "integrity": "sha512-eWpQYr67tqJLeaSUl0Q+TquuYfUdTibpOJlUMV2FfUP7+KqCC5TufnwnlXL6mobZBJbGAYRd7ZvEBDCbLInjhg==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=20" + }, + "peerDependencies": { + "hono": "^4" + } + }, "node_modules/@humanfs/core": { "version": "0.19.2", "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", @@ -822,6 +1040,71 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@modelcontextprotocol/sdk": { + "version": "1.30.0", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.30.0.tgz", + "integrity": "sha512-xKd8OIzlqNzcqcNumGAa6g+PW2kjD5vrpcKOnfldAUPP3j7lnqMPwlTXQm8gF+UwH72z0lqaRbjr9hqGz0eITA==", + "license": "MIT", + "peer": true, + "dependencies": { + "@hono/node-server": "^1.19.9 || ^2.0.5", + "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" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@cfworker/json-schema": "^4.1.1", + "zod": "^3.25 || ^4.0" + }, + "peerDependenciesMeta": { + "@cfworker/json-schema": { + "optional": true + }, + "zod": { + "optional": false + } + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/ajv": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", + "license": "MIT", + "peer": true, + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "license": "MIT", + "peer": true + }, "node_modules/@napi-rs/wasm-runtime": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.2.0.tgz", @@ -1210,6 +1493,13 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/@stablelib/base64": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@stablelib/base64/-/base64-1.0.1.tgz", + "integrity": "sha512-1bnPQqSxSuc3Ii6MhBysoWCg58j97aUjuCSZrGSmDxNqtytIi0k8utUenAwTZN4V5mXXYGsVUI9zeBqy+jBOSQ==", + "license": "MIT", + "peer": true + }, "node_modules/@standard-schema/spec": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", @@ -1317,23 +1607,24 @@ "undici-types": "~7.18.0" } }, - "node_modules/@types/yargs": { - "version": "17.0.35", - "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.35.tgz", - "integrity": "sha512-qUHkeCyQFxMXg79wQfTtfndEC+N9ZZg76HJftDJp+qH2tV7Gj4OJi7l+PiWwJ+pWtW8GwSmqsDj/oymhrTWXjg==", - "dev": true, + "node_modules/@types/prop-types": { + "version": "15.7.15", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", + "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "18.3.31", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.31.tgz", + "integrity": "sha512-vfEqpXTvwT91yhmwdfouStN2hSKwTvyRs8qpLfADyrq/kxDw0hZM7Wk9Ug1FELj8hIby+S/+kQCSRFF32nv2Qw==", + "devOptional": true, "license": "MIT", "dependencies": { - "@types/yargs-parser": "*" + "@types/prop-types": "*", + "csstype": "^3.2.2" } }, - "node_modules/@types/yargs-parser": { - "version": "21.0.3", - "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", - "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", - "dev": true, - "license": "MIT" - }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "8.65.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.65.0.tgz", @@ -1773,17 +2064,18 @@ "url": "https://opencollective.com/vitest" } }, - "node_modules/abort-controller": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", - "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", - "dev": true, + "node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", "license": "MIT", + "peer": true, "dependencies": { - "event-target-shim": "^5.0.0" + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" }, "engines": { - "node": ">=6.5" + "node": ">= 0.6" } }, "node_modules/acorn": { @@ -1826,14 +2118,61 @@ "url": "https://github.com/sponsors/epoberezkin" } }, - "node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, + "node_modules/ajv-formats": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", + "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", "license": "MIT", + "peer": true, + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/ajv-formats/node_modules/ajv": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", + "license": "MIT", + "peer": true, + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "license": "MIT", + "peer": true + }, + "node_modules/ansi-escapes": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.3.0.tgz", + "integrity": "sha512-BvU8nYgGQBxcmMuEeUEmNTvrMVjJNSH7RgW24vXexN4Ven6qCvy4TntnvlnwnMLTVlcRQQdbRY8NKnaIoeWDNg==", + "license": "MIT", + "dependencies": { + "environment": "^1.0.0" + }, "engines": { - "node": ">=8" + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/ansi-styles": { @@ -2075,14 +2414,16 @@ "node": ">= 0.4" } }, - "node_modules/atomic-sleep": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz", - "integrity": "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==", - "dev": true, + "node_modules/auto-bind": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/auto-bind/-/auto-bind-5.0.1.tgz", + "integrity": "sha512-ooviqdwwgfIfNmDwo94wlshcdzfO64XV0Cg6oDsDYBJfITDz1EngD2z7DkbvCWn+XIMsIqW27sEVF6qcpJrRcg==", "license": "MIT", "engines": { - "node": ">=8.0.0" + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/available-typed-arrays": { @@ -2108,27 +2449,6 @@ "dev": true, "license": "MIT" }, - "node_modules/base64-js": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", - "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, "node_modules/binary-extensions": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", @@ -2142,6 +2462,45 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/body-parser": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.3.0.tgz", + "integrity": "sha512-2cGmJupaNgg+QUwVLAucDuWuoMZ6EX9iHDRswZ5lsNYEmwPaRknMPCLZz07yTzVq/83p4o/wzbDZbBrTvGGTIw==", + "license": "MIT", + "peer": true, + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^2.0.0", + "debug": "^4.4.3", + "http-errors": "^2.0.1", + "iconv-lite": "^0.7.2", + "on-finished": "^2.4.1", + "qs": "^6.15.2", + "raw-body": "^3.0.2", + "type-is": "^2.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/body-parser/node_modules/content-type": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.0.0.tgz", + "integrity": "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/brace-expansion": { "version": "1.1.16", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.16.tgz", @@ -2166,30 +2525,30 @@ "node": ">=8" } }, - "node_modules/buffer": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", - "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], + "node_modules/bundle-name": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-4.1.0.tgz", + "integrity": "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==", "license": "MIT", "dependencies": { - "base64-js": "^1.3.1", - "ieee754": "^1.2.1" - } + "run-applescript": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.8" + } }, "node_modules/call-bind": { "version": "1.0.9", @@ -2214,7 +2573,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", - "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -2228,7 +2586,6 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", - "dev": true, "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.2", @@ -2316,19 +2673,161 @@ "node": ">= 6" } }, - "node_modules/cliui": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", - "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", - "dev": true, - "license": "ISC", + "node_modules/cli-boxes": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cli-boxes/-/cli-boxes-3.0.0.tgz", + "integrity": "sha512-/lzGpEWL/8PfI0BmBOPRwp0c/wFNX1RdUML3jK/RcSBA9T8mZDdQpqYBKtCFTOfQbwPqWEOpjqW+Fnayc0969g==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-cursor": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-4.0.0.tgz", + "integrity": "sha512-VGtlMu3x/4DOtIUwEkRezxUZ2lBacNJCHash0N0WeZDBS+7Ux1dm3XWAgWYxLJFMMdOeXMHXorshEFhbMSGelg==", + "license": "MIT", + "dependencies": { + "restore-cursor": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-spinners": { + "version": "2.9.2", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz", + "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==", + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-truncate": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-4.0.0.tgz", + "integrity": "sha512-nPdaFdQ0h/GEigbPClz11D0v/ZJEwxmeVZGeMo3Z5StPtUTkA9o1lD6QwoirYiSDzbcwn2XcjwmCp68W1IS4TA==", + "license": "MIT", + "dependencies": { + "slice-ansi": "^5.0.0", + "string-width": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-truncate/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/cli-truncate/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/cli-truncate/node_modules/emoji-regex": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", + "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", + "license": "MIT" + }, + "node_modules/cli-truncate/node_modules/is-fullwidth-code-point": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-4.0.0.tgz", + "integrity": "sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-truncate/node_modules/slice-ansi": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-5.0.0.tgz", + "integrity": "sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.0.0", + "is-fullwidth-code-point": "^4.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/cli-truncate/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "license": "MIT", "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.1", - "wrap-ansi": "^7.0.0" + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-truncate/node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" }, "engines": { "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/code-excerpt": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/code-excerpt/-/code-excerpt-4.0.0.tgz", + "integrity": "sha512-xxodCmBen3iy2i0WtAK8FlFNrRzjUqjRsMfho58xT/wvZU1YTM3fCnRjcy1gJPMepaRlgm/0e6w8SpWHpn3/cA==", + "license": "MIT", + "dependencies": { + "convert-to-spaces": "^2.0.1" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" } }, "node_modules/color-convert": { @@ -2351,13 +2850,6 @@ "dev": true, "license": "MIT" }, - "node_modules/colorette": { - "version": "2.0.20", - "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", - "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", - "dev": true, - "license": "MIT" - }, "node_modules/commander": { "version": "9.5.0", "resolved": "https://registry.npmjs.org/commander/-/commander-9.5.0.tgz", @@ -2375,6 +2867,30 @@ "dev": true, "license": "MIT" }, + "node_modules/content-disposition": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.1.0.tgz", + "integrity": "sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/convert-source-map": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", @@ -2382,11 +2898,57 @@ "dev": true, "license": "MIT" }, + "node_modules/convert-to-spaces": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/convert-to-spaces/-/convert-to-spaces-2.0.1.tgz", + "integrity": "sha512-rcQ1bsQO9799wq24uE5AM2tAILy4gXGIK/njFWcVQkGNZ96edlpY+A7bjwvzjYvLDyzmG1MmMLZhpcsb+klNMQ==", + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + } + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=6.6.0" + } + }, + "node_modules/cors": { + "version": "2.8.6", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", + "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==", + "license": "MIT", + "peer": true, + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", - "dev": true, "license": "MIT", "dependencies": { "path-key": "^3.1.0", @@ -2397,6 +2959,13 @@ "node": ">= 8" } }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "devOptional": true, + "license": "MIT" + }, "node_modules/data-view-buffer": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz", @@ -2451,21 +3020,10 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/dateformat": { - "version": "4.6.3", - "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-4.6.3.tgz", - "integrity": "sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA==", - "dev": true, - "license": "MIT", - "engines": { - "node": "*" - } - }, "node_modules/debug": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "dev": true, "license": "MIT", "dependencies": { "ms": "^2.1.3" @@ -2486,6 +3044,34 @@ "dev": true, "license": "MIT" }, + "node_modules/default-browser": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-5.5.0.tgz", + "integrity": "sha512-H9LMLr5zwIbSxrmvikGuI/5KGhZ8E2zH3stkMgM5LpOWDutGM2JZaj460Udnf1a+946zc7YBgrqEWwbk7zHvGw==", + "license": "MIT", + "dependencies": { + "bundle-name": "^4.1.0", + "default-browser-id": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/default-browser-id": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-5.0.1.tgz", + "integrity": "sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/define-data-property": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", @@ -2504,6 +3090,18 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/define-lazy-prop": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz", + "integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/define-properties": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", @@ -2566,6 +3164,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/detect-libc": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", @@ -2616,7 +3224,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", - "dev": true, "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.1", @@ -2627,21 +3234,21 @@ "node": ">= 0.4" } }, - "node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true, - "license": "MIT" + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT", + "peer": true }, - "node_modules/end-of-stream": { - "version": "1.4.5", - "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", - "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", - "dev": true, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", "license": "MIT", - "dependencies": { - "once": "^1.4.0" + "peer": true, + "engines": { + "node": ">= 0.8" } }, "node_modules/enhanced-resolve": { @@ -2658,6 +3265,18 @@ "node": ">=10.13.0" } }, + "node_modules/environment": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/environment/-/environment-1.1.0.tgz", + "integrity": "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/es-abstract": { "version": "1.24.2", "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.2.tgz", @@ -2750,7 +3369,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -2760,7 +3378,6 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -2805,7 +3422,6 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", - "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0" @@ -2864,6 +3480,17 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/es-toolkit": { + "version": "1.50.0", + "resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.50.0.tgz", + "integrity": "sha512-OyZKhUVvEep9ITEiwHn8GKnMRQIVqoSIX7WnRbkWgJkllCujilqP2rD0u979tkl8wqyc8ICwlc1UBVv/Sl1G6w==", + "license": "MIT", + "workspaces": [ + "docs", + "benchmarks", + "tests/types" + ] + }, "node_modules/esbuild": { "version": "0.28.1", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.1.tgz", @@ -2906,15 +3533,12 @@ "@esbuild/win32-x64": "0.28.1" } }, - "node_modules/escalade": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", - "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", - "dev": true, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", "license": "MIT", - "engines": { - "node": ">=6" - } + "peer": true }, "node_modules/escape-string-regexp": { "version": "4.0.0", @@ -3369,24 +3993,37 @@ "node": ">=0.10.0" } }, - "node_modules/event-target-shim": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", - "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", - "dev": true, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", "license": "MIT", + "peer": true, "engines": { - "node": ">=6" + "node": ">= 0.6" } }, - "node_modules/events": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", - "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", - "dev": true, + "node_modules/eventsource": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz", + "integrity": "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==", + "license": "MIT", + "peer": true, + "dependencies": { + "eventsource-parser": "^3.0.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/eventsource-parser": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.1.0.tgz", + "integrity": "sha512-kJezFj9YFAMLeORyi7aCLxLbD5/qWMQnoMVlVPyHIll7lgRJCc3JVln9Vgl9nwQi0YkMnhdGTMNn7CkRRAptMg==", "license": "MIT", + "peer": true, "engines": { - "node": ">=0.8.x" + "node": ">=18.0.0" } }, "node_modules/expect-type": { @@ -3399,18 +4036,74 @@ "node": ">=12.0.0" } }, - "node_modules/fast-copy": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/fast-copy/-/fast-copy-3.0.2.tgz", - "integrity": "sha512-dl0O9Vhju8IrcLndv2eU4ldt1ftXMqqfgN4H1cpmGV7P6jeB9FwpN9a2c8DPGE1Ys88rNUJVYDHq73CGAGOPfQ==", - "dev": true, - "license": "MIT" + "node_modules/express": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", + "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", + "license": "MIT", + "peer": true, + "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" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express-rate-limit": { + "version": "8.6.1", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.6.1.tgz", + "integrity": "sha512-0D493aP61w0TJ2A0wy27riRsO7FMQ7FK+KUHOKCSfPvYo0R55aiC6emCVgFUeShH0fq0ICPVzNcgoS+BsbXQCA==", + "license": "MIT", + "peer": true, + "dependencies": { + "debug": "^4.4.3", + "ip-address": "^10.2.0" + }, + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/express-rate-limit" + }, + "peerDependencies": { + "express": ">= 4.11" + } }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true, "license": "MIT" }, "node_modules/fast-glob": { @@ -3457,22 +4150,29 @@ "dev": true, "license": "MIT" }, - "node_modules/fast-redact": { - "version": "3.5.0", - "resolved": "https://registry.npmjs.org/fast-redact/-/fast-redact-3.5.0.tgz", - "integrity": "sha512-dwsoQlS7h9hMeYUq1W++23NDcBLV4KqONnITDV9DjfS3q1SgDGVrBdvvTLUotWtPSD7asWDV9/CmsZPy8Hf70A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/fast-safe-stringify": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", - "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==", - "dev": true, - "license": "MIT" + "node_modules/fast-sha256": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/fast-sha256/-/fast-sha256-1.3.0.tgz", + "integrity": "sha512-n11RGP/lrWEFI/bWdygLxhI+pVeo1ZYIVwvvPkW7azl/rOy+F3HYRZ2K5zeE9mmkhQppyv9sQFx0JM9UabnpPQ==", + "license": "Unlicense", + "peer": true + }, + "node_modules/fast-uri": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.4.tgz", + "integrity": "sha512-8JnbkQ4juDyvYs4mgFGQqg4yCYtFDtUtmp2QIQq11ZZe5CFQ5wcqm1rqDgAh/QdMySuBnPzMUiJUNZG5N/AiQw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause", + "peer": true }, "node_modules/fastq": { "version": "1.20.1", @@ -3484,6 +4184,21 @@ "reusify": "^1.0.4" } }, + "node_modules/figures": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-6.1.0.tgz", + "integrity": "sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg==", + "license": "MIT", + "dependencies": { + "is-unicode-supported": "^2.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/file-entry-cache": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", @@ -3510,6 +4225,28 @@ "node": ">=8" } }, + "node_modules/finalhandler": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", + "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", + "license": "MIT", + "peer": true, + "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" + }, + "engines": { + "node": ">= 18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/find-up": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", @@ -3564,12 +4301,25 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", - "dev": true, - "license": "ISC" + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.8" + } }, "node_modules/fsevents": { "version": "2.3.3", @@ -3590,7 +4340,6 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "dev": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" @@ -3640,21 +4389,22 @@ "node": ">= 0.4" } }, - "node_modules/get-caller-file": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", - "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", - "dev": true, - "license": "ISC", + "node_modules/get-east-asian-width": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.6.0.tgz", + "integrity": "sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA==", + "license": "MIT", "engines": { - "node": "6.* || 8.* || >= 10.*" + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/get-intrinsic": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", - "dev": true, "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.2", @@ -3679,7 +4429,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", - "dev": true, "license": "MIT", "dependencies": { "dunder-proto": "^1.0.1", @@ -3720,27 +4469,6 @@ "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" } }, - "node_modules/glob": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", - "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", - "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", - "dev": true, - "license": "ISC", - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^5.0.1", - "once": "^1.3.0" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/glob-parent": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", @@ -3754,29 +4482,6 @@ "node": ">=10.13.0" } }, - "node_modules/glob/node_modules/brace-expansion": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.3.tgz", - "integrity": "sha512-DRdx5neNsG/QXbniLFWi2YmC/68oeOOmKz6zOjVk6ZS1ZLXgLIKqVEc6hWsmkjBbgii0SwaBTcJ5XKj5gzY/4A==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/glob/node_modules/minimatch": { - "version": "5.1.9", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.9.tgz", - "integrity": "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/globals": { "version": "14.0.0", "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", @@ -3849,7 +4554,6 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -3921,7 +4625,6 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -3950,7 +4653,6 @@ "version": "2.0.4", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", - "dev": true, "license": "MIT", "dependencies": { "function-bind": "^1.1.2" @@ -3959,30 +4661,14 @@ "node": ">= 0.4" } }, - "node_modules/help-me": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/help-me/-/help-me-4.2.0.tgz", - "integrity": "sha512-TAOnTB8Tz5Dw8penUuzHVrKNKlCIbwwbHnXraNJxPwf8LRtE2HlM84RYuezMFcwOJmoYOCWVDyJ8TQGxn9PgxA==", - "dev": true, - "license": "MIT", - "dependencies": { - "glob": "^8.0.0", - "readable-stream": "^3.6.0" - } - }, - "node_modules/help-me/node_modules/readable-stream": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", - "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", - "dev": true, + "node_modules/hono": { + "version": "4.12.32", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.32.tgz", + "integrity": "sha512-XcuyW9qE2kJn07PkecMOBd5Vq/hMy7mmGw+idz1yblbg9N17ijJODrvPkn7/dwL3Kulj8LcRJ69DLOWf91dRUg==", "license": "MIT", - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, + "peer": true, "engines": { - "node": ">= 6" + "node": ">=16.9.0" } }, "node_modules/html-escaper": { @@ -3992,26 +4678,43 @@ "dev": true, "license": "MIT" }, - "node_modules/ieee754": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", - "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "BSD-3-Clause" + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/iconv-lite": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.3.tgz", + "integrity": "sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } }, "node_modules/ignore": { "version": "5.3.2", @@ -4050,24 +4753,243 @@ "node": ">=0.8.19" } }, - "node_modules/inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", - "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", - "dev": true, - "license": "ISC", - "dependencies": { - "once": "^1.3.0", - "wrappy": "1" + "node_modules/indent-string": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-5.0.0.tgz", + "integrity": "sha512-m6FAo/spmsW2Ab2fU35JTYwtOKa2yAwXSwgjSv1TJzh4Mh7mC3lzAOVLBprb72XsTrgkEIsl7YrFNAiDiRhIGg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC", + "peer": true + }, + "node_modules/ink": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/ink/-/ink-5.2.1.tgz", + "integrity": "sha512-BqcUyWrG9zq5HIwW6JcfFHsIYebJkWWb4fczNah1goUO0vv5vneIlfwuS85twyJ5hYR/y18FlAYUxrO9ChIWVg==", + "license": "MIT", + "dependencies": { + "@alcalzone/ansi-tokenize": "^0.1.3", + "ansi-escapes": "^7.0.0", + "ansi-styles": "^6.2.1", + "auto-bind": "^5.0.1", + "chalk": "^5.3.0", + "cli-boxes": "^3.0.0", + "cli-cursor": "^4.0.0", + "cli-truncate": "^4.0.0", + "code-excerpt": "^4.0.0", + "es-toolkit": "^1.22.0", + "indent-string": "^5.0.0", + "is-in-ci": "^1.0.0", + "patch-console": "^2.0.0", + "react-reconciler": "^0.29.0", + "scheduler": "^0.23.0", + "signal-exit": "^3.0.7", + "slice-ansi": "^7.1.0", + "stack-utils": "^2.0.6", + "string-width": "^7.2.0", + "type-fest": "^4.27.0", + "widest-line": "^5.0.0", + "wrap-ansi": "^9.0.0", + "ws": "^8.18.0", + "yoga-layout": "~3.2.1" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/react": ">=18.0.0", + "react": ">=18.0.0", + "react-devtools-core": "^4.19.1" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "react-devtools-core": { + "optional": true + } + } + }, + "node_modules/ink-select-input": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/ink-select-input/-/ink-select-input-6.2.0.tgz", + "integrity": "sha512-304fZXxkpYxJ9si5lxRCaX01GNlmPBgOZumXXRnPYbHW/iI31cgQynqk2tRypGLOF1cMIwPUzL2LSm6q4I5rQQ==", + "license": "MIT", + "dependencies": { + "figures": "^6.1.0", + "to-rotated": "^1.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "ink": ">=5.0.0", + "react": ">=18.0.0" + } + }, + "node_modules/ink-spinner": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/ink-spinner/-/ink-spinner-5.0.0.tgz", + "integrity": "sha512-EYEasbEjkqLGyPOUc8hBJZNuC5GvXGMLu0w5gdTNskPc7Izc5vO3tdQEYnzvshucyGCBXc86ig0ujXPMWaQCdA==", + "license": "MIT", + "dependencies": { + "cli-spinners": "^2.7.0" + }, + "engines": { + "node": ">=14.16" + }, + "peerDependencies": { + "ink": ">=4.0.0", + "react": ">=18.0.0" + } + }, + "node_modules/ink-testing-library": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/ink-testing-library/-/ink-testing-library-4.0.0.tgz", + "integrity": "sha512-yF92kj3pmBvk7oKbSq5vEALO//o7Z9Ck/OaLNlkzXNeYdwfpxMQkSowGTFUCS5MSu9bWfSZMewGpp7bFc66D7Q==", "dev": true, - "license": "ISC" + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/react": ">=18.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/ink-text-input": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/ink-text-input/-/ink-text-input-6.0.0.tgz", + "integrity": "sha512-Fw64n7Yha5deb1rHY137zHTAbSTNelUKuB5Kkk2HACXEtwIHBCf9OH2tP/LQ9fRYTl1F0dZgbW0zPnZk6FA9Lw==", + "license": "MIT", + "dependencies": { + "chalk": "^5.3.0", + "type-fest": "^4.18.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "ink": ">=5", + "react": ">=18" + } + }, + "node_modules/ink-text-input/node_modules/chalk": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/ink/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/ink/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/ink/node_modules/chalk": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/ink/node_modules/emoji-regex": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", + "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", + "license": "MIT" + }, + "node_modules/ink/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ink/node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/ink/node_modules/wrap-ansi": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", + "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } }, "node_modules/internal-slot": { "version": "1.1.0", @@ -4084,6 +5006,26 @@ "node": ">= 0.4" } }, + "node_modules/ip-address": { + "version": "10.3.1", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.3.1.tgz", + "integrity": "sha512-1e9d3kb97NHJTIJDZW9rKqW2h6+dFa50Dy0fpPSMQp2ADje5gvKsXmdiK6dwY5t76TaTt5+P5N1Y/LoToIxP6g==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 12" + } + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.10" + } + }, "node_modules/is-array-buffer": { "version": "3.0.5", "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", @@ -4232,6 +5174,21 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-docker": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz", + "integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==", + "license": "MIT", + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-document.all": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-document.all/-/is-document.all-1.0.0.tgz", @@ -4274,16 +5231,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/is-generator-function": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.2.tgz", @@ -4317,6 +5264,39 @@ "node": ">=0.10.0" } }, + "node_modules/is-in-ci": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-in-ci/-/is-in-ci-1.0.0.tgz", + "integrity": "sha512-eUuAjybVTHMYWm/U+vBO1sY/JOCgoPCXRxzdju0K+K0BiGW0SChEL1MLC0PoCIR1OlPo5YAp8HuQoUlsWEICwg==", + "license": "MIT", + "bin": { + "is-in-ci": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-inside-container": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz", + "integrity": "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==", + "license": "MIT", + "dependencies": { + "is-docker": "^3.0.0" + }, + "bin": { + "is-inside-container": "cli.js" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-map": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", @@ -4396,6 +5376,13 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "license": "MIT", + "peer": true + }, "node_modules/is-regex": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", @@ -4495,6 +5482,18 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-unicode-supported": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-2.1.0.tgz", + "integrity": "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-weakmap": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", @@ -4541,6 +5540,21 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-wsl": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.1.tgz", + "integrity": "sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw==", + "license": "MIT", + "dependencies": { + "is-inside-container": "^1.0.0" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/isarray": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", @@ -4552,7 +5566,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true, "license": "ISC" }, "node_modules/istanbul-lib-coverage": { @@ -4622,14 +5635,14 @@ "jiti": "lib/jiti-cli.mjs" } }, - "node_modules/joycon": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/joycon/-/joycon-3.1.1.tgz", - "integrity": "sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==", - "dev": true, + "node_modules/jose": { + "version": "6.2.4", + "resolved": "https://registry.npmjs.org/jose/-/jose-6.2.4.tgz", + "integrity": "sha512-N8acGzVsQy6M/fjFcxtysNc4Q379TcM5dM/qKkNtsHFji88yANnXTr7BLeP75iPnFwBfQzM/jg2BZ9+HZrHCZA==", "license": "MIT", - "engines": { - "node": ">=10" + "peer": true, + "funding": { + "url": "https://github.com/sponsors/panva" } }, "node_modules/js-tokens": { @@ -4669,6 +5682,20 @@ "dev": true, "license": "MIT" }, + "node_modules/json-schema-to-ts": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/json-schema-to-ts/-/json-schema-to-ts-3.1.1.tgz", + "integrity": "sha512-+DWg8jCJG2TEnpy7kOm/7/AxaYoaRbjVB4LFZLySZlWn8exGs3A4OLJR966cVvU26N7X9TWxl+Jsw7dzAqKT6g==", + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/runtime": "^7.18.3", + "ts-algebra": "^2.0.0" + }, + "engines": { + "node": ">=16" + } + }, "node_modules/json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", @@ -4676,6 +5703,13 @@ "dev": true, "license": "MIT" }, + "node_modules/json-schema-typed": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/json-schema-typed/-/json-schema-typed-8.0.2.tgz", + "integrity": "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==", + "license": "BSD-2-Clause", + "peer": true + }, "node_modules/json-stable-stringify-without-jsonify": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", @@ -4722,23 +5756,6 @@ "json-buffer": "3.0.1" } }, - "node_modules/landlubber": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/landlubber/-/landlubber-2.0.0.tgz", - "integrity": "sha512-BhqvT4eCGY78NothDI7H/rW5ZrEurXS4nK8iCetoSyvjBfzuD8DikJhpAig9nwRBKOqv7mnD1qbxJIap/XMv+A==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/yargs": "^17.0.20", - "pino": "^8.8.0", - "pino-pretty": "^9.1.1", - "yargs": "^17.6.2" - }, - "engines": { - "node": ">=18.12.0", - "npm": ">= 9.0.0" - } - }, "node_modules/levn": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", @@ -5041,7 +6058,6 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", - "dev": true, "license": "MIT", "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" @@ -5054,7 +6070,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "dev": true, "license": "MIT" }, "node_modules/magic-string": { @@ -5112,12 +6127,25 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" } }, + "node_modules/media-typer": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.1.tgz", + "integrity": "sha512-yz3xRaG20c6/BOzvYoDaGtPmGscs7YivItZEEqe6GbwNfHuxu9YNmvnEkMzKldAGY4/80pRcQRZSEnhquk9XuQ==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/meow": { "version": "14.1.0", "resolved": "https://registry.npmjs.org/meow/-/meow-14.1.0.tgz", @@ -5131,6 +6159,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/merge-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", @@ -5155,6 +6196,42 @@ "node": ">=8.6" } }, + "node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "license": "MIT", + "peer": true, + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/minimatch": { "version": "3.1.5", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", @@ -5181,7 +6258,6 @@ "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, "license": "MIT" }, "node_modules/mylas": { @@ -5224,6 +6300,16 @@ "dev": true, "license": "MIT" }, + "node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/neostandard": { "version": "0.13.0", "resolved": "https://registry.npmjs.org/neostandard/-/neostandard-0.13.0.tgz", @@ -5375,7 +6461,6 @@ "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -5385,7 +6470,6 @@ "version": "1.13.4", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -5508,26 +6592,62 @@ "node": ">=12.20.0" } }, - "node_modules/on-exit-leak-free": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-2.1.2.tgz", - "integrity": "sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==", - "dev": true, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", "license": "MIT", + "peer": true, + "dependencies": { + "ee-first": "1.1.1" + }, "engines": { - "node": ">=14.0.0" + "node": ">= 0.8" } }, "node_modules/once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "dev": true, "license": "ISC", + "peer": true, "dependencies": { "wrappy": "1" } }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "license": "MIT", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/open": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/open/-/open-10.2.0.tgz", + "integrity": "sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA==", + "license": "MIT", + "dependencies": { + "default-browser": "^5.2.1", + "define-lazy-prop": "^3.0.0", + "is-inside-container": "^1.0.0", + "wsl-utils": "^0.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/optionator": { "version": "0.9.4", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", @@ -5623,6 +6743,25 @@ "node": ">=6" } }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/patch-console": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/patch-console/-/patch-console-2.0.0.tgz", + "integrity": "sha512-0YNdUceMdaQwoKce1gatDScmMo5pu/tfABfnzEqeG0gtTmd7mh/WcwgUjtAeOU7N8nFFlbQBnFK2gXW5fGvmMA==", + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + } + }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -5637,7 +6776,6 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -5650,6 +6788,17 @@ "dev": true, "license": "MIT" }, + "node_modules/path-to-regexp": { + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.2.tgz", + "integrity": "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==", + "license": "MIT", + "peer": true, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/path-type": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/path-type/-/path-type-6.0.0.tgz", @@ -5701,73 +6850,16 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, - "node_modules/pino": { - "version": "8.21.0", - "resolved": "https://registry.npmjs.org/pino/-/pino-8.21.0.tgz", - "integrity": "sha512-ip4qdzjkAyDDZklUaZkcRFb2iA118H9SgRh8yzTkSQK8HilsOJF7rSY8HoW5+I0M46AZgX/pxbprf2vvzQCE0Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "atomic-sleep": "^1.0.0", - "fast-redact": "^3.1.1", - "on-exit-leak-free": "^2.1.0", - "pino-abstract-transport": "^1.2.0", - "pino-std-serializers": "^6.0.0", - "process-warning": "^3.0.0", - "quick-format-unescaped": "^4.0.3", - "real-require": "^0.2.0", - "safe-stable-stringify": "^2.3.1", - "sonic-boom": "^3.7.0", - "thread-stream": "^2.6.0" - }, - "bin": { - "pino": "bin.js" - } - }, - "node_modules/pino-abstract-transport": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-1.2.0.tgz", - "integrity": "sha512-Guhh8EZfPCfH+PMXAb6rKOjGQEoy0xlAIn+irODG5kgfYV+BQ0rGYYWTIel3P5mmyXqkYkPmdIkywsn6QKUR1Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "readable-stream": "^4.0.0", - "split2": "^4.0.0" - } - }, - "node_modules/pino-pretty": { - "version": "9.4.1", - "resolved": "https://registry.npmjs.org/pino-pretty/-/pino-pretty-9.4.1.tgz", - "integrity": "sha512-loWr5SNawVycvY//hamIzyz3Fh5OSpvkcO13MwdDW+eKIGylobPLqnVGTDwDXkdmpJd1BhEG+qhDw09h6SqJiQ==", - "dev": true, + "node_modules/pkce-challenge": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.1.tgz", + "integrity": "sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==", "license": "MIT", - "dependencies": { - "colorette": "^2.0.7", - "dateformat": "^4.6.3", - "fast-copy": "^3.0.0", - "fast-safe-stringify": "^2.1.1", - "help-me": "^4.0.1", - "joycon": "^3.1.1", - "minimist": "^1.2.6", - "on-exit-leak-free": "^2.1.0", - "pino-abstract-transport": "^1.0.0", - "pump": "^3.0.0", - "readable-stream": "^4.0.0", - "secure-json-parse": "^2.4.0", - "sonic-boom": "^3.0.0", - "strip-json-comments": "^3.1.1" - }, - "bin": { - "pino-pretty": "bin.js" + "peer": true, + "engines": { + "node": ">=16.20.0" } }, - "node_modules/pino-std-serializers": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-6.2.2.tgz", - "integrity": "sha512-cHjPPsE+vhj/tnhCy/wiMh3M3z3h/j15zHQX+S9GkTBgqJuTuJzYJ4gUyACLhDaJ7kk9ba9iRDmbH2tJU03OiA==", - "dev": true, - "license": "MIT" - }, "node_modules/plimit-lit": { "version": "1.6.1", "resolved": "https://registry.npmjs.org/plimit-lit/-/plimit-lit-1.6.1.tgz", @@ -5859,23 +6951,6 @@ "url": "https://github.com/prettier/prettier?sponsor=1" } }, - "node_modules/process": { - "version": "0.11.10", - "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", - "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6.0" - } - }, - "node_modules/process-warning": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-3.0.0.tgz", - "integrity": "sha512-mqn0kFRl0EoqhnL0GQ0veqFHyIN1yig9RHh/InzORTUiZHFRAur+aMtRkELNwGs9aNwKS6tg/An4NYBPGwvtzQ==", - "dev": true, - "license": "MIT" - }, "node_modules/prop-types": { "version": "15.8.1", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", @@ -5888,15 +6963,18 @@ "react-is": "^16.13.1" } }, - "node_modules/pump": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", - "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", - "dev": true, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", "license": "MIT", + "peer": true, "dependencies": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" } }, "node_modules/punycode": { @@ -5909,6 +6987,23 @@ "node": ">=6" } }, + "node_modules/qs": { + "version": "6.15.3", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.3.tgz", + "integrity": "sha512-O9gl3zCl5h5blw1KGUzQKhA5oUXSl8rwUIM5o0S3nCXMliSvy5Dzx7/DJcI+SwgICv+IneSZwhBh1oSyEHA71A==", + "license": "BSD-3-Clause", + "peer": true, + "dependencies": { + "es-define-property": "^1.0.1", + "side-channel": "^1.1.1" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/queue-lit": { "version": "1.5.2", "resolved": "https://registry.npmjs.org/queue-lit/-/queue-lit-1.5.2.tgz", @@ -5940,12 +7035,47 @@ ], "license": "MIT" }, - "node_modules/quick-format-unescaped": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz", - "integrity": "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==", - "dev": true, - "license": "MIT" + "node_modules/range-parser": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.3.0.tgz", + "integrity": "sha512-hek2mFQpPuI4E1BBKrSto+BU3e3x4xuarsbiwr3+lf7p44juvFMV0XFWQAP3xUyqXA4RrXLIoaSUGbSt056ZMw==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/raw-body": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", + "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", + "license": "MIT", + "peer": true, + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.7.0", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/react": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } }, "node_modules/react-is": { "version": "16.13.1", @@ -5954,21 +7084,20 @@ "dev": true, "license": "MIT" }, - "node_modules/readable-stream": { - "version": "4.7.0", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz", - "integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==", - "dev": true, + "node_modules/react-reconciler": { + "version": "0.29.2", + "resolved": "https://registry.npmjs.org/react-reconciler/-/react-reconciler-0.29.2.tgz", + "integrity": "sha512-zZQqIiYgDCTP/f1N/mAR10nJGrPD2ZR+jDSEsKWJHYC7Cm2wodlwbR3upZRdC3cjIjSlTLNVyO7Iu0Yy7t2AYg==", "license": "MIT", "dependencies": { - "abort-controller": "^3.0.0", - "buffer": "^6.0.3", - "events": "^3.3.0", - "process": "^0.11.10", - "string_decoder": "^1.3.0" + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": ">=0.10.0" + }, + "peerDependencies": { + "react": "^18.3.1" } }, "node_modules/readdirp": { @@ -5984,16 +7113,6 @@ "node": ">=8.10.0" } }, - "node_modules/real-require": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/real-require/-/real-require-0.2.0.tgz", - "integrity": "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 12.13.0" - } - }, "node_modules/reflect.getprototypeof": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", @@ -6038,12 +7157,12 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/require-directory": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", - "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", - "dev": true, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", "license": "MIT", + "peer": true, "engines": { "node": ">=0.10.0" } @@ -6092,6 +7211,22 @@ "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" } }, + "node_modules/restore-cursor": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-4.0.0.tgz", + "integrity": "sha512-I9fPXU9geO9bHOt9pHHOhOkYerIMsmVaWB0rA2AI9ERh/+x/i7MV5HKBNrg+ljO5eoPVgCcnFuRjJ9uH6I/3eg==", + "license": "MIT", + "dependencies": { + "onetime": "^5.1.0", + "signal-exit": "^3.0.2" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/reusify": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", @@ -6137,6 +7272,35 @@ "@rolldown/binding-win32-x64-msvc": "1.1.5" } }, + "node_modules/router": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/run-applescript": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-7.1.0.tgz", + "integrity": "sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -6181,27 +7345,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, "node_modules/safe-push-apply": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz", @@ -6237,22 +7380,21 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/safe-stable-stringify": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz", - "integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==", - "dev": true, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "license": "MIT", - "engines": { - "node": ">=10" - } + "peer": true }, - "node_modules/secure-json-parse": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/secure-json-parse/-/secure-json-parse-2.7.0.tgz", - "integrity": "sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw==", - "dev": true, - "license": "BSD-3-Clause" + "node_modules/scheduler": { + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + } }, "node_modules/semver": { "version": "6.3.1", @@ -6264,6 +7406,53 @@ "semver": "bin/semver.js" } }, + "node_modules/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", + "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", + "license": "MIT", + "peer": true, + "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" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/serve-static": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz", + "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==", + "license": "MIT", + "peer": true, + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/set-function-length": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", @@ -6313,11 +7502,17 @@ "node": ">= 0.4" } }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC", + "peer": true + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, "license": "MIT", "dependencies": { "shebang-regex": "^3.0.0" @@ -6330,7 +7525,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -6340,7 +7534,6 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz", "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==", - "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -6360,7 +7553,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", - "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -6377,7 +7569,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", - "dev": true, "license": "MIT", "dependencies": { "call-bound": "^1.0.2", @@ -6396,7 +7587,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", - "dev": true, "license": "MIT", "dependencies": { "call-bound": "^1.0.2", @@ -6419,6 +7609,12 @@ "dev": true, "license": "ISC" }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "license": "ISC" + }, "node_modules/slash": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/slash/-/slash-5.1.0.tgz", @@ -6432,14 +7628,47 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/sonic-boom": { - "version": "3.8.1", - "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-3.8.1.tgz", - "integrity": "sha512-y4Z8LCDBuum+PBP3lSV7RHrXscqksve/bi0as7mhwVnBW+/wUqKT/2Kb7um8yqcFy0duYbbPxzt89Zy2nOCaxg==", - "dev": true, + "node_modules/slice-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-7.1.2.tgz", + "integrity": "sha512-iOBWFgUX7caIZiuutICxVgX1SdxwAVFFKwt1EvMYYec/NWO5meOJ6K5uQxhrYBdQJne4KxiqZc+KptFOWFSI9w==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "is-fullwidth-code-point": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/slice-ansi/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/slice-ansi/node_modules/is-fullwidth-code-point": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-5.1.0.tgz", + "integrity": "sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ==", "license": "MIT", "dependencies": { - "atomic-sleep": "^1.0.0" + "get-east-asian-width": "^1.3.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/source-map-js": { @@ -6452,14 +7681,25 @@ "node": ">=0.10.0" } }, - "node_modules/split2": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", - "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", - "dev": true, - "license": "ISC", + "node_modules/stack-utils": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", + "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^2.0.0" + }, "engines": { - "node": ">= 10.x" + "node": ">=10" + } + }, + "node_modules/stack-utils/node_modules/escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "license": "MIT", + "engines": { + "node": ">=8" } }, "node_modules/stackback": { @@ -6469,6 +7709,27 @@ "dev": true, "license": "MIT" }, + "node_modules/standardwebhooks": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/standardwebhooks/-/standardwebhooks-1.0.0.tgz", + "integrity": "sha512-BbHGOQK9olHPMvQNHWul6MYlrRTAOKn03rOe4A8O3CLWhNf4YHBqq2HJKKC+sfqpxiBY52pNeesD6jIiLDz8jg==", + "license": "MIT", + "peer": true, + "dependencies": { + "@stablelib/base64": "^1.0.0", + "fast-sha256": "^1.3.0" + } + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/std-env": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.2.0.tgz", @@ -6490,31 +7751,6 @@ "node": ">= 0.4" } }, - "node_modules/string_decoder": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", - "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", - "dev": true, - "license": "MIT", - "dependencies": { - "safe-buffer": "~5.2.0" - } - }, - "node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/string.prototype.matchall": { "version": "4.0.12", "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.12.tgz", @@ -6614,19 +7850,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/strip-bom": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", @@ -6690,16 +7913,6 @@ "url": "https://opencollective.com/webpack" } }, - "node_modules/thread-stream": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-2.7.0.tgz", - "integrity": "sha512-qQiRWsU/wvNolI6tbbCKd9iKaTnCXsTwVxhhKM6nctPdujTyztjlbUkUTUymidWcMnZ5pWR0ej4a0tjsW021vw==", - "dev": true, - "license": "MIT", - "dependencies": { - "real-require": "^0.2.0" - } - }, "node_modules/tinybench": { "version": "2.9.0", "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", @@ -6788,6 +8001,35 @@ "node": ">=8.0" } }, + "node_modules/to-rotated": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/to-rotated/-/to-rotated-1.0.0.tgz", + "integrity": "sha512-KsEID8AfgUy+pxVRLsWp0VzCa69wxzUDZnzGbyIST/bcgcrMvTYoFBX/QORH4YApoD89EDuUovx4BTdpOn319Q==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=0.6" + } + }, + "node_modules/ts-algebra": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ts-algebra/-/ts-algebra-2.0.0.tgz", + "integrity": "sha512-FPAhNPFMrkwz76P7cdjdmiShwMynZYN6SgOujD1urY4oNm80Ou9oMdmbR45LotcKOXoy7wSmHkRFE6Mxbrhefw==", + "license": "MIT", + "peer": true + }, "node_modules/ts-api-utils": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", @@ -6943,6 +8185,51 @@ "node": ">= 0.8.0" } }, + "node_modules/type-fest": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", + "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/type-is": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.1.0.tgz", + "integrity": "sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==", + "license": "MIT", + "peer": true, + "dependencies": { + "content-type": "^2.0.0", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/type-is/node_modules/content-type": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.0.0.tgz", + "integrity": "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/typed-array-buffer": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", @@ -7098,6 +8385,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/uri-js": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", @@ -7108,12 +8405,15 @@ "punycode": "^2.1.0" } }, - "node_modules/util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", - "dev": true, - "license": "MIT" + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.8" + } }, "node_modules/vite": { "version": "8.1.5", @@ -7313,7 +8613,6 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, "license": "ISC", "dependencies": { "isexe": "^2.0.0" @@ -7431,78 +8730,122 @@ "node": ">=8" } }, - "node_modules/word-wrap": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", - "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", - "dev": true, + "node_modules/widest-line": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/widest-line/-/widest-line-5.0.0.tgz", + "integrity": "sha512-c9bZp7b5YtRj2wOe6dlj32MK+Bx/M/d+9VB2SHM1OtsUHR0aV0tdP6DWh/iMt0kWi1t5g1Iudu6hQRNd1A4PVA==", "license": "MIT", + "dependencies": { + "string-width": "^7.0.0" + }, "engines": { - "node": ">=0.10.0" + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/wrap-ansi": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dev": true, + "node_modules/widest-line/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", "license": "MIT", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, "engines": { - "node": ">=10" + "node": ">=12" }, "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + "url": "https://github.com/chalk/ansi-regex?sponsor=1" } }, - "node_modules/wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "dev": true, - "license": "ISC" + "node_modules/widest-line/node_modules/emoji-regex": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", + "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", + "license": "MIT" }, - "node_modules/y18n": { - "version": "5.0.8", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", - "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", - "dev": true, - "license": "ISC", + "node_modules/widest-line/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, "engines": { - "node": ">=10" + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/yargs": { - "version": "17.7.3", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.3.tgz", - "integrity": "sha512-GZtjxm/J/4TSxuL3FNYjCmLktBTnIw/rVmKSIyKeYAZpmJB2ig9VauCC5xsa82GNKVKDAqpOn3KVzNt0zmrU0g==", - "dev": true, + "node_modules/widest-line/node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", "license": "MIT", "dependencies": { - "cliui": "^8.0.1", - "escalade": "^3.1.1", - "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", - "string-width": "^4.2.3", - "y18n": "^5.0.5", - "yargs-parser": "^21.1.1" + "ansi-regex": "^6.2.2" }, "engines": { "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" } }, - "node_modules/yargs-parser": { - "version": "21.1.1", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", - "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", "license": "ISC", + "peer": true + }, + "node_modules/ws": { + "version": "8.21.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.1.tgz", + "integrity": "sha512-+0NTnW77fFN/DjQi6k/Sq/Yvk4Sgajw7urW8V+asjXnRgDs9gyGkdb7EzgfhA4goXsRIZKE28fzIXBHEzhuiWw==", + "license": "MIT", "engines": { - "node": ">=12" + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/wsl-utils": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/wsl-utils/-/wsl-utils-0.1.0.tgz", + "integrity": "sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw==", + "license": "MIT", + "dependencies": { + "is-wsl": "^3.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/yocto-queue": { @@ -7517,6 +8860,32 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } + }, + "node_modules/yoga-layout": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/yoga-layout/-/yoga-layout-3.2.1.tgz", + "integrity": "sha512-0LPOt3AxKqMdFBZA3HBAt/t/8vIKq7VaQYbuA8WxCgung+p9TVyKRYdpvCb80HcdTN2NkbIKbhNwKUfm3tQywQ==", + "license": "MIT" + }, + "node_modules/zod": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz", + "integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==", + "license": "MIT", + "peer": true, + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-to-json-schema": { + "version": "3.25.2", + "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.25.2.tgz", + "integrity": "sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA==", + "license": "ISC", + "peer": true, + "peerDependencies": { + "zod": "^3.25.28 || ^4" + } } } } diff --git a/package.json b/package.json index 38ded7b..5d1da93 100644 --- a/package.json +++ b/package.json @@ -34,7 +34,8 @@ "src", "!src/bin", "!test", - "!**/*.test.ts" + "!**/*.test.ts", + "!**/*.test.tsx" ], "scripts": { "build": "npm run build:entrypoints", @@ -55,8 +56,6 @@ "postversion": "git push --follow-tags", "wizard": "tsx src/bin/cli.ts", "inspect": "tsx --inspect src/bin/cli.ts", - "example": "tsx examples", - "example:inspect": "tsx --inspect examples", "format": "prettier --write --ignore-path .gitignore .", "preformat": "eslint --fix .", "report": "vitest run --coverage" @@ -76,19 +75,27 @@ } }, "dependencies": { - "minimist": "^1.2.8" + "@anthropic-ai/claude-agent-sdk": "^0.3.216", + "ink": "^5.2.1", + "ink-select-input": "^6.2.0", + "ink-spinner": "^5.0.0", + "ink-text-input": "^6.0.0", + "minimist": "^1.2.8", + "open": "^10.1.0", + "react": "^18.3.1" }, "devDependencies": { "@types/minimist": "^1.2.5", "@types/node": "^24.10.9", + "@types/react": "^18.3.27", "@vitest/coverage-v8": "^4.1.10", "del-cli": "^7.0.0", "eslint": "^9.31.0", "eslint-plugin-import": "^2.32.0", "eslint-plugin-simple-import-sort": "^12.1.1", "eslint-plugin-unused-imports": "^4.1.4", + "ink-testing-library": "^4.0.0", "jiti": "^2.4.2", - "landlubber": "^2.0.0", "neostandard": "^0.13.0", "prettier": "^3.0.0", "tsc-alias": "^1.8.2", diff --git a/src/bin/cli.ts b/src/bin/cli.ts index f6af9e1..4adb58d 100644 --- a/src/bin/cli.ts +++ b/src/bin/cli.ts @@ -7,7 +7,16 @@ import wizard from 'lib/wizard.js' -wizard({ argv: process.argv.slice(2) }).catch((err: unknown) => { +// The wizard sets up whichever project it is run in, which here is this +// repository. '--cwd ' points it at a scratch project instead. It is +// consumed here rather than forwarded, since it is an option of the wizard +// rather than one of its arguments. Every other argument is passed through +// untouched, so 'npm run wizard -- --help' reaches the wizard as '--help'. +const argv = process.argv.slice(2) +const cwdIndex = argv.indexOf('--cwd') +const cwd = cwdIndex === -1 ? undefined : argv.splice(cwdIndex, 2)[1] + +wizard({ argv, ...(cwd == null ? {} : { cwd }) }).catch((err: unknown) => { const { message, stack } = err instanceof Error ? err : new Error(String(err)) // eslint-disable-next-line no-console console.error(`Wizard Error: ${message}\n${stack ?? ''}`) diff --git a/src/lib/app.test.tsx b/src/lib/app.test.tsx new file mode 100644 index 0000000..0697ecf --- /dev/null +++ b/src/lib/app.test.tsx @@ -0,0 +1,27 @@ +import { render } from 'ink-testing-library' +import { afterEach, expect, test, vi } from 'vitest' + +import { App } from './app.js' + +// A project root that does not exist, with no key in the environment, keeps the +// render offline: the first phase looks for an existing key, finds none, and +// returns before making a request. A full interactive run of the wizard cannot +// be exercised headlessly, so this covers the mount and the first frame only. +const NONEXISTENT_ROOT = '/nonexistent/seam-wizard-test-project' + +afterEach(() => { + vi.unstubAllEnvs() +}) + +test('App: renders the first frame while looking for an existing key', () => { + vi.stubEnv('SEAM_API_KEY', '') + + const { lastFrame, unmount } = render() + try { + const frame = lastFrame() ?? '' + expect(frame).toContain('Seam setup wizard') + expect(frame).toContain('Checking for an existing key') + } finally { + unmount() + } +}) diff --git a/src/lib/app.tsx b/src/lib/app.tsx new file mode 100644 index 0000000..9962243 --- /dev/null +++ b/src/lib/app.tsx @@ -0,0 +1,989 @@ +import { Box, Text, useApp, useStdout } from 'ink' +import SelectInput from 'ink-select-input' +import Spinner from 'ink-spinner' +import TextInput from 'ink-text-input' +import { + type ReactElement, + type ReactNode, + useEffect, + useRef, + useState, +} from 'react' + +import { CheckboxList } from './components/checkbox-list.js' +import { + analyzeProject, + type ProjectAnalysis, +} from './steps/analyze-project.js' +import { + findVerifiedExistingKey, + verifyAndSaveKey, +} from './steps/authenticate.js' +import { + type BuildMode, + COMMON_BLOCKS, + composeGoal, + CORE_BLOCKS, + type OnboardingRecord, + writeOnboardingRecord, +} from './steps/build-plan.js' +import { connectViaWeb } from './steps/connect-web.js' +import { + detectProject, + installSeamSdkCommand, + type ProjectInfo, + type Sdk, +} from './steps/detect-project.js' +import { + CLAUDE_CODE_COMMANDS, + detectPluginTarget, + SEAM_PLUGIN_NPX_COMMAND, +} from './steps/install-seam-plugin.js' +import { type IntegrateEvent, runIntegration } from './steps/integrate.js' +import { findExistingApiKey } from './util/env-file.js' +import { runInstall } from './util/run-install.js' +import { + ApiKeyError, + exchangeWizardInferenceToken, + looksLikeSeamApiKey, + SEAM_INFERENCE_BASE_URL, + type SeamWorkspace, + type WizardInferenceSession, +} from './util/seam-api.js' + +const MAX_ATTEMPTS = 3 + +type Tone = 'ok' | 'info' | 'warn' | 'plain' +interface Msg { + tone: Tone + text: string +} + +type Phase = + | { t: 'init' } + | { t: 'method' } + | { t: 'browser' } + | { t: 'paste' } + | { t: 'verify-paste'; api_key: string } + | { t: 'sdk' } + | { t: 'install-sdk' } + | { t: 'install-plugin' } + | { t: 'offer-integrate' } + | { t: 'analyze' } + | { t: 'integrate-mode' } + | { t: 'checklist' } + | { t: 'note' } + | { t: 'integrate'; goal: string } + | { t: 'done' } + | { t: 'error'; message: string } + +export function App({ + root, + onExit, +}: { + root: string + onExit?: (lines: string[]) => void +}): ReactElement { + const { exit } = useApp() + const { stdout } = useStdout() + const projectRef = useRef(detectProject(root)) + const attemptRef = useRef(0) + + const [dimensions, setDimensions] = useState({ + rows: stdout?.rows ?? 24, + columns: stdout?.columns ?? 80, + }) + + const [messages, setMessages] = useState([]) + const [phase, setPhase] = useState({ t: 'init' }) + const [sdk, setSdk] = useState(null) + const [workspace, setWorkspace] = useState(null) + + const [browser, setBrowser] = useState<{ + url: string | null + received: boolean + }>({ + url: null, + received: false, + }) + const [pasteValue, setPasteValue] = useState('') + const [pasteError, setPasteError] = useState(null) + const [installLines, setInstallLines] = useState([]) + const [noteValue, setNoteValue] = useState('') + const [agentLines, setAgentLines] = useState([]) + const [session, setSession] = useState(null) + const [analysis, setAnalysis] = useState(null) + const [mode, setMode] = useState(null) + const [selections, setSelections] = useState([]) + + // The plan record (written to .seam/onboarding.json before the agent runs); + // the done handler rewrites it with the run result. + const planRef = useRef | null>(null) + + // Kept in sync with `messages` so the exit handler can hand the full + // transcript to index.tsx to reprint after leaving the alt screen. + const messagesRef = useRef([]) + const addMessage = (message: Msg): void => + setMessages((previous) => { + const next = [...previous, message] + messagesRef.current = next + return next + }) + + const finishWithNextSteps = (): void => { + pushNextSteps(addMessage, sdk, workspace?.name ?? 'your workspace') + setPhase({ t: 'done' }) + } + + // Compose the goal from the chosen mode + building blocks + optional note, + // persist the plan to .seam/onboarding.json, then hand off to the agent. + const startIntegration = (noteInput: string): void => { + const note = noteInput.trim().length > 0 ? noteInput.trim() : null + const effectiveMode: BuildMode = mode ?? 'full_api' + const effectiveSelections = + effectiveMode === 'customer_portal' ? [] : selections + const goal = composeGoal({ + mode: effectiveMode, + selections: effectiveSelections, + note, + framework: analysis?.signals.framework ?? null, + }) + const record: Omit = { + created_at: new Date().toISOString(), + mode: effectiveMode, + selections: effectiveSelections, + note, + goal, + analysis: { + sdk: analysis?.signals.sdk ?? null, + framework: analysis?.signals.framework ?? null, + app_type_guess: analysis?.recommendation.app_type_guess ?? null, + seam_already_setup: analysis?.signals.seam_already_setup ?? false, + used_onboarding: analysis?.used_onboarding ?? false, + recommendation_source: analysis?.recommendation.source ?? 'heuristic', + }, + } + planRef.current = record + try { + writeOnboardingRecord(root, record) + addMessage({ + tone: 'info', + text: 'Saved your plan to .seam/onboarding.json', + }) + } catch { + // Writing the record is best-effort; proceed with the integration. + } + setPhase({ t: 'integrate', goal }) + } + + const handleIntegrateEvent = (event: IntegrateEvent): void => { + if (event.kind === 'text') { + setAgentLines((previous) => [ + ...previous.slice(-5), + truncate(event.text, 100), + ]) + } else if (event.kind === 'tool') { + setAgentLines((previous) => [ + ...previous.slice(-5), + formatTool(event.name, event.detail), + ]) + } else { + setAgentLines([]) + if (planRef.current != null) { + try { + writeOnboardingRecord(root, { + ...planRef.current, + result: { + ok: event.ok, + files_summary: event.summary.trim().slice(0, 4000), + cost_usd: event.cost_usd, + }, + }) + } catch { + // Recording the result is best-effort; never block finishing. + } + } + addMessage( + event.ok + ? { + tone: 'ok', + text: 'Integration written — review it with `git diff`', + } + : { + tone: 'warn', + text: 'Agent stopped early — review what it changed with `git diff`', + }, + ) + for (const line of event.summary.trim().split('\n').slice(0, 15)) { + if (line.trim().length > 0) { + addMessage({ tone: 'plain', text: ` ${line}` }) + } + } + if (event.cost_usd != null) { + addMessage({ + tone: 'info', + text: `Model cost: $${event.cost_usd.toFixed(2)}`, + }) + } + } + } + + // Once a workspace is known, choose SDK (or skip if detected) then install. + const advanceAfterAuth = (): void => { + const detected = projectRef.current.detected_sdk + if (detected != null) { + setSdk(detected) + addMessage({ tone: 'info', text: `Detected ${detected} project` }) + setPhase({ t: 'install-sdk' }) + } else { + setPhase({ t: 'sdk' }) + } + } + + // init: reuse an existing key if valid, else ask how to connect. + useEffect(() => { + if (phase.t !== 'init') return + let cancelled = false + const run = async (): Promise => { + const existing = await findVerifiedExistingKey(root) + if (cancelled) return + if (existing != null) { + setWorkspace(existing.workspace) + addMessage({ + tone: 'ok', + text: `Using existing key from ${existing.source} · workspace ${existing.workspace.name}`, + }) + advanceAfterAuth() + } else { + setPhase({ t: 'method' }) + } + } + run().catch((error: unknown) => { + if (cancelled) return + setPhase({ + t: 'error', + message: + error instanceof Error + ? error.message + : 'The wizard hit an unexpected error.', + }) + }) + return () => { + cancelled = true + } + }, [phase.t]) + + // browser handoff + useEffect(() => { + if (phase.t !== 'browser') return + let cancelled = false + const run = async (): Promise => { + try { + const result = await connectViaWeb(root, { + onUrl: (url) => !cancelled && setBrowser((b) => ({ ...b, url })), + onReceived: () => + !cancelled && setBrowser((b) => ({ ...b, received: true })), + }) + if (cancelled) return + setWorkspace(result.workspace) + addMessage({ + tone: 'ok', + text: `Connected · workspace ${result.workspace.name}`, + }) + advanceAfterAuth() + } catch (error) { + if (!cancelled) { + setPhase({ + t: 'error', + message: + error instanceof Error + ? error.message + : 'Browser connection failed.', + }) + } + } + } + run().catch((error: unknown) => { + if (cancelled) return + setPhase({ + t: 'error', + message: + error instanceof Error + ? error.message + : 'The wizard hit an unexpected error.', + }) + }) + return () => { + cancelled = true + } + }, [phase.t]) + + // verify a pasted key + useEffect(() => { + if (phase.t !== 'verify-paste') return + const { api_key: apiKey } = phase + let cancelled = false + const run = async (): Promise => { + try { + const result = await verifyAndSaveKey(root, apiKey) + if (cancelled) return + setWorkspace(result.workspace) + addMessage({ tone: 'ok', text: `Workspace: ${result.workspace.name}` }) + advanceAfterAuth() + } catch (error) { + if (cancelled) return + const message = + error instanceof ApiKeyError + ? error.message + : "Couldn't verify the key." + if (attemptRef.current >= MAX_ATTEMPTS) { + setPhase({ + t: 'error', + message: 'Too many attempts. Re-run with a valid key.', + }) + } else { + setPasteError(message) + setPasteValue('') + setPhase({ t: 'paste' }) + } + } + } + run().catch((error: unknown) => { + if (cancelled) return + setPhase({ + t: 'error', + message: + error instanceof Error + ? error.message + : 'The wizard hit an unexpected error.', + }) + }) + return () => { + cancelled = true + } + }, [phase.t]) + + // install the Seam SDK (streamed), then install the plugin + useEffect(() => { + if (phase.t !== 'install-sdk' || sdk == null) return + let cancelled = false + const command = installSeamSdkCommand(sdk, projectRef.current) + const run = async (): Promise => { + try { + await runInstall(command, root, (line) => { + if (!cancelled) { + setInstallLines((previous) => [...previous.slice(-3), line]) + } + }) + if (!cancelled) addMessage({ tone: 'ok', text: 'Seam SDK installed' }) + } catch { + if (!cancelled) { + addMessage({ + tone: 'warn', + text: `Couldn't finish installing — run it yourself: ${command.join(' ')}`, + }) + } + } + if (!cancelled) { + setInstallLines([]) + setPhase({ t: 'install-plugin' }) + } + } + run().catch((error: unknown) => { + if (cancelled) return + setPhase({ + t: 'error', + message: + error instanceof Error + ? error.message + : 'The wizard hit an unexpected error.', + }) + }) + return () => { + cancelled = true + } + }, [phase.t, sdk]) + + // install the official Seam plugin skills, then finish. We always run the + // universal installer (works everywhere); for Claude Code we additionally + // point at the native /plugin path, which also wires up the seam-docs MCP. + useEffect(() => { + if (phase.t !== 'install-plugin') return + const target = detectPluginTarget(root) + + let cancelled = false + const run = async (): Promise => { + try { + await runInstall(SEAM_PLUGIN_NPX_COMMAND, root, (line) => { + if (!cancelled) { + setInstallLines((previous) => [...previous.slice(-3), line]) + } + }) + if (!cancelled) { + addMessage({ tone: 'ok', text: 'Installed the Seam plugin skills' }) + } + } catch { + if (!cancelled) { + addMessage({ + tone: 'warn', + text: `Couldn't install the plugin — run it yourself: ${SEAM_PLUGIN_NPX_COMMAND.join(' ')}`, + }) + } + } + if (!cancelled) { + if (target === 'claude-code') { + addMessage({ + tone: 'info', + text: 'Claude Code: for the native plugin + seam-docs MCP, you can also run:', + }) + for (const command of CLAUDE_CODE_COMMANDS) { + addMessage({ tone: 'plain', text: ` ${command}` }) + } + } + setPhase({ t: 'offer-integrate' }) + } + } + run().catch((error: unknown) => { + if (cancelled) return + setPhase({ + t: 'error', + message: + error instanceof Error + ? error.message + : 'The wizard hit an unexpected error.', + }) + }) + return () => { + cancelled = true + } + }, [phase.t]) + + // analyze: exchange the API key for a scoped wizard token (which also returns + // the Console onboarding answers), scan the project, and get a mode/checklist + // recommendation to pre-fill the next steps. The token is reused by the + // integration agent below, so it is minted once. + useEffect(() => { + if (phase.t !== 'analyze') return + let cancelled = false + const run = async (): Promise => { + const found = findExistingApiKey(root) + if (found == null) { + addMessage({ + tone: 'warn', + text: "Couldn't find your Seam API key to plan the integration.", + }) + if (!cancelled) finishWithNextSteps() + return + } + + let currentSession: WizardInferenceSession + try { + currentSession = await exchangeWizardInferenceToken(found.api_key) + } catch (error) { + if (!cancelled) { + addMessage({ + tone: 'warn', + text: `Couldn't start the AI session: ${ + error instanceof Error ? error.message : 'unknown error' + }`, + }) + finishWithNextSteps() + } + return + } + if (cancelled) return + setSession(currentSession) + + const result = await analyzeProject({ + root, + project: projectRef.current, + onboarding: currentSession.onboarding, + inference: { + base_url: SEAM_INFERENCE_BASE_URL, + token: currentSession.token, + }, + }) + if (cancelled) return + setAnalysis(result) + setMode(result.recommendation.mode) + setSelections(result.recommendation.selections) + + const detail = [ + result.signals.framework ?? result.signals.sdk, + result.recommendation.app_type_guess, + ] + .filter((value): value is string => value != null && value.length > 0) + .join(' · ') + addMessage({ + tone: 'info', + text: `Analyzed your project${detail.length > 0 ? `: ${detail}` : ''}`, + }) + if (currentSession.onboarding != null) { + addMessage({ + tone: 'plain', + text: ' Used your Console onboarding answers', + }) + } + setPhase({ t: 'integrate-mode' }) + } + run().catch((error: unknown) => { + if (cancelled) return + setPhase({ + t: 'error', + message: + error instanceof Error + ? error.message + : 'The wizard hit an unexpected error.', + }) + }) + return () => { + cancelled = true + } + }, [phase.t]) + + // run the embedded integration agent (Claude Agent SDK), routed through + // Seam-hosted inference using the token minted during the analyze step — no + // developer Anthropic key. + useEffect(() => { + if (phase.t !== 'integrate') return + const { goal } = phase + const controller = new AbortController() + const run = async (): Promise => { + if (session == null) { + addMessage({ + tone: 'warn', + text: 'Lost the AI session — re-run the wizard to try again.', + }) + if (!controller.signal.aborted) finishWithNextSteps() + return + } + addMessage({ tone: 'info', text: 'Starting the Seam integration agent…' }) + try { + await runIntegration({ + root, + sdk: sdk ?? 'javascript', + workspace_name: workspace?.name ?? 'your workspace', + goal, + inference: { + base_url: SEAM_INFERENCE_BASE_URL, + token: session.token, + }, + framework: analysis?.signals.framework ?? null, + mode: mode ?? 'full_api', + signal: controller.signal, + onEvent: (event) => { + if (!controller.signal.aborted) handleIntegrateEvent(event) + }, + }) + } catch (error) { + if (!controller.signal.aborted) { + addMessage({ + tone: 'warn', + text: `Couldn't run the integration agent: ${ + error instanceof Error ? error.message : 'unknown error' + }`, + }) + } + } + if (!controller.signal.aborted) finishWithNextSteps() + } + run().catch((error: unknown) => { + if (controller.signal.aborted) return + setPhase({ + t: 'error', + message: + error instanceof Error + ? error.message + : 'The wizard hit an unexpected error.', + }) + }) + return () => controller.abort() + }, [phase.t]) + + // keep the full-screen layout sized to the terminal + useEffect(() => { + if (stdout == null) return + const onResize = (): void => + setDimensions({ rows: stdout.rows ?? 24, columns: stdout.columns ?? 80 }) + stdout.on('resize', onResize) + return () => { + stdout.off('resize', onResize) + } + }, [stdout]) + + // exit when finished + useEffect(() => { + if (phase.t !== 'done' && phase.t !== 'error') return + if (phase.t === 'error') { + addMessage({ tone: 'warn', text: phase.message }) + process.exitCode = 1 + } + // Hand the transcript back so index.tsx can reprint it once the alt screen + // is torn down (its contents are otherwise discarded). Small delay lets the + // final addMessage above flush into messagesRef. + const id = setTimeout(() => { + onExit?.(messagesRef.current.map(formatMessageLine)) + exit() + }, 40) + return () => clearTimeout(id) + }, [phase.t]) + + // Full-screen: a header, the transcript (bounded to what fits), then the + // active step. The outer box is sized to the terminal so it fills the alt + // screen; older transcript lines scroll off the top and are reprinted on exit. + const transcriptCapacity = Math.max(1, dimensions.rows - 8) + const visibleMessages = messages.slice(-transcriptCapacity) + + return ( + +
+ + {visibleMessages.map((message, index) => ( + + ))} + + {renderActive()} + + ) + + function renderActive(): ReactElement | null { + switch (phase.t) { + case 'init': + return + case 'method': + return ( + + { + if (item.value === 'paste') { + attemptRef.current = 0 + setPhase({ t: 'paste' }) + } else { + setPhase({ t: 'browser' }) + } + }} + /> + + ) + case 'browser': + return ( + + + {browser.url != null && {browser.url}} + + ) + case 'paste': + return ( + + + {'› '} + { + if (!looksLikeSeamApiKey(value)) { + setPasteError( + "That doesn't look like a Seam key (expected seam_…).", + ) + return + } + setPasteError(null) + attemptRef.current += 1 + setPhase({ t: 'verify-paste', api_key: value }) + }} + /> + + {pasteError != null && {pasteError}} + + ) + case 'verify-paste': + return + case 'sdk': + return ( + + { + const chosen: Sdk = + item.value === 'python' ? 'python' : 'javascript' + setSdk(chosen) + addMessage({ tone: 'info', text: `SDK: ${chosen}` }) + setPhase({ t: 'install-sdk' }) + }} + /> + + ) + case 'install-sdk': + return ( + + + {installLines.map((line, index) => ( + + {' '} + {line} + + ))} + + ) + case 'install-plugin': + return ( + + + {installLines.map((line, index) => ( + + {' '} + {line} + + ))} + + ) + case 'offer-integrate': + return ( + + { + if (item.value === 'yes') setPhase({ t: 'analyze' }) + else finishWithNextSteps() + }} + /> + + ) + case 'analyze': + return + case 'integrate-mode': { + const recommended: BuildMode = mode ?? 'full_api' + const portalItem = { + label: 'Customer Portal — Seam hosts the UI (you call ~2 endpoints)', + value: 'customer_portal', + } + const apiItem = { + label: 'Full API control — you build the UI, wire up the API', + value: 'full_api', + } + const items = + recommended === 'customer_portal' + ? [portalItem, apiItem] + : [apiItem, portalItem] + return ( + + {analysis?.recommendation.rationale != null && + analysis.recommendation.rationale.length > 0 && ( + {` ${analysis.recommendation.rationale}`} + )} + { + const chosen: BuildMode = + item.value === 'customer_portal' + ? 'customer_portal' + : 'full_api' + setMode(chosen) + addMessage({ + tone: 'info', + text: `Mode: ${ + chosen === 'customer_portal' + ? 'Customer Portal' + : 'Full API' + }`, + }) + setPhase( + chosen === 'customer_portal' + ? { t: 'note' } + : { t: 'checklist' }, + ) + }} + /> + + ) + } + case 'checklist': + return ( + + ({ + id: block.id, + label: block.label, + group: block.group, + }))} + initial_selected={selections} + onSubmit={(chosen) => { + setSelections(chosen) + setPhase({ t: 'note' }) + }} + /> + + ) + case 'note': + return ( + + + {'› '} + startIntegration(value)} + /> + + + ) + case 'integrate': + return ( + + + {agentLines.map((line, index) => ( + + {' '} + {line} + + ))} + + ) + case 'done': + case 'error': + return null + } + } +} + +function pushNextSteps( + addMessage: (message: Msg) => void, + sdk: Sdk | null, + workspaceName: string, +): void { + const envHint = + sdk === 'python' + ? "Make sure SEAM_API_KEY is exported (it's in .env)." + : 'Add .env to .gitignore — it holds your API key.' + addMessage({ tone: 'plain', text: '' }) + addMessage({ tone: 'ok', text: `You're set up in ${workspaceName}` }) + addMessage({ tone: 'plain', text: 'Next steps:' }) + addMessage({ + tone: 'plain', + text: ' 1. Describe your integration to your AI assistant — e.g. "add Seam access grants". The Seam skill will guide it.', + }) + addMessage({ tone: 'plain', text: ` 2. ${envHint}` }) + addMessage({ tone: 'plain', text: ' 3. Docs: https://docs.seam.co' }) +} + +function truncate(text: string, maxLength: number): string { + const collapsed = text.replace(/\s+/g, ' ').trim() + return collapsed.length > maxLength + ? `${collapsed.slice(0, maxLength - 1)}…` + : collapsed +} + +// A compact one-line label for a tool the agent just invoked. +function formatTool(name: string, detail: string): string { + const verb = + name === 'Write' + ? 'write' + : name === 'Edit' + ? 'edit' + : name === 'Read' + ? 'read' + : name === 'Glob' || name === 'Grep' + ? 'search' + : name === 'WebFetch' + ? 'fetch' + : name.startsWith('mcp__seam-docs__') + ? 'docs' + : name + return detail.length > 0 ? `${verb} ${truncate(detail, 60)}` : verb +} + +function Header(): ReactElement { + return ( + + + {' Seam setup wizard '} + + + ) +} + +// Plain-text rendering of a message, matching MessageLine's symbols — used to +// reprint the transcript into the normal terminal after the alt screen closes. +function formatMessageLine(message: Msg): string { + if (message.tone === 'plain') return message.text + const symbol = + message.tone === 'ok' ? '✔' : message.tone === 'warn' ? '▲' : '•' + return `${symbol} ${message.text}` +} + +function MessageLine({ message }: { message: Msg }): ReactElement { + if (message.tone === 'plain') return {message.text} + const symbol = + message.tone === 'ok' ? '✔' : message.tone === 'warn' ? '▲' : '•' + const color = + message.tone === 'ok' + ? 'green' + : message.tone === 'warn' + ? 'yellow' + : 'cyan' + return ( + + {symbol} {message.text} + + ) +} + +function Pending({ label }: { label: string }): ReactElement { + return ( + + + + {' '} + {label} + + ) +} + +function Prompt({ + title, + children, +}: { + title: string + children: ReactNode +}): ReactElement { + return ( + + {title} + {children} + + ) +} diff --git a/src/lib/components/checkbox-list.tsx b/src/lib/components/checkbox-list.tsx new file mode 100644 index 0000000..f8d7596 --- /dev/null +++ b/src/lib/components/checkbox-list.tsx @@ -0,0 +1,72 @@ +import { Box, Text, useInput } from 'ink' +import { Fragment, type ReactElement, useState } from 'react' + +export interface CheckboxItem { + id: string + label: string + group?: string +} + +// A minimal multi-select: ↑/↓ to move, space to toggle, enter to confirm. Ink +// ships single-select (ink-select-input) but no checkbox list, so we hand-roll +// one over useInput. Only mounted during the checklist phase. +export function CheckboxList({ + items, + initial_selected: initialSelected, + onSubmit, +}: { + items: CheckboxItem[] + initial_selected: string[] + onSubmit: (selected: string[]) => void +}): ReactElement { + const [cursor, setCursor] = useState(0) + const [selected, setSelected] = useState>( + () => new Set(initialSelected), + ) + + useInput((input, key) => { + if (key.upArrow || input === 'k') { + setCursor((current) => (current - 1 + items.length) % items.length) + } else if (key.downArrow || input === 'j') { + setCursor((current) => (current + 1) % items.length) + } else if (input === ' ') { + const id = items[cursor]?.id + if (id == null) return + setSelected((previous) => { + const next = new Set(previous) + if (next.has(id)) next.delete(id) + else next.add(id) + return next + }) + } else if (key.return) { + onSubmit( + items.filter((item) => selected.has(item.id)).map((item) => item.id), + ) + } + }) + + return ( + + {items.map((item, index) => { + const isCursor = index === cursor + const isChecked = selected.has(item.id) + const showGroup = + item.group != null && item.group !== items[index - 1]?.group + return ( + + {showGroup && {` ${item.group ?? ''}`}} + {/* Non-cursor rows omit `color` entirely (rather than passing + undefined, which exactOptionalPropertyTypes rejects) so they + inherit the terminal's default color. */} + + {isCursor ? '❯ ' : ' '} + {isChecked ? '◉ ' : '◯ '} + {item.label} + + + ) + })} + {' ↑/↓ move · space toggle · enter confirm'} + + ) +} diff --git a/src/lib/index.ts b/src/lib/index.ts index f3b05e0..3069cf6 100644 --- a/src/lib/index.ts +++ b/src/lib/index.ts @@ -1,2 +1 @@ -export { todo } from './todo.js' export { default, type WizardOptions } from './wizard.js' diff --git a/src/lib/render.tsx b/src/lib/render.tsx new file mode 100644 index 0000000..6b79ac1 --- /dev/null +++ b/src/lib/render.tsx @@ -0,0 +1,49 @@ +import { render } from 'ink' + +import { App } from './app.js' + +const ESC = String.fromCharCode(27) +// Enter alt screen + clear + home / leave alt screen (restores normal buffer). +const ENTER_ALT_SCREEN = `${ESC}[?1049h${ESC}[2J${ESC}[H` +const LEAVE_ALT_SCREEN = `${ESC}[?1049l` + +export interface RenderAppOptions { + /** The project root the wizard sets up. */ + root: string +} + +/** + * Render the wizard full-screen and resolve once the user has finished. + * + * The app runs in the alternate screen buffer (like less or vim). Entering it + * before the first render avoids a flash of the initial frame in the normal + * buffer. The alternate buffer is discarded on the way out, so the transcript + * the app hands back is reprinted afterwards, leaving the usual record of the + * run behind in the terminal's scrollback. + * + * Rejects if the app fails to run, leaving the terminal restored either way. + */ +export const renderApp = async ({ root }: RenderAppOptions): Promise => { + const isTty = Boolean(process.stdout.isTTY) + if (isTty) process.stdout.write(ENTER_ALT_SCREEN) + + let transcript: readonly string[] = [] + + const app = render( + { + transcript = lines + }} + />, + ) + + try { + await app.waitUntilExit() + } finally { + if (isTty) process.stdout.write(LEAVE_ALT_SCREEN) + if (transcript.length > 0) { + process.stdout.write(`\n${transcript.join('\n')}\n`) + } + } +} diff --git a/src/lib/steps/analyze-project.ts b/src/lib/steps/analyze-project.ts new file mode 100644 index 0000000..8f7d642 --- /dev/null +++ b/src/lib/steps/analyze-project.ts @@ -0,0 +1,333 @@ +import { existsSync, readFileSync } from 'node:fs' +import { join } from 'node:path' + +import { findExistingApiKey } from 'lib/util/env-file.js' +import { + callInferenceForText, + type WizardOnboarding, +} from 'lib/util/seam-api.js' + +import { ALL_BLOCKS, type BuildMode } from './build-plan.js' +import type { ProjectInfo, Sdk } from './detect-project.js' + +// A cheap classification model — the deep work happens in the integration +// agent, so the pre-analysis only needs a good recommendation, not deep +// reasoning. +const RECOMMENDATION_MODEL = 'claude-haiku-4-5' + +export interface ProjectSignals { + sdk: Sdk | null + framework: string | null + package_name: string | null + description: string | null + keywords: string[] + dependency_names: string[] + readme_excerpt: string | null + seam_already_setup: boolean +} + +export interface Recommendation { + mode: BuildMode + selections: string[] + app_type_guess: string | null + rationale: string + source: 'llm' | 'heuristic' +} + +export interface ProjectAnalysis { + signals: ProjectSignals + recommendation: Recommendation + used_onboarding: boolean +} + +const VALID_BLOCK_IDS = new Set(ALL_BLOCKS.map((block) => block.id)) + +// Gather what the project reveals about itself, then recommend a mode + build +// blocks by combining those signals with the Console onboarding answers. The +// recommendation comes from a short LLM call, falling back to a deterministic +// heuristic if that call fails or returns something unusable. +export async function analyzeProject(args: { + root: string + project: ProjectInfo + onboarding: WizardOnboarding | null + inference: { base_url: string; token: string } +}): Promise { + const { root, project, onboarding, inference } = args + const signals = gatherProjectSignals(root, project) + + let recommendation: Recommendation + try { + recommendation = await recommendViaLlm(signals, onboarding, inference) + } catch { + recommendation = heuristicRecommendation(signals, onboarding) + } + + return { + signals, + recommendation, + used_onboarding: onboarding != null, + } +} + +function gatherProjectSignals( + root: string, + project: ProjectInfo, +): ProjectSignals { + const packageJson = readJsonIfExists(join(root, 'package.json')) as { + name?: string + description?: string + keywords?: string[] + dependencies?: Record + devDependencies?: Record + } | null + + const dependencyNames = [ + ...Object.keys(packageJson?.dependencies ?? {}), + ...Object.keys(packageJson?.devDependencies ?? {}), + ] + + return { + sdk: project.detected_sdk, + framework: detectFramework(root, project.detected_sdk, dependencyNames), + package_name: packageJson?.name ?? null, + description: packageJson?.description ?? null, + keywords: packageJson?.keywords ?? [], + dependency_names: dependencyNames.slice(0, 40), + readme_excerpt: readReadmeExcerpt(root), + seam_already_setup: + dependencyNames.includes('seam') || findExistingApiKey(root) != null, + } +} + +function detectFramework( + root: string, + sdk: Sdk | null, + dependencyNames: string[], +): string | null { + const has = (name: string): boolean => dependencyNames.includes(name) + + if (sdk === 'javascript') { + if (has('next')) return 'Next.js' + if (has('@remix-run/react') || has('@remix-run/node')) return 'Remix' + if (has('nuxt')) return 'Nuxt' + if (has('@nestjs/core')) return 'NestJS' + if (has('fastify')) return 'Fastify' + if (has('express')) return 'Express' + if (has('react')) return 'React' + return null + } + + if (sdk === 'python') { + if (existsSync(join(root, 'manage.py')) || hasPythonDep(root, 'django')) { + return 'Django' + } + if (hasPythonDep(root, 'fastapi')) return 'FastAPI' + if (hasPythonDep(root, 'flask')) return 'Flask' + return null + } + + return null +} + +function hasPythonDep(root: string, name: string): boolean { + for (const marker of ['requirements.txt', 'pyproject.toml', 'Pipfile']) { + const path = join(root, marker) + if (!existsSync(path)) continue + try { + if (readFileSync(path, 'utf8').toLowerCase().includes(name)) return true + } catch { + // Unreadable dependency file — treat as absent. + } + } + return false +} + +function readReadmeExcerpt(root: string): string | null { + for (const name of ['README.md', 'README.MD', 'readme.md', 'README']) { + const path = join(root, name) + if (!existsSync(path)) continue + try { + return readFileSync(path, 'utf8').slice(0, 1200) + } catch { + return null + } + } + return null +} + +function readJsonIfExists(path: string): unknown { + if (!existsSync(path)) return null + try { + return JSON.parse(readFileSync(path, 'utf8')) + } catch { + return null + } +} + +async function recommendViaLlm( + signals: ProjectSignals, + onboarding: WizardOnboarding | null, + inference: { base_url: string; token: string }, +): Promise { + const blockMenu = ALL_BLOCKS.map( + (block) => ` "${block.id}" — ${block.label}`, + ).join('\n') + + const system = + 'You help developers integrate Seam, an API for smart locks, access ' + + 'control, and physical access. Given signals about a project, recommend ' + + 'how to integrate Seam. Reply with ONLY a JSON object, no prose.' + + const user = [ + 'Project signals:', + `- SDK: ${signals.sdk ?? 'unknown'}`, + `- Framework: ${signals.framework ?? 'unknown'}`, + `- Package: ${signals.package_name ?? 'unknown'} — ${signals.description ?? ''}`, + `- Keywords: ${signals.keywords.join(', ') || 'none'}`, + `- Dependencies: ${signals.dependency_names.join(', ') || 'none'}`, + `- Seam already set up: ${signals.seam_already_setup}`, + `- README excerpt: ${signals.readme_excerpt ?? 'none'}`, + '', + 'Console onboarding answers (any may be null):', + `- use_case: ${onboarding?.use_case ?? 'null'}`, + `- primary_goal: ${onboarding?.primary_goal ?? 'null'}`, + `- build_target: ${onboarding?.build_target ?? 'null'}`, + `- embed_customer_portal: ${onboarding?.embed_customer_portal ?? 'null'}`, + `- device_categories: ${onboarding?.device_categories?.join(', ') ?? 'null'}`, + '', + 'Decide:', + '1) mode: "customer_portal" (Seam hosts the UI; the app calls ~2 endpoints)', + ' or "full_api" (control everything via the API, build your own UI).', + ' If embed_customer_portal is true, strongly prefer customer_portal.', + '2) selections: building-block ids to scaffold — only for "full_api" (use', + ' [] for customer_portal). Always include "access_grants" for full_api.', + ' Valid ids:', + blockMenu, + '3) app_type_guess: short phrase (e.g. "vacation rental", "coworking",', + ' "property management", "unknown").', + '4) rationale: one short sentence.', + '', + 'Return exactly: {"mode":"...","selections":["..."],"app_type_guess":"...","rationale":"..."}', + ].join('\n') + + const text = await callInferenceForText(inference, { + model: RECOMMENDATION_MODEL, + max_tokens: 400, + system, + user, + }) + + const parsed = parseRecommendationJson(text) + if (parsed == null) return heuristicRecommendation(signals, onboarding) + + const mode: BuildMode = + parsed.mode === 'customer_portal' ? 'customer_portal' : 'full_api' + const selections = + mode === 'customer_portal' + ? [] + : normalizeSelections(parsed.selections ?? []) + + return { + mode, + selections, + app_type_guess: parsed.app_type_guess ?? null, + rationale: parsed.rationale ?? '', + source: 'llm', + } +} + +function parseRecommendationJson(text: string): { + mode?: string + selections?: unknown + app_type_guess?: string + rationale?: string +} | null { + const match = /\{[\S\s]*\}/.exec(text) + if (match == null) return null + try { + return JSON.parse(match[0]) + } catch { + return null + } +} + +// Keep only known ids, always guarantee access_grants for the full-API path. +function normalizeSelections(raw: unknown): string[] { + const ids = Array.isArray(raw) + ? raw.filter( + (id): id is string => typeof id === 'string' && VALID_BLOCK_IDS.has(id), + ) + : [] + const unique = [...new Set(ids)] + if (!unique.includes('access_grants')) unique.unshift('access_grants') + return unique +} + +function heuristicRecommendation( + signals: ProjectSignals, + onboarding: WizardOnboarding | null, +): Recommendation { + if (onboarding?.embed_customer_portal === true) { + return { + mode: 'customer_portal', + selections: [], + app_type_guess: onboarding.use_case ?? onboarding.org_type ?? null, + rationale: 'You chose to embed the Customer Portal during onboarding.', + source: 'heuristic', + } + } + + const haystack = [ + onboarding?.use_case, + onboarding?.primary_goal, + onboarding?.org_type, + signals.description, + signals.package_name, + ...signals.keywords, + ] + .filter((value): value is string => value != null) + .join(' ') + .toLowerCase() + + const mentions = (...terms: string[]): boolean => + terms.some((term) => haystack.includes(term)) + + const selections = new Set(['access_grants', 'connect_device']) + let appTypeGuess: string | null = null + + if ( + mentions( + 'hotel', + 'booking', + 'reservation', + 'vacation', + 'rental', + 'pms', + 'guest', + 'hospitality', + ) + ) { + selections.add('reservations') + appTypeGuess = 'hospitality / short-term rental' + } else if ( + mentions( + 'coworking', + 'tenant', + 'member', + 'property', + 'apartment', + 'resident', + ) + ) { + selections.add('user_identities') + appTypeGuess = 'property / coworking' + } + + return { + mode: 'full_api', + selections: [...selections], + app_type_guess: appTypeGuess, + rationale: 'Recommended from your project and onboarding answers.', + source: 'heuristic', + } +} diff --git a/src/lib/steps/authenticate.ts b/src/lib/steps/authenticate.ts new file mode 100644 index 0000000..9b08aa5 --- /dev/null +++ b/src/lib/steps/authenticate.ts @@ -0,0 +1,43 @@ +import { join } from 'node:path' + +import { findExistingApiKey, upsertEnvVar } from 'lib/util/env-file.js' +import { getWorkspaceForApiKey, type SeamWorkspace } from 'lib/util/seam-api.js' + +export interface AuthResult { + workspace: SeamWorkspace +} + +export interface ExistingKeyResult { + workspace: SeamWorkspace + source: string +} + +// Pure auth logic (no UI). The Ink app drives the prompts and renders progress. +// The wizard never handles credentials — keys are created in the browser or +// pasted, never minted from a password here. + +// Return the workspace for an already-present SEAM_API_KEY (env or .env*), or +// null when there is none / it doesn't verify. +export async function findVerifiedExistingKey( + root: string, +): Promise { + const existing = findExistingApiKey(root) + if (existing == null) return null + try { + const workspace = await getWorkspaceForApiKey(existing.api_key) + return { workspace, source: existing.source } + } catch { + return null + } +} + +// Verify a pasted key and save it to .env. Throws (ApiKeyError) if invalid. +export async function verifyAndSaveKey( + root: string, + apiKey: string, +): Promise { + const trimmed = apiKey.trim() + const workspace = await getWorkspaceForApiKey(trimmed) + upsertEnvVar(join(root, '.env'), 'SEAM_API_KEY', trimmed) + return { workspace } +} diff --git a/src/lib/steps/build-plan.test.ts b/src/lib/steps/build-plan.test.ts new file mode 100644 index 0000000..139b7f5 --- /dev/null +++ b/src/lib/steps/build-plan.test.ts @@ -0,0 +1,211 @@ +import { mkdtempSync, readFileSync, rmSync } from 'node:fs' +import { tmpdir } from 'node:os' +import { join } from 'node:path' + +import { afterEach, beforeEach, expect, test } from 'vitest' + +import { + blockById, + composeGoal, + type OnboardingRecord, + writeOnboardingRecord, +} from './build-plan.js' + +let dir = '' + +beforeEach(() => { + dir = mkdtempSync(join(tmpdir(), 'seam-wizard-')) +}) + +afterEach(() => { + rmSync(dir, { recursive: true, force: true }) +}) + +const hintFor = (id: string): string => { + const block = blockById(id) + if (block == null) throw new Error(`Missing build block: ${id}`) + return block.agent_hint +} + +const hintLines = (goal: string): string[] => + goal.split('\n').filter((line) => line.startsWith('- ')) + +test('blockById: finds a core block', () => { + expect(blockById('access_grants')).toMatchObject({ + id: 'access_grants', + group: 'Core', + }) +}) + +test('blockById: finds a common block', () => { + expect(blockById('webhooks')).toMatchObject({ + id: 'webhooks', + group: 'Common', + }) +}) + +test('blockById: returns undefined for an unknown id', () => { + expect(blockById('not_a_block')).toBeUndefined() +}) + +test('composeGoal: describes the Customer Portal for customer_portal', () => { + const goal = composeGoal({ + mode: 'customer_portal', + selections: [], + note: null, + framework: null, + }) + + expect(goal).toContain('Customer Portal') +}) + +test('composeGoal: ignores selections for customer_portal', () => { + const goal = composeGoal({ + mode: 'customer_portal', + selections: ['access_codes', 'webhooks'], + note: null, + framework: null, + }) + + expect(goal).not.toContain(hintFor('access_codes')) + expect(hintLines(goal)).toEqual([]) +}) + +test('composeGoal: includes the agent hint of each selected block for full_api', () => { + const goal = composeGoal({ + mode: 'full_api', + selections: ['connect_device', 'webhooks'], + note: null, + framework: null, + }) + + expect(hintLines(goal)).toEqual([ + `- ${hintFor('connect_device')}`, + `- ${hintFor('webhooks')}`, + ]) +}) + +test('composeGoal: skips unknown block ids for full_api', () => { + const goal = composeGoal({ + mode: 'full_api', + selections: ['not_a_block', 'access_grants'], + note: null, + framework: null, + }) + + expect(hintLines(goal)).toEqual([`- ${hintFor('access_grants')}`]) +}) + +test('composeGoal: names the framework when given', () => { + const goal = composeGoal({ + mode: 'full_api', + selections: ['access_grants'], + note: null, + framework: 'Next.js', + }) + + expect(goal).toContain("Next.js's conventions") +}) + +test('composeGoal: falls back to a generic phrase without a framework', () => { + const goal = composeGoal({ + mode: 'full_api', + selections: ['access_grants'], + note: null, + framework: null, + }) + + expect(goal).toContain("the project's conventions") +}) + +test('composeGoal: names the framework for customer_portal too', () => { + const goal = composeGoal({ + mode: 'customer_portal', + selections: [], + note: null, + framework: 'Django', + }) + + expect(goal).toContain("Django's conventions") +}) + +test('composeGoal: appends the developer note', () => { + const goal = composeGoal({ + mode: 'full_api', + selections: ['access_grants'], + note: ' Use the existing service layer. ', + framework: null, + }) + + expect(goal).toContain( + 'Additional context from the developer: Use the existing service layer.', + ) +}) + +test('composeGoal: omits the note when it is null', () => { + const goal = composeGoal({ + mode: 'full_api', + selections: ['access_grants'], + note: null, + framework: null, + }) + + expect(goal).not.toContain('Additional context from the developer') +}) + +test('composeGoal: omits the note when it is only whitespace', () => { + const goal = composeGoal({ + mode: 'customer_portal', + selections: [], + note: ' \n ', + framework: null, + }) + + expect(goal).not.toContain('Additional context from the developer') +}) + +const exampleRecord: Omit = { + created_at: '2026-07-29T00:00:00.000Z', + mode: 'full_api', + selections: ['access_grants'], + note: null, + goal: 'Set up a Seam integration.', + analysis: { + sdk: 'javascript', + framework: 'Next.js', + app_type_guess: 'property management', + seam_already_setup: false, + used_onboarding: true, + recommendation_source: 'llm', + }, +} + +test('writeOnboardingRecord: writes the record to .seam/onboarding.json', () => { + writeOnboardingRecord(dir, exampleRecord) + + const contents = readFileSync(join(dir, '.seam', 'onboarding.json'), 'utf8') + expect(JSON.parse(contents)).toEqual({ + schema_version: 1, + ...exampleRecord, + }) +}) + +test('writeOnboardingRecord: ends the file with a trailing newline', () => { + writeOnboardingRecord(dir, exampleRecord) + + const contents = readFileSync(join(dir, '.seam', 'onboarding.json'), 'utf8') + expect(contents.endsWith('}\n')).toBe(true) +}) + +test('writeOnboardingRecord: keeps an optional result in the record', () => { + writeOnboardingRecord(dir, { + ...exampleRecord, + result: { ok: true, files_summary: 'Added src/seam.ts', cost_usd: 0.42 }, + }) + + const contents = readFileSync(join(dir, '.seam', 'onboarding.json'), 'utf8') + expect(JSON.parse(contents)).toMatchObject({ + schema_version: 1, + result: { ok: true, files_summary: 'Added src/seam.ts', cost_usd: 0.42 }, + }) +}) diff --git a/src/lib/steps/build-plan.ts b/src/lib/steps/build-plan.ts new file mode 100644 index 0000000..4b097a6 --- /dev/null +++ b/src/lib/steps/build-plan.ts @@ -0,0 +1,163 @@ +import { mkdirSync, writeFileSync } from 'node:fs' +import { join } from 'node:path' + +// Top-level fork: let Seam host the UI (the app calls ~2 endpoints), or drive +// everything through the API and build the UI yourself. +export type BuildMode = 'customer_portal' | 'full_api' + +export interface BuildBlock { + id: string + group: 'Core' | 'Common' + label: string + // Appended (as an instruction line) to the agent goal when selected. + agent_hint: string +} + +// Core building blocks — the foundation almost every integration needs. +// `access_grants` is Seam's recommended way to grant a person access, so it is +// the default-checked item. +export const CORE_BLOCKS: BuildBlock[] = [ + { + id: 'connect_device', + group: 'Core', + label: 'Connect a device (Connect Webview + connected accounts)', + agent_hint: + 'Set up a Connect Webview so an end user can connect their account and devices, then list the connected devices.', + }, + { + id: 'access_grants', + group: 'Core', + label: 'Access Grants — grant a person access', + agent_hint: + "Create an Access Grant to give a person access to a space or device (Seam's recommended API for granting access).", + }, + { + id: 'user_identities', + group: 'Core', + label: 'User Identities (the people who receive access)', + agent_hint: + 'Create and manage User Identities representing the people who receive access.', + }, +] + +// Common building blocks — frequent next steps beyond the core. +export const COMMON_BLOCKS: BuildBlock[] = [ + { + id: 'access_codes', + group: 'Common', + label: 'Access codes (PIN codes on locks)', + agent_hint: 'Program PIN access codes on smart locks.', + }, + { + id: 'reservations', + group: 'Common', + label: 'Reservations → access (PMS / booking flow)', + agent_hint: + 'Wire a booking/reservation flow so each reservation automatically provisions and revokes access.', + }, + { + id: 'mobile_keys', + group: 'Common', + label: 'Mobile keys / credentials', + agent_hint: 'Issue mobile-key credentials to users.', + }, + { + id: 'webhooks', + group: 'Common', + label: 'Webhooks & events', + agent_hint: + 'Subscribe to Seam webhooks and handle the events (e.g. access granted, device connected).', + }, +] + +export const ALL_BLOCKS: BuildBlock[] = [...CORE_BLOCKS, ...COMMON_BLOCKS] + +export function blockById(id: string): BuildBlock | undefined { + return ALL_BLOCKS.find((block) => block.id === id) +} + +// Turn the chosen mode + building blocks + free-text note into the goal string +// that drives the embedded integration agent. +export function composeGoal(args: { + mode: BuildMode + selections: string[] + note: string | null + framework: string | null +}): string { + const { mode, selections, note, framework } = args + const target = framework ?? 'the project' + const noteSuffix = + note != null && note.trim().length > 0 + ? ` Additional context from the developer: ${note.trim()}` + : '' + + if (mode === 'customer_portal') { + return ( + 'Integrate Seam using the Customer Portal so the UI is Seam-hosted and ' + + 'this app only calls a couple of endpoints. Create a Customer Portal for ' + + 'a customer (it returns a hosted URL), embed it in the app (iframe or a ' + + 'magic link), and regenerate the portal per visit since the session is ' + + `short-lived. Wire it into ${target}'s conventions, load SEAM_API_KEY from ` + + 'the existing .env, and add a short runnable example.' + + noteSuffix + ) + } + + const hints = selections + .map((id) => blockById(id)?.agent_hint) + .filter((hint): hint is string => hint != null) + .map((hint) => `- ${hint}`) + .join('\n') + + return ( + 'Set up a Seam integration that controls everything through the Seam API. ' + + `Implement the following, wired into ${target}'s conventions:\n${hints}\n` + + 'Load SEAM_API_KEY from the existing .env, keep changes minimal and ' + + 'idiomatic, and add a short runnable example.' + + noteSuffix + ) +} + +// Schema version for the on-disk record; bump on any breaking shape change. +const RECORD_SCHEMA_VERSION = 1 + +export interface OnboardingRecord { + schema_version: number + created_at: string + mode: BuildMode + selections: string[] + note: string | null + goal: string + analysis: { + sdk: string | null + framework: string | null + app_type_guess: string | null + seam_already_setup: boolean + used_onboarding: boolean + recommendation_source: 'llm' | 'heuristic' + } + result?: { + ok: boolean + files_summary: string + cost_usd: number | null + } +} + +// Write the wizard's run record to /.seam/onboarding.json. Holds no +// secrets (the API key stays in .env), so it is safe to commit as a record of +// what was set up, and the embedded agent / a later editor agent can read it. +export function writeOnboardingRecord( + root: string, + record: Omit, +): void { + const dir = join(root, '.seam') + mkdirSync(dir, { recursive: true }) + const full: OnboardingRecord = { + schema_version: RECORD_SCHEMA_VERSION, + ...record, + } + writeFileSync( + join(dir, 'onboarding.json'), + `${JSON.stringify(full, null, 2)}\n`, + ) +} diff --git a/src/lib/steps/connect-web.ts b/src/lib/steps/connect-web.ts new file mode 100644 index 0000000..0f5dc6a --- /dev/null +++ b/src/lib/steps/connect-web.ts @@ -0,0 +1,124 @@ +import { randomBytes } from 'node:crypto' +import { createServer, type ServerResponse } from 'node:http' +import { join } from 'node:path' + +import open from 'open' + +import { upsertEnvVar } from 'lib/util/env-file.js' +import { getWorkspaceForApiKey, type SeamWorkspace } from 'lib/util/seam-api.js' + +// The dashboard "wizard" page mints a key and posts it back to the local +// callback. Override the console host with SEAM_CONSOLE_URL for dev. +const CONSOLE_URL = process.env['SEAM_CONSOLE_URL'] ?? 'https://console.seam.co' +const CONSOLE_WIZARD_PATH = '/dashboard/wizard' +const CALLBACK_TIMEOUT_MS = 5 * 60 * 1000 + +export interface WebConnectResult { + workspace: SeamWorkspace +} + +// Progress callbacks so the Ink UI can render the handoff without any logging +// living in this module. +export interface WebConnectEvents { + onUrl?: (url: string) => void + onWaiting?: () => void + onReceived?: () => void +} + +interface CallbackPayload { + state: string + api_key: string +} + +// Browser → CLI handoff: start a localhost callback, open the dashboard wizard +// with the port + a random state, and wait for the page to post the freshly +// created key back. Throws on error/timeout; resolves with the workspace. +// +// Wire protocol (must match the dashboard page): the page is opened at +// {CONSOLE_URL}/dashboard/wizard?cli_connect=1&cli_port=&cli_state= +// and POSTs JSON { state, api_key } to http://127.0.0.1:/ (CORS *). +export async function connectViaWeb( + root: string, + events: WebConnectEvents = {}, +): Promise { + const state = randomBytes(16).toString('hex') + + const payload = await new Promise((resolve, reject) => { + const server = createServer((request, response) => { + response.setHeader('Access-Control-Allow-Origin', '*') + response.setHeader('Access-Control-Allow-Methods', 'POST, OPTIONS') + response.setHeader('Access-Control-Allow-Headers', 'content-type') + + if (request.method === 'OPTIONS') { + response.writeHead(204) + response.end() + return + } + if (request.method !== 'POST') { + response.writeHead(405) + response.end() + return + } + + let body = '' + request.on('data', (chunk) => { + body += chunk + }) + request.on('end', () => { + let parsed: Partial = {} + try { + parsed = JSON.parse(body) as Partial + } catch { + respondJson(response, 400, { ok: false, error: 'invalid_json' }) + return + } + if (parsed.state !== state) { + respondJson(response, 403, { ok: false, error: 'state_mismatch' }) + return + } + if (parsed.api_key == null || parsed.api_key.length === 0) { + respondJson(response, 400, { ok: false, error: 'missing_api_key' }) + return + } + respondJson(response, 200, { ok: true }) + events.onReceived?.() + server.close() + resolve({ state, api_key: parsed.api_key }) + }) + }) + + server.on('error', reject) + server.listen(0, '127.0.0.1', () => { + const address = server.address() + if (address == null || typeof address === 'string') { + reject(new Error('Could not start the local callback server.')) + return + } + const url = `${CONSOLE_URL}${CONSOLE_WIZARD_PATH}?cli_connect=1&cli_port=${address.port}&cli_state=${state}` + events.onUrl?.(url) + open(url).catch(() => { + // Browser may not open (headless/SSH) — the UI shows the URL to visit. + }) + events.onWaiting?.() + }) + + const timeout = setTimeout(() => { + server.close() + reject(new Error('Timed out waiting for the browser.')) + }, CALLBACK_TIMEOUT_MS) + timeout.unref() + }) + + const workspace = await getWorkspaceForApiKey(payload.api_key) + upsertEnvVar(join(root, '.env'), 'SEAM_API_KEY', payload.api_key) + return { workspace } +} + +function respondJson( + response: ServerResponse, + status: number, + body: unknown, +): void { + response.writeHead(status, { 'content-type': 'application/json' }) + response.end(JSON.stringify(body)) +} diff --git a/src/lib/steps/detect-project.test.ts b/src/lib/steps/detect-project.test.ts new file mode 100644 index 0000000..ba699d7 --- /dev/null +++ b/src/lib/steps/detect-project.test.ts @@ -0,0 +1,186 @@ +import { mkdtempSync, rmSync, writeFileSync } from 'node:fs' +import { tmpdir } from 'node:os' +import { join } from 'node:path' + +import { afterEach, beforeEach, expect, test } from 'vitest' + +import { + detectProject, + installSeamSdkCommand, + type JsPackageManager, + type ProjectInfo, + type PythonInstaller, +} from './detect-project.js' + +let dir = '' + +beforeEach(() => { + dir = mkdtempSync(join(tmpdir(), 'seam-wizard-')) +}) + +afterEach(() => { + rmSync(dir, { recursive: true, force: true }) +}) + +const touch = (fileName: string): void => { + writeFileSync(join(dir, fileName), '') +} + +const jsProject = (packageManager: JsPackageManager): ProjectInfo => ({ + root: '/example', + detected_sdk: 'javascript', + js_package_manager: packageManager, + python_installer: 'pip', +}) + +const pythonProject = (installer: PythonInstaller): ProjectInfo => ({ + root: '/example', + detected_sdk: 'python', + js_package_manager: 'npm', + python_installer: installer, +}) + +test('detectProject: reports the given directory as the root', () => { + expect(detectProject(dir).root).toBe(dir) +}) + +test('detectProject: detects javascript from package.json', () => { + touch('package.json') + + expect(detectProject(dir).detected_sdk).toBe('javascript') +}) + +test.each(['pyproject.toml', 'requirements.txt', 'setup.py', 'Pipfile'])( + 'detectProject: detects python from %s', + (marker) => { + touch(marker) + + expect(detectProject(dir).detected_sdk).toBe('python') + }, +) + +test('detectProject: detects no sdk when javascript and python markers are both present', () => { + touch('package.json') + touch('requirements.txt') + + expect(detectProject(dir).detected_sdk).toBeNull() +}) + +test('detectProject: detects no sdk when neither marker is present', () => { + expect(detectProject(dir).detected_sdk).toBeNull() +}) + +test.each([ + ['pnpm-lock.yaml', 'pnpm'], + ['yarn.lock', 'yarn'], + ['bun.lockb', 'bun'], +])( + 'detectProject: detects the %s package manager as %s', + (lockfile, expected) => { + touch(lockfile) + + expect(detectProject(dir).js_package_manager).toBe(expected) + }, +) + +test('detectProject: defaults to the npm package manager', () => { + touch('package.json') + + expect(detectProject(dir).js_package_manager).toBe('npm') +}) + +test.each([ + ['poetry.lock', 'poetry'], + ['uv.lock', 'uv'], +])( + 'detectProject: detects the %s python installer as %s', + (lockfile, expected) => { + touch(lockfile) + + expect(detectProject(dir).python_installer).toBe(expected) + }, +) + +test('detectProject: defaults to the pip python installer', () => { + touch('pyproject.toml') + + expect(detectProject(dir).python_installer).toBe('pip') +}) + +test('detectProject: prefers pnpm over yarn when both lockfiles exist', () => { + touch('pnpm-lock.yaml') + touch('yarn.lock') + + expect(detectProject(dir).js_package_manager).toBe('pnpm') +}) + +test('installSeamSdkCommand: installs the javascript sdk with npm', () => { + expect(installSeamSdkCommand('javascript', jsProject('npm'))).toEqual([ + 'npm', + 'install', + 'seam', + ]) +}) + +test('installSeamSdkCommand: installs the javascript sdk with pnpm', () => { + expect(installSeamSdkCommand('javascript', jsProject('pnpm'))).toEqual([ + 'pnpm', + 'add', + 'seam', + ]) +}) + +test('installSeamSdkCommand: installs the javascript sdk with yarn', () => { + expect(installSeamSdkCommand('javascript', jsProject('yarn'))).toEqual([ + 'yarn', + 'add', + 'seam', + ]) +}) + +test('installSeamSdkCommand: installs the javascript sdk with bun', () => { + expect(installSeamSdkCommand('javascript', jsProject('bun'))).toEqual([ + 'bun', + 'add', + 'seam', + ]) +}) + +test('installSeamSdkCommand: installs the python sdk with pip', () => { + expect(installSeamSdkCommand('python', pythonProject('pip'))).toEqual([ + 'pip', + 'install', + 'seam', + ]) +}) + +test('installSeamSdkCommand: installs the python sdk with poetry', () => { + expect(installSeamSdkCommand('python', pythonProject('poetry'))).toEqual([ + 'poetry', + 'add', + 'seam', + ]) +}) + +test('installSeamSdkCommand: installs the python sdk with uv', () => { + expect(installSeamSdkCommand('python', pythonProject('uv'))).toEqual([ + 'uv', + 'add', + 'seam', + ]) +}) + +test('installSeamSdkCommand: ignores the python installer for the javascript sdk', () => { + const project: ProjectInfo = { + root: '/example', + detected_sdk: 'javascript', + js_package_manager: 'yarn', + python_installer: 'poetry', + } + + expect(installSeamSdkCommand('javascript', project)).toEqual([ + 'yarn', + 'add', + 'seam', + ]) +}) diff --git a/src/lib/steps/detect-project.ts b/src/lib/steps/detect-project.ts new file mode 100644 index 0000000..43fa4a0 --- /dev/null +++ b/src/lib/steps/detect-project.ts @@ -0,0 +1,75 @@ +import { existsSync } from 'node:fs' +import { join } from 'node:path' + +export type Sdk = 'javascript' | 'python' +export type JsPackageManager = 'npm' | 'pnpm' | 'yarn' | 'bun' +export type PythonInstaller = 'pip' | 'poetry' | 'uv' + +export interface ProjectInfo { + root: string + detected_sdk: Sdk | null + js_package_manager: JsPackageManager + python_installer: PythonInstaller +} + +export function detectProject(cwd: string): ProjectInfo { + return { + root: cwd, + detected_sdk: detectSdk(cwd), + js_package_manager: detectJsPackageManager(cwd), + python_installer: detectPythonInstaller(cwd), + } +} + +// null when the project is ambiguous (both or neither) — the wizard then asks. +function detectSdk(cwd: string): Sdk | null { + const isJavascript = existsSync(join(cwd, 'package.json')) + const isPython = [ + 'pyproject.toml', + 'requirements.txt', + 'setup.py', + 'Pipfile', + ].some((marker) => existsSync(join(cwd, marker))) + if (isJavascript && !isPython) return 'javascript' + if (isPython && !isJavascript) return 'python' + return null +} + +function detectJsPackageManager(cwd: string): JsPackageManager { + if (existsSync(join(cwd, 'pnpm-lock.yaml'))) return 'pnpm' + if (existsSync(join(cwd, 'yarn.lock'))) return 'yarn' + if (existsSync(join(cwd, 'bun.lockb'))) return 'bun' + return 'npm' +} + +function detectPythonInstaller(cwd: string): PythonInstaller { + if (existsSync(join(cwd, 'poetry.lock'))) return 'poetry' + if (existsSync(join(cwd, 'uv.lock'))) return 'uv' + return 'pip' +} + +export function installSeamSdkCommand( + sdk: Sdk, + project: ProjectInfo, +): string[] { + if (sdk === 'python') { + switch (project.python_installer) { + case 'poetry': + return ['poetry', 'add', 'seam'] + case 'uv': + return ['uv', 'add', 'seam'] + default: + return ['pip', 'install', 'seam'] + } + } + switch (project.js_package_manager) { + case 'pnpm': + return ['pnpm', 'add', 'seam'] + case 'yarn': + return ['yarn', 'add', 'seam'] + case 'bun': + return ['bun', 'add', 'seam'] + default: + return ['npm', 'install', 'seam'] + } +} diff --git a/src/lib/steps/install-seam-plugin.ts b/src/lib/steps/install-seam-plugin.ts new file mode 100644 index 0000000..eafd3f5 --- /dev/null +++ b/src/lib/steps/install-seam-plugin.ts @@ -0,0 +1,38 @@ +import { existsSync } from 'node:fs' +import { join } from 'node:path' + +export type PluginTarget = 'claude-code' | 'universal' + +// The official Seam plugin: 3 integration skills + the seam-docs MCP. +const SEAM_PLUGIN = 'seamapi/seam-plugin' + +// Install the plugin's three skills into .claude/skills. We must be +// non-interactive (`-y`) — the wizard spawns this with stdin ignored, and +// without a target the `skills` CLI prompts for an agent and installs nothing. +// We target `claude-code` explicitly so the skills land in .claude/skills, +// which is exactly where the embedded agent reads them (settingSources: +// ["project"]) and where Claude Code picks them up too. +export const SEAM_PLUGIN_NPX_COMMAND = [ + 'npx', + 'skills', + 'add', + SEAM_PLUGIN, + '-y', + '-a', + 'claude-code', +] + +// Claude Code installs plugins via in-app slash commands, which an external CLI +// can't drive — so we print these for the user to run. This path also wires up +// the seam-docs MCP. +export const CLAUDE_CODE_COMMANDS = [ + '/plugin marketplace add seamapi/seam-plugin', + '/plugin install seam@seamapi', +] + +// Detect Claude Code so we print its slash commands instead of running npx. +export function detectPluginTarget(root: string): PluginTarget { + const hasClaudeCode = + existsSync(join(root, '.claude')) || existsSync(join(root, 'CLAUDE.md')) + return hasClaudeCode ? 'claude-code' : 'universal' +} diff --git a/src/lib/steps/integrate.ts b/src/lib/steps/integrate.ts new file mode 100644 index 0000000..5381ddc --- /dev/null +++ b/src/lib/steps/integrate.ts @@ -0,0 +1,200 @@ +import { query } from '@anthropic-ai/claude-agent-sdk' + +import type { Sdk } from './detect-project.js' + +// The official Seam MCP (same server the seam-plugin wires up). +const SEAM_MCP_URL = 'https://mcp.seam.co/mcp' + +// Read/search/write + the docs MCP. Deliberately no Bash, no subagents, no task +// tools: the agent writes integration code, it does not run the developer's +// shell. `mcp__seam-docs__*` grants every seam-docs tool. +const ALLOWED_TOOLS = [ + 'Read', + 'Glob', + 'Grep', + 'Edit', + 'Write', + 'WebFetch', + 'mcp__seam-docs__*', +] + +export type IntegrateEvent = + | { kind: 'text'; text: string } + | { kind: 'tool'; name: string; detail: string } + | { kind: 'done'; ok: boolean; summary: string; cost_usd: number | null } + +export interface RunIntegrationArgs { + root: string + sdk: Sdk + workspace_name: string + goal: string + inference: { base_url: string; token: string } + // The detected framework + chosen mode, so the agent fetches the matching + // reference app from the Seam MCP (get_example_app) to model its work on. + framework?: string | null + mode?: 'full_api' | 'customer_portal' + signal: AbortSignal + onEvent: (event: IntegrateEvent) => void +} + +// Drive the Claude Agent SDK to write the integration into the developer's +// project, routed through Seam-hosted inference. Streams progress via onEvent; +// resolves when the agent finishes or the signal aborts. +export async function runIntegration(args: RunIntegrationArgs): Promise { + const { + root, + sdk, + workspace_name: workspaceName, + goal, + inference, + framework, + mode, + signal, + onEvent, + } = args + const abortController = new AbortController() + const forwardAbort = (): void => abortController.abort() + signal.addEventListener('abort', forwardAbort, { once: true }) + + try { + for await (const message of query({ + prompt: goal, + options: { + cwd: root, + // Cost-tuned for a starter integration (Seam pays for this): Sonnet 5 is + // near-Opus on coding at ~half the token price, medium effort trims the + // thinking spend, and maxBudgetUsd is a hard per-run dollar ceiling. + model: 'claude-sonnet-5', + effort: 'medium', + maxBudgetUsd: 2, + env: buildAgentEnv(inference), + allowedTools: ALLOWED_TOOLS, + // Auto-apply file edits so the agent runs uninterrupted; the developer + // reviews the result as a git diff afterward. Read/search tools and the + // docs MCP are read-only, so nothing destructive runs unattended. + permissionMode: 'acceptEdits', + mcpServers: { + // Wired exactly like the seam-plugin: mcp-remote bridges to the hosted + // Seam MCP and runs the OAuth browser flow on first use, caching the + // token in ~/.mcp-auth. The developer's Claude Code (also using + // mcp-remote to the same server) then reuses it, already authenticated. + 'seam-docs': { + command: 'npx', + args: ['-y', 'mcp-remote', SEAM_MCP_URL], + }, + }, + // Pick up any Seam skill installed into the project's .claude/skills. + settingSources: ['project'], + skills: 'all', + systemPrompt: { + type: 'preset', + preset: 'claude_code', + append: buildSystemAppend(sdk, workspaceName, framework, mode), + }, + maxTurns: 40, + abortController, + }, + })) { + if (signal.aborted) break + + if (message.type === 'assistant') { + for (const block of message.message.content) { + if (block.type === 'text') { + const text = block.text.trim() + if (text.length > 0) onEvent({ kind: 'text', text }) + } else if (block.type === 'tool_use') { + onEvent({ + kind: 'tool', + name: block.name, + detail: describeToolInput(block.input), + }) + } + } + } else if (message.type === 'result') { + const ok = message.subtype === 'success' + onEvent({ + kind: 'done', + ok, + summary: ok ? message.result : '', + cost_usd: + typeof message.total_cost_usd === 'number' + ? message.total_cost_usd + : null, + }) + } + } + } finally { + signal.removeEventListener('abort', forwardAbort) + } +} + +function buildSystemAppend( + sdk: Sdk, + workspaceName: string, + framework?: string | null, + mode?: 'full_api' | 'customer_portal', +): string { + const language = sdk === 'python' ? 'Python' : 'JavaScript/TypeScript' + const frameworkLabel = framework ?? "this project's framework" + const modeLabel = mode === 'customer_portal' ? 'Customer Portal' : 'full-API' + return [ + `You are the Seam integration agent, embedded in the seam-wizard CLI.`, + `The developer has just connected their Seam account (workspace "${workspaceName}"),`, + `and their SEAM_API_KEY is already saved in a local .env file. This is a ${language} project.`, + ``, + `Before writing any code:`, + `- Fetch the reference integration: call mcp__seam-docs__list_example_apps, then`, + ` mcp__seam-docs__get_example_app to pull the example matching ${frameworkLabel} and the`, + ` ${modeLabel} approach. Model your integration on it — match its structure and Seam API`, + ` usage — but ADAPT it to this project's actual framework version, conventions, and file`, + ` layout. Do not copy it verbatim, and don't add files this project doesn't need.`, + `- Find and read the installed Seam skill. Glob for a directory named like "*seam*" under`, + ` .claude/skills and .agents/skills, and read its SKILL.md and any referenced files.`, + `- Use the seam-docs MCP tools (prefixed mcp__seam-docs__) to confirm current Seam API usage.`, + ` Prefer Access Grants for granting a person access — they are Seam's recommended API.`, + `- Read the surrounding project files first and match its language, framework, and conventions.`, + ``, + `Then implement exactly what the developer asked for — nothing more. Load SEAM_API_KEY from the`, + `existing .env; never hardcode or print it. Keep changes minimal and idiomatic. When finished,`, + `give a short summary of the files you changed and how to run the result.`, + ].join('\n') +} + +// Route the embedded agent through Seam-hosted inference: point the SDK at the +// Seam proxy with the scoped wizard token, and drop any developer Anthropic key +// so it can't override the proxy routing. The SDK reads credentials from the +// child-process env (ANTHROPIC_AUTH_TOKEN is sent as a Bearer, which is what the +// proxy authenticates). +function buildAgentEnv(inference: { + base_url: string + token: string +}): Record { + const env: Record = {} + for (const [key, value] of Object.entries(process.env)) { + if (value != null) env[key] = value + } + delete env['ANTHROPIC_API_KEY'] + env['ANTHROPIC_BASE_URL'] = inference.base_url + env['ANTHROPIC_AUTH_TOKEN'] = inference.token + return env +} + +// Pull the most useful identifier out of a tool's input for a one-line UI label, +// without asserting the input's shape. +function describeToolInput(input: unknown): string { + return ( + stringField(input, 'file_path') ?? + stringField(input, 'pattern') ?? + stringField(input, 'query') ?? + stringField(input, 'url') ?? + '' + ) +} + +function stringField(input: unknown, key: string): string | null { + if (typeof input !== 'object' || input === null || !(key in input)) { + return null + } + const value = (input as Record)[key] + return typeof value === 'string' ? value : null +} diff --git a/src/lib/todo.test.ts b/src/lib/todo.test.ts deleted file mode 100644 index c90fd94..0000000 --- a/src/lib/todo.test.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { expect, test } from 'vitest' - -import { todo } from './todo.js' - -test('todo: returns argument', () => { - expect(todo('todo')).toBe('todo') -}) diff --git a/src/lib/todo.ts b/src/lib/todo.ts deleted file mode 100644 index 5633fe7..0000000 --- a/src/lib/todo.ts +++ /dev/null @@ -1 +0,0 @@ -export const todo = (x: string): string => x diff --git a/src/lib/util/env-file.test.ts b/src/lib/util/env-file.test.ts new file mode 100644 index 0000000..6f35e05 --- /dev/null +++ b/src/lib/util/env-file.test.ts @@ -0,0 +1,162 @@ +import { mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs' +import { tmpdir } from 'node:os' +import { join } from 'node:path' + +import { afterEach, beforeEach, expect, test, vi } from 'vitest' + +import { findExistingApiKey, upsertEnvVar } from './env-file.js' + +let dir = '' + +beforeEach(() => { + dir = mkdtempSync(join(tmpdir(), 'seam-wizard-')) + // The module reads process.env['SEAM_API_KEY']; start every test without it. + vi.stubEnv('SEAM_API_KEY', undefined) +}) + +afterEach(() => { + rmSync(dir, { recursive: true, force: true }) + vi.unstubAllEnvs() +}) + +const writeEnvFile = (fileName: string, contents: string): void => { + writeFileSync(join(dir, fileName), contents) +} + +test('findExistingApiKey: prefers the process environment', () => { + vi.stubEnv('SEAM_API_KEY', 'seam_from_environment') + writeEnvFile('.env.local', 'SEAM_API_KEY=seam_from_file\n') + + expect(findExistingApiKey(dir)).toEqual({ + api_key: 'seam_from_environment', + source: 'environment', + }) +}) + +test('findExistingApiKey: trims the process environment value', () => { + vi.stubEnv('SEAM_API_KEY', ' seam_padded ') + + expect(findExistingApiKey(dir)).toEqual({ + api_key: 'seam_padded', + source: 'environment', + }) +}) + +test('findExistingApiKey: reads .env.local before .env', () => { + writeEnvFile('.env.local', 'SEAM_API_KEY=seam_local\n') + writeEnvFile('.env', 'SEAM_API_KEY=seam_plain\n') + + expect(findExistingApiKey(dir)).toEqual({ + api_key: 'seam_local', + source: '.env.local', + }) +}) + +test('findExistingApiKey: falls back to .env when .env.local is missing', () => { + writeEnvFile('.env', 'OTHER=1\nSEAM_API_KEY=seam_plain\n') + + expect(findExistingApiKey(dir)).toEqual({ + api_key: 'seam_plain', + source: '.env', + }) +}) + +test('findExistingApiKey: falls back to the development dotenv files', () => { + writeEnvFile('.env.development', 'SEAM_API_KEY=seam_dev\n') + + expect(findExistingApiKey(dir)).toEqual({ + api_key: 'seam_dev', + source: '.env.development', + }) +}) + +test('findExistingApiKey: strips surrounding double quotes', () => { + writeEnvFile('.env', 'SEAM_API_KEY="seam_quoted"\n') + + expect(findExistingApiKey(dir)?.api_key).toBe('seam_quoted') +}) + +test('findExistingApiKey: strips surrounding single quotes', () => { + writeEnvFile('.env', "SEAM_API_KEY = 'seam_quoted'\n") + + expect(findExistingApiKey(dir)?.api_key).toBe('seam_quoted') +}) + +test('findExistingApiKey: returns null when there is no key anywhere', () => { + writeEnvFile('.env', 'OTHER=1\n') + + expect(findExistingApiKey(dir)).toBeNull() +}) + +test('findExistingApiKey: returns null when the project has no dotenv files', () => { + expect(findExistingApiKey(dir)).toBeNull() +}) + +test('findExistingApiKey: treats a whitespace-only environment value as absent', () => { + vi.stubEnv('SEAM_API_KEY', ' ') + writeEnvFile('.env', 'SEAM_API_KEY=seam_plain\n') + + expect(findExistingApiKey(dir)).toEqual({ + api_key: 'seam_plain', + source: '.env', + }) +}) + +test('findExistingApiKey: treats an empty environment value as absent', () => { + vi.stubEnv('SEAM_API_KEY', '') + + expect(findExistingApiKey(dir)).toBeNull() +}) + +test('upsertEnvVar: creates a missing file', () => { + const filePath = join(dir, '.env') + + expect(upsertEnvVar(filePath, 'SEAM_API_KEY', 'seam_new')).toBe('created') + expect(readFileSync(filePath, 'utf8')).toBe('SEAM_API_KEY=seam_new\n') +}) + +test('upsertEnvVar: adds the key to an existing file', () => { + const filePath = join(dir, '.env') + writeFileSync(filePath, 'OTHER=1\n') + + expect(upsertEnvVar(filePath, 'SEAM_API_KEY', 'seam_new')).toBe('added') + expect(readFileSync(filePath, 'utf8')).toBe( + 'OTHER=1\nSEAM_API_KEY=seam_new\n', + ) +}) + +test('upsertEnvVar: appends a newline separator when the file does not end in one', () => { + const filePath = join(dir, '.env') + writeFileSync(filePath, 'OTHER=1') + + expect(upsertEnvVar(filePath, 'SEAM_API_KEY', 'seam_new')).toBe('added') + expect(readFileSync(filePath, 'utf8')).toBe( + 'OTHER=1\nSEAM_API_KEY=seam_new\n', + ) +}) + +test('upsertEnvVar: updates an existing value', () => { + const filePath = join(dir, '.env') + writeFileSync(filePath, 'SEAM_API_KEY=seam_old\n') + + expect(upsertEnvVar(filePath, 'SEAM_API_KEY', 'seam_new')).toBe('updated') + expect(readFileSync(filePath, 'utf8')).toBe('SEAM_API_KEY=seam_new\n') +}) + +test('upsertEnvVar: leaves other entries intact when updating', () => { + const filePath = join(dir, '.env') + writeFileSync(filePath, 'BEFORE=1\nSEAM_API_KEY=seam_old\nAFTER=2\n') + + expect(upsertEnvVar(filePath, 'SEAM_API_KEY', 'seam_new')).toBe('updated') + expect(readFileSync(filePath, 'utf8')).toBe( + 'BEFORE=1\nSEAM_API_KEY=seam_new\nAFTER=2\n', + ) +}) + +test('upsertEnvVar: writes into an existing empty file without a leading newline', () => { + const filePath = join(dir, '.env') + writeFileSync(filePath, '') + + expect(upsertEnvVar(filePath, 'SEAM_API_KEY', 'seam_new')).toBe('added') + expect(readFileSync(filePath, 'utf8')).toBe('SEAM_API_KEY=seam_new\n') +}) diff --git a/src/lib/util/env-file.ts b/src/lib/util/env-file.ts new file mode 100644 index 0000000..874d4c5 --- /dev/null +++ b/src/lib/util/env-file.ts @@ -0,0 +1,67 @@ +import { existsSync, readFileSync, writeFileSync } from 'node:fs' +import { join } from 'node:path' + +export type EnvWriteResult = 'created' | 'updated' | 'added' + +// dotenv files we look at for an existing key, in priority order. +const ENV_FILE_NAMES = [ + '.env.local', + '.env', + '.env.development', + '.env.development.local', +] + +export interface FoundApiKey { + api_key: string + source: string // e.g. ".env.local" or "environment" +} + +// Look for an existing SEAM_API_KEY: first the process environment, then the +// project's dotenv files. Returns the first hit so the wizard can skip auth. +export function findExistingApiKey(root: string): FoundApiKey | null { + const fromProcess = process.env['SEAM_API_KEY']?.trim() + if (fromProcess != null && fromProcess.length > 0) { + return { api_key: fromProcess, source: 'environment' } + } + + for (const fileName of ENV_FILE_NAMES) { + const filePath = join(root, fileName) + if (!existsSync(filePath)) continue + const match = readFileSync(filePath, 'utf8').match( + /^\s*SEAM_API_KEY\s*=\s*(.+?)\s*$/m, + ) + const value = match?.[1]?.replace(/^["']|["']$/g, '').trim() + if (value != null && value.length > 0) { + return { api_key: value, source: fileName } + } + } + + return null +} + +// Upsert a KEY=value line into a dotenv file without disturbing other entries. +// Returns what happened so the wizard can report it accurately. +export function upsertEnvVar( + filePath: string, + key: string, + value: string, +): EnvWriteResult { + const line = `${key}=${value}` + + if (!existsSync(filePath)) { + writeFileSync(filePath, `${line}\n`) + return 'created' + } + + const content = readFileSync(filePath, 'utf8') + const existingLine = new RegExp(`^${key}=.*$`, 'm') + + if (existingLine.test(content)) { + writeFileSync(filePath, content.replace(existingLine, line)) + return 'updated' + } + + const separator = content.length === 0 || content.endsWith('\n') ? '' : '\n' + writeFileSync(filePath, `${content}${separator}${line}\n`) + return 'added' +} diff --git a/src/lib/util/run-install.ts b/src/lib/util/run-install.ts new file mode 100644 index 0000000..0d8db48 --- /dev/null +++ b/src/lib/util/run-install.ts @@ -0,0 +1,35 @@ +import { spawn } from 'node:child_process' + +// Run a command with piped output, streaming each non-empty line to `onLine` +// so the Ink UI can render progress instead of clobbering the frame with +// inherited stdio. +export function runInstall( + command: string[], + cwd: string, + onLine: (line: string) => void, +): Promise { + const [binary, ...args] = command + if (binary == null) throw new Error('runInstall: empty command') + + return new Promise((resolve, reject) => { + const child = spawn(binary, args, { + cwd, + stdio: ['ignore', 'pipe', 'pipe'], + shell: false, + }) + + const handle = (data: Buffer): void => { + for (const line of data.toString().split('\n')) { + if (line.trim().length > 0) onLine(line.trimEnd()) + } + } + child.stdout?.on('data', handle) + child.stderr?.on('data', handle) + + child.on('error', reject) + child.on('close', (code) => { + if (code === 0) resolve() + else reject(new Error(`${binary} exited with code ${code ?? 'unknown'}`)) + }) + }) +} diff --git a/src/lib/util/seam-api.ts b/src/lib/util/seam-api.ts new file mode 100644 index 0000000..0f55ed3 --- /dev/null +++ b/src/lib/util/seam-api.ts @@ -0,0 +1,189 @@ +// Minimal Seam API access used to validate a pasted API key. We deliberately +// avoid pulling in the full SDK just for a health check — a single fetch keeps +// the wizard's install footprint tiny. + +const SEAM_API_BASE = 'https://connect.getseam.com' + +export interface SeamWorkspace { + workspace_id: string + name: string + is_sandbox: boolean +} + +export class ApiKeyError extends Error {} + +// Validates the key by fetching the workspace it belongs to. Returns the +// workspace so the wizard can show which workspace the key is for. +export async function getWorkspaceForApiKey( + apiKey: string, +): Promise { + let response: Response + try { + response = await fetch(`${SEAM_API_BASE}/workspaces/get`, { + method: 'POST', + headers: { + authorization: `Bearer ${apiKey}`, + 'content-type': 'application/json', + }, + body: '{}', + }) + } catch { + throw new ApiKeyError( + 'Could not reach the Seam API. Check your network connection and try again.', + ) + } + + if (response.status === 401) { + throw new ApiKeyError( + 'That key was rejected (401). Make sure you copied the full key, including the seam_ prefix.', + ) + } + if (!response.ok) { + throw new ApiKeyError( + `The Seam API returned ${response.status}. Please try again in a moment.`, + ) + } + + const body = (await response.json()) as { workspace?: SeamWorkspace } + if (body.workspace == null) { + throw new ApiKeyError('Unexpected response from the Seam API.') + } + return body.workspace +} + +export function looksLikeSeamApiKey(value: string): boolean { + return /^seam_[A-Za-z0-9]/.test(value.trim()) +} + +// Base URL for Seam-hosted inference. The embedded agent's SDK appends +// /v1/messages; the exchange endpoint below lives at /session. +export const SEAM_INFERENCE_BASE_URL = `${SEAM_API_BASE}/internal/wizard_inference` + +// The Console-collected onboarding answers Seam returns alongside the token, so +// the wizard can pre-fill its plan instead of re-asking (null if none recorded). +export interface WizardOnboarding { + org_type: string | null + primary_goal: string | null + use_case: string | null + build_target: string | null + embed_customer_portal: boolean | null + device_categories: string[] | null +} + +export interface WizardInferenceSession { + token: string + expires_at: string + onboarding: WizardOnboarding | null +} + +// Exchange a Seam API key for a short-lived wizard inference token. The token — +// not the API key — is what the embedded agent sends to Seam-hosted inference, +// so the long-lived key stays off the repeated inference path. +export async function exchangeWizardInferenceToken( + apiKey: string, +): Promise { + let response: Response + try { + response = await fetch(`${SEAM_INFERENCE_BASE_URL}/session`, { + method: 'POST', + headers: { + authorization: `Bearer ${apiKey}`, + 'content-type': 'application/json', + }, + body: '{}', + }) + } catch { + throw new ApiKeyError( + 'Could not reach Seam to start the AI session. Check your network connection and try again.', + ) + } + + if (!response.ok) { + throw new ApiKeyError( + `Seam couldn't start the AI session (${response.status}). Please try again in a moment.`, + ) + } + + const body = (await response.json()) as { + wizard_session?: { token: string; expires_at: string } + onboarding?: WizardOnboarding | null + } + if (body.wizard_session == null) { + throw new ApiKeyError( + 'Unexpected response from Seam starting the AI session.', + ) + } + return { + token: body.wizard_session.token, + expires_at: body.wizard_session.expires_at, + onboarding: body.onboarding ?? null, + } +} + +// One-shot call to Seam-hosted inference (Anthropic Messages API shape). Sent +// streamed so the proxy meters usage the same way it does for the agent; the +// text deltas are concatenated and returned. Used for the cheap project-analysis +// recommendation — not the full integration (that goes through the agent SDK). +export async function callInferenceForText( + inference: { base_url: string; token: string }, + args: { model: string; max_tokens: number; system: string; user: string }, +): Promise { + const response = await fetch(`${inference.base_url}/v1/messages`, { + method: 'POST', + headers: { + authorization: `Bearer ${inference.token}`, + 'content-type': 'application/json', + 'anthropic-version': '2023-06-01', + }, + body: JSON.stringify({ + model: args.model, + max_tokens: args.max_tokens, + system: args.system, + stream: true, + messages: [{ role: 'user', content: args.user }], + }), + }) + + if (!response.ok || response.body == null) { + throw new Error(`Seam inference returned ${response.status}`) + } + + return await readTextDeltas(response.body) +} + +async function readTextDeltas( + body: ReadableStream, +): Promise { + const reader = body.getReader() + const decoder = new TextDecoder() + let buffer = '' + let text = '' + for (;;) { + const { value, done } = await reader.read() + if (done) break + buffer += decoder.decode(value, { stream: true }) + const lines = buffer.split('\n') + buffer = lines.pop() ?? '' + for (const line of lines) { + const trimmed = line.trim() + if (!trimmed.startsWith('data:')) continue + const payload = trimmed.slice('data:'.length).trim() + if (payload.length === 0 || payload === '[DONE]') continue + try { + const event = JSON.parse(payload) as { + type?: string + delta?: { type?: string; text?: string } + } + if ( + event.type === 'content_block_delta' && + event.delta?.type === 'text_delta' + ) { + text += event.delta.text ?? '' + } + } catch { + // Ignore keep-alive pings and any non-JSON SSE lines. + } + } + } + return text +} diff --git a/src/lib/wizard.test.ts b/src/lib/wizard.test.ts index 5b083fe..4ea9a89 100644 --- a/src/lib/wizard.test.ts +++ b/src/lib/wizard.test.ts @@ -1,16 +1,23 @@ -import { expect, test, vi } from 'vitest' +import { beforeEach, expect, test, vi } from 'vitest' +import { renderApp } from './render.js' import wizard from './wizard.js' +vi.mock('./render.js', () => ({ renderApp: vi.fn() })) + +beforeEach(() => { + vi.mocked(renderApp).mockClear() +}) + const captureOutput = async ( options: Parameters[0], ): Promise => { - const log = vi.spyOn(console, 'log').mockImplementation(() => {}) + const write = vi.spyOn(process.stdout, 'write').mockImplementation(() => true) try { await wizard(options) - return log.mock.calls.map(([message]) => String(message)).join('\n') + return write.mock.calls.map(([chunk]) => String(chunk)).join('') } finally { - log.mockRestore() + write.mockRestore() } } @@ -33,12 +40,17 @@ test('wizard: uses the given command name in usage', async () => { expect(output).toContain('$ seam wizard [options]') }) -test('wizard: runs with no arguments', async () => { - const output = await captureOutput({}) - expect(output).toContain('not implemented yet') +test('wizard: does not run the app when displaying usage', async () => { + await captureOutput({ argv: ['--help'] }) + expect(renderApp).not.toHaveBeenCalled() +}) + +test('wizard: runs the app in the working directory by default', async () => { + await wizard() + expect(renderApp).toHaveBeenCalledWith({ root: process.cwd() }) }) -test('wizard: reports forwarded arguments', async () => { - const output = await captureOutput({ argv: ['setup', 'devices'] }) - expect(output).toContain('setup devices') +test('wizard: runs the app in the given directory', async () => { + await wizard({ argv: [], cwd: '/tmp/example-project' }) + expect(renderApp).toHaveBeenCalledWith({ root: '/tmp/example-project' }) }) diff --git a/src/lib/wizard.ts b/src/lib/wizard.ts index f461577..8d1820f 100644 --- a/src/lib/wizard.ts +++ b/src/lib/wizard.ts @@ -1,5 +1,7 @@ import parseArgs from 'minimist' +import { renderApp } from './render.js' + export interface WizardOptions { /** * Command line arguments for the wizard, e.g., `process.argv.slice(2)`. @@ -17,6 +19,15 @@ export interface WizardOptions { * The Seam CLI mounts this wizard and passes `seam wizard`. */ commandName?: string + + /** + * The project root the wizard sets up. + * + * Defaults to `process.cwd()`, which is the project the developer ran the + * command in. The wizard reads and writes files here, e.g., `.env` and + * `.seam/onboarding.json`. + */ + cwd?: string } /** @@ -24,9 +35,13 @@ export interface WizardOptions { * * This is the entrypoint used by the Seam CLI to mount the entire wizard * as a subcommand. It is also used by the development CLI in `src/bin/cli.ts`. + * + * Resolves once the wizard has finished, having taken over the terminal for + * the duration of the run. Rejects only if the wizard could not be run at + * all: a step that fails reports itself to the user and does not reject. */ const wizard = async (options: WizardOptions = {}): Promise => { - const { argv = [], commandName = 'wizard' } = options + const { argv = [], commandName = 'wizard', cwd = process.cwd() } = options const args = parseArgs([...argv], { boolean: ['help'], @@ -38,16 +53,7 @@ const wizard = async (options: WizardOptions = {}): Promise => { return } - // TODO: Implement the wizard. - await Promise.resolve() - - write( - [ - `The ${commandName} is not implemented yet.`, - `Run '${commandName} --help' for usage.`, - ...(args._.length > 0 ? [`Received arguments: ${args._.join(' ')}`] : []), - ].join('\n'), - ) + await renderApp({ root: cwd }) } export default wizard @@ -58,6 +64,9 @@ const usage = (commandName: string): string => '', ' The AI powered Seam setup wizard.', '', + ' Connects your Seam account, installs the Seam SDK and the Seam plugin', + ' skills, and optionally writes a Seam integration into your project.', + '', 'Usage', '', ` $ ${commandName} [options]`, @@ -70,6 +79,5 @@ const usage = (commandName: string): string => // TODO: Replace this with a logger wrapper. const write = (message: string): void => { - // eslint-disable-next-line no-console - console.log(message) + process.stdout.write(`${message}\n`) } diff --git a/test/todo.test.ts b/test/todo.test.ts deleted file mode 100644 index b9908e0..0000000 --- a/test/todo.test.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { expect, test } from 'vitest' - -import { todo } from '@seamapi/wizard' - -test('todo: returns argument', () => { - expect(todo('todo')).toBe('todo') -}) diff --git a/tsconfig.build.json b/tsconfig.build.json index 01d2f6d..45cd5ca 100644 --- a/tsconfig.build.json +++ b/tsconfig.build.json @@ -11,5 +11,5 @@ }, "files": ["src/index.ts"], "include": ["src/**/*"], - "exclude": ["**/*.test.ts", "src/bin/**/*"] + "exclude": ["**/*.test.ts", "**/*.test.tsx", "src/bin/**/*"] } diff --git a/tsconfig.json b/tsconfig.json index cdfc697..b47625e 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -30,11 +30,5 @@ } }, "files": ["src/index.ts", "src/bin/cli.ts"], - "include": [ - "src/**/*", - "test/**/*", - "examples/**/*", - "eslint.config.ts", - "vitest.config.ts" - ] + "include": ["src/**/*", "test/**/*", "eslint.config.ts", "vitest.config.ts"] } diff --git a/vitest.config.ts b/vitest.config.ts index 01a4447..ac14356 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -13,12 +13,17 @@ export default defineConfig({ '**/index.ts', 'src/bin/cli.ts', 'package/**/*.ts', - 'examples/**/*.ts', '**/*.test.ts', + '**/*.test.tsx', ], provider: 'v8', reporter: ['html', 'lcov', 'text'], }, - include: ['src/**/*.test.ts', 'test/**/*.test.ts'], + include: [ + 'src/**/*.test.ts', + 'src/**/*.test.tsx', + 'test/**/*.test.ts', + 'test/**/*.test.tsx', + ], }, })