diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml new file mode 100644 index 0000000..fee1a48 --- /dev/null +++ b/.github/workflows/check.yml @@ -0,0 +1,33 @@ +name: Check OpenCode plugin + +on: + pull_request: + push: + branches: [main] + workflow_dispatch: + +permissions: + contents: read + +jobs: + check: + runs-on: ubuntu-latest + strategy: + matrix: + opencode: ["1.15.11", "1.18.35"] + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 24 + cache: npm + - run: npm ci + - run: npm run check + - name: Check generated standalone plugin + run: git diff --exit-code -- assets/opencode/plugins/render.ts + - name: Install OpenCode CLI + run: npm install --prefix "$RUNNER_TEMP/opencode-cli" --no-audit --no-fund "opencode-ai@${{ matrix.opencode }}" + - name: Test packed npm package and GitHub installer in OpenCode + run: npm run test:integration + env: + OPENCODE_BIN: ${{ runner.temp }}/opencode-cli/node_modules/.bin/opencode diff --git a/README.md b/README.md index ab8cdbe..50475a4 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ Until the package is published to npm, install the local OpenCode plugin, skills curl -fsSL https://raw.githubusercontent.com/render-oss/render-opencode-plugin/main/install.sh | bash ``` -This writes files to `~/.config/opencode/`: +The installer requires Node.js 18+ or Bun. It writes files to OpenCode's config directory: `$OPENCODE_CONFIG_DIR` when set, otherwise `$XDG_CONFIG_HOME/opencode`, otherwise `~/.config/opencode/`: - `plugins/render.ts` - `skills/render-*/SKILL.md` @@ -29,7 +29,7 @@ This writes files to `~/.config/opencode/`: - `commands/check-render-status.md` - `agents/render.md` -The installer doesn't overwrite existing files unless you pass `--force`: +On upgrades, unchanged bundled files update automatically and obsolete bundled files are removed. A tracking manifest records installed file hashes; known unchanged files from version 0.1.0 are also recognized. Your modified files and unrelated files are preserved. Use `--force` to replace modified files: ```bash curl -fsSL https://raw.githubusercontent.com/render-oss/render-opencode-plugin/main/install.sh | bash -s -- --force @@ -62,7 +62,9 @@ Add the npm package to your OpenCode config: } ``` -OpenCode installs npm plugins with Bun at startup and caches them in `~/.cache/opencode/node_modules/`. This flow requires the package to be published to npm. +OpenCode installs npm plugins with Bun at startup and caches them in its XDG cache directory (normally `~/.cache/opencode/node_modules/`). This flow requires the package to be published to npm. + +Choose either the GitHub plugin or the npm plugin. When switching from GitHub to npm, remove the installed `plugins/render.ts`; OpenCode loads local and npm plugins independently and would otherwise run both validation hooks. ## Installing skills, commands, and the agent @@ -74,14 +76,14 @@ Run the explicit setup command after adding the plugin: npx @render/opencode-plugin setup ``` -This opt-in step writes files to `~/.config/opencode/`: +This opt-in step uses the same config-directory defaults as the GitHub installer and writes: - `skills/render-*/SKILL.md` - `commands/deploy-to-render.md` - `commands/check-render-status.md` - `agents/render.md` -The setup command doesn't overwrite existing files unless you pass `--force`. +Rerun setup after updating the npm package to upgrade the bundled skills, commands, and agent. Unchanged installed files update automatically, obsolete bundled files are removed, and your edits are preserved. Use `--force` to replace modified files. ```bash npx @render/opencode-plugin setup --force @@ -125,11 +127,17 @@ This merges the following server into `opencode.json`: } ``` -Set `RENDER_API_KEY` in your environment before using Render MCP. +Set `RENDER_API_KEY` in the environment of the process that launches OpenCode, then verify the connection: + +```bash +opencode mcp list +``` + +The generated configuration uses API-key authentication with OAuth disabled. Setup preserves an existing `mcp.render` entry, including disabled or custom authentication settings, unless you pass `--force`. ## Setting up the Render CLI -The plugin can validate Blueprints with the Render CLI. +The plugin validates Blueprints with the Render CLI. Render CLI 2.28.0 is tested with this plugin. 1. Install the Render CLI: @@ -143,13 +151,17 @@ brew install render render login ``` -3. Verify access: +3. Select the workspace used for validation and verify access: ```bash +render workspace set render whoami -o json +render blueprints validate ./render.yaml --output json ``` -If `render whoami -o json` fails, fix authentication before relying on live Render workflows in OpenCode. +For API-key authentication, set `RENDER_API_KEY` instead of running `render login`. You can also set `RENDER_WORKSPACE` to select the workspace without changing saved CLI configuration. OpenCode must inherit these environment variables. Validation sends the Blueprint to Render for checking; it does not create or deploy resources. + +The plugin passes the requested filename explicitly and resolves relative paths from the OpenCode session directory. It validates after `write`, `edit`, and `apply_patch` changes, including moved files, and skips reads and deletions. The explicit validation tool runs once per invocation. ## Using the plugin @@ -187,6 +199,17 @@ Verify the package before publishing: npm run check ``` +`npm run build` generates `assets/opencode/plugins/render.ts` from the same source as the npm plugin. Commit that generated file; do not edit it directly. Both installers use `assets/setup.mjs` for copying, upgrade tracking, and MCP configuration. + +Install the OpenCode CLI and run the packaged integration tests: + +```bash +npm install --prefix /tmp/render-opencode-cli opencode-ai@1.18.35 +OPENCODE_BIN=/tmp/render-opencode-cli/node_modules/.bin/opencode npm run test:integration +``` + +The tests pack and install the npm package, exercise its executable, and start isolated OpenCode servers for both installation flows. A local scripted model and fake Render CLI test skills, command delegation, filename resolution, and validation hooks without model charges or Render credentials. CI runs the suite against OpenCode 1.15.11 and 1.18.35. Set `OPENCODE_INTEGRATION_KEEP=1` to retain temporary test artifacts. + ## License MIT. See [LICENSE](LICENSE). diff --git a/assets/legacy-assets.json b/assets/legacy-assets.json new file mode 100644 index 0000000..c30b74c --- /dev/null +++ b/assets/legacy-assets.json @@ -0,0 +1,97 @@ +{ + "version": 1, + "release": "0.1.0", + "files": { + "agents/render.md": "f42e605266a0d7ab63742947cc355240e4e5dcd132a0bc753e72de466e1ef6b2", + "commands/check-render-status.md": "151f5486e6d98de77ca887cd78a3a4974a0de2a9ea9583882a3a7e48a92f0bd1", + "commands/deploy-to-render.md": "0f538b22278bbe51936328ddae331ed9015d924736ba4fc86674d0a1afffd26f", + "plugins/render.ts": "eec3c2057cf8132af815ac90a1de9ef384fd13179b841244b43418f75791753f", + "skills/render-background-workers/SKILL.md": "fd9959a0c5d9271a504619ba4d15a1fd2904545e72323afd6863fa90a3e5f273", + "skills/render-background-workers/references/graceful-shutdown.md": "54f352641990b797c348d88b5d4b5264038498d67a58f5acd3cc19b31ba40387", + "skills/render-background-workers/references/queue-framework-setup.md": "1d887bdee550f5267b8d8573978ac9c5cb2051a563ea6d3623a49de1911a73ac", + "skills/render-blueprints/SKILL.md": "70ce7fae4f13ddd53402ab42dc30d66dafa8f8e75060a0976e8879d361febc79", + "skills/render-blueprints/references/common-mistakes.md": "0a3107c8451f0c84f52f315e15192f93073709fcf83cfd993cc1a63587a64445", + "skills/render-blueprints/references/field-reference.md": "809e6ee5af86d396626e33c969691ba2a49c1d306c2fea8a3626e3355b05794a", + "skills/render-blueprints/references/preview-environments.md": "a42e2e1db3897fa0bbe6e5da2bc549361c130d3faa88485e907e8672d095820d", + "skills/render-blueprints/references/wiring-patterns.md": "fa950b248129a7256c3ee4bcfc4c07a145ff103c327905c43c9caf03103de718", + "skills/render-cli/SKILL.md": "92a67216566df7f243b718aa88b6790d5b6205b33c66788bccfb2b697a29f418", + "skills/render-cli/references/command-cheatsheet.md": "384ded822a3e9b1bc1f69d7dacdfcc50f78263834aab8aa39d2afb8eeaf4c23c", + "skills/render-cron-jobs/SKILL.md": "19c2e2986049b2ae11799e3d2a8e746095ed0ac3ee313ea4b8ac1126bf86a195", + "skills/render-cron-jobs/references/cron-patterns.md": "d0af830f2c04285957e326253b5b2b29ac26f7586608ea62a68d609e8c6a0dbf", + "skills/render-cron-jobs/references/migration-from-scheduler.md": "2e5256e3ce4cb9a550fb7c852e4298d5a98913d750cec7358cb013f94158df31", + "skills/render-debug/SKILL.md": "412c2cff904fbdff6bb561ed18583e64ae6ab172d2979f1cc34464374cf04d91", + "skills/render-debug/references/database-debugging.md": "56074ca16ad9c7ddcd54406d28cc8191361154441ba9ea3c26b2233e839cc0cc", + "skills/render-debug/references/error-patterns.md": "181adb011f797dea7a8e803ba305f66b2d6276af0a9017c1e944679714882809", + "skills/render-debug/references/log-analysis.md": "0794530dbf7171035794d570929974f071983ec70d0d00f2f3028ce7c630d3ee", + "skills/render-debug/references/metrics-debugging.md": "b1aced9c341013f4860845914ccf628d9553f50745894ffd989b5b55433c25f5", + "skills/render-debug/references/quick-workflows.md": "2fd73b7a9d3fa228bd25edfb9b93e510a99af1f760714a43244c560d20276d56", + "skills/render-debug/references/troubleshooting.md": "e7e1076db7c50d620809ec47764e459d2b95b0f59d248afcdb0287f90515a306", + "skills/render-deploy/SKILL.md": "e5104c536096f19f8ba1d0a46a277fc0e87fde76fd39fa4a45ced044e42935f8", + "skills/render-deploy/assets/docker.yaml": "3fd98d72fc490df5c2a57ee5ddb3997347d31ea72ac68035f6442e787a19e091", + "skills/render-deploy/assets/go-api.yaml": "fed7f5f2f59dc98cfc505533177d7b35dd3a993e6a93d0a6be1fae59a676af0b", + "skills/render-deploy/assets/nextjs-postgres.yaml": "0c3d482632b88c0e5f3dc620d32b06216b356205b1fd988f69b6fc69ee41d2d3", + "skills/render-deploy/assets/node-express.yaml": "1785a380074f7439e983f0cd50381559fd804a76c56f8d2da7d988a30ac8b519", + "skills/render-deploy/assets/python-django.yaml": "ce9e540e8cff7171557223044f61a83a058f0012831d07f99750cac70a4e236c", + "skills/render-deploy/assets/static-site.yaml": "d0a7df7c7a987049ec3609c28f1fdeef8e2e01f534c64372c6079746df286ac1", + "skills/render-deploy/references/blueprint-spec.md": "52cdb41c930c22ec5578c2e56cef33ffbba910ef6c72de57965908be7ec54139", + "skills/render-deploy/references/codebase-analysis.md": "54c4fe84ae50a4b358f7c9048dfcd68f9eddd88ddfbe24c50b4be7949dc9f6a7", + "skills/render-deploy/references/configuration-guide.md": "41070d43ab849759b09f0c2bf463bacabf7ae3044343e7c78f3e33b7ff38a886", + "skills/render-deploy/references/deployment-details.md": "fa6aa90a707494fcc9438b65829ef4891b9ed03c1cf69d6323fbd997b1f6782a", + "skills/render-deploy/references/direct-creation.md": "3b148def3c12061582229599616b680f57972c86a8782e22a765cf261ec80adc", + "skills/render-deploy/references/error-patterns.md": "b5556cdd3ba832e8f33602cf207cd80872df9c89ed17fe92129a56903176675b", + "skills/render-deploy/references/post-deploy-checks.md": "bd1e3e24abe6c9eb63164ac8ada9b285ceaffbd1722f58bbc1d3d94af4c59911", + "skills/render-deploy/references/runtimes.md": "285b66f9fbb7b20195be80d3bc91bdfc82944c779debbffad85f08ba338b414f", + "skills/render-deploy/references/service-types.md": "2e95dab603df7f692d1e403431985dba88303d490507623165369c0c7b4c2b22", + "skills/render-deploy/references/troubleshooting-basics.md": "f999b13fc56ff62ccf6eec593d0e4cee94e52dec27a815e40d68532bf558a4ca", + "skills/render-disks/SKILL.md": "12d9bb7f5db6eb964e12584bc25d2d7006e53d04c75c178146c4d18acef5ebf3", + "skills/render-disks/references/sizing-and-snapshots.md": "91f75261c62caa2d8827cfcad216a0718ff997458b47daa7b67e762ad08b2282", + "skills/render-docker/SKILL.md": "dc892116e86904a6336dad6e801c9f0acc6431a60ab646599c78070aa167c288", + "skills/render-docker/references/dockerfile-patterns.md": "cc0255c7f8ef1da6a120b44d169a34b301b81a96018260b1491513d23a4a7dee", + "skills/render-docker/references/optimization-guide.md": "9f86683edf8b7a9b49d1030f2742b2355079189fd5b6fd73d2eb17a086ce67fd", + "skills/render-docker/references/registry-setup.md": "758a6af56fb49e08345121e87fee1786ac167da0dd865807af5bf01540efb4ef", + "skills/render-domains/SKILL.md": "ef6dc4bd1c66b87c15a66679a8a8b8eb584f874ec2b8fcb8403588d01e30f91b", + "skills/render-domains/references/dns-configuration.md": "74fce3662688ad8d82da77ab0ac784c7da9898e1f0b3f45ac93a309986fbd06a", + "skills/render-env-vars/SKILL.md": "ed9182eb9983aa07d3b19aca9c85da7b1d743280077b7363aec576e8c9c3a249", + "skills/render-env-vars/references/platform-variables.md": "1aa9dee6543cbe2415a592dd243fb8875484df32f8b9b4b6d8aad99c1520f96e", + "skills/render-env-vars/references/wiring-reference.md": "d9142f8f94edff7226231194a3637f1f25f91579314ae6b625dedbd70daad9cb", + "skills/render-keyvalue/SKILL.md": "c9e72f9fbbaa7c0e004774ac09ecf91a7d9e85d0b282574bbfc60c578cbab856", + "skills/render-keyvalue/references/connection-examples.md": "d569bf0c06d0ec4a548537a869a63fb794ebca5ae18d222ed3735ab9fbf8c70a", + "skills/render-keyvalue/references/troubleshooting.md": "7a22bdd8d6256bebf2fa5df02aa192389e8f676a5aae13a6ec8ebd2d686e5cb4", + "skills/render-mcp/SKILL.md": "0034cbba6e97c895efd5cf3400947a628f064949383a8e003a52241449d6a8bd", + "skills/render-mcp/references/troubleshooting.md": "3210fb908bb22ceffef9fc3fe0999314724eb9bbe07e1e80f68725757099c507", + "skills/render-migrate-from-heroku/SKILL.md": "03634b5a7ad35d98b9eab63b293a422e81cd406a314ae573c32f11e07dd2c0c5", + "skills/render-migrate-from-heroku/references/blueprint-example.md": "03952bb581ccf98b6dda59736103ad24f7ebf36feb4f26134327cbc673e289bc", + "skills/render-migrate-from-heroku/references/buildpack-mapping.md": "70efd55e8059bb130c4487acef860028758d5aeebb68f8c0e187feb3975e95a6", + "skills/render-migrate-from-heroku/references/data-migration.md": "7fbb0a09b7f2884f9c22ce1568ef7fbff3394d6992e4db23fbac638a7206c190", + "skills/render-migrate-from-heroku/references/mcp-setup.md": "c0336d1c9761be36662dc5b605273852b63da9559fdc7186b9c0d72cf03855d0", + "skills/render-migrate-from-heroku/references/preflight-checklist.md": "8c11c40cc8289bdf2bac9c8570e58b88975bf362b7ef1cf479d1efc886064447", + "skills/render-migrate-from-heroku/references/service-mapping.md": "c1ff1e178b2ac43f97756d9d22f70a502ac79a6e396d6c4ef49a1997a97a1b5c", + "skills/render-monitor/SKILL.md": "17f1b66a73bab95209182828f4442fc11b6bcf1ef0cc4714e473b6c307ce0e8b", + "skills/render-monitor/references/metrics-guide.md": "2957ba36ff7f595627685278ee6bf440be8c6b8f501d077863dfcba08915eada", + "skills/render-networking/SKILL.md": "abf617735c016c444e183b4edfcaacbcc2263c5a744de291e3baa6ff27865e0e", + "skills/render-networking/references/communication-patterns.md": "1f1f32c3c5f3a24bd6d572638a869c9ae198aa25030068fd357bd79d0a7b7cb2", + "skills/render-networking/references/troubleshooting.md": "522c8d45c53fb0b0e4b0ad6603cf8b87a5a66578ad475d8e5b573f6d9f834dfa", + "skills/render-postgres/SKILL.md": "bf12cea2449e74870841ae28c540bee97ac1a979125b51160ff547a8a72b5609", + "skills/render-postgres/references/backup-and-recovery.md": "c53c61ec76b3a4c2385b6c713fac6284a778178c06b14b0b31b16bf61f87bc00", + "skills/render-postgres/references/connection-guide.md": "1fbcb0cc9fc9c7d7a07b05c1393839ef9dd489b409d1c09dd2b03c1144688f39", + "skills/render-postgres/references/performance-tuning.md": "0b215c4fa702e54a988d2b2e95fc1404f198b099fac890577e64f8dd160ac051", + "skills/render-private-services/SKILL.md": "c2abc9b0663fe23837084ef6ba642fa5dce9f481fad3d160c2c920461a4d8d5c", + "skills/render-private-services/references/patterns.md": "1367babfd6ad092772b69372893f41061d54b1beb228205628d3d629d4370b72", + "skills/render-scaling/SKILL.md": "d9741ad68172e7e476bb08d1f2ecede357209eec8e4c1b3b6f8a2d2b9daabad3", + "skills/render-scaling/references/autoscaling-guide.md": "d3e171860bd2e8fbf2bdcf5391e80357947e42db243ad7431da38fdc484fdd1a", + "skills/render-scaling/references/instance-types.md": "fd44fbb18fecbe276a4f1d0d2e21f438bb52d6a213042ae61934a72165647dde", + "skills/render-static-sites/SKILL.md": "5d927eb5a89770aabd19034771925d643272d4b42aa4368035072a453b7a6a69", + "skills/render-static-sites/references/framework-configs.md": "69e686b2505599fbda0e9ab7149703cc64834352570a2c0e0d946303cfcc75fa", + "skills/render-static-sites/references/routing-and-headers.md": "309af4b48a91f6fc7b92599eb4a23817040fe1b48807b1328bcd376d356f4940", + "skills/render-web-services/SKILL.md": "4ce0323558f56635994018f5f80824b7d2be98cee52df0ec8a10f89b79f85186", + "skills/render-web-services/references/custom-domains.md": "ddeca625a4545d4d58f70505f1f1390903377ee74fa185ff233ac812cfd8cc37", + "skills/render-web-services/references/deploy-lifecycle.md": "f798b49c5b80b908151e833b8649f0f966af4c933dd2dcd30e1bee3598195256", + "skills/render-web-services/references/health-check-patterns.md": "dcd9e317aa262b2debcacef1990cb1472c27d2833415f964ce1d5e19087043e9", + "skills/render-workflows/SKILL.md": "b682cba179c244ae4c64a6f7b2f45fa50ecad8836f423a39d020a50cf94de773", + "skills/render-workflows/references/local-development.md": "54bcaa96eefffdabb45ed60a6e10d860c23232fdfae253dfb134b05b83e22e1e", + "skills/render-workflows/references/manual-scaffolding.md": "a0fff3a2704da17e6559de41622b399f0f747dc79f0c6e24c187662b81adb66e", + "skills/render-workflows/references/quick-reference.md": "e160a91db989d27560194a7ebebabecfce5b863f3be10399d17619b9f0080044", + "skills/render-workflows/references/task-patterns.md": "456acdf32d1bed89e933f81f5458d8b0bbf2eb81c1985cce3671cacfee7cab61", + "skills/render-workflows/references/troubleshooting.md": "acd0b028c533f7ac242215005a6f33e65ba7c8141af2de7474047d1259a2e56e" + } +} diff --git a/assets/opencode/plugins/render.ts b/assets/opencode/plugins/render.ts index f796a54..80a3647 100644 --- a/assets/opencode/plugins/render.ts +++ b/assets/opencode/plugins/render.ts @@ -1,111 +1,35 @@ -import { execFile } from "node:child_process" -import { basename, dirname, resolve } from "node:path" -import { promisify } from "node:util" - -import type { Plugin } from "@opencode-ai/plugin" -import { tool } from "@opencode-ai/plugin" - -const execFileAsync = promisify(execFile) - -type ValidationResult = { - ok: boolean - command: string - cwd: string - output: string - error?: string +// Generated by npm run build from src/index.ts. Do not edit directly. + +// src/index.ts +import { tool } from "@opencode-ai/plugin"; +import { resolve as resolve2 } from "node:path"; + +// src/blueprint.ts +import { execFile } from "node:child_process"; +import { basename, dirname, resolve } from "node:path"; +import { promisify } from "node:util"; +var execFileAsync = promisify(execFile); +function isBlueprintFile(filePath) { + const name = basename(filePath); + return name === "render.yaml" || name === "render.yml"; } - -export const RenderPlugin: Plugin = async ({ client }) => { - await client.app.log({ - body: { - service: "render-opencode-plugin", - level: "info", - message: "Render OpenCode plugin initialized", - }, - }) - - return { - tool: { - render_validate_blueprint: tool({ - description: "Validate a Render Blueprint file with the Render CLI.", - args: { - path: tool.schema.string().describe("Path to render.yaml or render.yml."), - }, - async execute(args) { - const result = await validateBlueprint(args.path) - return { - title: result.ok ? "Render Blueprint valid" : "Render Blueprint validation failed", - output: result.output, - metadata: { - command: result.command, - cwd: result.cwd, - ok: result.ok, - error: result.error, - }, - } - }, - }), - }, - - "tool.execute.after": async (input, output) => { - const blueprintPath = extractTouchedFiles(input.args).find(isBlueprintFile) - if (!blueprintPath) { - return - } - - const result = await validateBlueprint(blueprintPath) - if (result.ok) { - await client.app.log({ - body: { - service: "render-opencode-plugin", - level: "info", - message: "Validated Render Blueprint", - extra: { - path: blueprintPath, - cwd: result.cwd, - }, - }, - }) - return - } - - output.title = "Render Blueprint validation" - output.output = [output.output, result.output].filter(Boolean).join("\n\n") - output.metadata = { - ...output.metadata, - renderBlueprintValidation: { - ok: false, - command: result.command, - cwd: result.cwd, - error: result.error, - }, - } - }, - } -} - -function isBlueprintFile(filePath: string) { - const name = basename(filePath) - return name === "render.yaml" || name === "render.yml" -} - -function getBlueprintWorkingDirectory(filePath: string) { - return dirname(resolve(filePath)) -} - -async function validateBlueprint(filePath: string): Promise { - const cwd = getBlueprintWorkingDirectory(filePath) - const command = "render blueprints validate" - +async function validateBlueprint(filePath, directory = process.cwd(), signal) { + const path = resolve(directory, filePath); + const cwd = dirname(path); + const command = `render blueprints validate ${JSON.stringify(path)} --output json`; try { - const result = await execFileAsync("render", ["blueprints", "validate"], { cwd }) - const output = [result.stdout, result.stderr].filter(Boolean).join("\n").trim() + const result = await execFileAsync("render", ["blueprints", "validate", path, "--output", "json"], { + cwd, + signal, + timeout: 3e4 + }); + const output = [result.stdout, result.stderr].filter(Boolean).join("\n").trim(); return { ok: true, command, cwd, - output: output || "render blueprints validate completed successfully.", - } + output: output || "render blueprints validate completed successfully." + }; } catch (error) { if (isNodeError(error) && error.code === "ENOENT") { return { @@ -113,67 +37,148 @@ async function validateBlueprint(filePath: string): Promise { command, cwd, output: "Render CLI not found. Install it to validate render.yaml:\n macOS: brew install render\n Linux: curl -fsSL https://raw.githubusercontent.com/render-oss/cli/main/bin/install.sh | sh", - error: "render-cli-not-found", - } + error: "render-cli-not-found" + }; } - - const output = getExecErrorOutput(error) + const output = getExecErrorOutput(error); return { ok: false, command, cwd, output: output || String(error), - error: "validation-failed", - } + error: "validation-failed" + }; } } - -function extractTouchedFiles(value: unknown): string[] { - const files: string[] = [] - collectTouchedFiles(value, files) - return files +function getExecErrorOutput(error) { + if (typeof error === "object" && error !== null) { + const withOutput = error; + return [withOutput.stdout, withOutput.stderr, withOutput.message].filter((value) => typeof value === "string" && value.length > 0).join("\n").trim(); + } + return ""; +} +function isNodeError(error) { + return typeof error === "object" && error !== null && "code" in error; } -const filePathKeys = new Set(["file", "filePath", "file_path", "filename", "path"]) - -function collectTouchedFiles(value: unknown, files: string[], key?: string) { +// src/touched-files.ts +var FILE_PATH_KEYS = /* @__PURE__ */ new Set(["file", "filePath", "file_path", "filename", "path"]); +var MUTATING_TOOLS = /* @__PURE__ */ new Set(["write", "edit", "multiedit", "apply_patch"]); +function extractMutatedFiles(tool2, args, metadata) { + if (!MUTATING_TOOLS.has(tool2)) return []; + if (tool2 !== "apply_patch") return [...new Set(extractTouchedFiles(args))]; + if (isRecord(metadata) && Array.isArray(metadata.files)) { + return [...new Set(metadata.files.flatMap((file) => { + if (!isRecord(file) || file.type === "delete") return []; + const path = file.movePath ?? file.filePath; + return typeof path === "string" ? [path] : []; + }))]; + } + if (!isRecord(args) || typeof args.patchText !== "string") return []; + const files = []; + for (const line of args.patchText.split(/\r?\n/)) { + const match = /^\*\*\* (Add File|Update File|Move to): (.+)$/.exec(line); + if (!match) continue; + if (match[1] === "Move to") files.pop(); + files.push(match[2]); + } + return [...new Set(files)]; +} +function isRecord(value) { + return typeof value === "object" && value !== null && !Array.isArray(value); +} +function extractTouchedFiles(value) { + const files = []; + collectTouchedFiles(value, files); + return files; +} +function collectTouchedFiles(value, files, key) { if (typeof value === "string") { - if (key && filePathKeys.has(key)) { - files.push(value) + if (key && FILE_PATH_KEYS.has(key)) { + files.push(value); } - return + return; } - if (Array.isArray(value)) { for (const item of value) { - collectTouchedFiles(item, files) + collectTouchedFiles(item, files); } - return + return; } - if (typeof value !== "object" || value === null) { - return + return; } - for (const [entryKey, entryValue] of Object.entries(value)) { - collectTouchedFiles(entryValue, files, entryKey) - } -} - -function getExecErrorOutput(error: unknown) { - if (typeof error === "object" && error !== null) { - const withOutput = error as { stdout?: unknown; stderr?: unknown; message?: unknown } - return [withOutput.stdout, withOutput.stderr, withOutput.message] - .filter((value): value is string => typeof value === "string" && value.length > 0) - .join("\n") - .trim() + collectTouchedFiles(entryValue, files, entryKey); } - - return "" -} - -function isNodeError(error: unknown): error is NodeJS.ErrnoException { - return typeof error === "object" && error !== null && "code" in error } -export default RenderPlugin +// src/index.ts +var RenderPlugin = async ({ client, directory }) => { + await client.app.log({ + body: { + service: "render-opencode-plugin", + level: "info", + message: "Render OpenCode plugin initialized" + } + }); + return { + tool: { + render_validate_blueprint: tool({ + description: "Validate a Render Blueprint file with the Render CLI.", + args: { + path: tool.schema.string().describe("Path to render.yaml or render.yml.") + }, + async execute(args, context) { + const result = await validateBlueprint(args.path, context.directory, context.abort); + return { + title: result.ok ? "Render Blueprint valid" : "Render Blueprint validation failed", + output: result.output, + metadata: { + command: result.command, + cwd: result.cwd, + ok: result.ok, + error: result.error + } + }; + } + }) + }, + "tool.execute.after": async (input, output) => { + const paths = [...new Set(extractMutatedFiles(input.tool, input.args, output.metadata).filter(isBlueprintFile).map((path) => resolve2(directory, path)))]; + const failures = []; + for (const path of paths) { + const result = await validateBlueprint(path, directory); + if (!result.ok) { + failures.push({ path, ...result }); + continue; + } + await client.app.log({ body: { + service: "render-opencode-plugin", + level: "info", + message: "Validated Render Blueprint", + extra: { path, cwd: result.cwd } + } }); + } + if (failures.length === 0) return; + output.title = "Render Blueprint validation"; + output.output = [output.output, ...failures.map((result) => `${result.path} +${result.output}`)].filter(Boolean).join("\n\n"); + output.metadata = { + ...output.metadata, + renderBlueprintValidation: { + ok: false, + command: failures[0].command, + cwd: failures[0].cwd, + error: failures[0].error, + results: failures + } + }; + } + }; +}; +var index_default = RenderPlugin; +export { + RenderPlugin, + index_default as default +}; diff --git a/assets/setup.d.mts b/assets/setup.d.mts new file mode 100644 index 0000000..90c0c93 --- /dev/null +++ b/assets/setup.d.mts @@ -0,0 +1,13 @@ +export type InstallOptions = { + sourceAssetsDir?: string + targetConfigDir?: string + force?: boolean + enableMcp?: boolean + dryRun?: boolean + includePlugin?: boolean +} +export type InstallResult = { written: string[]; skipped: string[]; removed: string[] } +export function installOpenCodeAssets(options?: InstallOptions): Promise +export function getDefaultConfigDir(): string +export function mergeMcpConfig(configPath: string, result: InstallResult, dryRun?: boolean, force?: boolean): Promise +export function main(argv?: string[]): Promise diff --git a/assets/setup.mjs b/assets/setup.mjs new file mode 100644 index 0000000..5a6d93f --- /dev/null +++ b/assets/setup.mjs @@ -0,0 +1,202 @@ +#!/usr/bin/env node +import { createHash } from "node:crypto" +import { realpathSync } from "node:fs" +import { copyFile, lstat, mkdir, readdir, readFile, unlink, writeFile } from "node:fs/promises" +import { dirname, isAbsolute, join, relative, resolve } from "node:path" +import { homedir } from "node:os" +import { fileURLToPath } from "node:url" + +const moduleDir = dirname(fileURLToPath(import.meta.url)) +const MANIFEST_NAME = ".render-opencode-manifest.json" +const ASSET_DIRECTORIES = ["skills", "commands", "agents", "plugins"] +const RENDER_MCP_CONFIG = { + type: "remote", + url: "https://mcp.render.com/mcp", + enabled: true, + oauth: false, + headers: { Authorization: "Bearer {env:RENDER_API_KEY}" }, +} + +export function getDefaultConfigDir() { + if (process.env.OPENCODE_CONFIG_DIR) return resolve(process.env.OPENCODE_CONFIG_DIR) + if (process.env.XDG_CONFIG_HOME) return join(resolve(process.env.XDG_CONFIG_HOME), "opencode") + return join(homedir(), ".config", "opencode") +} + +export async function installOpenCodeAssets(options = {}) { + const sourceAssetsDir = options.sourceAssetsDir ?? join(moduleDir, "opencode") + if (!(await pathExists(sourceAssetsDir))) throw new Error(`Cannot locate bundled OpenCode assets: ${sourceAssetsDir}`) + const targetConfigDir = options.targetConfigDir ?? getDefaultConfigDir() + const result = { written: [], skipped: [], removed: [] } + const manifestPath = join(targetConfigDir, MANIFEST_NAME) + const previous = await readManifest(manifestPath) + const legacy = await readManifest(join(moduleDir, "legacy-assets.json")) + const directories = ASSET_DIRECTORIES.filter((name) => name !== "plugins" || options.includePlugin) + const belongsToInstall = (path) => directories.includes(path.split("/")[0]) + const next = { ...previous.files } + const bundled = new Map() + + for (const directory of directories) { + const source = join(sourceAssetsDir, directory) + if (!(await pathExists(source))) continue + for (const file of await listFiles(source)) { + const path = `${directory}/${relative(source, file).split("\\").join("/")}` + bundled.set(path, { file, hash: await fileHash(file) }) + } + } + + for (const [path, asset] of bundled) { + const destination = join(targetConfigDir, path) + const exists = await pathExists(destination) + const current = exists ? await fileHash(destination) : undefined + if (current === asset.hash) { + next[path] = asset.hash + result.skipped.push(destination) + continue + } + const unmodified = current !== undefined && (current === previous.files[path] || current === legacy.files[path]) + if (exists && !options.force && !unmodified) { + result.skipped.push(destination) + continue + } + next[path] = asset.hash + result.written.push(destination) + if (!options.dryRun) { + await mkdir(dirname(destination), { recursive: true }) + // Replace a file symlink itself instead of following it outside the installation. + if (exists && (await lstat(destination)).isSymbolicLink()) await unlink(destination) + await copyFile(asset.file, destination) + } + } + + // A manifest tracks ownership; legacy hashes safely adopt unmodified pre-manifest installs. + for (const path of new Set([...Object.keys(previous.files), ...Object.keys(legacy.files)])) { + if (!belongsToInstall(path) || bundled.has(path)) continue + const destination = join(targetConfigDir, path) + if (!(await pathExists(destination))) { + delete next[path] + continue + } + const current = await fileHash(destination) + const unmodified = current !== undefined && (current === previous.files[path] || current === legacy.files[path]) + if (!unmodified && !(options.force && previous.files[path])) { + result.skipped.push(destination) + continue + } + delete next[path] + result.removed.push(destination) + if (!options.dryRun) await unlink(destination) + } + + if (options.enableMcp) { + await mergeMcpConfig(join(targetConfigDir, "opencode.json"), result, Boolean(options.dryRun), Boolean(options.force)) + } + + if (!options.dryRun && (bundled.size > 0 || Object.keys(previous.files).length > 0)) { + await mkdir(targetConfigDir, { recursive: true }) + await writeFile(manifestPath, `${JSON.stringify({ version: 1, files: next }, null, 2)}\n`) + } + return result +} + +export async function mergeMcpConfig(configPath, result, dryRun = false, force = false) { + const existing = await readJsonFile(configPath) + if (existing.mcp !== undefined && !isRecord(existing.mcp)) throw new Error(`${configPath} has a non-object mcp field.`) + if (Object.hasOwn(existing.mcp ?? {}, "render") && !force) { + result.skipped.push(configPath) + return + } + const next = { ...existing, mcp: { ...existing.mcp, render: RENDER_MCP_CONFIG } } + result.written.push(configPath) + if (!dryRun) { + await mkdir(dirname(configPath), { recursive: true }) + await writeFile(configPath, `${JSON.stringify(next, null, 2)}\n`) + } +} + +async function readJsonFile(path) { + if (!(await pathExists(path))) return { $schema: "https://opencode.ai/config.json" } + const text = await readFile(path, "utf8") + if (!text.trim()) return { $schema: "https://opencode.ai/config.json" } + const value = JSON.parse(text) + if (!isRecord(value)) throw new Error(`${path} must contain a JSON object.`) + return value +} + +async function readManifest(path) { + if (!(await pathExists(path))) return { version: 1, files: {} } + const value = JSON.parse(await readFile(path, "utf8")) + if (value?.version !== 1 || !isRecord(value.files)) throw new Error(`Invalid Render installation manifest: ${path}`) + for (const [file, hash] of Object.entries(value.files)) { + if (isAbsolute(file) || file.includes("\\") || file.split("/").some((part) => part === ".." || part === "" || part === ".") || !ASSET_DIRECTORIES.includes(file.split("/")[0]) || !/^[a-f0-9]{64}$/.test(hash)) { + throw new Error(`Invalid Render installation manifest entry: ${file}`) + } + } + return value +} + +async function fileHash(path) { + const stat = await lstat(path) + if (!stat.isFile()) return undefined + return createHash("sha256").update(await readFile(path)).digest("hex") +} + +async function listFiles(directory) { + const entries = await readdir(directory, { withFileTypes: true }) + const nested = await Promise.all(entries.map(async (entry) => { + const path = join(directory, entry.name) + if (entry.isDirectory()) return listFiles(path) + return entry.isFile() ? [path] : [] + })) + return nested.flat() +} + +async function pathExists(path) { + try { await lstat(path); return true } + catch (error) { if (error.code === "ENOENT") return false; throw error } +} + +function isRecord(value) { return typeof value === "object" && value !== null && !Array.isArray(value) } + +export async function main(argv = process.argv.slice(2)) { + const options = { force: false, enableMcp: false, dryRun: false, includePlugin: false } + for (let index = 0; index < argv.length; index += 1) { + const arg = argv[index] + if (arg === "setup") continue + if (arg === "--config-dir") { + const path = argv[++index] + if (!path || path.startsWith("--")) throw new Error("--config-dir requires a path.") + options.targetConfigDir = resolve(path) + continue + } + if (arg === "--force") { options.force = true; continue } + if (arg === "--enable-mcp") { options.enableMcp = true; continue } + if (arg === "--dry-run") { options.dryRun = true; continue } + if (arg === "--include-plugin") { options.includePlugin = true; continue } + if (arg === "--help" || arg === "-h") { + console.log(`render-opencode setup + +Install or upgrade Render skills, commands, and agent files. +Unmodified bundled files update automatically; user edits are preserved. + +Options: + --config-dir OpenCode config directory (OPENCODE_CONFIG_DIR or XDG_CONFIG_HOME/opencode). + --enable-mcp Add Render MCP config; preserve an existing entry unless forced. + --force Overwrite modified or unmanaged files and existing Render MCP config. + --dry-run Print planned writes, removals, and skipped files without changing files. + --include-plugin Also install the standalone plugin (used by install.sh). + -h, --help Show this help. +`) + return + } + throw new Error(`Unknown argument: ${arg}`) + } + const result = await installOpenCodeAssets(options) + for (const path of result.written) console.log(`${options.dryRun ? "would write" : "wrote"} ${path}`) + for (const path of result.removed) console.log(`${options.dryRun ? "would remove" : "removed"} ${path}`) + for (const path of result.skipped) console.log(`skipped unchanged or user-owned ${path}`) +} + +if (process.argv[1] && realpathSync(process.argv[1]) === fileURLToPath(import.meta.url)) { + main().catch((error) => { console.error(error.message); process.exitCode = 1 }) +} diff --git a/install.sh b/install.sh index cfb339d..073660f 100755 --- a/install.sh +++ b/install.sh @@ -2,76 +2,57 @@ set -euo pipefail REPO_TARBALL_URL="https://github.com/render-oss/render-opencode-plugin/archive/refs/heads/main.tar.gz" -CONFIG_DIR="${OPENCODE_CONFIG_DIR:-${HOME:-}/.config/opencode}" SOURCE_DIR="" -FORCE="0" -DRY_RUN="0" -ENABLE_MCP="0" +SETUP_ARGS=(setup --include-plugin) usage() { - cat <<'EOF' -Install the Render OpenCode plugin from GitHub. + cat <<'HELP' +Install or upgrade the Render OpenCode plugin from GitHub. +Requires Node.js 18+ or Bun. -Usage: - install.sh [options] +Usage: install.sh [options] -Options: - --config-dir Target OpenCode config directory. Defaults to ~/.config/opencode. + --config-dir Target OpenCode config directory; defaults to OpenCode's XDG location. --source Use a local repo checkout instead of downloading from GitHub. - --enable-mcp Add the Render MCP server to opencode.json. - --force Overwrite existing files. - --dry-run Print what would change without writing files. + --enable-mcp Add Render MCP config, preserving an existing entry unless forced. + --force Overwrite user-modified files and existing Render MCP config. + --dry-run Preview writes and removals without changing files. -h, --help Show this help. -EOF + +Unmodified bundled files update automatically. Modified files are preserved. +HELP } while [[ $# -gt 0 ]]; do case "$1" in - --config-dir) - CONFIG_DIR="$2" - shift 2 - ;; - --source) - SOURCE_DIR="$2" + --source|--config-dir) + if [[ $# -lt 2 || -z "$2" || "$2" == --* ]]; then + echo "$1 requires a path." >&2 + exit 1 + fi + if [[ "$1" == "--source" ]]; then SOURCE_DIR="$2"; else SETUP_ARGS+=("$1" "$2"); fi shift 2 ;; - --force) - FORCE="1" - shift - ;; - --enable-mcp) - ENABLE_MCP="1" - shift - ;; - --dry-run) - DRY_RUN="1" - shift - ;; - -h|--help) - usage - exit 0 - ;; - *) - echo "Unknown option: $1" >&2 - usage >&2 - exit 1 - ;; + --force|--enable-mcp|--dry-run) SETUP_ARGS+=("$1"); shift ;; + -h|--help) usage; exit 0 ;; + *) echo "Unknown option: $1" >&2; usage >&2; exit 1 ;; esac done -if [[ -z "$CONFIG_DIR" || "$CONFIG_DIR" == "/.config/opencode" ]]; then - echo "Cannot determine OpenCode config directory. Set HOME or pass --config-dir." >&2 +if command -v node >/dev/null 2>&1; then + RUNTIME="node" +elif command -v bun >/dev/null 2>&1; then + RUNTIME="bun" +else + echo "Install requires Node.js 18+ or Bun for the shared asset installer." >&2 exit 1 fi -TMPDIR="$(mktemp -d)" -cleanup() { - rm -rf "$TMPDIR" -} -trap cleanup EXIT +INSTALL_TMP_DIR="$(mktemp -d)" +trap 'rm -rf "$INSTALL_TMP_DIR"' EXIT if [[ -z "$SOURCE_DIR" ]]; then - ARCHIVE="$TMPDIR/render-opencode-plugin.tar.gz" + ARCHIVE="$INSTALL_TMP_DIR/render-opencode-plugin.tar.gz" if command -v curl >/dev/null 2>&1; then curl -fsSL "$REPO_TARBALL_URL" -o "$ARCHIVE" elif command -v wget >/dev/null 2>&1; then @@ -80,107 +61,14 @@ if [[ -z "$SOURCE_DIR" ]]; then echo "Install requires curl or wget." >&2 exit 1 fi - - tar -xzf "$ARCHIVE" -C "$TMPDIR" - SOURCE_DIR="$(find "$TMPDIR" -mindepth 1 -maxdepth 1 -type d | head -n 1)" + tar -xzf "$ARCHIVE" -C "$INSTALL_TMP_DIR" + SOURCE_DIR="$(find "$INSTALL_TMP_DIR" -mindepth 1 -maxdepth 1 -type d | head -n 1)" fi -ASSETS_DIR="$SOURCE_DIR/assets/opencode" -if [[ ! -d "$ASSETS_DIR" ]]; then - echo "Could not find assets/opencode in $SOURCE_DIR." >&2 +if [[ ! -f "$SOURCE_DIR/assets/setup.mjs" || ! -d "$SOURCE_DIR/assets/opencode" ]]; then + echo "Could not find the Render installer and assets in $SOURCE_DIR." >&2 exit 1 fi -install_file() { - local src="$1" - local dest="$2" - - if [[ -e "$dest" && "$FORCE" != "1" ]]; then - echo "skipped existing $dest" - return - fi - - if [[ "$DRY_RUN" == "1" ]]; then - echo "would write $dest" - return - fi - - mkdir -p "$(dirname "$dest")" - cp "$src" "$dest" - echo "wrote $dest" -} - -for dir in plugins skills commands agents; do - [[ -d "$ASSETS_DIR/$dir" ]] || continue - while IFS= read -r -d '' src; do - rel="${src#"$ASSETS_DIR/$dir/"}" - install_file "$src" "$CONFIG_DIR/$dir/$rel" - done < <(find "$ASSETS_DIR/$dir" -type f -print0) -done - -install_mcp_config() { - local config_path="$CONFIG_DIR/opencode.json" - - if ! command -v python3 >/dev/null 2>&1; then - echo "Cannot enable MCP because python3 is not installed." >&2 - exit 1 - fi - - if [[ "$DRY_RUN" == "1" ]]; then - echo "would merge Render MCP server into $config_path" - return - fi - - mkdir -p "$CONFIG_DIR" - - CONFIG_PATH="$config_path" FORCE="$FORCE" python3 <<'PY' -import json -import os -from pathlib import Path - -config_path = Path(os.environ["CONFIG_PATH"]) -force = os.environ.get("FORCE") == "1" - -if config_path.exists() and config_path.read_text().strip(): - config = json.loads(config_path.read_text()) -else: - config = {"$schema": "https://opencode.ai/config.json"} - -if not isinstance(config, dict): - raise SystemExit(f"{config_path} must contain a JSON object.") - -mcp = config.setdefault("mcp", {}) -if not isinstance(mcp, dict): - raise SystemExit(f"{config_path} has a non-object mcp field.") - -if "render" in mcp and not force: - print(f"skipped existing Render MCP config in {config_path}") - raise SystemExit(0) - -mcp["render"] = { - "type": "remote", - "url": "https://mcp.render.com/mcp", - "enabled": True, - "oauth": False, - "headers": { - "Authorization": "Bearer {env:RENDER_API_KEY}", - }, -} - -config_path.write_text(json.dumps(config, indent=2) + "\n") -print(f"merged Render MCP server into {config_path}") -PY -} - -if [[ "$ENABLE_MCP" == "1" ]]; then - install_mcp_config -fi - -cat <=18" } }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.0.tgz", - "integrity": "sha512-lhRUCeuOyJQURhTxl4WkpFTjIsbDayJHih5kZC1giwE+MhIzAb7mEsQMqMf18rHLsrb5qI1tafG20mLxEWcWlA==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.2.tgz", + "integrity": "sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ==", "cpu": [ "ppc64" ], @@ -75,9 +43,9 @@ } }, "node_modules/@esbuild/android-arm": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.0.tgz", - "integrity": "sha512-wqh0ByljabXLKHeWXYLqoJ5jKC4XBaw6Hk08OfMrCRd2nP2ZQ5eleDZC41XHyCNgktBGYMbqnrJKq/K/lzPMSQ==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.2.tgz", + "integrity": "sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg==", "cpu": [ "arm" ], @@ -92,9 +60,9 @@ } }, "node_modules/@esbuild/android-arm64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.0.tgz", - "integrity": "sha512-+WzIXQOSaGs33tLEgYPYe/yQHf0WTU0X42Jca3y8NWMbUVhp7rUnw+vAsRC/QiDrdD31IszMrZy+qwPOPjd+rw==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.2.tgz", + "integrity": "sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A==", "cpu": [ "arm64" ], @@ -109,9 +77,9 @@ } }, "node_modules/@esbuild/android-x64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.0.tgz", - "integrity": "sha512-+VJggoaKhk2VNNqVL7f6S189UzShHC/mR9EE8rDdSkdpN0KflSwWY/gWjDrNxxisg8Fp1ZCD9jLMo4m0OUfeUA==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.2.tgz", + "integrity": "sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q==", "cpu": [ "x64" ], @@ -126,9 +94,9 @@ } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.0.tgz", - "integrity": "sha512-0T+A9WZm+bZ84nZBtk1ckYsOvyA3x7e2Acj1KdVfV4/2tdG4fzUp91YHx+GArWLtwqp77pBXVCPn2We7Letr0Q==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.2.tgz", + "integrity": "sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw==", "cpu": [ "arm64" ], @@ -143,9 +111,9 @@ } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.0.tgz", - "integrity": "sha512-fyzLm/DLDl/84OCfp2f/XQ4flmORsjU7VKt8HLjvIXChJoFFOIL6pLJPH4Yhd1n1gGFF9mPwtlN5Wf82DZs+LQ==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.2.tgz", + "integrity": "sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw==", "cpu": [ "x64" ], @@ -160,9 +128,9 @@ } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.0.tgz", - "integrity": "sha512-l9GeW5UZBT9k9brBYI+0WDffcRxgHQD8ShN2Ur4xWq/NFzUKm3k5lsH4PdaRgb2w7mI9u61nr2gI2mLI27Nh3Q==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.2.tgz", + "integrity": "sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw==", "cpu": [ "arm64" ], @@ -177,9 +145,9 @@ } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.0.tgz", - "integrity": "sha512-BXoQai/A0wPO6Es3yFJ7APCiKGc1tdAEOgeTNy3SsB491S3aHn4S4r3e976eUnPdU+NbdtmBuLncYir2tMU9Nw==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.2.tgz", + "integrity": "sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg==", "cpu": [ "x64" ], @@ -194,9 +162,9 @@ } }, "node_modules/@esbuild/linux-arm": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.0.tgz", - "integrity": "sha512-CjaaREJagqJp7iTaNQjjidaNbCKYcd4IDkzbwwxtSvjI7NZm79qiHc8HqciMddQ6CKvJT6aBd8lO9kN/ZudLlw==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.2.tgz", + "integrity": "sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w==", "cpu": [ "arm" ], @@ -211,9 +179,9 @@ } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.0.tgz", - "integrity": "sha512-RVyzfb3FWsGA55n6WY0MEIEPURL1FcbhFE6BffZEMEekfCzCIMtB5yyDcFnVbTnwk+CLAgTujmV/Lgvih56W+A==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.2.tgz", + "integrity": "sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug==", "cpu": [ "arm64" ], @@ -228,9 +196,9 @@ } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.0.tgz", - "integrity": "sha512-KBnSTt1kxl9x70q+ydterVdl+Cn0H18ngRMRCEQfrbqdUuntQQ0LoMZv47uB97NljZFzY6HcfqEZ2SAyIUTQBQ==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.2.tgz", + "integrity": "sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ==", "cpu": [ "ia32" ], @@ -245,9 +213,9 @@ } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.0.tgz", - "integrity": "sha512-zpSlUce1mnxzgBADvxKXX5sl8aYQHo2ezvMNI8I0lbblJtp8V4odlm3Yzlj7gPyt3T8ReksE6bK+pT3WD+aJRg==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.2.tgz", + "integrity": "sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ==", "cpu": [ "loong64" ], @@ -262,9 +230,9 @@ } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.0.tgz", - "integrity": "sha512-2jIfP6mmjkdmeTlsX/9vmdmhBmKADrWqN7zcdtHIeNSCH1SqIoNI63cYsjQR8J+wGa4Y5izRcSHSm8K3QWmk3w==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.2.tgz", + "integrity": "sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA==", "cpu": [ "mips64el" ], @@ -279,9 +247,9 @@ } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.0.tgz", - "integrity": "sha512-bc0FE9wWeC0WBm49IQMPSPILRocGTQt3j5KPCA8os6VprfuJ7KD+5PzESSrJ6GmPIPJK965ZJHTUlSA6GNYEhg==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.2.tgz", + "integrity": "sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ==", "cpu": [ "ppc64" ], @@ -296,9 +264,9 @@ } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.0.tgz", - "integrity": "sha512-SQPZOwoTTT/HXFXQJG/vBX8sOFagGqvZyXcgLA3NhIqcBv1BJU1d46c0rGcrij2B56Z2rNiSLaZOYW5cUk7yLQ==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.2.tgz", + "integrity": "sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA==", "cpu": [ "riscv64" ], @@ -313,9 +281,9 @@ } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.0.tgz", - "integrity": "sha512-SCfR0HN8CEEjnYnySJTd2cw0k9OHB/YFzt5zgJEwa+wL/T/raGWYMBqwDNAC6dqFKmJYZoQBRfHjgwLHGSrn3Q==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.2.tgz", + "integrity": "sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg==", "cpu": [ "s390x" ], @@ -330,9 +298,9 @@ } }, "node_modules/@esbuild/linux-x64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.0.tgz", - "integrity": "sha512-us0dSb9iFxIi8srnpl931Nvs65it/Jd2a2K3qs7fz2WfGPHqzfzZTfec7oxZJRNPXPnNYZtanmRc4AL/JwVzHQ==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.2.tgz", + "integrity": "sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ==", "cpu": [ "x64" ], @@ -347,9 +315,9 @@ } }, "node_modules/@esbuild/netbsd-arm64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.0.tgz", - "integrity": "sha512-CR/RYotgtCKwtftMwJlUU7xCVNg3lMYZ0RzTmAHSfLCXw3NtZtNpswLEj/Kkf6kEL3Gw+BpOekRX0BYCtklhUw==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.2.tgz", + "integrity": "sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw==", "cpu": [ "arm64" ], @@ -364,9 +332,9 @@ } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.0.tgz", - "integrity": "sha512-nU1yhmYutL+fQ71Kxnhg8uEOdC0pwEW9entHykTgEbna2pw2dkbFSMeqjjyHZoCmt8SBkOSvV+yNmm94aUrrqw==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.2.tgz", + "integrity": "sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw==", "cpu": [ "x64" ], @@ -381,9 +349,9 @@ } }, "node_modules/@esbuild/openbsd-arm64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.0.tgz", - "integrity": "sha512-cXb5vApOsRsxsEl4mcZ1XY3D4DzcoMxR/nnc4IyqYs0rTI8ZKmW6kyyg+11Z8yvgMfAEldKzP7AdP64HnSC/6g==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.2.tgz", + "integrity": "sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ==", "cpu": [ "arm64" ], @@ -398,9 +366,9 @@ } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.0.tgz", - "integrity": "sha512-8wZM2qqtv9UP3mzy7HiGYNH/zjTA355mpeuA+859TyR+e+Tc08IHYpLJuMsfpDJwoLo1ikIJI8jC3GFjnRClzA==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.2.tgz", + "integrity": "sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw==", "cpu": [ "x64" ], @@ -415,9 +383,9 @@ } }, "node_modules/@esbuild/openharmony-arm64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.0.tgz", - "integrity": "sha512-FLGfyizszcef5C3YtoyQDACyg95+dndv79i2EekILBofh5wpCa1KuBqOWKrEHZg3zrL3t5ouE5jgr94vA+Wb2w==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.2.tgz", + "integrity": "sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q==", "cpu": [ "arm64" ], @@ -432,9 +400,9 @@ } }, "node_modules/@esbuild/sunos-x64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.0.tgz", - "integrity": "sha512-1ZgjUoEdHZZl/YlV76TSCz9Hqj9h9YmMGAgAPYd+q4SicWNX3G5GCyx9uhQWSLcbvPW8Ni7lj4gDa1T40akdlw==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.2.tgz", + "integrity": "sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g==", "cpu": [ "x64" ], @@ -449,9 +417,9 @@ } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.0.tgz", - "integrity": "sha512-Q9StnDmQ/enxnpxCCLSg0oo4+34B9TdXpuyPeTedN/6+iXBJ4J+zwfQI28u/Jl40nOYAxGoNi7mFP40RUtkmUA==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.2.tgz", + "integrity": "sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ==", "cpu": [ "arm64" ], @@ -466,9 +434,9 @@ } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.0.tgz", - "integrity": "sha512-zF3ag/gfiCe6U2iczcRzSYJKH1DCI+ByzSENHlM2FcDbEeo5Zd2C86Aq0tKUYAJJ1obRP84ymxIAksZUcdztHA==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.2.tgz", + "integrity": "sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA==", "cpu": [ "ia32" ], @@ -483,9 +451,9 @@ } }, "node_modules/@esbuild/win32-x64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.0.tgz", - "integrity": "sha512-pEl1bO9mfAmIC+tW5btTmrKaujg3zGtUmWNdCw/xs70FBjwAL3o9OEKNHvNmnyylD6ubxUERiEhdsL0xBQ9efw==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.2.tgz", + "integrity": "sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g==", "cpu": [ "x64" ], @@ -500,9 +468,9 @@ } }, "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.5", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", - "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.6.0.tgz", + "integrity": "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==", "dev": true, "license": "MIT" }, @@ -584,25 +552,6 @@ "win32" ] }, - "node_modules/@napi-rs/wasm-runtime": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.4.tgz", - "integrity": "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@tybys/wasm-util": "^0.10.1" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/Brooooooklyn" - }, - "peerDependencies": { - "@emnapi/core": "^1.7.1", - "@emnapi/runtime": "^1.7.1" - } - }, "node_modules/@opencode-ai/plugin": { "version": "1.15.11", "resolved": "https://registry.npmjs.org/@opencode-ai/plugin/-/plugin-1.15.11.tgz", @@ -640,19 +589,36 @@ } }, "node_modules/@oxc-project/types": { - "version": "0.132.0", - "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.132.0.tgz", - "integrity": "sha512-FESMOxil5Se014ui/Eq8fT5uHJo6nIRwH0PfJrZJXs6Gek3ZVFOrpUv3YIZT20m+extU98Hg1Ym72U58rlsxUQ==", + "version": "0.152.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.152.0.tgz", + "integrity": "sha512-oM/5rLBm2tPkg0iBgkH/FOeR3PCDpY19GTgAZjMFM8h9WI9VW7cLgzp6nwtarYKmovavIQZ+Fe/RKX/8C8O/Rw==", "dev": true, "license": "MIT", "funding": { - "url": "https://github.com/sponsors/Boshen" + "url": "https://github.com/sponsors/oxc-project" + } + }, + "node_modules/@rolldown/binding-android-arm-eabi": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm-eabi/-/binding-android-arm-eabi-1.2.12.tgz", + "integrity": "sha512-dB/a1214qKfHMXCpgqR4OZT+jS4kTyEXbQGJPqzobt5EwH5rX080pxE37alt3RzvR1bf1Yz/yGqRfrYAxuPw0A==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" } }, "node_modules/@rolldown/binding-android-arm64": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.2.tgz", - "integrity": "sha512-ZS4D1JPGn/MYQN/SYDWftIE/nVsM8j/AFOYEzAoOE2O3NktQOZru+/vYXGbR/qtdLdIfGCP0lcoJiYVzsEz+iQ==", + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.12.tgz", + "integrity": "sha512-7KHFgQ5VJxIHcLlrwrc3Xbds7oTNQT7Pgi9gQCJKrd2VGab/UksIOYp6VD8MzCstGxOKMgNamPwUCfxPdP1OHg==", "cpu": [ "arm64" ], @@ -667,9 +633,9 @@ } }, "node_modules/@rolldown/binding-darwin-arm64": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.2.tgz", - "integrity": "sha512-vdFA9+C/rekyGce7WqHs/xoT0ioZEWaOFyZLIV1mEeNFaFDUQrPIo8Vs2GvJ6eetb3rzDUtUBgzto3ExpXJB3w==", + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.12.tgz", + "integrity": "sha512-3YIhqHD96nA5SaYNRBR16HnGv4oavZvXfD/ayHM+oYZ0WD/8lBAtf6zQua4kEyAvpqrluKXl0lnOBoiNby7x9w==", "cpu": [ "arm64" ], @@ -684,9 +650,9 @@ } }, "node_modules/@rolldown/binding-darwin-x64": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.2.tgz", - "integrity": "sha512-BewSOwTHazv77DTYiAZXSqqKZ4KP/KonFisDMVU7PImxoWfB2aepnPhd2E4SWz3zDzYgDNbs6jBmTdgNnF02GA==", + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.12.tgz", + "integrity": "sha512-UuuJ35MFw4gmFOrE9pEqIV+K3syIKveph+Qc1/ljHZVdoDW4pz/JHR/eMVom+TZGl/5OOvGJOWaOCVt3ZfqhxA==", "cpu": [ "x64" ], @@ -701,9 +667,9 @@ } }, "node_modules/@rolldown/binding-freebsd-x64": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.2.tgz", - "integrity": "sha512-m41o7M0YWtUdqk61Tb+jnKb2rN++iRdIASlExkUoKfIAH30DOHCB8fVLzSUpbWHHU8esmEioY62PxzexE8MBuA==", + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.12.tgz", + "integrity": "sha512-uMvssit0a4W+/7D8CbHUvG719mH3R2jwXAlh/XcPvuHTE0g++LymF88DCGNX0HM2rBOn0xrzgXktIB6fLSJBTQ==", "cpu": [ "x64" ], @@ -718,9 +684,9 @@ } }, "node_modules/@rolldown/binding-linux-arm-gnueabihf": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.2.tgz", - "integrity": "sha512-jcojB9H7W/jS29pMKWAK1N+fU99vXodHDTatS3b3y/XSOCiHo0kkA74pL3jJmkoQtYpOCxDvaKs1fo2Ij/1X5w==", + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.12.tgz", + "integrity": "sha512-XcFu0R0xWnwzSf4IQgFH1rJIckPN1pLy2R+4r9IDB7Yfu/ys9cVqfa4pBrMHj7a3gl8mIR4nRNPg0e5IvEVs6g==", "cpu": [ "arm" ], @@ -735,13 +701,16 @@ } }, "node_modules/@rolldown/binding-linux-arm64-gnu": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.2.tgz", - "integrity": "sha512-1jn6qDU5iiOgFgygDzKUuKP0maTi0/f1+sBLgvij/76C77Nm3ts6ufz9Bjg5q5dduxiUIxtq86JIoBvo1xQ4Ig==", + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.12.tgz", + "integrity": "sha512-260UrKgn8tz39ak+SMDOirKzr7V04M9dWPw5llW00SwBivCZoWcRBKV1d8cXnRkUmSZA3BdiUmBHWk7734Ulpw==", "cpu": [ "arm64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -752,13 +721,16 @@ } }, "node_modules/@rolldown/binding-linux-arm64-musl": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.2.tgz", - "integrity": "sha512-QVLO/czFMdoMFSqlX3bcswcJNm/23r+qoa/jgtmFc/qEp6/jXmIkDjF/XIo8dPfGaiwy1xfQn8o77L79GeXFgw==", + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.12.tgz", + "integrity": "sha512-5YK1I9SqDkbPgc1IA8BgDl34suqUS2q0KWnBrirm0E51YjOs6eo6dV6jbQfNE/argHRSvd0QUGgtpIoYx+WWpw==", "cpu": [ "arm64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -769,13 +741,16 @@ } }, "node_modules/@rolldown/binding-linux-ppc64-gnu": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.2.tgz", - "integrity": "sha512-hgO5Abm0w5UL6FEa2iFnZqo2KlK7TQ5QhV5x09hujBf7t5KzHQ1VmfPuTpqRy/rNlSxua3eWH374xxiVrP+lcA==", + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.12.tgz", + "integrity": "sha512-Rkcrmp7eFRg74yL5fXEU91JEWbdEPLevWwGtXpmhbjlD1StScbWTmO94Bhly+Mo+ketKYkdmM1vNUKeWSlx8cQ==", "cpu": [ "ppc64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -786,13 +761,16 @@ } }, "node_modules/@rolldown/binding-linux-s390x-gnu": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.2.tgz", - "integrity": "sha512-fy8rXxuYEu602abC8MUNaPjYLIFzReOaEIEMKMUa0rFEUxNpVXhs15KSSQ4qlqSaM7B6rcj9rDZgADh/IGDzLQ==", + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.12.tgz", + "integrity": "sha512-qvK4DuAsQc2BSjlx+Xr+IzOIvvxbGZqxFwdWfG6F518Erj0GGISyQbJ6pIappnOxlNPzNHvo/L0BwB30GZ+zVw==", "cpu": [ "s390x" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -803,13 +781,16 @@ } }, "node_modules/@rolldown/binding-linux-x64-gnu": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.2.tgz", - "integrity": "sha512-0+bOkiQ779+r1WpoHOWHqncvyySci0vKph+myNDYb+im6meJAzHQXay6oEgnkHuUGouM1LKTZwqKpBow6Kj7CQ==", + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.12.tgz", + "integrity": "sha512-Q9uLBO53Xd4QIq1WOycVQyPP1O4HhraEV2qqb3uTrnVw6QZih9duY4vNXOivL1xoUS1/z+W8eF4NMfl2a8Sdjw==", "cpu": [ "x64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -820,13 +801,16 @@ } }, "node_modules/@rolldown/binding-linux-x64-musl": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.2.tgz", - "integrity": "sha512-mjSkrzZK5Qsl0a9d1JgILOiuZOSDTVdKENcSXBoqbzSrspLR/4/IRVDo5wd2GgZjNss/viBFJdeq+j7qH2nypw==", + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.12.tgz", + "integrity": "sha512-3IBxWFMjbOZskDPKv8Lf9BCnahlKuHthWkYnyIxOH/QcJrFcS4EmcenthApkwr/5+nEqZlLzeYbxeMaX7A5u4g==", "cpu": [ "x64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -837,9 +821,9 @@ } }, "node_modules/@rolldown/binding-openharmony-arm64": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.2.tgz", - "integrity": "sha512-1v5vHasdfQAZoEHakBV72LIFAC9JjnymsiKxp+GEr/ma3+NJCPSaYK+qavInOovJkgwFrs7GccX2d6IgDA3Z5w==", + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.12.tgz", + "integrity": "sha512-xtX61xg4LKPkPWilZU1ynKClz5Gj4bf74LML4r3eVLWumKnGjoEr1OSHQhMdbBDoYTi+yjrujvpZe2pUnqCrrA==", "cpu": [ "arm64" ], @@ -853,29 +837,10 @@ "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@rolldown/binding-wasm32-wasi": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.2.tgz", - "integrity": "sha512-mb1VobWn6NheziTk5/WEaR6AKVbrwT5sOi6C7zk3gy/pD1qtJfU1j4PgTo2NJnOtbL9Dl3Aeei8w9jJ7qC2jZQ==", - "cpu": [ - "wasm32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/core": "1.10.0", - "@emnapi/runtime": "1.10.0", - "@napi-rs/wasm-runtime": "^1.1.4" - }, - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, "node_modules/@rolldown/binding-win32-arm64-msvc": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.2.tgz", - "integrity": "sha512-SqKonF56vA/L2yHwHYcEp2P34URpOZ7d1fS635cTkpDnUtEGdUbhI6NzsPdqeSWvAAeGDrxjWjNmibDIdFf9/A==", + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.12.tgz", + "integrity": "sha512-At7fPB6PCaIjzgIhEZFxuT+BBFqiQibJDT4d3PhiR3f4E7bbMZF4aKblbFfEM3sETRDd1YiQx/+U/g/B/ou5Ew==", "cpu": [ "arm64" ], @@ -890,9 +855,9 @@ } }, "node_modules/@rolldown/binding-win32-x64-msvc": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.2.tgz", - "integrity": "sha512-v7qRI7gXLRINcOGXt+7YmAZ6iFuyZVMIoXAxhd8oP+DR9dLfL9GfNIx7PLMxmhZdvq8waUJBQiWN9EKNy+TRBQ==", + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.12.tgz", + "integrity": "sha512-WIw2haVKwjuYdXkHaoC0mF8Le71TuCBxjrdKqLbJGctbBABj+ClfmNvtbOnzpq3RokNo5+V1qhtSzJyXorsklQ==", "cpu": [ "x64" ], @@ -919,17 +884,6 @@ "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", "license": "MIT" }, - "node_modules/@tybys/wasm-util": { - "version": "0.10.2", - "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.2.tgz", - "integrity": "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, "node_modules/@types/chai": { "version": "5.2.3", "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", @@ -961,22 +915,21 @@ "integrity": "sha512-xfrlY7UD5rMJk3ZVJP8BNzS28J36YJg+xp+LPXV1TdWxr8uMH5A860QNxYDGQe/ylDSgjxE52Q9VnO7p75tJxg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "undici-types": ">=7.24.0 <7.24.7" } }, "node_modules/@vitest/expect": { - "version": "4.1.7", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.7.tgz", - "integrity": "sha512-1R+tw0ortHEbZDGMymm+pN7/AFQ/RkFFdtd7EN+VBpynKmLbP8A3rpEXdshBJ7+8hQ9zBJh/i1s0yKNtxAnU7w==", + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.11.tgz", + "integrity": "sha512-VX2x5vNJXET47KAFzwERI+KRMtTTCSWTfSMKsW7JsUsXV4psq++e3DvZpuTDOpHcxytiDs6p2nhVb2tVDiiUYw==", "dev": true, "license": "MIT", "dependencies": { "@standard-schema/spec": "^1.1.0", "@types/chai": "^5.2.2", - "@vitest/spy": "4.1.7", - "@vitest/utils": "4.1.7", + "@vitest/spy": "4.1.11", + "@vitest/utils": "4.1.11", "chai": "^6.2.2", "tinyrainbow": "^3.1.0" }, @@ -985,13 +938,13 @@ } }, "node_modules/@vitest/mocker": { - "version": "4.1.7", - "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.7.tgz", - "integrity": "sha512-vY7nuamKgfvpA1Koa3oYIw/k7D6kZnpGyNMZW8loow2bsBYla1TFdqTaXncWdRn4pgwNs+90RhnXhJScDwQeJA==", + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.11.tgz", + "integrity": "sha512-2XJVD55d1o5AZous5CCGKS74g/riOj9odEt2bQpCVZeblHyHdnMeFl4jl0XjU21stf4mbjUkew2eXQZt65g5CQ==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/spy": "4.1.7", + "@vitest/spy": "4.1.11", "estree-walker": "^3.0.3", "magic-string": "^0.30.21" }, @@ -1012,9 +965,9 @@ } }, "node_modules/@vitest/pretty-format": { - "version": "4.1.7", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.7.tgz", - "integrity": "sha512-umgCarTOYQWIaDMvGDRZij+6b9oVeLIyJzfN+AS88e0ZOU3QTgNNSTtjQOpcvWr3np1N0j4WgZj+sb3oYBDscw==", + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.11.tgz", + "integrity": "sha512-yiZzPbGTS9Sr/JpFl8zHrcIkAofNbFV6k21vIgQN/cY/oxZeXhJv5sc/MBJ5jFKWmWs+oJHw0UXLZjmf931+Vw==", "dev": true, "license": "MIT", "dependencies": { @@ -1025,13 +978,13 @@ } }, "node_modules/@vitest/runner": { - "version": "4.1.7", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.7.tgz", - "integrity": "sha512-BapjmAQ2aI78WdMEfeUWivnfVzB+VPGwWRQcJE0OUq7qEeEcBsCSf+0T5iREBNE5nBb4wA5Ya0W6IA+sghdEFw==", + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.11.tgz", + "integrity": "sha512-LztvUgdwMNJMIkj3hQnnxiC2Xy1zNxq928W/xhjCLaNCzqTZOudjwbQf6v9IntZGPw132i2Lq2rgTRZHD3JHNw==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/utils": "4.1.7", + "@vitest/utils": "4.1.11", "pathe": "^2.0.3" }, "funding": { @@ -1039,14 +992,14 @@ } }, "node_modules/@vitest/snapshot": { - "version": "4.1.7", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.7.tgz", - "integrity": "sha512-ZacLzja+TmJeZ1h14xW2FB/WpeimUD3haBXQPyJqxvo8jQTmfeA8zv58mtjN2C7EHXZDYVcVYdYmAxjkWVvKCw==", + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.11.tgz", + "integrity": "sha512-pN7ikn1ON7h8ee4gIAp4AzyK+zBtJPzVbqOgu5LCEh4VaJVbPQcgYQYJIMGQPXVeJJq1fnfazis7a5pFNPahog==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "4.1.7", - "@vitest/utils": "4.1.7", + "@vitest/pretty-format": "4.1.11", + "@vitest/utils": "4.1.11", "magic-string": "^0.30.21", "pathe": "^2.0.3" }, @@ -1055,9 +1008,9 @@ } }, "node_modules/@vitest/spy": { - "version": "4.1.7", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.7.tgz", - "integrity": "sha512-kbkI5LMWakyuTIvs6fUJ5qdIVb1XVKsYJAT4OJ938cHMROYMSfmoQdZy0aaAnjbbc8F61vkoTqz/Az+/HiIu5Q==", + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.11.tgz", + "integrity": "sha512-apNa/prQy2qCeywhnixOHPRCgGNhvg7T4Dapfl1GahLp/R+uhBm5cPyFoNVyqsNd2h1nJxL6BqqdIjiABL60YA==", "dev": true, "license": "MIT", "funding": { @@ -1065,13 +1018,13 @@ } }, "node_modules/@vitest/utils": { - "version": "4.1.7", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.7.tgz", - "integrity": "sha512-T532WBu791cBxJlCl6SO+J14l81DQx6uQHm1bQbmCDY7nqlEIgkza/UFnSBNaUtSf41unldDFjdOBYEQC4b5Hw==", + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.11.tgz", + "integrity": "sha512-zTCVGpyFsGWBhllOyKlTw/vnr6D9qxsfSDyfbyZmTyjHw5N/VuvzHpHoQjm2ZJzn4RJgx5w4r7V0er69CmLgPQ==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "4.1.7", + "@vitest/pretty-format": "4.1.11", "convert-source-map": "^2.0.0", "tinyrainbow": "^3.1.0" }, @@ -1090,9 +1043,9 @@ } }, "node_modules/chai": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", - "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.3.0.tgz", + "integrity": "sha512-XWAtwJ6OHO+tj0EKCs0Y2UamnyOxseZWltU4x2U2wh8g4AigdjwvtUjvLP2tqkA/avxHEtzxNaqGq/YGNwckKg==", "dev": true, "license": "MIT", "engines": { @@ -1156,9 +1109,9 @@ "license": "MIT" }, "node_modules/esbuild": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.0.tgz", - "integrity": "sha512-sNR9MHpXSUV/XB4zmsFKN+QgVG82Cc7+/aaxJ8Adi8hyOac+EXptIp45QBPaVyX3N70664wRbTcLTOemCAnyqw==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.2.tgz", + "integrity": "sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -1169,32 +1122,32 @@ "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.28.0", - "@esbuild/android-arm": "0.28.0", - "@esbuild/android-arm64": "0.28.0", - "@esbuild/android-x64": "0.28.0", - "@esbuild/darwin-arm64": "0.28.0", - "@esbuild/darwin-x64": "0.28.0", - "@esbuild/freebsd-arm64": "0.28.0", - "@esbuild/freebsd-x64": "0.28.0", - "@esbuild/linux-arm": "0.28.0", - "@esbuild/linux-arm64": "0.28.0", - "@esbuild/linux-ia32": "0.28.0", - "@esbuild/linux-loong64": "0.28.0", - "@esbuild/linux-mips64el": "0.28.0", - "@esbuild/linux-ppc64": "0.28.0", - "@esbuild/linux-riscv64": "0.28.0", - "@esbuild/linux-s390x": "0.28.0", - "@esbuild/linux-x64": "0.28.0", - "@esbuild/netbsd-arm64": "0.28.0", - "@esbuild/netbsd-x64": "0.28.0", - "@esbuild/openbsd-arm64": "0.28.0", - "@esbuild/openbsd-x64": "0.28.0", - "@esbuild/openharmony-arm64": "0.28.0", - "@esbuild/sunos-x64": "0.28.0", - "@esbuild/win32-arm64": "0.28.0", - "@esbuild/win32-ia32": "0.28.0", - "@esbuild/win32-x64": "0.28.0" + "@esbuild/aix-ppc64": "0.28.2", + "@esbuild/android-arm": "0.28.2", + "@esbuild/android-arm64": "0.28.2", + "@esbuild/android-x64": "0.28.2", + "@esbuild/darwin-arm64": "0.28.2", + "@esbuild/darwin-x64": "0.28.2", + "@esbuild/freebsd-arm64": "0.28.2", + "@esbuild/freebsd-x64": "0.28.2", + "@esbuild/linux-arm": "0.28.2", + "@esbuild/linux-arm64": "0.28.2", + "@esbuild/linux-ia32": "0.28.2", + "@esbuild/linux-loong64": "0.28.2", + "@esbuild/linux-mips64el": "0.28.2", + "@esbuild/linux-ppc64": "0.28.2", + "@esbuild/linux-riscv64": "0.28.2", + "@esbuild/linux-s390x": "0.28.2", + "@esbuild/linux-x64": "0.28.2", + "@esbuild/netbsd-arm64": "0.28.2", + "@esbuild/netbsd-x64": "0.28.2", + "@esbuild/openbsd-arm64": "0.28.2", + "@esbuild/openbsd-x64": "0.28.2", + "@esbuild/openharmony-arm64": "0.28.2", + "@esbuild/sunos-x64": "0.28.2", + "@esbuild/win32-arm64": "0.28.2", + "@esbuild/win32-ia32": "0.28.2", + "@esbuild/win32-x64": "0.28.2" } }, "node_modules/estree-walker": { @@ -1300,9 +1253,9 @@ "license": "Apache-2.0" }, "node_modules/lightningcss": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", - "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.33.0.tgz", + "integrity": "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==", "dev": true, "license": "MPL-2.0", "dependencies": { @@ -1316,23 +1269,23 @@ "url": "https://opencollective.com/parcel" }, "optionalDependencies": { - "lightningcss-android-arm64": "1.32.0", - "lightningcss-darwin-arm64": "1.32.0", - "lightningcss-darwin-x64": "1.32.0", - "lightningcss-freebsd-x64": "1.32.0", - "lightningcss-linux-arm-gnueabihf": "1.32.0", - "lightningcss-linux-arm64-gnu": "1.32.0", - "lightningcss-linux-arm64-musl": "1.32.0", - "lightningcss-linux-x64-gnu": "1.32.0", - "lightningcss-linux-x64-musl": "1.32.0", - "lightningcss-win32-arm64-msvc": "1.32.0", - "lightningcss-win32-x64-msvc": "1.32.0" + "lightningcss-android-arm64": "1.33.0", + "lightningcss-darwin-arm64": "1.33.0", + "lightningcss-darwin-x64": "1.33.0", + "lightningcss-freebsd-x64": "1.33.0", + "lightningcss-linux-arm-gnueabihf": "1.33.0", + "lightningcss-linux-arm64-gnu": "1.33.0", + "lightningcss-linux-arm64-musl": "1.33.0", + "lightningcss-linux-x64-gnu": "1.33.0", + "lightningcss-linux-x64-musl": "1.33.0", + "lightningcss-win32-arm64-msvc": "1.33.0", + "lightningcss-win32-x64-msvc": "1.33.0" } }, "node_modules/lightningcss-android-arm64": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", - "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.33.0.tgz", + "integrity": "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==", "cpu": [ "arm64" ], @@ -1351,9 +1304,9 @@ } }, "node_modules/lightningcss-darwin-arm64": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", - "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.33.0.tgz", + "integrity": "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==", "cpu": [ "arm64" ], @@ -1372,9 +1325,9 @@ } }, "node_modules/lightningcss-darwin-x64": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", - "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.33.0.tgz", + "integrity": "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==", "cpu": [ "x64" ], @@ -1393,9 +1346,9 @@ } }, "node_modules/lightningcss-freebsd-x64": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", - "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.33.0.tgz", + "integrity": "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==", "cpu": [ "x64" ], @@ -1414,9 +1367,9 @@ } }, "node_modules/lightningcss-linux-arm-gnueabihf": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", - "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.33.0.tgz", + "integrity": "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==", "cpu": [ "arm" ], @@ -1435,13 +1388,16 @@ } }, "node_modules/lightningcss-linux-arm64-gnu": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", - "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.33.0.tgz", + "integrity": "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==", "cpu": [ "arm64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MPL-2.0", "optional": true, "os": [ @@ -1456,13 +1412,16 @@ } }, "node_modules/lightningcss-linux-arm64-musl": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", - "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.33.0.tgz", + "integrity": "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==", "cpu": [ "arm64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MPL-2.0", "optional": true, "os": [ @@ -1477,13 +1436,16 @@ } }, "node_modules/lightningcss-linux-x64-gnu": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", - "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.33.0.tgz", + "integrity": "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==", "cpu": [ "x64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MPL-2.0", "optional": true, "os": [ @@ -1498,13 +1460,16 @@ } }, "node_modules/lightningcss-linux-x64-musl": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", - "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.33.0.tgz", + "integrity": "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==", "cpu": [ "x64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MPL-2.0", "optional": true, "os": [ @@ -1519,9 +1484,9 @@ } }, "node_modules/lightningcss-win32-arm64-msvc": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", - "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.33.0.tgz", + "integrity": "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==", "cpu": [ "arm64" ], @@ -1540,9 +1505,9 @@ } }, "node_modules/lightningcss-win32-x64-msvc": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", - "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.33.0.tgz", + "integrity": "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==", "cpu": [ "x64" ], @@ -1608,9 +1573,9 @@ "license": "MIT" }, "node_modules/nanoid": { - "version": "3.3.12", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", - "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", + "version": "3.3.20", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.20.tgz", + "integrity": "sha512-uKdg2G3GNCKQn9byYOpxbGqrT2fGO5KRt5J/8b3pok8rT6qxGWF6hxMyJiEYtAf+FVyYuD9hRaDqX5uPFYJ4ZQ==", "dev": true, "funding": [ { @@ -1676,12 +1641,11 @@ "license": "ISC" }, "node_modules/picomatch": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", - "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz", + "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -1690,9 +1654,9 @@ } }, "node_modules/postcss": { - "version": "8.5.15", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz", - "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==", + "version": "8.5.29", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.29.tgz", + "integrity": "sha512-49cGhUbXj8Qenv0iTMxA1cFBzxXoctpC9Ujd77t1WcbJIr6nF/eI7g/8MgxrYldFRuAXvja7xQRwavoW7kgrxQ==", "dev": true, "funding": [ { @@ -1710,9 +1674,9 @@ ], "license": "MIT", "dependencies": { - "nanoid": "^3.3.12", + "nanoid": "^3.3.19", "picocolors": "^1.1.1", - "source-map-js": "^1.2.1" + "source-map-js": "^1.2.2" }, "engines": { "node": "^10 || ^12 || >=14" @@ -1735,13 +1699,13 @@ "license": "MIT" }, "node_modules/rolldown": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.2.tgz", - "integrity": "sha512-oZx5zVDtVB44AW3eaifgDml1gWRDZGvjcfdxonE4swNPG98PrrXjaO/KrnUjzlMnztCCRVlUueA1kCXhARGk6g==", + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.12.tgz", + "integrity": "sha512-8wafseiaG80xmXSfqidUNqZcylTlhmPZZt+za2m+js2sFZ8dTNlhIOV2WcbIPx2hgwPBJpEUGFAMZ9bgBBLTSQ==", "dev": true, "license": "MIT", "dependencies": { - "@oxc-project/types": "=0.132.0", + "@oxc-project/types": "=0.152.0", "@rolldown/pluginutils": "^1.0.0" }, "bin": { @@ -1751,21 +1715,21 @@ "node": "^20.19.0 || >=22.12.0" }, "optionalDependencies": { - "@rolldown/binding-android-arm64": "1.0.2", - "@rolldown/binding-darwin-arm64": "1.0.2", - "@rolldown/binding-darwin-x64": "1.0.2", - "@rolldown/binding-freebsd-x64": "1.0.2", - "@rolldown/binding-linux-arm-gnueabihf": "1.0.2", - "@rolldown/binding-linux-arm64-gnu": "1.0.2", - "@rolldown/binding-linux-arm64-musl": "1.0.2", - "@rolldown/binding-linux-ppc64-gnu": "1.0.2", - "@rolldown/binding-linux-s390x-gnu": "1.0.2", - "@rolldown/binding-linux-x64-gnu": "1.0.2", - "@rolldown/binding-linux-x64-musl": "1.0.2", - "@rolldown/binding-openharmony-arm64": "1.0.2", - "@rolldown/binding-wasm32-wasi": "1.0.2", - "@rolldown/binding-win32-arm64-msvc": "1.0.2", - "@rolldown/binding-win32-x64-msvc": "1.0.2" + "@rolldown/binding-android-arm-eabi": "1.2.12", + "@rolldown/binding-android-arm64": "1.2.12", + "@rolldown/binding-darwin-arm64": "1.2.12", + "@rolldown/binding-darwin-x64": "1.2.12", + "@rolldown/binding-freebsd-x64": "1.2.12", + "@rolldown/binding-linux-arm-gnueabihf": "1.2.12", + "@rolldown/binding-linux-arm64-gnu": "1.2.12", + "@rolldown/binding-linux-arm64-musl": "1.2.12", + "@rolldown/binding-linux-ppc64-gnu": "1.2.12", + "@rolldown/binding-linux-s390x-gnu": "1.2.12", + "@rolldown/binding-linux-x64-gnu": "1.2.12", + "@rolldown/binding-linux-x64-musl": "1.2.12", + "@rolldown/binding-openharmony-arm64": "1.2.12", + "@rolldown/binding-win32-arm64-msvc": "1.2.12", + "@rolldown/binding-win32-x64-msvc": "1.2.12" } }, "node_modules/shebang-command": { @@ -1797,9 +1761,9 @@ "license": "ISC" }, "node_modules/source-map-js": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", - "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.2.tgz", + "integrity": "sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw==", "dev": true, "license": "BSD-3-Clause", "engines": { @@ -1838,9 +1802,9 @@ } }, "node_modules/tinyglobby": { - "version": "0.2.16", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz", - "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==", + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", "dev": true, "license": "MIT", "dependencies": { @@ -1855,9 +1819,9 @@ } }, "node_modules/tinyrainbow": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.0.tgz", - "integrity": "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.2.0.tgz", + "integrity": "sha512-LgO3D9yZJjApUiuUfl9iFAwrtaX4+lok3wJIqttGoKCHlWUqHqbQpnxCf82L8FjgKsh4iGo78hqJwgL8F6To2A==", "dev": true, "license": "MIT", "engines": { @@ -1865,29 +1829,20 @@ } }, "node_modules/toml": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/toml/-/toml-4.1.1.tgz", - "integrity": "sha512-EBJnVBr3dTXdA89WVFoAIPUqkBjxPMwRqsfuo1r240tKFHXv3zgca4+NJib/h6TyvGF7vOawz0jGuryJCdNHrw==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/toml/-/toml-4.3.0.tgz", + "integrity": "sha512-lVb8X9BsPVuH0M4BKeS91tXAmJvCjQ5UIyAbQFaxkKGyUFK2RPkhwaFSQH8vbpl1d23eu/IBH+dwVMHWaq9A5A==", "license": "MIT", "engines": { "node": ">=20" } }, - "node_modules/tslib": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "dev": true, - "license": "0BSD", - "optional": true - }, "node_modules/tsx": { "version": "4.22.3", "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.22.3.tgz", "integrity": "sha512-mdoNxBC/cSQObGGVQ5Bpn5i+yv7j68gk3Nfm3wFjcJg3Z0Mix9jzAFfP12prmm5eVGmDKtp0yyArrs0Q+8gZHg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "esbuild": "~0.28.0" }, @@ -1936,18 +1891,17 @@ } }, "node_modules/vite": { - "version": "8.0.14", - "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.14.tgz", - "integrity": "sha512-s4BJJ+5y1pYL6Otw51FHhVJQhPnuRinKig64g/1+EUNaJsd3gCKdD31IPFvswUgW9/60QT9oFHbZHbQK5imcxw==", + "version": "8.3.3", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.3.3.tgz", + "integrity": "sha512-cTAldKPImjg6c+gk48U19POPn3GCBzZwpdsN8ZMEEcbpes+6/wvfqUd0C2y3qYY4wsj8PwgFwrZ/1jBPVttMSg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { - "lightningcss": "^1.32.0", - "picomatch": "^4.0.4", - "postcss": "^8.5.15", - "rolldown": "1.0.2", - "tinyglobby": "^0.2.16" + "lightningcss": "^1.33.0", + "picomatch": "^4.0.7", + "postcss": "^8.5.28", + "rolldown": "~1.2.11", + "tinyglobby": "^0.2.17" }, "bin": { "vite": "bin/vite.js" @@ -1963,7 +1917,7 @@ }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", - "@vitejs/devtools": "^0.1.18", + "@vitejs/devtools": "^0.7.1", "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.0", @@ -2015,19 +1969,19 @@ } }, "node_modules/vitest": { - "version": "4.1.7", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.7.tgz", - "integrity": "sha512-flYyaFd2CgoCoU+0UKt3pxksgC+S02iTDN0n3LtqaMeXsI9SBcdNujc2k0DeFLzUn/0k538yNjOSdwgCqcrwJA==", + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.11.tgz", + "integrity": "sha512-fhACrNXUidIbGSBr5FlbuBkO7VWC1ZyLl0DO4CU2DrQoAPxX84Ysxs+HeGQpii5lZWV1Q4gBZTTu49mF+A6Edw==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/expect": "4.1.7", - "@vitest/mocker": "4.1.7", - "@vitest/pretty-format": "4.1.7", - "@vitest/runner": "4.1.7", - "@vitest/snapshot": "4.1.7", - "@vitest/spy": "4.1.7", - "@vitest/utils": "4.1.7", + "@vitest/expect": "4.1.11", + "@vitest/mocker": "4.1.11", + "@vitest/pretty-format": "4.1.11", + "@vitest/runner": "4.1.11", + "@vitest/snapshot": "4.1.11", + "@vitest/spy": "4.1.11", + "@vitest/utils": "4.1.11", "es-module-lexer": "^2.0.0", "expect-type": "^1.3.0", "magic-string": "^0.30.21", @@ -2055,12 +2009,12 @@ "@edge-runtime/vm": "*", "@opentelemetry/api": "^1.9.0", "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", - "@vitest/browser-playwright": "4.1.7", - "@vitest/browser-preview": "4.1.7", - "@vitest/browser-webdriverio": "4.1.7", - "@vitest/coverage-istanbul": "4.1.7", - "@vitest/coverage-v8": "4.1.7", - "@vitest/ui": "4.1.7", + "@vitest/browser-playwright": "4.1.11", + "@vitest/browser-preview": "4.1.11", + "@vitest/browser-webdriverio": "4.1.11", + "@vitest/coverage-istanbul": "4.1.11", + "@vitest/coverage-v8": "4.1.11", + "@vitest/ui": "4.1.11", "happy-dom": "*", "jsdom": "*", "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" diff --git a/package.json b/package.json index 2119f5e..197595c 100644 --- a/package.json +++ b/package.json @@ -3,6 +3,9 @@ "version": "0.1.0", "description": "OpenCode plugin for deploying, debugging, and monitoring applications on Render.", "type": "module", + "engines": { + "node": ">=18" + }, "license": "MIT", "author": { "name": "Render", @@ -26,7 +29,8 @@ "debugging" ], "exports": { - ".": "./dist/index.js" + ".": "./dist/index.js", + "./server": "./dist/index.js" }, "bin": { "render-opencode": "./dist/setup.js" @@ -39,16 +43,18 @@ "LICENSE" ], "scripts": { - "build": "tsc -p tsconfig.json", + "build": "tsc -p tsconfig.json && node scripts/build-plugin.mjs", "test": "vitest run", "prepack": "npm run build", - "check": "npm run build && npm test" + "check": "npm run build && npm test", + "test:integration": "npm run build && node scripts/test-integration.mjs" }, "dependencies": { "@opencode-ai/plugin": "^1.15.11" }, "devDependencies": { "@types/node": "^25.9.1", + "esbuild": "^0.28.0", "tsx": "^4.22.3", "typescript": "^6.0.3", "vitest": "^4.1.7" diff --git a/scripts/build-plugin.mjs b/scripts/build-plugin.mjs new file mode 100644 index 0000000..b8fa1a6 --- /dev/null +++ b/scripts/build-plugin.mjs @@ -0,0 +1,13 @@ +import { build } from "esbuild" + +// Keep the standalone GitHub entry point in sync with the npm implementation. +await build({ + entryPoints: ["src/index.ts"], + outfile: "assets/opencode/plugins/render.ts", + bundle: true, + platform: "node", + format: "esm", + target: "es2022", + external: ["@opencode-ai/plugin"], + banner: { js: "// Generated by npm run build from src/index.ts. Do not edit directly." }, +}) diff --git a/scripts/test-integration.mjs b/scripts/test-integration.mjs new file mode 100644 index 0000000..a761200 --- /dev/null +++ b/scripts/test-integration.mjs @@ -0,0 +1,225 @@ +// Exercise both public installation flows inside real OpenCode, without a paid model or Render account. +import assert from "node:assert/strict" +import { execFile, spawn } from "node:child_process" +import { createServer } from "node:http" +import { chmod, mkdir, mkdtemp, readFile, readdir, realpath, rm, writeFile } from "node:fs/promises" +import { tmpdir } from "node:os" +import { dirname, join, resolve } from "node:path" +import { fileURLToPath } from "node:url" +import { promisify } from "node:util" + +const run = promisify(execFile) +const repo = resolve(dirname(fileURLToPath(import.meta.url)), "..") +const root = await realpath(await mkdtemp(join(tmpdir(), "render-opencode-integration-"))) +const cli = process.env.OPENCODE_BIN || "opencode" +let scenario = [] +let modelError +let succeeded = false + +// Only tool-enabled turns consume the script; OpenCode also requests session titles. +const model = createServer(async (request, response) => { + try { + let body = "" + for await (const chunk of request) body += chunk + const input = JSON.parse(body) + const call = input.tools?.length ? scenario.shift() : undefined + if (call) assert(input.tools.some((t) => t.function.name === call[0]), `Tool unavailable: ${call[0]}`) + response.writeHead(200, { "Content-Type": "text/event-stream" }) + const send = (delta, finish_reason = null) => response.write(`data: ${JSON.stringify({ + id: "integration", object: "chat.completion.chunk", created: 1, model: input.model, + choices: [{ index: 0, delta, finish_reason }], + })}\n\n`) + send({ role: "assistant", content: "" }) + send(call ? { tool_calls: [{ index: 0, id: `call_${Date.now()}`, type: "function", function: { name: call[0], arguments: JSON.stringify(call[1]) } }] } : { content: "Integration test complete." }) + send({}, call ? "tool_calls" : "stop") + response.end("data: [DONE]\n\n") + } catch (error) { + modelError = error + response.writeHead(500).end("Local test model failed") + } +}) + +async function listen(server) { + await new Promise((resolve, reject) => { server.once("error", reject); server.listen(0, "127.0.0.1", resolve) }) + return server.address().port +} +async function freePort() { + const server = createServer() + const port = await listen(server) + await new Promise((resolve) => server.close(resolve)) + return port +} +async function calls(path) { + try { return (await readFile(path, "utf8")).trim().split("\n").filter(Boolean).map((line) => JSON.parse(line)) } + catch (error) { if (error.code === "ENOENT") return []; throw error } +} + +function environment(base, configDir, log) { + const env = { ...process.env, + OPENCODE_TEST_HOME: join(base, "home"), OPENCODE_CONFIG_DIR: configDir, + XDG_CONFIG_HOME: join(base, "config"), XDG_DATA_HOME: join(base, "data"), + XDG_CACHE_HOME: join(base, "cache"), XDG_STATE_HOME: join(base, "state"), + OPENCODE_DISABLE_PROJECT_CONFIG: "1", OPENCODE_DISABLE_AUTOUPDATE: "1", + OPENCODE_DISABLE_MODELS_FETCH: "1", OPENCODE_DISABLE_DEFAULT_PLUGINS: "1", + OPENCODE_DISABLE_EXTERNAL_SKILLS: "1", OPENCODE_DISABLE_AUTOCOMPACT: "1", + OPENCODE_DISABLE_LSP_DOWNLOAD: "1", OPENCODE_AUTH_CONTENT: "{}", + PATH: `${join(root, "bin")}:${process.env.PATH}`, RENDER_PLUGIN_TEST_LOG: log, + } + delete env.RENDER_API_KEY + return env +} + +async function exercise(kind, tarball, consumer, modelPort) { + const base = join(root, kind) + const cfg = join(base, "opencode") + const project = join(base, "project") + const serverCwd = join(base, "server-cwd") + const log = join(base, "render-calls.jsonl") + for (const path of [cfg, project, serverCwd]) await mkdir(path, { recursive: true }) + const env = environment(base, cfg, log) + if (kind === "npm") { + // Invoke the actual npm-created executable symlink, rather than the module directly. + await run(join(consumer, "node_modules", ".bin", "render-opencode"), ["setup", "--config-dir", cfg], { env, cwd: consumer }) + } else { + await run("bash", [join(repo, "install.sh"), "--source", repo, "--config-dir", cfg], { env }) + } + const modelDefinition = { name: "Local integration model", attachment: false, reasoning: false, temperature: false, tool_call: true, + release_date: "2025-01-01", limit: { context: 100000, output: 10000 }, cost: { input: 0, output: 0 }, options: {} } + const config = { + $schema: "https://opencode.ai/config.json", formatter: false, lsp: false, share: "disabled", + model: "review/review-model", small_model: "review/review-model", enabled_providers: ["review"], + permission: { edit: "allow", read: "allow", skill: "allow", external_directory: "deny" }, + provider: { review: { id: "review", name: "Local test model", env: [], npm: "@ai-sdk/openai-compatible", + models: { "review-model": { ...modelDefinition, id: "review-model" }, "gpt-review": { ...modelDefinition, id: "gpt-review" } }, + options: { apiKey: "local-test-only", baseURL: `http://127.0.0.1:${modelPort}/v1` } } }, + } + // This goes through OpenCode's npm installation/cache/package-entrypoint resolution. + if (kind === "npm") config.plugin = [`@render/opencode-plugin@file:${tarball}`] + await writeFile(join(cfg, "opencode.json"), JSON.stringify(config)) + await writeFile(join(serverCwd, "render.yaml"), "INVALID server working directory\n") + const port = await freePort() + const server = spawn(cli, ["serve", "--hostname", "127.0.0.1", "--port", String(port)], { env, cwd: serverCwd }) + let serverOutput = "" + server.stdout.on("data", (chunk) => { serverOutput += chunk }) + server.stderr.on("data", (chunk) => { serverOutput += chunk }) + const api = async (path, body) => { + const response = await fetch(`http://127.0.0.1:${port}${path}`, { + method: body === undefined ? "GET" : "POST", + headers: { "Content-Type": "application/json", "x-opencode-directory": project }, + body: body === undefined ? undefined : JSON.stringify(body), signal: AbortSignal.timeout(75000), + }) + const text = await response.text() + assert(response.ok, `${path}: ${response.status} ${text.slice(0, 500)}`) + return JSON.parse(text) + } + try { + let ready = false + for (let attempt = 0; attempt < 120; attempt++) { + assert.equal(server.exitCode, null, `OpenCode exited: ${serverOutput.slice(-2000)}`) + try { await api("/global/health"); ready = true; break } catch {} + await new Promise((resolve) => setTimeout(resolve, 250)) + } + assert(ready, "OpenCode did not start") + const skills = await api("/skill") + const names = await readdir(join(repo, "assets", "opencode", "skills")) + assert.deepEqual(skills.filter((s) => s.name.startsWith("render-")).map((s) => s.name).sort(), names.sort()) + const commands = await api("/command") + for (const name of ["deploy-to-render", "check-render-status"]) assert(commands.some((c) => c.name === name)) + assert((await api("/agent")).some((a) => a.name === "render")) + assert((await api("/experimental/tool/ids")).includes("render_validate_blueprint")) + + async function test(name, script, expectedPaths, modelID = "review-model", verify = () => {}) { + const before = (await calls(log)).length + scenario = [...script] + modelError = undefined + const session = await api("/session", {}) + await api(`/session/${session.id}/message`, { parts: [{ type: "text", text: "Run the scripted integration test." }], model: { providerID: "review", modelID } }) + if (modelError) throw modelError + assert.equal(scenario.length, 0, `${name}: not all scripted tools ran`) + const parts = (await api(`/session/${session.id}/message`)).flatMap((m) => m.parts).filter((p) => p.type === "tool") + for (const part of parts) assert.equal(part.state.status, "completed", `${name}: ${JSON.stringify(part.state)}`) + const actual = (await calls(log)).slice(before) + assert.deepEqual(actual.map((c) => c.args[2]).sort(), expectedPaths.map((p) => resolve(project, p)).sort(), `${name}: validation call count or filename`) + for (const call of actual) { + assert.deepEqual(call.args, ["blueprints", "validate", call.args[2], "--output", "json"]) + assert.equal(call.cwd, dirname(call.args[2])) + assert.equal(call.exists, true) + } + verify(parts, actual) + console.log(`${kind}: ${name} passed`) + } + const yaml = join(project, "render.yaml") + const yml = join(project, "render.yml") + await writeFile(yaml, "services: []\n") + await test("skill loading", [["skill", { name: "render-deploy" }]], [], "review-model", (parts) => { + assert(parts.some((p) => p.tool === "skill" && p.state.output.includes("Render"))) + }) + const valid = (parts, actual) => { assert.equal(parts.at(-1).state.metadata.ok, true); assert(actual.every((c) => c.valid)) } + await test("absolute custom validation once", [["render_validate_blueprint", { path: yaml }]], [yaml], "review-model", valid) + await test("relative custom validation from session directory", [["render_validate_blueprint", { path: "render.yaml" }]], [yaml], "review-model", valid) + await rm(yaml) + await writeFile(yml, "services: []\n") + await test("render.yml without render.yaml", [["render_validate_blueprint", { path: "render.yml" }]], [yml], "review-model", valid) + await writeFile(yaml, "INVALID decoy\n") + await test("render.yml selected when render.yaml also exists", [["render_validate_blueprint", { path: "render.yml" }]], [yml], "review-model", valid) + const failed = (parts) => { + const p = parts.find((p) => p.state.metadata.renderBlueprintValidation) + assert(p && p.state.output.includes("invalid blueprint"), "Validation failure must be visible to the agent") + assert.equal(p.state.metadata.renderBlueprintValidation.ok, false) + } + await test("write hook reports validation failure", [["write", { filePath: yaml, content: "INVALID\n" }]], [yaml], "review-model", failed) + await test("read skips validation", [["read", { filePath: yaml }]], []) + await test("edit hook", [["read", { filePath: yaml }], ["edit", { filePath: yaml, oldString: "INVALID", newString: "services: []" }]], [yaml], "review-model", (_, actual) => assert(actual[0].valid)) + await test("GPT apply_patch hook", [["apply_patch", { patchText: `*** Begin Patch\n*** Update File: ${yaml}\n@@\n-services: []\n+INVALID\n*** End Patch` }]], [yaml], "gpt-review", failed) + for (const dir of ["moved", "nested", "deleted"]) await mkdir(join(project, dir)) + await writeFile(join(project, "deleted", "render.yaml"), "services: []\n") + const moved = join(project, "moved", "render.yaml") + const nested = join(project, "nested", "render.yml") + const patchText = `*** Begin Patch\n*** Update File: ${yaml}\n*** Move to: ${moved}\n@@\n-INVALID\n+services: []\n*** Add File: ${nested}\n+INVALID\n*** Delete File: ${join(project, "deleted", "render.yaml")}\n*** End Patch` + await test("multi-file patch with move and deletion", [["apply_patch", { patchText }]], [moved, nested], "gpt-review", failed) + for (const [command, skill] of [["check-render-status", "render-monitor"], ["deploy-to-render", "render-deploy"]]) { + scenario = [["skill", { name: skill }]] + const session = await api("/session", {}) + await api(`/session/${session.id}/command`, { command, arguments: "", model: "review/review-model" }) + if (modelError) throw modelError + const children = await api(`/session/${session.id}/children`) + const messages = (await Promise.all(children.map((c) => api(`/session/${c.id}/message`)))).flat() + assert(messages.some((m) => m.info.agent === "render")) + assert(messages.flatMap((m) => m.parts).some((p) => p.tool === "skill" && p.state.status === "completed" && p.state.input.name === skill)) + assert.equal(scenario.length, 0) + console.log(`${kind}: /${command} delegates to @render and loads ${skill}`) + } + await writeFile(join(base, "summary.json"), JSON.stringify({ kind, skills: names.length, commands: 2, renderCalls: (await calls(log)).length })) + } finally { + server.kill("SIGTERM") + if (server.exitCode === null) { + await new Promise((resolve) => { + const timeout = setTimeout(() => { server.kill("SIGKILL"); resolve() }, 5000) + server.once("exit", () => { clearTimeout(timeout); resolve() }) + }) + } + await writeFile(join(base, "server.log"), serverOutput) + } +} + +try { + console.log(`Testing OpenCode ${(await run(cli, ["--version"])).stdout.trim()}`) + await mkdir(join(root, "bin")) + const render = join(root, "bin", "render") + await writeFile(render, `#!${process.execPath}\nconst fs = require('node:fs');\nconst args = process.argv.slice(2);\nconst exists = fs.existsSync(args[2]);\nconst valid = exists && !fs.readFileSync(args[2], 'utf8').includes('INVALID');\nfs.appendFileSync(process.env.RENDER_PLUGIN_TEST_LOG, JSON.stringify({args,cwd:process.cwd(),exists,valid})+'\\n');\nconsole.log(valid ? 'valid blueprint' : 'invalid blueprint');\nprocess.exitCode = valid ? 0 : 1;\n`) + await chmod(render, 0o755) + const packed = JSON.parse((await run("npm", ["pack", "--ignore-scripts", "--json", "--pack-destination", root], { cwd: repo })).stdout)[0] + const tarball = join(root, packed.filename) + const consumer = join(root, "consumer") + await mkdir(consumer) + await run("npm", ["install", "--prefix", consumer, "--ignore-scripts", "--no-audit", "--no-fund", tarball], { timeout: 120000 }) + const modelPort = await listen(model) + for (const kind of ["github", "npm"]) await exercise(kind, tarball, consumer, modelPort) + succeeded = true + console.log("Both plugin installation flows passed all real OpenCode integration checks.") +} finally { + model.closeAllConnections() + await new Promise((resolve) => model.close(resolve)) + if (succeeded && !process.env.OPENCODE_INTEGRATION_KEEP) await rm(root, { recursive: true, force: true }) + else console.log(`Integration artifacts: ${root}`) +} diff --git a/src/blueprint.ts b/src/blueprint.ts index 3a51917..bd483b3 100644 --- a/src/blueprint.ts +++ b/src/blueprint.ts @@ -17,16 +17,21 @@ export function isBlueprintFile(filePath: string) { return name === "render.yaml" || name === "render.yml" } -export function getBlueprintWorkingDirectory(filePath: string) { - return dirname(resolve(filePath)) +export function getBlueprintWorkingDirectory(filePath: string, directory = process.cwd()) { + return dirname(resolve(directory, filePath)) } -export async function validateBlueprint(filePath: string): Promise { - const cwd = getBlueprintWorkingDirectory(filePath) - const command = "render blueprints validate" +export async function validateBlueprint(filePath: string, directory = process.cwd(), signal?: AbortSignal): Promise { + const path = resolve(directory, filePath) + const cwd = dirname(path) + const command = `render blueprints validate ${JSON.stringify(path)} --output json` try { - const result = await execFileAsync("render", ["blueprints", "validate"], { cwd }) + const result = await execFileAsync("render", ["blueprints", "validate", path, "--output", "json"], { + cwd, + signal, + timeout: 30_000, + }) const output = [result.stdout, result.stderr].filter(Boolean).join("\n").trim() return { ok: true, diff --git a/src/index.ts b/src/index.ts index a325ce1..e44ba84 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,10 +1,11 @@ import type { Plugin } from "@opencode-ai/plugin" import { tool } from "@opencode-ai/plugin" +import { resolve } from "node:path" import { isBlueprintFile, validateBlueprint } from "./blueprint.js" -import { extractTouchedFiles } from "./touched-files.js" +import { extractMutatedFiles } from "./touched-files.js" -export const RenderPlugin: Plugin = async ({ client }) => { +export const RenderPlugin: Plugin = async ({ client, directory }) => { await client.app.log({ body: { service: "render-opencode-plugin", @@ -20,8 +21,8 @@ export const RenderPlugin: Plugin = async ({ client }) => { args: { path: tool.schema.string().describe("Path to render.yaml or render.yml."), }, - async execute(args) { - const result = await validateBlueprint(args.path) + async execute(args, context) { + const result = await validateBlueprint(args.path, context.directory, context.abort) return { title: result.ok ? "Render Blueprint valid" : "Render Blueprint validation failed", output: result.output, @@ -37,36 +38,35 @@ export const RenderPlugin: Plugin = async ({ client }) => { }, "tool.execute.after": async (input, output) => { - const blueprintPath = extractTouchedFiles(input.args).find(isBlueprintFile) - if (!blueprintPath) { - return - } - - const result = await validateBlueprint(blueprintPath) - if (result.ok) { - await client.app.log({ - body: { - service: "render-opencode-plugin", - level: "info", - message: "Validated Render Blueprint", - extra: { - path: blueprintPath, - cwd: result.cwd, - }, - }, - }) - return + const paths = [...new Set(extractMutatedFiles(input.tool, input.args, output.metadata) + .filter(isBlueprintFile) + .map((path) => resolve(directory, path)))] + const failures = [] + for (const path of paths) { + const result = await validateBlueprint(path, directory) + if (!result.ok) { + failures.push({ path, ...result }) + continue + } + await client.app.log({ body: { + service: "render-opencode-plugin", + level: "info", + message: "Validated Render Blueprint", + extra: { path, cwd: result.cwd }, + } }) } + if (failures.length === 0) return output.title = "Render Blueprint validation" - output.output = [output.output, result.output].filter(Boolean).join("\n\n") + output.output = [output.output, ...failures.map((result) => `${result.path}\n${result.output}`)].filter(Boolean).join("\n\n") output.metadata = { ...output.metadata, renderBlueprintValidation: { ok: false, - command: result.command, - cwd: result.cwd, - error: result.error, + command: failures[0].command, + cwd: failures[0].cwd, + error: failures[0].error, + results: failures, }, } }, diff --git a/src/setup.ts b/src/setup.ts index 330ce8d..2266fa1 100644 --- a/src/setup.ts +++ b/src/setup.ts @@ -1,222 +1,13 @@ #!/usr/bin/env node -import { constants } from "node:fs" -import { access, copyFile, mkdir, readdir, readFile, writeFile } from "node:fs/promises" -import { dirname, join, relative, resolve } from "node:path" +import { realpathSync } from "node:fs" import { fileURLToPath } from "node:url" +import { main } from "../assets/setup.mjs" -type InstallOptions = { - sourceAssetsDir?: string - targetConfigDir?: string - force?: boolean - enableMcp?: boolean - dryRun?: boolean -} - -type InstallResult = { - written: string[] - skipped: string[] -} - -const ASSET_DIRECTORIES = ["skills", "commands", "agents"] as const - -const RENDER_MCP_CONFIG = { - type: "remote", - url: "https://mcp.render.com/mcp", - enabled: true, - oauth: false, - headers: { - Authorization: "Bearer {env:RENDER_API_KEY}", - }, -} - -const moduleDir = dirname(fileURLToPath(import.meta.url)) - -export async function installOpenCodeAssets(options: InstallOptions = {}): Promise { - const sourceAssetsDir = options.sourceAssetsDir ?? join(moduleDir, "..", "assets", "opencode") - const targetConfigDir = options.targetConfigDir ?? getDefaultConfigDir() - const result: InstallResult = { written: [], skipped: [] } - - for (const directory of ASSET_DIRECTORIES) { - const sourceDirectory = join(sourceAssetsDir, directory) - if (!(await pathExists(sourceDirectory))) { - continue - } - - const files = await listFiles(sourceDirectory) - for (const sourceFile of files) { - const destination = join(targetConfigDir, directory, relative(sourceDirectory, sourceFile)) - if ((await pathExists(destination)) && !options.force) { - result.skipped.push(destination) - continue - } - - result.written.push(destination) - if (!options.dryRun) { - await mkdir(dirname(destination), { recursive: true }) - await copyFile(sourceFile, destination) - } - } - } - - if (options.enableMcp) { - const configPath = join(targetConfigDir, "opencode.json") - await mergeMcpConfig(configPath, result, Boolean(options.dryRun)) - } - - return result -} - -export function getDefaultConfigDir() { - if (process.env.OPENCODE_CONFIG_DIR) { - return resolve(process.env.OPENCODE_CONFIG_DIR) - } - - const home = process.env.HOME - if (!home) { - throw new Error("Cannot locate OpenCode config directory because HOME is not set.") - } - - return join(home, ".config", "opencode") -} - -export async function mergeMcpConfig(configPath: string, result: InstallResult, dryRun = false) { - const existing = await readJsonFile(configPath) - const nextConfig = { - ...existing, - mcp: { - ...(isRecord(existing.mcp) ? existing.mcp : {}), - render: RENDER_MCP_CONFIG, - }, - } - - result.written.push(configPath) - if (!dryRun) { - await mkdir(dirname(configPath), { recursive: true }) - await writeFile(configPath, `${JSON.stringify(nextConfig, null, 2)}\n`) - } -} - -async function readJsonFile(filePath: string): Promise> { - if (!(await pathExists(filePath))) { - return { $schema: "https://opencode.ai/config.json" } - } - - const contents = await readFile(filePath, "utf8") - if (contents.trim().length === 0) { - return { $schema: "https://opencode.ai/config.json" } - } - - const parsed = JSON.parse(contents) as unknown - if (!isRecord(parsed)) { - throw new Error(`${filePath} must contain a JSON object.`) - } - - return parsed -} - -async function listFiles(directory: string): Promise { - const entries = await readdir(directory, { withFileTypes: true }) - const files = await Promise.all( - entries.map(async (entry) => { - const entryPath = join(directory, entry.name) - if (entry.isDirectory()) { - return listFiles(entryPath) - } - - if (entry.isFile()) { - return [entryPath] - } - - return [] - }), - ) - - return files.flat() -} - -async function pathExists(filePath: string) { - try { - await access(filePath, constants.F_OK) - return true - } catch { - return false - } -} - -function isRecord(value: unknown): value is Record { - return typeof value === "object" && value !== null && !Array.isArray(value) -} - -function parseArgs(argv: string[]) { - const args: Required> & Pick = { - dryRun: false, - enableMcp: false, - force: false, - } - - for (let index = 0; index < argv.length; index += 1) { - const arg = argv[index] - switch (arg) { - case "setup": - break - case "--config-dir": - index += 1 - if (!argv[index]) { - throw new Error("--config-dir requires a path.") - } - args.targetConfigDir = resolve(argv[index]) - break - case "--enable-mcp": - args.enableMcp = true - break - case "--force": - args.force = true - break - case "--dry-run": - args.dryRun = true - break - case "--help": - case "-h": - printHelp() - process.exit(0) - default: - throw new Error(`Unknown argument: ${arg}`) - } - } - - return args -} - -function printHelp() { - console.log(`render-opencode setup - -Install Render skills, commands, and agent files into your OpenCode config directory. - -Options: - --config-dir Target OpenCode config directory. Defaults to ~/.config/opencode. - --enable-mcp Add the Render MCP server to opencode.json. - --force Overwrite existing files. - --dry-run Print what would change without writing files. - -h, --help Show this help. -`) -} - -async function main() { - const options = parseArgs(process.argv.slice(2)) - const result = await installOpenCodeAssets(options) - - for (const filePath of result.written) { - console.log(`${options.dryRun ? "would write" : "wrote"} ${filePath}`) - } - - for (const filePath of result.skipped) { - console.log(`skipped existing ${filePath}`) - } -} +export { getDefaultConfigDir, installOpenCodeAssets, mergeMcpConfig } from "../assets/setup.mjs" -if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) { +if (process.argv[1] && realpathSync(process.argv[1]) === fileURLToPath(import.meta.url)) { main().catch((error: unknown) => { console.error(error instanceof Error ? error.message : String(error)) - process.exit(1) + process.exitCode = 1 }) } diff --git a/src/touched-files.ts b/src/touched-files.ts index 9b9b92a..bc15467 100644 --- a/src/touched-files.ts +++ b/src/touched-files.ts @@ -1,4 +1,34 @@ const FILE_PATH_KEYS = new Set(["file", "filePath", "file_path", "filename", "path"]) +const MUTATING_TOOLS = new Set(["write", "edit", "multiedit", "apply_patch"]) + +export function extractMutatedFiles(tool: string, args: unknown, metadata: unknown): string[] { + if (!MUTATING_TOOLS.has(tool)) return [] + if (tool !== "apply_patch") return [...new Set(extractTouchedFiles(args))] + + // OpenCode's patch result identifies every affected file, including moves and deletions. + if (isRecord(metadata) && Array.isArray(metadata.files)) { + return [...new Set(metadata.files.flatMap((file) => { + if (!isRecord(file) || file.type === "delete") return [] + const path = file.movePath ?? file.filePath + return typeof path === "string" ? [path] : [] + }))] + } + + // Older clients can omit file metadata. Read only path headers, never patch contents. + if (!isRecord(args) || typeof args.patchText !== "string") return [] + const files: string[] = [] + for (const line of args.patchText.split(/\r?\n/)) { + const match = /^\*\*\* (Add File|Update File|Move to): (.+)$/.exec(line) + if (!match) continue + if (match[1] === "Move to") files.pop() + files.push(match[2]) + } + return [...new Set(files)] +} + +function isRecord(value: unknown): value is Record { + return typeof value === "object" && value !== null && !Array.isArray(value) +} export function extractTouchedFiles(value: unknown): string[] { const files: string[] = [] diff --git a/tests/installer-cli.test.ts b/tests/installer-cli.test.ts new file mode 100644 index 0000000..a4f388b --- /dev/null +++ b/tests/installer-cli.test.ts @@ -0,0 +1,35 @@ +import { execFile } from "node:child_process" +import { mkdtemp, mkdir, readFile, rm, symlink, writeFile } from "node:fs/promises" +import { join, resolve } from "node:path" +import { tmpdir } from "node:os" +import { promisify } from "node:util" +import { afterEach, describe, expect, it } from "vitest" + +const exec = promisify(execFile) +const roots: string[] = [] +const repo = resolve(".") + +afterEach(async () => { await Promise.all(roots.splice(0).map((path) => rm(path, { recursive: true, force: true }))) }) + +for (const mode of ["npm", "github"] as const) { + describe(`${mode} installer command`, () => { + it("installs into the XDG location and preserves existing MCP settings until forced", async () => { + const root = await mkdtemp(join(tmpdir(), "render-cli-test-")); roots.push(root) + const target = join(root, "config", "opencode") + await mkdir(target, { recursive: true }) + const original = { mcp: { render: { enabled: false, type: "remote", url: "https://example.invalid" } } } + await writeFile(join(target, "opencode.json"), JSON.stringify(original)) + const env = { ...process.env, OPENCODE_CONFIG_DIR: "", XDG_CONFIG_HOME: join(root, "config") } + const path = join(root, "render-opencode") + if (mode === "npm") await symlink(join(repo, "dist", "setup.js"), path) + const command = mode === "npm" ? process.execPath : "bash" + const args = mode === "npm" ? [path, "setup"] : [join(repo, "install.sh"), "--source", repo] + await exec(command, [...args, "--enable-mcp"], { env }) + expect(await readFile(join(target, "skills", "render-deploy", "SKILL.md"), "utf8")).toContain("name: render-deploy") + expect(JSON.parse(await readFile(join(target, "opencode.json"), "utf8"))).toEqual(original) + await exec(command, [...args, "--enable-mcp", "--force"], { env }) + expect(JSON.parse(await readFile(join(target, "opencode.json"), "utf8")).mcp.render.oauth).toBe(false) + if (mode === "github") expect(await readFile(join(target, "plugins", "render.ts"), "utf8")).toContain("Generated by npm run build") + }) + }) +} diff --git a/tests/plugin.test.ts b/tests/plugin.test.ts new file mode 100644 index 0000000..d57df7c --- /dev/null +++ b/tests/plugin.test.ts @@ -0,0 +1,81 @@ +import { chmod, mkdir, mkdtemp, readFile, realpath, rm, writeFile } from "node:fs/promises" +import { join } from "node:path" +import { tmpdir } from "node:os" +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest" +import { RenderPlugin } from "../src/index.js" + +let root: string +let project: string +let log: string +let hooks: Awaited> + +beforeEach(async () => { + root = await realpath(await mkdtemp(join(tmpdir(), "render-plugin-test-"))) + project = join(root, "project") + log = join(root, "calls.jsonl") + await mkdir(join(root, "bin")) + await mkdir(project) + const executable = join(root, "bin", "render") + await writeFile(executable, `#!/usr/bin/env node +const fs = require('node:fs'); +const args = process.argv.slice(2); +fs.appendFileSync(process.env.RENDER_PLUGIN_TEST_LOG, JSON.stringify({args,cwd:process.cwd()})+'\\n'); +const contents = fs.readFileSync(args[2], 'utf8'); +console.log(contents.includes('INVALID') ? 'invalid blueprint' : 'valid blueprint'); +process.exitCode = contents.includes('INVALID') ? 1 : 0; +`) + await chmod(executable, 0o755) + vi.stubEnv("PATH", `${join(root, "bin")}:${process.env.PATH}`) + vi.stubEnv("RENDER_PLUGIN_TEST_LOG", log) + hooks = await RenderPlugin({ directory: project, client: { app: { log: vi.fn().mockResolvedValue({}) } } } as unknown as Parameters[0]) +}) + +afterEach(async () => { vi.unstubAllEnvs(); await rm(root, { recursive: true, force: true }) }) + +async function calls() { + try { return (await readFile(log, "utf8")).trim().split("\n").map((line) => JSON.parse(line)) } + catch (error) { if ((error as NodeJS.ErrnoException).code === "ENOENT") return []; throw error } +} + +describe("Render OpenCode integration", () => { + it("resolves a relative filename against the tool's session directory and passes it to Render", async () => { + const sessionDirectory = join(root, "session") + await mkdir(sessionDirectory) + await writeFile(join(sessionDirectory, "render.yml"), "services: []") + const result = await hooks.tool!.render_validate_blueprint.execute({ path: "render.yml" }, { + directory: sessionDirectory, abort: new AbortController().signal, + } as Parameters[string]["execute"]>[1]) + expect(result).toMatchObject({ metadata: { ok: true, cwd: sessionDirectory } }) + expect(await calls()).toEqual([{ cwd: sessionDirectory, args: ["blueprints", "validate", join(sessionDirectory, "render.yml"), "--output", "json"] }]) + }) + + it("does not validate reads or repeat explicit validation", async () => { + for (const tool of ["read", "render_validate_blueprint"]) { + await hooks["tool.execute.after"]!({ tool, sessionID: "test", callID: "test", args: { path: "render.yaml" } }, { title: "read", output: "original", metadata: {} }) + } + expect(await calls()).toEqual([]) + }) + + it("validates a relative write and preserves the tool's metadata", async () => { + await writeFile(join(project, "render.yaml"), "INVALID") + const output = { title: "write", output: "wrote", metadata: { existing: true } } + await hooks["tool.execute.after"]!({ tool: "write", sessionID: "test", callID: "test", args: { filePath: "render.yaml" } }, output) + expect(output.output).toContain("invalid blueprint") + expect(output.metadata).toMatchObject({ existing: true, renderBlueprintValidation: { ok: false } }) + expect(await calls()).toHaveLength(1) + }) + + it("validates every surviving patched Blueprint and skips deleted files", async () => { + await mkdir(join(project, "nested")) + await writeFile(join(project, "render.yaml"), "INVALID") + await writeFile(join(project, "nested", "render.yml"), "INVALID") + const output = { title: "patch", output: "patched", metadata: { files: [ + { type: "update", filePath: join(project, "render.yaml") }, + { type: "add", filePath: join(project, "nested", "render.yml") }, + { type: "delete", filePath: join(project, "deleted", "render.yaml") }, + ] } } + await hooks["tool.execute.after"]!({ tool: "apply_patch", sessionID: "test", callID: "test", args: { patchText: "patch" } }, output) + expect(await calls()).toHaveLength(2) + expect(output.metadata).toMatchObject({ renderBlueprintValidation: { ok: false, results: [{ path: join(project, "render.yaml") }, { path: join(project, "nested", "render.yml") }] } }) + }) +}) diff --git a/tests/setup.test.ts b/tests/setup.test.ts index a5d592a..8da28f5 100644 --- a/tests/setup.test.ts +++ b/tests/setup.test.ts @@ -1,10 +1,10 @@ -import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises" +import { cp, mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises" import { join } from "node:path" import { tmpdir } from "node:os" -import { afterEach, describe, expect, it } from "vitest" +import { afterEach, describe, expect, it, vi } from "vitest" -import { installOpenCodeAssets } from "../src/setup.js" +import { getDefaultConfigDir, installOpenCodeAssets } from "../src/setup.js" const tempRoots: string[] = [] @@ -15,6 +15,7 @@ async function tempDir() { } afterEach(async () => { + vi.unstubAllEnvs() await Promise.all(tempRoots.splice(0).map((dir) => rm(dir, { force: true, recursive: true }))) }) @@ -74,4 +75,95 @@ describe("installOpenCodeAssets", () => { }, }) }) + + it("preserves existing Render MCP settings and other fields unless forced", async () => { + const targetConfigDir = await tempDir() + const sourceAssetsDir = await tempDir() + const path = join(targetConfigDir, "opencode.json") + const render = { type: "remote", url: "https://example.invalid/mcp", enabled: false, oauth: { clientId: "custom" } } + const existing = { model: "test/model", mcp: { render, other: { type: "local", command: ["other"] } } } + await writeFile(path, JSON.stringify(existing)) + const skipped = await installOpenCodeAssets({ sourceAssetsDir, targetConfigDir, enableMcp: true }) + expect(JSON.parse(await readFile(path, "utf8"))).toEqual(existing) + expect(skipped.skipped).toContain(path) + await installOpenCodeAssets({ sourceAssetsDir, targetConfigDir, enableMcp: true, force: true }) + const changed = JSON.parse(await readFile(path, "utf8")) + expect(changed.model).toBe(existing.model) + expect(changed.mcp.other).toEqual(existing.mcp.other) + expect(changed.mcp.render.oauth).toBe(false) + }) + + it("updates owned files and removes obsolete files while preserving user edits", async () => { + const targetConfigDir = await tempDir() + const sourceAssetsDir = await tempDir() + await mkdir(join(sourceAssetsDir, "commands")) + for (const name of ["update", "edited", "obsolete", "obsolete-edited"]) { + await writeFile(join(sourceAssetsDir, "commands", `${name}.md`), "old") + } + await installOpenCodeAssets({ sourceAssetsDir, targetConfigDir }) + for (const name of ["edited", "obsolete-edited"]) { + await writeFile(join(targetConfigDir, "commands", `${name}.md`), "user edit") + } + for (const name of ["update", "edited"]) { + await writeFile(join(sourceAssetsDir, "commands", `${name}.md`), "new") + } + for (const name of ["obsolete", "obsolete-edited"]) { + await rm(join(sourceAssetsDir, "commands", `${name}.md`)) + } + const result = await installOpenCodeAssets({ sourceAssetsDir, targetConfigDir }) + expect(await readFile(join(targetConfigDir, "commands", "update.md"), "utf8")).toBe("new") + expect(await readFile(join(targetConfigDir, "commands", "edited.md"), "utf8")).toBe("user edit") + expect(await readFile(join(targetConfigDir, "commands", "obsolete-edited.md"), "utf8")).toBe("user edit") + await expect(readFile(join(targetConfigDir, "commands", "obsolete.md"))).rejects.toMatchObject({ code: "ENOENT" }) + expect(result.removed).toEqual([join(targetConfigDir, "commands", "obsolete.md")]) + await installOpenCodeAssets({ sourceAssetsDir, targetConfigDir, force: true }) + expect(await readFile(join(targetConfigDir, "commands", "edited.md"), "utf8")).toBe("new") + await expect(readFile(join(targetConfigDir, "commands", "obsolete-edited.md"))).rejects.toMatchObject({ code: "ENOENT" }) + }) + + it("adopts unmodified 0.1.0 installs that predate tracking", async () => { + const targetConfigDir = await tempDir() + const sourceAssetsDir = await tempDir() + // Command fixtures predate tracking and are unaffected by later skill syncs. + await cp("assets/opencode/commands", join(targetConfigDir, "commands"), { recursive: true }) + await cp("assets/opencode/commands", join(sourceAssetsDir, "commands"), { recursive: true }) + const path = join("commands", "deploy-to-render.md") + await writeFile(join(sourceAssetsDir, path), "upgraded command") + await rm(join(sourceAssetsDir, "commands", "check-render-status.md")) + const result = await installOpenCodeAssets({ sourceAssetsDir, targetConfigDir }) + expect(await readFile(join(targetConfigDir, path), "utf8")).toBe("upgraded command") + expect(result.removed).toContain(join(targetConfigDir, "commands", "check-render-status.md")) + }) + + it("plans an upgrade without changing assets or their tracking manifest", async () => { + const targetConfigDir = await tempDir() + const sourceAssetsDir = await tempDir() + await mkdir(join(sourceAssetsDir, "commands")) + await writeFile(join(sourceAssetsDir, "commands", "old.md"), "old") + await installOpenCodeAssets({ sourceAssetsDir, targetConfigDir }) + const manifest = await readFile(join(targetConfigDir, ".render-opencode-manifest.json"), "utf8") + await rm(join(sourceAssetsDir, "commands", "old.md")) + await writeFile(join(sourceAssetsDir, "commands", "new.md"), "new") + const result = await installOpenCodeAssets({ sourceAssetsDir, targetConfigDir, dryRun: true, enableMcp: true }) + expect(result.removed).toContain(join(targetConfigDir, "commands", "old.md")) + expect(await readFile(join(targetConfigDir, "commands", "old.md"), "utf8")).toBe("old") + expect(await readFile(join(targetConfigDir, ".render-opencode-manifest.json"), "utf8")).toBe(manifest) + await expect(readFile(join(targetConfigDir, "commands", "new.md"))).rejects.toMatchObject({ code: "ENOENT" }) + await expect(readFile(join(targetConfigDir, "opencode.json"))).rejects.toMatchObject({ code: "ENOENT" }) + }) + + it("rejects unsafe tracking paths before changing files", async () => { + const targetConfigDir = await tempDir() + const sourceAssetsDir = await tempDir() + await writeFile(join(targetConfigDir, ".render-opencode-manifest.json"), JSON.stringify({ version: 1, files: { "../outside": "0".repeat(64) } })) + await expect(installOpenCodeAssets({ sourceAssetsDir, targetConfigDir })).rejects.toThrow("Invalid Render installation manifest entry") + }) + + it("follows OpenCode config-directory precedence", () => { + vi.stubEnv("OPENCODE_CONFIG_DIR", "") + vi.stubEnv("XDG_CONFIG_HOME", "/tmp/test-xdg") + expect(getDefaultConfigDir()).toBe("/tmp/test-xdg/opencode") + vi.stubEnv("OPENCODE_CONFIG_DIR", "/tmp/explicit-opencode") + expect(getDefaultConfigDir()).toBe("/tmp/explicit-opencode") + }) }) diff --git a/tests/touched-files.test.ts b/tests/touched-files.test.ts index fb3dd25..f003e20 100644 --- a/tests/touched-files.test.ts +++ b/tests/touched-files.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from "vitest" -import { extractTouchedFiles } from "../src/touched-files.js" +import { extractMutatedFiles, extractTouchedFiles } from "../src/touched-files.js" describe("extractTouchedFiles", () => { it("extracts likely file paths from OpenCode tool arguments", () => { @@ -17,3 +17,25 @@ describe("extractTouchedFiles", () => { expect(extractTouchedFiles({ path: 1, other: "render.yaml" })).toEqual([]) }) }) + +describe("OpenCode mutation detection", () => { + it("ignores reads, MCP tools, and explicit validation", () => { + for (const tool of ["read", "grep", "render_validate_blueprint", "render_get_service"]) { + expect(extractMutatedFiles(tool, { path: "render.yaml" }, {})).toEqual([]) + } + }) + + it("uses patch result metadata for multiple files, moves, and deletions", () => { + expect(extractMutatedFiles("apply_patch", {}, { files: [ + { filePath: "render.yaml", type: "update" }, + { filePath: "render.yaml", type: "update" }, + { filePath: "old/render.yml", movePath: "new/render.yml", type: "move" }, + { filePath: "deleted/render.yaml", type: "delete" }, + ] })).toEqual(["render.yaml", "new/render.yml"]) + }) + + it("supports older patch clients without file metadata", () => { + const patchText = "*** Begin Patch\r\n*** Update File: old/render.yaml\r\n*** Move to: new/render.yaml\r\n@@\r\n-old\r\n+new\r\n*** Add File: render.yml\r\n+services: []\r\n*** Delete File: deleted/render.yaml\r\n*** End Patch" + expect(extractMutatedFiles("apply_patch", { patchText }, {})).toEqual(["new/render.yaml", "render.yml"]) + }) +})