diff --git a/.gitignore b/.gitignore index 39bd9d6..9e612d9 100644 --- a/.gitignore +++ b/.gitignore @@ -20,6 +20,10 @@ Thumbs.db .idea/ .vscode/ +# Agent tooling +.serena/ +.claude/ + # Env / local .env .env.local diff --git a/README.md b/README.md index 09d13e2..a56289b 100644 --- a/README.md +++ b/README.md @@ -13,9 +13,9 @@ memories — into a private Git repo you own. [![License](https://img.shields.io/badge/license-AGPL--3.0--only-blue.svg)](./LICENSE)  ![Node](https://img.shields.io/badge/node-%E2%89%A518-43853d?logo=node.js&logoColor=white)  ![Platforms](https://img.shields.io/badge/platform-Linux%20%C2%B7%20macOS%20%C2%B7%20Windows-555) - ![Status](https://img.shields.io/badge/status-v0.1-orange) + ![Status](https://img.shields.io/badge/status-v1.0.0-green) -[Quick start](#quick-start) · [Commands](#commands) · [How sign-in works](#how-sign-in-works) · [Secrets](#secrets) +[Quick start](#quick-start) · [Commands](#commands) · [How sign-in works](#how-sign-in-works) · [What still needs you after a pull](#what-still-needs-you-after-a-pull) · [Secrets](#secrets) @@ -51,18 +51,28 @@ arbella update # later: update arbella itself ## What it does ```text - push ~/.claude · ~/.codex · Cursor ──▶ strip · template ──▶ private repo - pull private repo ──▶ install · place ──▶ tools + Cursor User data + push ~/.claude · ~/.codex · Cursor · $HOME ──▶ strip · template ──▶ private repo + pull private repo ──▶ install · place ──▶ tools + Cursor User data + $HOME ``` -`push` reads the parts of your setup that matter, strips anything secret, swaps machine-specific paths for placeholders, and pushes the result to your repo. `pull` does the reverse: installs the CLIs you don't have, drops files back with this machine's paths, reinstalls plugins and skills, and wires shared Claude/Codex instructions back into `CLAUDE.md` and `AGENTS.md`. +`push` reads the parts of your setup that matter, strips anything secret, swaps machine-specific paths for placeholders, and pushes the result to your repo. `pull` does the reverse: installs the CLIs you don't have, drops files back with this machine's paths, merges MCP servers into `~/.claude.json`, writes shared home files back under `$HOME`, reinstalls plugins, skills, and external tools, and wires shared Claude/Codex instructions back into `CLAUDE.md` and `AGENTS.md`. + +Beyond the obvious config files, Arbella also carries: + +| It also carries | Where it comes from | Notes | +| --- | --- | --- | +| MCP servers | `~/.claude.json` → `mcpServers` | Never the whole file — only the server definitions, sanitized and templated; env values are redacted unless `includeSecrets` is on | +| The rest of `~/.claude` | `rules/`, `scripts/`, `output-styles/`, `keybindings.json`, `mcp-configs/`, `AGENTS.md` | The pieces your hooks and `CLAUDE.md` actually reference | +| Shared home files | `shared/home/` | Scripts your hooks, statusLine, or MCP commands point at outside any tool's home — picked up automatically (one hop, files only, 1 MiB cap) — plus anything you list in `extraPaths`, and the claude-mem companion `~/.claude-mem/settings.json` | +| Claude memories | `claude/memories/` | Per-project memory, only when `includeMemories` is on | +| External tools | manifest `externalTools` | Binaries behind an MCP/hook command that brew, `uv tool`, or pipx installed — reinstalled best-effort on pull | Two things it deliberately won't do: -- **Never commits secrets.** API keys, OAuth tokens, `auth.json`, `.credentials.json` — all excluded, no exceptions. You sign back in after a pull, or carry them yourself with [`arbella secrets`](#arbella-secrets). -- **Doesn't copy what it can reinstall.** Plugins and registry skills are saved as a list and pulled fresh, so the repo stays small and never goes stale. +- **Excludes secrets by default.** Whole credential files — `auth.json`, `.credentials.json`, and the like — sit on a hard denylist and are never read into the repo, no exceptions. Inline secret-shaped values (API keys, OAuth tokens) found in files that *are* captured are redacted by default; [`includeSecrets`](#arbella-init) is the explicit opt-in that carries them verbatim into your (private) repo instead. You sign back in after a pull, or carry credentials yourself with [`arbella secrets`](#arbella-secrets). +- **Reinstalls plugins from their sources.** Plugins are saved as a list and pulled fresh. Local and shared skills include their contents, preserving local edits and avoiding guesses about their original source. -Supported today: **Claude Code**, **Codex**, **Cursor** (IDE + the `cursor-agent` CLI), **opencode**, **GitHub Copilot CLI**, **Kilo Code**, and **Google Antigravity**. Cursor support covers global MCP config, user settings, keybindings, snippets, local skills, skills.sh symlinks, the `cursor-agent` CLI config + global commands, and extension IDs; runtime state and credentials stay out. opencode, Copilot CLI, and Kilo are config-dir CLIs — Arbella captures each one's config dir (the JSON/JSONC config, custom agents/commands, MCP config), strips token-shaped values, and skips machine-local state, plugin install artifacts, and reinstallable skills (regenerated from the config on first run). Antigravity spans three roots — its VS Code-style User settings, the `~/.antigravity` extension list, and the shared `~/.gemini` agent config (global rules, MCP servers, skills) — while its Google OAuth tokens never leave your machine. +Supported today: **Claude Code**, **Codex**, **Cursor** (IDE + the `cursor-agent` CLI), **opencode**, **GitHub Copilot CLI**, **Kilo Code**, and **Google Antigravity**. Claude Code coverage also reaches into `~/.claude.json` for MCP servers, per-project memories, and any linked scripts living under `$HOME`. Cursor support covers global MCP config, user settings, keybindings, snippets, local skills, shared skill contents with their symlinks, the `cursor-agent` CLI config + global commands, and extension IDs; runtime state and credentials stay out. opencode, Copilot CLI, and Kilo are config-dir CLIs — Arbella captures each one's config dir (the JSON/JSONC config, custom agents/commands, MCP config), strips token-shaped values, and skips machine-local state, plugin install artifacts, and reinstallable skills (regenerated from the config on first run). Antigravity spans three roots — its VS Code-style User settings, the `~/.antigravity` extension list, and the shared `~/.gemini` agent config (global rules, MCP servers, skills) — while its Google OAuth tokens never leave your machine. ## Commands @@ -100,6 +110,7 @@ The one-time wiring. Pick a host (GitHub, GitLab, or a plain Git URL); if the re ```sh arbella init arbella init --provider github --repo you/your-setup --auto-push daily -y +arbella init --extra-paths ~/.agents/hooks,~/.agents/memory -y ``` Saves your preferences: @@ -108,6 +119,7 @@ Saves your preferences: - who wins a conflict — your machine or the repo - auto-push cadence — off, once per session, or daily - whether secrets are allowed into the repo (off by default), and whether to include memories (also off) +- extra `$HOME` paths to carry along (`--extra-paths`, comma-separated, or the interactive prompt) — Arbella pre-fills the prompt with suggestions from the scripts your hooks and MCP servers already point at. Suggestions are never accepted on your behalf: `-y` keeps whatever is already configured, so pass `--extra-paths` to carry them non-interactively ### `arbella push` @@ -129,11 +141,18 @@ arbella pull https://github.com/you/your-setup arbella pull --dry-run ``` +`pull --dry-run` does not alter your tool setup or persistent backup clone. It +plans from an existing local clone without refreshing it; when there is no clone +yet, it makes a temporary download for the preview and removes it afterwards. + - First copies your current `~/.claude` and `~/.codex` to a timestamped safety folder, so a pull can't quietly wreck what's already there. - Installs any missing CLIs — reaching for `sudo` only when the global npm folder actually needs root. - Writes files with this machine's paths, then reinstalls your plugins and skills from the manifest. +- Merges MCP servers into `~/.claude.json` — a safety copy is taken first; the file is read in full, only the `mcpServers` keys (user- and project-scope) are changed, every other key (auth, telemetry) is carried over unchanged, and the whole file is rewritten atomically so it's never left half-written. +- Writes shared home files (linked scripts, `extraPaths`) back under `$HOME` — an existing file is snapshotted first and never overwritten while your machine is the source of truth. +- Reinstalls external tools behind MCP/hook commands via brew, `uv tool`, or pipx, best-effort. - Deploys your shared instructions to `CLAUDE.md` and `AGENTS.md`. -- Reminds you to sign back in at the end, since no credentials came along for the ride. +- Reminds you to sign back in for credential stores that never travel. With the default secret setting, it also lists inline values that were redacted and need to be supplied again. ### `arbella status` @@ -144,7 +163,7 @@ arbella status arbella status --json # pipe it somewhere ``` -New and modified files, plugin drift, the secrets it would skip. Writes nothing, installs nothing. +New and modified files, plugin drift, MCP server and external-tool drift, and the secrets it would skip. Files it doesn't recognize print as `not backed up (unknown to arbella)` — add them via `extraPaths` or open an issue. Writes nothing, installs nothing. ### `arbella update` @@ -188,12 +207,25 @@ Short version: **gh and glab are preferred, not required.** They're the smoothes For the device flow, point Arbella at your own registered OAuth app with `ARBELLA_GITHUB_CLIENT_ID` (or `ARBELLA_GITLAB_CLIENT_ID`). There's no sensible default to ship — the client ID is public but tied to your account — so with none set, Arbella skips the device flow and asks for a token instead. SSH and `file://` remotes are left alone, so those keep working however your Git already has them. +## What still needs you after a pull + +Arbella restores everything it safely can. A few things are yours to finish: + +- **Credentials.** Credential stores never travel, so sign back in when Arbella tells you to, or carry them yourself with [`arbella secrets`](#arbella-secrets). By default, inline API keys and OAuth tokens are redacted too; `includeSecrets` is the explicit private-repo opt-in that carries those inline values. +- **Redacted MCP env values.** If an MCP server's config held a secret-shaped value, push replaced it with `{{REDACTED}}`. Pull prints one line per key it couldn't restore, e.g. `claude: MCP server serena needs env SERENA_TOKEN re-supplied (redacted on backup)`. +- **Per-project MCP servers for a project you haven't cloned yet.** A project's MCP servers are only registered when its directory exists on this machine; if it doesn't, pull skips them and warns you which directory was skipped. Clone the project, then run `arbella pull` again. +- **External tools Arbella couldn't classify or install.** brew/`uv tool`/pipx tools are reinstalled best-effort; anything unknown, or that failed, is listed by name and by what uses it, so you can install it yourself. +- **`~/.claude.json` is only merged, key by key.** Pull touches `mcpServers` (and per-project `mcpServers`) and nothing else in that file — your `oauthAccount`, telemetry, and everything else stay exactly as they were. +- **The repo is trusted.** A pull writes the hooks, MCP commands, and install lists your repo describes, and runs `brew` / `uv tool` / `pipx` / `npm` for the tools it lists — exactly like the machine you pushed from. Keep the repo private, and run `arbella pull --dry-run` first on anything you did not push yourself: the plan prints every command and target path. + ## Secrets This is the part I most wanted to get right. - Whole credential files (`auth.json`, `.credentials.json`, and the like) sit on a hard denylist and are never read into the repo. - Everything that *does* get committed runs through a sanitizer that redacts token-shaped values. +- `~/.claude.json` itself is still never stored — only its `mcpServers` sub-objects are lifted out, sanitized, and templated; the rest of that file (OAuth account, telemetry) never leaves your machine. +- Shared home files go through the same sanitizer, plus a home-specific denylist — `.env`, `*.pem`, `*.key`, `id_rsa*`, `.netrc`, `.npmrc`, `.git-credentials`, and friends are never captured even when something points at them. - Any token Arbella holds for itself lives in one local file with `0600` permissions — not in the repo, not in a logged command, not baked into a Git remote. - A test fails the build if a credential can reach the repo. This isn't on the honor system. @@ -209,7 +241,20 @@ The Arbella was the flagship of the 1630 Winthrop Fleet — a few hundred settle ## Status -v0.1 — early, but it builds clean, the full test suite passes, and I use it daily. Issues and pull requests welcome. +v1.0.0 — builds clean, the full test suite passes, and I use it daily. Issues and pull requests welcome. + +## Releases + +For **1.0.0**, merge the version bump to `main`, then publish a GitHub release with +tag **v1.0.0** targeting that commit. `publish.yml` checks that the tag matches +`package.json`, runs the tests, and publishes the package to npm automatically. +For later releases, update `package.json` and `package-lock.json` and create the +matching GitHub release when ready. + +Publishing uses [npm trusted publishing](https://docs.npmjs.com/trusted-publishers/). +The `arbella` package's npm Settings must have a GitHub Actions trusted publisher with +owner **Fafoooo**, repository **arbella**, and workflow filename **publish.yml** +(no environment name). This avoids maintaining an npm token in GitHub secrets. ## License diff --git a/SECURITY.md b/SECURITY.md index d161795..240d574 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -5,12 +5,12 @@ carrying a secret into a Git repo**. Security reports are taken seriously. ## Supported versions -Arbella is pre-1.0 and ships from `main`. Only the latest published `0.1.x` -release on npm receives security fixes. +Arbella ships from `main`. Only the latest published release on npm receives +security fixes. | Version | Supported | | --- | --- | -| latest `0.1.x` | ✅ | +| latest published release | ✅ | | older | ❌ | ## Reporting a vulnerability diff --git a/package-lock.json b/package-lock.json index 89e7110..5309292 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,19 +1,19 @@ { "name": "arbella", - "version": "0.1.3", + "version": "1.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "arbella", - "version": "0.1.3", + "version": "1.0.0", "license": "AGPL-3.0-only", "dependencies": { "@clack/prompts": "^0.7.0", "commander": "^12.1.0", "execa": "^9.5.1", "picocolors": "^1.1.1", - "smol-toml": "^1.3.1", + "smol-toml": "^1.8.0", "zod": "^3.23.8" }, "bin": { @@ -24,7 +24,7 @@ "tsup": "^8.3.5", "tsx": "^4.19.2", "typescript": "^5.6.3", - "vitest": "^2.1.5" + "vitest": "^3.2.7" }, "engines": { "node": ">=18" @@ -57,6 +57,7 @@ }, "node_modules/@clack/prompts/node_modules/is-unicode-supported": { "version": "1.3.0", + "extraneous": true, "inBundle": true, "license": "MIT", "engines": { @@ -915,6 +916,24 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/estree": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", @@ -934,38 +953,39 @@ } }, "node_modules/@vitest/expect": { - "version": "2.1.9", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-2.1.9.tgz", - "integrity": "sha512-UJCIkTBenHeKT1TTlKMJWy1laZewsRIzYighyYiJKZreqtdxSos/S1t+ktRMQWu2CKqaarrkeszJx1cgC5tGZw==", + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.7.tgz", + "integrity": "sha512-E8eBXaKibuvH2pSZErOjdVb5vF4PbKYcrnluBTYxEk1l/VhhwZg1kZQsdtjq+CsF5CFydf2Rdkz7jDHKSisi3w==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/spy": "2.1.9", - "@vitest/utils": "2.1.9", - "chai": "^5.1.2", - "tinyrainbow": "^1.2.0" + "@types/chai": "^5.2.2", + "@vitest/spy": "3.2.7", + "@vitest/utils": "3.2.7", + "chai": "^5.2.0", + "tinyrainbow": "^2.0.0" }, "funding": { "url": "https://opencollective.com/vitest" } }, "node_modules/@vitest/mocker": { - "version": "2.1.9", - "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-2.1.9.tgz", - "integrity": "sha512-tVL6uJgoUdi6icpxmdrn5YNo3g3Dxv+IHJBr0GXHaEdTcw3F+cPKnsXFhli6nO+f/6SDKPHEK1UN+k+TQv0Ehg==", + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.7.tgz", + "integrity": "sha512-Trr0hYO9CM3Wj6ksWHRhK9IZpIY6wTMO5u/MqXurMxT57sWBaOPEtP3Oq60ihZuh5JsiagKfz95OcxdEP6dBrA==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/spy": "2.1.9", + "@vitest/spy": "3.2.7", "estree-walker": "^3.0.3", - "magic-string": "^0.30.12" + "magic-string": "^0.30.17" }, "funding": { "url": "https://opencollective.com/vitest" }, "peerDependencies": { "msw": "^2.4.9", - "vite": "^5.0.0" + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" }, "peerDependenciesMeta": { "msw": { @@ -977,84 +997,71 @@ } }, "node_modules/@vitest/pretty-format": { - "version": "2.1.9", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-2.1.9.tgz", - "integrity": "sha512-KhRIdGV2U9HOUzxfiHmY8IFHTdqtOhIzCpd8WRdJiE7D/HUcZVD0EgQCVjm+Q9gkUXWgBvMmTtZgIG48wq7sOQ==", + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.7.tgz", + "integrity": "sha512-KUHlwqVu0sRlhCdyPdQ/wBoTfRahjUky1MubOmYw9fWfIZy1gNoHpuaaQBPAaMaVYdQYHJLurzj8ECCj5OwTqA==", "dev": true, "license": "MIT", "dependencies": { - "tinyrainbow": "^1.2.0" + "tinyrainbow": "^2.0.0" }, "funding": { "url": "https://opencollective.com/vitest" } }, "node_modules/@vitest/runner": { - "version": "2.1.9", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-2.1.9.tgz", - "integrity": "sha512-ZXSSqTFIrzduD63btIfEyOmNcBmQvgOVsPNPe0jYtESiXkhd8u2erDLnMxmGrDCwHCCHE7hxwRDCT3pt0esT4g==", + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.2.7.tgz", + "integrity": "sha512-sB9y4ovltoQP+WaUPwmSxO9WIg9Ig694Di5PalVPsYHklAdE027mehpWF2SQSVq+k6sFgaivbTjTJwZLSHbedA==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/utils": "2.1.9", - "pathe": "^1.1.2" + "@vitest/utils": "3.2.7", + "pathe": "^2.0.3", + "strip-literal": "^3.0.0" }, "funding": { "url": "https://opencollective.com/vitest" } }, - "node_modules/@vitest/runner/node_modules/pathe": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", - "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", - "dev": true, - "license": "MIT" - }, "node_modules/@vitest/snapshot": { - "version": "2.1.9", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-2.1.9.tgz", - "integrity": "sha512-oBO82rEjsxLNJincVhLhaxxZdEtV0EFHMK5Kmx5sJ6H9L183dHECjiefOAdnqpIgT5eZwT04PoggUnW88vOBNQ==", + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.2.7.tgz", + "integrity": "sha512-7C+MwShwtBSI5Buwoyg3s/iY1eHL9PKAf+O1wVh/TdnjXUtkoL/9YQtre90i4MtNXM6edP1wJ2zOBpfCyhIS7g==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "2.1.9", - "magic-string": "^0.30.12", - "pathe": "^1.1.2" + "@vitest/pretty-format": "3.2.7", + "magic-string": "^0.30.17", + "pathe": "^2.0.3" }, "funding": { "url": "https://opencollective.com/vitest" } }, - "node_modules/@vitest/snapshot/node_modules/pathe": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", - "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", - "dev": true, - "license": "MIT" - }, "node_modules/@vitest/spy": { - "version": "2.1.9", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-2.1.9.tgz", - "integrity": "sha512-E1B35FwzXXTs9FHNK6bDszs7mtydNi5MIfUWpceJ8Xbfb1gBMscAnwLbEu+B44ed6W3XjL9/ehLPHR1fkf1KLQ==", + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.7.tgz", + "integrity": "sha512-Q2eQGI6d2L/hBtZ0qNuKcAGid68XK6cv1xsoaIma6PaJhHPoqcEJhYpXZ/5myCMqkNgtP6UKuBhbc0nHKnrkuQ==", "dev": true, "license": "MIT", "dependencies": { - "tinyspy": "^3.0.2" + "tinyspy": "^4.0.3" }, "funding": { "url": "https://opencollective.com/vitest" } }, "node_modules/@vitest/utils": { - "version": "2.1.9", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-2.1.9.tgz", - "integrity": "sha512-v0psaMSkNJ3A2NMrUEHFRzJtDPFn+/VWZ5WxImB21T9fjucJRmS7xCS3ppEnARb9y11OAzaD+P2Ps+b+BGX5iQ==", + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.7.tgz", + "integrity": "sha512-x6BDOd7dyo3PFLY3I9/HJ25X/6OurhGXk2/B9gOZNPF7XDVjeBK4k01lQE5uvDpbuheErh91qYuE1E2OEjK3Rw==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "2.1.9", - "loupe": "^3.1.2", - "tinyrainbow": "^1.2.0" + "@vitest/pretty-format": "3.2.7", + "loupe": "^3.1.4", + "tinyrainbow": "^2.0.0" }, "funding": { "url": "https://opencollective.com/vitest" @@ -1460,6 +1467,13 @@ "node": ">=10" } }, + "node_modules/js-tokens": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz", + "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==", + "dev": true, + "license": "MIT" + }, "node_modules/lilconfig": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", @@ -1540,9 +1554,9 @@ } }, "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.18", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.18.tgz", + "integrity": "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==", "dev": true, "funding": [ { @@ -1677,9 +1691,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.28", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.28.tgz", + "integrity": "sha512-RRuzqDtt5Y9h3quz5hWhK+TPnsmVs6WwSU6LkJMeY4HstUEDuYTG8UJSdawMRzmzAtV+KEoG8N3Qg2qLy5vM/A==", "dev": true, "funding": [ { @@ -1698,7 +1712,7 @@ "license": "MIT", "peer": true, "dependencies": { - "nanoid": "^3.3.12", + "nanoid": "^3.3.18", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" }, @@ -1880,9 +1894,9 @@ "license": "MIT" }, "node_modules/smol-toml": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/smol-toml/-/smol-toml-1.6.1.tgz", - "integrity": "sha512-dWUG8F5sIIARXih1DTaQAX4SsiTXhInKf1buxdY9DIg4ZYPZK5nGM1VRIYmEbDbsHt7USo99xSLFu5Q1IqTmsg==", + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/smol-toml/-/smol-toml-1.8.0.tgz", + "integrity": "sha512-kCZr2V3ch9i00x8zXRhjUNVcjG9ijES5dDudkXvUVCT5QlJNQWElSJdZqyPemffHoLNUYwOcou0Fy+ojN0uHSQ==", "license": "BSD-3-Clause", "engines": { "node": ">= 18" @@ -1937,6 +1951,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/strip-literal": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-3.1.0.tgz", + "integrity": "sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "js-tokens": "^9.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, "node_modules/sucrase": { "version": "3.35.1", "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz", @@ -2035,9 +2062,9 @@ } }, "node_modules/tinyrainbow": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-1.2.0.tgz", - "integrity": "sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-2.0.0.tgz", + "integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==", "dev": true, "license": "MIT", "engines": { @@ -2045,9 +2072,9 @@ } }, "node_modules/tinyspy": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-3.0.2.tgz", - "integrity": "sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==", + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-4.0.6.tgz", + "integrity": "sha512-u8KszXvGfU68hVcZpRHKG28T0krMuv2G5nDhiHaMLen/gIuFEgIJhaJuO69qjnXg5paSrbPMFfx3brNuN8eVSg==", "dev": true, "license": "MIT", "engines": { @@ -2145,9 +2172,9 @@ } }, "node_modules/tsx/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" ], @@ -2162,9 +2189,9 @@ } }, "node_modules/tsx/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" ], @@ -2179,9 +2206,9 @@ } }, "node_modules/tsx/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" ], @@ -2196,9 +2223,9 @@ } }, "node_modules/tsx/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" ], @@ -2213,9 +2240,9 @@ } }, "node_modules/tsx/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" ], @@ -2230,9 +2257,9 @@ } }, "node_modules/tsx/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" ], @@ -2247,9 +2274,9 @@ } }, "node_modules/tsx/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" ], @@ -2264,9 +2291,9 @@ } }, "node_modules/tsx/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" ], @@ -2281,9 +2308,9 @@ } }, "node_modules/tsx/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" ], @@ -2298,9 +2325,9 @@ } }, "node_modules/tsx/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" ], @@ -2315,9 +2342,9 @@ } }, "node_modules/tsx/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" ], @@ -2332,9 +2359,9 @@ } }, "node_modules/tsx/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" ], @@ -2349,9 +2376,9 @@ } }, "node_modules/tsx/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" ], @@ -2366,9 +2393,9 @@ } }, "node_modules/tsx/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" ], @@ -2383,9 +2410,9 @@ } }, "node_modules/tsx/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" ], @@ -2400,9 +2427,9 @@ } }, "node_modules/tsx/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" ], @@ -2417,9 +2444,9 @@ } }, "node_modules/tsx/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" ], @@ -2434,9 +2461,9 @@ } }, "node_modules/tsx/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" ], @@ -2451,9 +2478,9 @@ } }, "node_modules/tsx/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" ], @@ -2468,9 +2495,9 @@ } }, "node_modules/tsx/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" ], @@ -2485,9 +2512,9 @@ } }, "node_modules/tsx/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" ], @@ -2502,9 +2529,9 @@ } }, "node_modules/tsx/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" ], @@ -2519,9 +2546,9 @@ } }, "node_modules/tsx/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" ], @@ -2536,9 +2563,9 @@ } }, "node_modules/tsx/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" ], @@ -2553,9 +2580,9 @@ } }, "node_modules/tsx/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" ], @@ -2570,9 +2597,9 @@ } }, "node_modules/tsx/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" ], @@ -2587,9 +2614,9 @@ } }, "node_modules/tsx/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", @@ -2600,32 +2627,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/typescript": { @@ -2670,21 +2697,25 @@ } }, "node_modules/vite": { - "version": "5.4.21", - "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", - "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "version": "7.3.6", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.6.tgz", + "integrity": "sha512-4XP60spRGjSZFf1qYH+dJIkK2znL3zQfl9KkOV9MkkRR/3Dls0dxaBsQPTloEc5BLXWPL9vsOxopxyKoMmDueg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { - "esbuild": "^0.21.3", - "postcss": "^8.4.43", - "rollup": "^4.20.0" + "esbuild": "^0.27.0 || ^0.28.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" }, "bin": { "vite": "bin/vite.js" }, "engines": { - "node": "^18.0.0 || >=20.0.0" + "node": "^20.19.0 || >=22.12.0" }, "funding": { "url": "https://github.com/vitejs/vite?sponsor=1" @@ -2693,19 +2724,25 @@ "fsevents": "~2.3.3" }, "peerDependencies": { - "@types/node": "^18.0.0 || >=20.0.0", - "less": "*", + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", "lightningcss": "^1.21.0", - "sass": "*", - "sass-embedded": "*", - "stylus": "*", - "sugarss": "*", - "terser": "^5.4.0" + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" }, "peerDependenciesMeta": { "@types/node": { "optional": true }, + "jiti": { + "optional": true + }, "less": { "optional": true }, @@ -2726,511 +2763,84 @@ }, "terser": { "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true } } }, "node_modules/vite-node": { - "version": "2.1.9", - "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-2.1.9.tgz", - "integrity": "sha512-AM9aQ/IPrW/6ENLQg3AGY4K1N2TGZdR5e4gu/MmmR2xR3Ll1+dib+nook92g4TV3PXVyeyxdWwtaCAiUL0hMxA==", + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.2.4.tgz", + "integrity": "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==", "dev": true, "license": "MIT", "dependencies": { "cac": "^6.7.14", - "debug": "^4.3.7", - "es-module-lexer": "^1.5.4", - "pathe": "^1.1.2", - "vite": "^5.0.0" + "debug": "^4.4.1", + "es-module-lexer": "^1.7.0", + "pathe": "^2.0.3", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" }, "bin": { "vite-node": "vite-node.mjs" }, "engines": { - "node": "^18.0.0 || >=20.0.0" + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" }, "funding": { "url": "https://opencollective.com/vitest" } }, - "node_modules/vite-node/node_modules/pathe": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", - "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/vite/node_modules/@esbuild/aix-ppc64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", - "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/android-arm": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", - "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/android-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", - "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/android-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", - "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/darwin-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", - "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/darwin-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", - "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/freebsd-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", - "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/freebsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", - "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-arm": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", - "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", - "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-ia32": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", - "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-loong64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", - "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-mips64el": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", - "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", - "cpu": [ - "mips64el" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-ppc64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", - "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-riscv64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", - "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-s390x": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", - "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", - "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/netbsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", - "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/openbsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", - "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/sunos-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", - "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/win32-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", - "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/win32-ia32": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", - "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/win32-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", - "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/esbuild": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", - "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=12" - }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.21.5", - "@esbuild/android-arm": "0.21.5", - "@esbuild/android-arm64": "0.21.5", - "@esbuild/android-x64": "0.21.5", - "@esbuild/darwin-arm64": "0.21.5", - "@esbuild/darwin-x64": "0.21.5", - "@esbuild/freebsd-arm64": "0.21.5", - "@esbuild/freebsd-x64": "0.21.5", - "@esbuild/linux-arm": "0.21.5", - "@esbuild/linux-arm64": "0.21.5", - "@esbuild/linux-ia32": "0.21.5", - "@esbuild/linux-loong64": "0.21.5", - "@esbuild/linux-mips64el": "0.21.5", - "@esbuild/linux-ppc64": "0.21.5", - "@esbuild/linux-riscv64": "0.21.5", - "@esbuild/linux-s390x": "0.21.5", - "@esbuild/linux-x64": "0.21.5", - "@esbuild/netbsd-x64": "0.21.5", - "@esbuild/openbsd-x64": "0.21.5", - "@esbuild/sunos-x64": "0.21.5", - "@esbuild/win32-arm64": "0.21.5", - "@esbuild/win32-ia32": "0.21.5", - "@esbuild/win32-x64": "0.21.5" - } - }, "node_modules/vitest": { - "version": "2.1.9", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-2.1.9.tgz", - "integrity": "sha512-MSmPM9REYqDGBI8439mA4mWhV5sKmDlBKWIYbA3lRb2PTHACE0mgKwA8yQ2xq9vxDTuk4iPrECBAEW2aoFXY0Q==", + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.7.tgz", + "integrity": "sha512-KrxIJ62Fd89gfysR4WotlgZABiz2dqFPgqGzX7s+CwsqLFomRH7777ZcrOD6+WVAh7khPQP41A+BKbpcJFrdEg==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/expect": "2.1.9", - "@vitest/mocker": "2.1.9", - "@vitest/pretty-format": "^2.1.9", - "@vitest/runner": "2.1.9", - "@vitest/snapshot": "2.1.9", - "@vitest/spy": "2.1.9", - "@vitest/utils": "2.1.9", - "chai": "^5.1.2", - "debug": "^4.3.7", - "expect-type": "^1.1.0", - "magic-string": "^0.30.12", - "pathe": "^1.1.2", - "std-env": "^3.8.0", + "@types/chai": "^5.2.2", + "@vitest/expect": "3.2.7", + "@vitest/mocker": "3.2.7", + "@vitest/pretty-format": "^3.2.7", + "@vitest/runner": "3.2.7", + "@vitest/snapshot": "3.2.7", + "@vitest/spy": "3.2.7", + "@vitest/utils": "3.2.7", + "chai": "^5.2.0", + "debug": "^4.4.1", + "expect-type": "^1.2.1", + "magic-string": "^0.30.17", + "pathe": "^2.0.3", + "picomatch": "^4.0.2", + "std-env": "^3.9.0", "tinybench": "^2.9.0", - "tinyexec": "^0.3.1", - "tinypool": "^1.0.1", - "tinyrainbow": "^1.2.0", - "vite": "^5.0.0", - "vite-node": "2.1.9", + "tinyexec": "^0.3.2", + "tinyglobby": "^0.2.14", + "tinypool": "^1.1.1", + "tinyrainbow": "^2.0.0", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0", + "vite-node": "3.2.4", "why-is-node-running": "^2.3.0" }, "bin": { "vitest": "vitest.mjs" }, "engines": { - "node": "^18.0.0 || >=20.0.0" + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" }, "funding": { "url": "https://opencollective.com/vitest" }, "peerDependencies": { "@edge-runtime/vm": "*", - "@types/node": "^18.0.0 || >=20.0.0", - "@vitest/browser": "2.1.9", - "@vitest/ui": "2.1.9", + "@types/debug": "^4.1.12", + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "@vitest/browser": "3.2.7", + "@vitest/ui": "3.2.7", "happy-dom": "*", "jsdom": "*" }, @@ -3238,6 +2848,9 @@ "@edge-runtime/vm": { "optional": true }, + "@types/debug": { + "optional": true + }, "@types/node": { "optional": true }, @@ -3255,13 +2868,6 @@ } } }, - "node_modules/vitest/node_modules/pathe": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", - "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", - "dev": true, - "license": "MIT" - }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", diff --git a/package.json b/package.json index b53cc1d..3fd2341 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "arbella", - "version": "0.2.0", - "description": "Set up your AI coding tools once, then rebuild that setup on any machine. Claude Code, Codex, Cursor, opencode, GitHub Copilot CLI, Kilo Code & Antigravity — Linux, macOS, Windows.", + "version": "1.0.0", + "description": "Set up your AI coding tools once, then rebuild that setup on any machine — MCP servers included. Claude Code, Codex, Cursor, opencode, GitHub Copilot CLI, Kilo Code & Antigravity — Linux, macOS, Windows.", "type": "module", "license": "AGPL-3.0-only", "author": "Fafoooo", @@ -52,7 +52,7 @@ "commander": "^12.1.0", "execa": "^9.5.1", "picocolors": "^1.1.1", - "smol-toml": "^1.3.1", + "smol-toml": "^1.8.0", "zod": "^3.23.8" }, "devDependencies": { @@ -60,6 +60,6 @@ "tsup": "^8.3.5", "tsx": "^4.19.2", "typescript": "^5.6.3", - "vitest": "^2.1.5" + "vitest": "^3.2.7" } } diff --git a/src/adapters/antigravity/index.ts b/src/adapters/antigravity/index.ts index 1193c53..6b7f000 100644 --- a/src/adapters/antigravity/index.ts +++ b/src/adapters/antigravity/index.ts @@ -35,14 +35,16 @@ import type { OS, RestoreAction, SecretRef, - ToolManifest, } from "../../types.js"; import { denylistFor, matchesDeny } from "../../core/sanitizer/denylist.js"; +import { emptyManifest } from "../../core/manifest/index.js"; import { cliBinaryName, detectOS, installCommandFor } from "../../platform/os.js"; import { runInstall, which } from "../../platform/install.js"; import { normalizeCapturedSymlinkTarget } from "../../utils/symlink.js"; import { binaryScanViews, decodeForCapture } from "../../utils/capture-bytes.js"; +import { resolveContainedTarget } from "../../utils/safe-path.js"; +import type { ContainedTarget, ContainedTargetOptions } from "../../utils/safe-path.js"; import { GEMINI_FROZEN_PATHS, @@ -130,28 +132,29 @@ async function resolveRootDirs( }; } -/** Resolve an AntigravityTarget onto the target machine's resolved roots. */ -function targetAbsFor(dirs: RootDirs, target: AntigravityTarget): string { - return path.join(dirs[target.root], ...target.rel.split("/").filter(Boolean)); +/** + * Resolve an AntigravityTarget onto a CHECKED destination under its resolved + * root. + * + * The `rel` half is repo-supplied, so it goes through the shared containment + * gate rather than a bare `path.join`: no `..`/backslash/absolute segment may + * escape the root, and a symlinked component below it refuses the write instead + * of redirecting it. Callers decide what a refusal means — the planner drops the + * action, the writers warn — so plan and execution cannot disagree. + */ +async function targetAbsFor( + fs: FsService, + dirs: RootDirs, + target: AntigravityTarget, + opts: ContainedTargetOptions = {}, +): Promise { + return resolveContainedTarget(fs, dirs[target.root], target.rel, opts); } /* -------------------------------------------------------------------------- */ /* Small helpers */ /* -------------------------------------------------------------------------- */ -/** An empty manifest for antigravity (it tracks no reinstallable plugins here — - * the extension list is frozen as a file, not reinstalled from a manifest). */ -function emptyManifest(): ToolManifest { - return { - tool: "antigravity", - plugins: [], - marketplaces: [], - skills: [], - npmGlobals: [], - enabledPlugins: {}, - }; -} - /** Read mode bits best-effort so executable helper files keep their mode. */ async function readMode(abs: string): Promise { try { @@ -291,7 +294,7 @@ export async function capture(ctx: CaptureContext): Promise { const symlinks: CapturedSymlink[] = []; const secrets: SecretRef[] = []; const warnings: string[] = []; - const manifest = emptyManifest(); + const manifest = emptyManifest("antigravity"); const deny = denylistFor("antigravity"); const roots = await captureRoots(ctx); @@ -346,19 +349,27 @@ async function writeRestoredFile( ctx.log.debug(`antigravity: ignoring foreign repoPath ${file.repoPath}`); return; } - const dest = targetAbsFor(dirs, target); + const resolved = await targetAbsFor(ctx.fs, dirs, target); + if (!resolved.ok) { + ctx.log.warn(`antigravity: refusing to write ${file.repoPath} — ${resolved.reason}`); + return; + } + const dest = resolved.path; if (ctx.sourceOfTruth === "local" && (await ctx.fs.exists(dest))) { ctx.log.debug(`antigravity: keep local ${file.repoPath} (sourceOfTruth=local)`); return; } + // Rename-based writers: the containment check and the write cannot be one + // operation, and a plain write follows a link that appears at the leaf in the + // gap. `rename` replaces that entry instead. if (file.binary) { - await ctx.fs.writeBytes(dest, Buffer.from(file.content, "base64"), file.mode); + await ctx.fs.writeBytesAtomic(dest, Buffer.from(file.content, "base64"), file.mode); return; } const expanded = ctx.templater.fromTemplate(file.content, ctx.vars); - await ctx.fs.write(dest, expanded, file.mode); + await ctx.fs.writeAtomic(dest, expanded, file.mode); } /** Recreate one captured symlink under the right target root. */ @@ -372,7 +383,12 @@ async function writeRestoredSymlink( ctx.log.debug(`antigravity: ignoring foreign symlink ${link.repoPath}`); return; } - const dest = targetAbsFor(dirs, target); + const resolved = await targetAbsFor(ctx.fs, dirs, target, { allowLeafSymlink: true }); + if (!resolved.ok) { + ctx.log.warn(`antigravity: refusing to link ${link.repoPath} — ${resolved.reason}`); + return; + } + const dest = resolved.path; if (ctx.sourceOfTruth === "local" && (await ctx.fs.statKind(dest)) !== "missing") { ctx.log.debug(`antigravity: keep local symlink ${link.repoPath} (sourceOfTruth=local)`); @@ -392,7 +408,9 @@ export async function planActions(ctx: RestoreContext, data: RestoreData): Promi for (const file of data.files) { const target = targetFromRepoPath(file.repoPath); if (target === undefined) continue; - const targetPath = targetAbsFor(dirs, target); + const resolved = await targetAbsFor(ctx.fs, dirs, target); + if (!resolved.ok) continue; + const targetPath = resolved.path; const overwrites = await ctx.fs.exists(targetPath); if (ctx.sourceOfTruth === "local" && overwrites) continue; actions.push({ @@ -407,7 +425,9 @@ export async function planActions(ctx: RestoreContext, data: RestoreData): Promi for (const link of data.symlinks) { const target = targetFromRepoPath(link.repoPath); if (target === undefined) continue; - const targetPath = targetAbsFor(dirs, target); + const resolved = await targetAbsFor(ctx.fs, dirs, target, { allowLeafSymlink: true }); + if (!resolved.ok) continue; + const targetPath = resolved.path; const overwrites = (await ctx.fs.statKind(targetPath)) !== "missing"; if (ctx.sourceOfTruth === "local" && overwrites) continue; actions.push({ diff --git a/src/adapters/claude/capture.ts b/src/adapters/claude/capture.ts index 4003b94..73bd3c7 100644 --- a/src/adapters/claude/capture.ts +++ b/src/adapters/claude/capture.ts @@ -10,16 +10,26 @@ * paths to {{HOME}}/{{USER}}/... placeholders; emit a CapturedFile with * repoPath = "claude/files/". Preserve executable mode for hooks/ * statusline/commands scripts. - * - skills/ is MIXED: a relative symlink "-> ../../.agents/skills/" is a - * skills.sh install => emit a CapturedSymlink + a SkillEntry(source:"skills.sh", - * symlinked:true, installCommand:"npx skills add "). A real dir is - * hand-made => freeze its files + a SkillEntry(source:"frozen"). + * - Freeze local skills and the canonical contents of linked shared skills. + * Shared skills keep their portable symlinks; their contents live under + * shared/home/.agents/skills rather than an inferred install command. * - Parse plugins/installed_plugins.json + plugins/known_marketplaces.json + * settings.json.enabledPlugins into the manifest. * - Collect npm globals via the shared platform helper. * - Any denylisted-but-present secret file (.credentials.json, .claude.json) * is recorded as a SecretRef(kind:"file") so the user is reminded to - * re-supply it; its CONTENTS are never read or stored. + * re-supply it; its CONTENTS are never read or stored. The ONE exception is + * ~/.claude.json's `mcpServers` / `projects.*.mcpServers` sub-objects, which + * mcp.ts lifts into the manifest (sanitized + templated) — no other key of + * that file is ever read past the parse. + * - projects//memory/** is captured under its own `claude/memories/` + * root when ctx.includeMemories is on (see memories.ts). + * - Scripts the configs POINT AT but that live outside ~/.claude (hook + * dispatchers, statusline scripts, MCP launchers under ~/.agents, ~/.local/bin) + * are followed with core/homefiles and emitted as `shared/home/` files + * inside this same CaptureResult, together with the well-known plugin + * companion configs (~/.claude-mem/settings.json). The backup command merges + * every tool's shared/home files into one mirrored root. * * All fs/sanitizer/templater work goes through the injected CaptureContext, so * this module is unit-testable against a fixture dir. No clock, no direct @@ -42,30 +52,59 @@ import type { } from "../../types.js"; import { + COMMON_DENY, denylistFor, matchesDeny, } from "../../core/sanitizer/denylist.js"; +import type { CommandRef } from "../../core/homefiles/scan.js"; +import { collectCommandRefs } from "../../core/homefiles/scan.js"; +import { + captureHomeFile, + captureLinkedHomeFiles, + homeExcludeRoots, +} from "../../core/homefiles/capture.js"; +import { + capturedAbsolutePaths, + collectExternalTools, +} from "../../core/externaltools/collect.js"; import { normalizeCapturedSymlinkTarget } from "../../utils/symlink.js"; +import { captureSharedSkill } from "../../core/homefiles/skills.js"; import { listNpmGlobals } from "../../platform/install.js"; +import type { ClaudePaths } from "./paths.js"; import { REPO_PREFIX, FROZEN_PATHS, INSTRUCTIONS_FILE, paths } from "./paths.js"; import { parseInstalledPlugins, parseKnownMarketplaces, extractEnabledPlugins, } from "./plugins.js"; +import { captureMcpServers } from "./mcp.js"; +import { captureMemories, slugifyPath } from "./memories.js"; -/** Files known to hold whole-file secrets; recorded (never read) if present. */ -const SECRET_FILES: ReadonlyArray<{ rel: string; key: string; description: string }> = [ +/** + * Files known to hold whole-file secrets; recorded (never read) if present. + * `abs` resolves each one from the path set because they do not all live INSIDE + * the tool home — ~/.claude.json is a sibling of ~/.claude. + */ +const SECRET_FILES: ReadonlyArray<{ + rel: string; + key: string; + description: string; + abs: (p: ClaudePaths) => string; +}> = [ { rel: ".credentials.json", key: ".credentials.json", description: "Claude OAuth / API credentials — re-login after restore.", + abs: (p) => path.join(p.home, ".credentials.json"), }, { rel: ".claude.json", key: ".claude.json", - description: "Claude global state (auth tokens + project history) — excluded.", + description: + "Claude global state (auth + telemetry) — excluded; MCP server definitions " + + "are carried in manifest.mcpServers.", + abs: (p) => p.globalState, }, ]; @@ -121,15 +160,22 @@ async function walk( } // kind === "file" - await captureFile(ctx, home, abs, rel, files, warnings); + await captureFile(ctx, abs, repoPathFor(rel), rel, files, warnings); } -/** Read + sanitize + template a single file and push a CapturedFile. */ +/** + * Read + sanitize + template a single file and push a CapturedFile. + * + * `repoPath` is the destination inside the backup repo and `source` the label + * used for sanitizer SecretRefs and warnings. They are passed explicitly (rather + * than derived from a tool-home-relative path) because memories land under a + * different repo root than the frozen tree — see memories.ts. + */ async function captureFile( ctx: CaptureContext, - home: string, abs: string, - rel: string, + repoPath: string, + source: string, files: CapturedFile[], warnings: string[], ): Promise { @@ -147,7 +193,7 @@ async function captureFile( try { bytes = await ctx.fs.readBytes(abs); } catch (err) { - warnings.push(`claude: could not read ${rel}: ${(err as Error).message}`); + warnings.push(`claude: could not read ${source}: ${(err as Error).message}`); return; } @@ -161,13 +207,13 @@ async function captureFile( // a warning rather than base64'ing it in raw. if ( !ctx.includeSecrets && - binaryScanViews(bytes).some((view) => ctx.sanitizer.sanitizeText(view, "claude", rel).changed) + binaryScanViews(bytes).some((view) => ctx.sanitizer.sanitizeText(view, "claude", source).changed) ) { - warnings.push(`claude: skipped ${rel} — binary content with secret-shaped bytes`); + warnings.push(`claude: skipped ${source} — binary content with secret-shaped bytes`); return; } const file: CapturedFile = { - repoPath: repoPathFor(rel), + repoPath, content: bytes.toString("base64"), binary: true, }; @@ -194,11 +240,13 @@ async function captureFile( // UTF-16 files are decoded to text above, so a lone NUL byte never routes a // secret-bearing config around sanitizeFile. const raw = decoded.text; - const content = ctx.includeSecrets ? raw : ctx.sanitizer.sanitizeFile(raw, "claude", rel).content; + const content = ctx.includeSecrets + ? raw + : ctx.sanitizer.sanitizeFile(raw, "claude", source).content; const templated = ctx.templater.toTemplate(content, ctx.vars); const file: CapturedFile = { - repoPath: repoPathFor(rel), + repoPath, content: templated, }; if (mode !== undefined) file.mode = mode; @@ -206,8 +254,8 @@ async function captureFile( } /** - * Handle the skills/ directory specially: classify each entry as a skills.sh - * symlink (reinstallable) or a frozen hand-made dir. + * Freeze skills in place, or carry a shared skill's canonical contents alongside + * its portable symlink. */ async function captureSkills( ctx: CaptureContext, @@ -217,6 +265,7 @@ async function captureSkills( files: CapturedFile[], symlinks: CapturedSymlink[], skills: SkillEntry[], + secrets: SecretRef[], warnings: string[], ): Promise { if ((await ctx.fs.statKind(skillsDir)) !== "dir") return; @@ -234,16 +283,18 @@ async function captureSkills( const kind = await ctx.fs.statKind(abs); if (kind === "symlink") { - // skills.sh: ~/.claude/skills/ -> ../../.agents/skills/ - const target = normalizeCapturedSymlinkTarget(await ctx.fs.readLink(abs)); + const target = await captureSharedSkill( + ctx, "claude", name, abs, + normalizeCapturedSymlinkTarget(await ctx.fs.readLink(abs)), + { files, secrets, warnings }, + ); symlinks.push({ repoPath: repoPathFor(rel), target }); skills.push({ name, - source: "skills.sh", + source: "frozen", symlinked: true, - installCommand: `npx skills add ${name}`, }); - ctx.log.debug(`claude: skill (skills.sh) ${name} -> ${target}`); + ctx.log.debug(`claude: shared skill ${name} -> ${target}`); } else if (kind === "dir") { // Hand-made skill: freeze its files AND record it as frozen. await walk(ctx, home, abs, deny, files, symlinks, warnings); @@ -251,11 +302,56 @@ async function captureSkills( ctx.log.debug(`claude: skill (frozen) ${name}`); } else if (kind === "file") { // A loose file directly under skills/ (rare) — freeze it. - await captureFile(ctx, home, abs, rel, files, warnings); + await captureFile(ctx, abs, repoPathFor(rel), rel, files, warnings); } } } +/** + * Companion config dirs of Claude PLUGINS: reinstalled from the manifest, but + * their own settings live outside ~/.claude and would otherwise be lost. Paths + * are $HOME-relative POSIX and land under `shared/home/` like any linked script. + */ +const COMPANION_FILES: ReadonlyArray<{ rel: string; reason: string }> = [ + { rel: ".claude-mem/settings.json", reason: "companion:claude-mem" }, +]; + +/** + * Capture everything Claude needs that lives in $HOME but OUTSIDE ~/.claude: + * the hook/statusline/MCP scripts its configs point at, and the well-known + * plugin companion configs. + * + * The command refs come from the already-parsed settings files and from + * mcp.ts's raw pass over ~/.claude.json — no config file is read twice, and no + * key of ~/.claude.json beyond its MCP sub-objects is ever looked at. + * + * Returns the full ref list so the external-tool collector (WP-C) can classify + * the same commands without parsing anything again. + */ +async function captureHomeExtras( + ctx: CaptureContext, + settingsJson: unknown, + settingsLocalJson: unknown, + mcpRefs: readonly CommandRef[], + out: { files: CapturedFile[]; secrets: SecretRef[]; warnings: string[] }, +): Promise { + const opts = { excludeRoots: homeExcludeRoots(ctx.toolHome) }; + + const refs: CommandRef[] = [ + ...collectCommandRefs(settingsJson, "claude:settings.json"), + ...collectCommandRefs(settingsLocalJson, "claude:settings.local.json"), + ...mcpRefs, + ]; + await captureLinkedHomeFiles(ctx, refs, "claude", out, opts); + + for (const companion of COMPANION_FILES) { + const abs = path.join(ctx.vars.HOME, ...companion.rel.split("/")); + await captureHomeFile(ctx, abs, "claude", companion.reason, out, opts); + } + + return refs; +} + /** Read + parse a JSON file via the fs service; returns undefined if absent/bad. */ async function readJson( ctx: CaptureContext, @@ -302,16 +398,42 @@ export async function capture( continue; } if (top === "skills") { - await captureSkills(ctx, home, p.skillsDir, deny, files, symlinks, skills, warnings); + await captureSkills(ctx, home, p.skillsDir, deny, files, symlinks, skills, secrets, warnings); continue; } await walk(ctx, home, path.join(home, top), deny, files, symlinks, warnings); } + // ----- Per-project memories (R13, opt-in) ----- + // Stored under their OWN repo root (claude/memories/…), not claude/files/, + // because ~/.claude/projects is denylisted wholesale and the layout has to be + // portable across machines with different $HOME values. + if (ctx.includeMemories) { + await captureMemories( + ctx, + p.projectsDir, + slugifyPath(ctx.vars.HOME), + // COMMON_DENY, not the Claude list: memory paths are relative to a + // project's memory/ dir, so the root-anchored Claude patterns ("/feedback/", + // "/plugins/") would match a memory SUBDIRECTORY of the same name and drop + // it. Only the universal cruft rules make sense at this root. + [...COMMON_DENY], + (abs, repoPath, source) => captureFile(ctx, abs, repoPath, source, files, warnings), + ); + } else { + ctx.log.debug("claude: skipping projects/*/memory (includeMemories is off)"); + } + // ----- Manifest: plugins, marketplaces, enabledPlugins ----- const installedJson = await readJson(ctx, p.installedPlugins, warnings, "installed_plugins.json"); const marketplacesJson = await readJson(ctx, p.knownMarketplaces, warnings, "known_marketplaces.json"); const settingsJson = await readJson(ctx, p.settings, warnings, "settings.json"); + const settingsLocalJson = await readJson( + ctx, + p.settingsLocal, + warnings, + "settings.local.json", + ); // Fold project-scope plugin paths (projectPath) to {{HOME}}-style placeholders, // exactly like file contents — otherwise the raw machine path leaks into the repo. @@ -338,8 +460,7 @@ export async function capture( // ----- Secret files: record presence, never read contents ----- for (const sf of SECRET_FILES) { - const abs = path.join(home, sf.rel); - if ((await ctx.fs.exists(abs))) { + if (await ctx.fs.exists(sf.abs(p))) { secrets.push({ tool: "claude", source: sf.rel, @@ -350,6 +471,34 @@ export async function capture( } } + // ----- MCP servers lifted out of ~/.claude.json (see mcp.ts) ----- + const mcp = await captureMcpServers(ctx); + secrets.push(...mcp.secrets); + warnings.push(...mcp.warnings); + + // ----- Linked scripts + companion configs in $HOME (WP-B) ----- + const commandRefs = await captureHomeExtras( + ctx, + settingsJson, + settingsLocalJson, + mcp.commandRefs, + { files, secrets, warnings }, + ); + + // ----- External tools behind those same commands (WP-C) ----- + // Runs on the FULL file list so a command pointing at something this capture + // already carries (a statusline script under ~/.claude, a hook dispatcher in + // shared/home) is restored as a file rather than reported as a missing package. + const externalTools = await collectExternalTools(commandRefs, { + home: ctx.vars.HOME, + capturedPaths: capturedAbsolutePaths(files, { + home: ctx.vars.HOME, + toolHome: home, + filesPrefix: REPO_PREFIX, + }), + toTemplate: (value) => ctx.templater.toTemplate(value, ctx.vars), + }); + const manifest: ToolManifest = { tool: "claude", plugins, @@ -357,12 +506,17 @@ export async function capture( skills, npmGlobals, enabledPlugins, + mcpServers: mcp.mcpServers, + projectMcpServers: mcp.projectMcpServers, + externalTools, }; ctx.log.debug( `claude: captured ${files.length} files, ${symlinks.length} symlinks, ` + `${plugins.length} plugins, ${marketplaces.length} marketplaces, ` + - `${skills.length} skills, ${npmGlobals.length} npm globals`, + `${skills.length} skills, ${npmGlobals.length} npm globals, ` + + `${Object.keys(mcp.mcpServers).length} MCP servers, ` + + `${externalTools.length} external tools`, ); return { tool: "claude", files, symlinks, manifest, secrets, warnings }; diff --git a/src/adapters/claude/mcp.ts b/src/adapters/claude/mcp.ts new file mode 100644 index 0000000..6ef21df --- /dev/null +++ b/src/adapters/claude/mcp.ts @@ -0,0 +1,709 @@ +/** + * MCP server definitions from ~/.claude.json — the one thing arbella lifts OUT of + * Claude's global-state file. + * + * ~/.claude.json is denylisted wholesale and always will be: it holds the OAuth + * account, telemetry counters and the full project history. But it is ALSO the + * only place user-scope MCP servers live, and losing them is losing the setup. + * So this module reads exactly two sub-objects out of it — + * + * mcpServers -> manifest.mcpServers + * projects..mcpServers -> manifest.projectMcpServers + * + * — and NOTHING else. No other key is copied, stored, logged, or even retained + * past the parse: the parsed object is read for those two keys and dropped. + * + * Both directions go through the injected services: + * capture -> sanitizer.sanitizeJson (every leaf under env/environment/headers + * becomes {{REDACTED}}, unless includeSecrets) then templater + * .toTemplate over every string leaf, so an absolute command path + * folds to {{HOME}}/.... + * restore -> templater.fromTemplate over the serialized def, then a KEY-WISE + * merge into the target's ~/.claude.json. Keys arbella does not own + * are never touched, and a file that is not valid JSON is never + * overwritten. + * + * The restore side has exactly ONE decision function, `planMcpMerge`: it reads + * the target's current ~/.claude.json and reports which servers WOULD be written + * (plus the env keys those servers still need). `--dry-run` prints its actions, + * the post-restore reminder reads its `needsEnv`, and `restoreMcpServers` applies + * its decisions verbatim — so the three can never disagree about what a pull does. + * + * No direct node:fs, no clock. templateDeep/hydrateDef and the merge appliers are + * pure. + */ + +import path from "node:path"; + +import type { CaptureContext, RestoreContext } from "../adapter.interface.js"; +import type { RestoreAction, SecretRef, ToolManifest } from "../../types.js"; + +import { REDACTED } from "../../core/sanitizer/patterns.js"; +import { isPlainObject, isUnsafeObjectKey } from "../../utils/object.js"; +import type { CommandRef } from "../../core/homefiles/scan.js"; +import { collectCommandRefs } from "../../core/homefiles/scan.js"; + +import { paths } from "./paths.js"; + +/** What a capture pass lifted out of ~/.claude.json. */ +export interface CapturedMcp { + /** name -> server definition (sanitized + templated), name-sorted. */ + mcpServers: ToolManifest["mcpServers"]; + /** Project-scope servers, sorted by (templated) project path. */ + projectMcpServers: ToolManifest["projectMcpServers"]; + /** SecretRefs for every value the sanitizer redacted. Metadata only. */ + secrets: SecretRef[]; + /** Non-fatal problems (unreadable / unparseable global state). */ + warnings: string[]; + /** + * Command references taken from the RAW (pre-sanitize, pre-template) server + * definitions, so `shared/home` capture can follow them to the launcher + * scripts they point at (`~/.local/bin/serena-mcp-start`). Built here because + * ~/.claude.json is parsed exactly once, and the templated defs in + * `mcpServers` above no longer contain machine paths to resolve. + */ + commandRefs: CommandRef[]; +} + +/** An empty result — used for "file absent" and "file unparseable" alike. */ +function emptyMcp(warnings: string[] = []): CapturedMcp { + return { + mcpServers: {}, + projectMcpServers: [], + secrets: [], + warnings, + commandRefs: [], + }; +} + +/* -------------------------------------------------------------------------- */ +/* Pure helpers */ +/* -------------------------------------------------------------------------- */ + +/** + * Map every STRING leaf of a JSON-ish value through `fn`, returning a new value. + * Objects and arrays are cloned; non-string scalars pass through untouched. + * Pure — the input is never mutated. + */ +export function templateDeep(value: T, fn: (s: string) => string): T { + if (typeof value === "string") return fn(value) as unknown as T; + if (Array.isArray(value)) { + return value.map((el) => templateDeep(el, fn)) as unknown as T; + } + if (isPlainObject(value)) { + const out: Record = {}; + for (const [k, v] of Object.entries(value)) { + // `out["__proto__"] = …` would rewrite this object's prototype instead of + // adding a field. A server definition read out of ~/.claude.json (or a + // backup repo) is parsed JSON, which CAN carry such a key. + if (isUnsafeObjectKey(k)) continue; + out[k] = templateDeep(v, fn); + } + return out as unknown as T; + } + return value; +} + +/** Longest launch summary an action description carries before it is elided. */ +export const MAX_LAUNCH_SUMMARY = 120; + +/** + * One line describing WHAT a server definition will start on this machine: + * ` ` for a spawned server, `` for an http/sse one. + * + * The dry run exists so a user can see what a pull will do BEFORE it does it, + * and "Register MCP server serena" hides the only part that matters — the + * command a backup repo just talked this machine into launching. Returns "" when + * the definition names neither (nothing honest to show), so the caller can fall + * back to the bare label. Truncated to {@link MAX_LAUNCH_SUMMARY} characters: + * this is a plan line, not a config dump. Pure. + */ +export function describeServerLaunch(def: Record): string { + const url = typeof def.url === "string" ? def.url.trim() : ""; + const transport = typeof def.type === "string" ? def.type.toLowerCase() : ""; + const remote = transport === "http" || transport === "sse" || transport === "streamable-http"; + if (url !== "" && (remote || typeof def.command !== "string")) return truncate(url); + + const command = typeof def.command === "string" ? def.command.trim() : ""; + if (command === "") return url === "" ? "" : truncate(url); + + const args = Array.isArray(def.args) + ? def.args.filter((a): a is string => typeof a === "string") + : []; + return truncate([command, ...args].join(" ").trim()); +} + +/** Cut `value` to {@link MAX_LAUNCH_SUMMARY} characters, marking the elision. */ +function truncate(value: string): string { + const collapsed = value.replace(/\s+/g, " ").trim(); + return collapsed.length <= MAX_LAUNCH_SUMMARY + ? collapsed + : `${collapsed.slice(0, MAX_LAUNCH_SUMMARY - 1)}…`; +} + +/** `