diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..e9cb1d4 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,38 @@ +name: CI + +on: + push: + pull_request: + +jobs: + validate: + runs-on: windows-latest + steps: + - name: Check out plugin + uses: actions/checkout@v4 + with: + path: godmode-plugin-git + - name: Check out current GodMode API + uses: actions/checkout@v4 + with: + repository: ReBoticsAI/GodMode + ref: 732d993808d641645ed706d8560719cba5349d6f + path: GodMode + - name: Set up Node + uses: actions/setup-node@v4 + with: + node-version: 22 + cache: npm + cache-dependency-path: godmode-plugin-git/package-lock.json + - name: Install GodMode workspace + run: npm ci + working-directory: GodMode + - name: Build current plugin API + run: npm run build -w @godmode/kernel && npm run build -w @godmode/plugin-api + working-directory: GodMode + - name: Install + run: npm ci + working-directory: godmode-plugin-git + - name: Validate + run: npm run validate + working-directory: godmode-plugin-git diff --git a/README.md b/README.md index 2886e47..f46ed0e 100644 --- a/README.md +++ b/README.md @@ -5,29 +5,67 @@ Official marketplace plugin: structured **git** tools for Intelligence over the ## Requirements - `git` on the host PATH +- GodMode kernel client API version 1. The plugin rejects a mismatched host before + registering its ObjectType or tools. - Working directory must be a git checkout (local GodMode / operator workspace). Hub tenant sandboxes only work if they are real clones with remotes. +## Kernel model + +The plugin registers an adapter-backed `GitRepository` ObjectType with one +tenant-local record: + +- ObjectType: `GitRepository` +- record ID: `coding-root` +- operations: `list`, `get` +- record actions: `branch`, `add`, `commit`, `push`, `fetch`, `pull` + +The generic HTTP form of those actions is: + +`POST /api/records/GitRepository/coding-root/actions/:action` + +The JSON request body is the action input itself. HTTP clients send +`X-Kernel-Confirmation` when satisfying a confirmation challenge. The plugin's +compatibility tools do not make that HTTP request: they call the versioned +in-process `api.kernel.runAction(...)` client against the same record action. + ## Tools | Tool | Mode | Purpose | |------|------|---------| | `git_status` | auto | Branch + porcelain status | -| `git_diff` | auto | Staged / unstaged / range (size-capped) | -| `git_log` | auto | Recent commits | -| `git_branches` | auto | List branches | -| `git_checkout` | confirm | Create (`create=true`) and/or checkout a branch | +| `git_diff` | auto | Unstaged, staged, triple-dot revision-range, or path-limited diff | +| `git_log` | auto | Up to 50 recent commits | +| `git_branches` | auto | List local branches and upstream details | +| `git_branch` | confirm | Check out a branch, creating it when `create=true` | | `git_add` | confirm | Stage pathspecs | -| `git_commit` | confirm | Commit staged changes (no force amend in v1) | -| `git_push` | confirm | Push current branch (no `--force`) | -| `git_fetch` | auto | Fetch remotes | -| `git_pull` | confirm | Pull with ff-only by default | +| `git_commit` | confirm | Commit staged changes; hooks run unless `skipHooks=true` | +| `git_push` | confirm | Push `HEAD` to the same branch name; set upstream by default | +| `git_fetch` | confirm | Fetch the Git-configured default or one optional remote | +| `git_pull` | confirm | Pull the configured upstream or one optional remote; ff-only by default | ## Auth / safety -- Mutating tools use confirm mode. +- The four read tools execute bounded Git reads directly. The six confirm-mode + tools are semantic wrappers over the corresponding `GitRepository` record + actions. +- Every action requires kernel confirmation. `branch`, `add`, and `commit` have + `write` effect; `push`, `fetch`, and `pull` have `external` effect. +- A confirmed compatibility tool first calls `api.kernel.runAction(...)`. If the + kernel returns `KERNEL_CONFIRMATION_REQUIRED`, the wrapper retries once with + the returned confirmation grant because the host tool gate was already + approved. +- Every `cwd` is contained by the active tenant coding root. - Force push and hard reset are **rejected** in this plugin. +- Git is spawned directly without a Windows command shell. - Does not store credentials; uses the host git config / credential helper. +## Verification + +`npm run validate` runs TypeScript checking, the Vitest contract/security/tenant +suite, and the production bundle. The contract tests validate the ObjectType +definition, action policy, API-version fail-fast behavior, semantic tool +delegation, and confirmation-grant retry. + ## Install Marketplace → Official → **Git**, or Unofficial with this repo URL. GodMode builds `dist/` on activate if needed. diff --git a/data/ai/skills/git-workflow/SKILL.md b/data/ai/skills/git-workflow/SKILL.md index c5ccf58..c529b9f 100644 --- a/data/ai/skills/git-workflow/SKILL.md +++ b/data/ai/skills/git-workflow/SKILL.md @@ -1,12 +1,28 @@ --- name: git-workflow -description: Local git workflow for Intelligence — status, diff, stage, commit, push via plugin tools -tools: ["git_status", "git_diff", "git_log", "git_branches", "git_checkout", "git_add", "git_commit", "git_push", "git_fetch", "git_pull"] +description: Local Git workflow for Intelligence using bounded read tools and confirmed GitRepository record actions +tools: ["git_status", "git_diff", "git_log", "git_branches", "git_branch", "git_add", "git_commit", "git_push", "git_fetch", "git_pull"] --- -1. `git_status` — note branch and dirty files. -2. `git_diff` (and staged diff if needed) — summarize what will ship; exclude secret files. -3. `git_checkout` with `create=true` when you need a feature branch (avoid committing straight to main unless asked). -4. `git_add` with explicit pathspecs for the change set. -5. `git_commit` with a concise message focused on why. -6. `git_push` to origin (confirm). Then use **godmode-plugin-github** (`gh_pr_create`) when a pull request is needed. +1. Start with the direct read tools: use `git_status`, then `git_diff`; use + `staged=true` to review the index or `base` (and optional `head`) for a + triple-dot range. Use `git_log` and `git_branches` when history or upstream + state matters. +2. Use `git_branch` with `create=true` when a feature branch is needed. The same + tool checks out an existing branch with `create=false`; there is no + `git_checkout` tool. +3. Use `git_add` with explicit pathspecs. Re-run `git_diff` with `staged=true`, + summarize exactly what will ship, and exclude secret files. +4. Use `git_commit` with a concise message focused on why. Hooks run by default; + set `skipHooks=true` only when the user explicitly requests it. +5. Use `git_fetch` or `git_pull` only when remote synchronization is needed. + `git_pull` is fast-forward-only by default. +6. Use `git_push` to push `HEAD` to the same branch name on `origin` and set its + upstream by default. Then use **godmode-plugin-github** (`gh_pr_create`) when + a pull request is needed. + +`git_branch`, `git_add`, `git_commit`, `git_push`, `git_fetch`, and `git_pull` +are confirm-mode compatibility tools over record actions on +`GitRepository/coding-root`. They use the kernel client and its confirmation +grant flow; do not call a legacy plugin route or construct an HTTP action +request manually. diff --git a/godmode.plugin.json b/godmode.plugin.json index cb4e43e..17b89cc 100644 --- a/godmode.plugin.json +++ b/godmode.plugin.json @@ -3,5 +3,6 @@ "version": "0.1.0", "name": "Git", "engine": "^0.1.0", + "kernelApiVersion": 1, "bridge": { "entry": "dist/bridge.js" } } diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..705643f --- /dev/null +++ b/package-lock.json @@ -0,0 +1,2136 @@ +{ + "name": "@godmode-plugin/git", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@godmode-plugin/git", + "version": "0.1.0", + "devDependencies": { + "@godmode/kernel": "file:../GodMode/packages/kernel", + "@godmode/plugin-api": "file:../GodMode/packages/plugin-api", + "@types/node": "^24.0.0", + "esbuild": "^0.25.0", + "typescript": "^5.8.0", + "vitest": "^3.2.0" + } + }, + "../GodMode/packages/kernel": { + "name": "@godmode/kernel", + "version": "0.1.0", + "dev": true, + "devDependencies": { + "@types/node": "^22.10.5", + "typescript": "^5.7.3" + } + }, + "../GodMode/packages/plugin-api": { + "name": "@godmode/plugin-api", + "version": "0.1.0", + "dev": true, + "dependencies": { + "@godmode/kernel": "*", + "express": "^4.21.2" + }, + "devDependencies": { + "@types/express": "^5.0.0", + "@types/react": "^19.0.2", + "typescript": "^5.7.3" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@godmode/kernel": { + "resolved": "../GodMode/packages/kernel", + "link": true + }, + "node_modules/@godmode/plugin-api": { + "resolved": "../GodMode/packages/plugin-api", + "link": true + }, + "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==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.2.tgz", + "integrity": "sha512-6o7ZLZK+BeenkZCFNDXqpbjw9bD6nuWonvS/lwQJp7NoVVxm6p3qE7qQ5jGuBjiFsgvqjD8mZAU5oWxTmbOeOg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.62.2.tgz", + "integrity": "sha512-BaH7BllCACHoH1LguOU56UItGfUWjujlO65kS9LAodViaN4bwIKd7oeW/ZHJ/4ljr/7MIiENnNy3HJ0zXv8Zkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.62.2.tgz", + "integrity": "sha512-v39RCCvj4He82I9sFmk+M1VZ0PLM9sfsLVikjfx2hYBNALhrrOR2D3JjQA6AhlaSOgcR+RzrKY7e1+bT6SUO/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.62.2.tgz", + "integrity": "sha512-yl0y2vq3S3lHeuXhEdss6TWfKW8vkujImO12tn4ZkG/4oghr09LvdYm2RElVjokTQiUvDUGXLGsYeLqUMCKpGA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.62.2.tgz", + "integrity": "sha512-tT4pvt4qXD+vEoezupCWi+a1F0vvDiksiHc+PxRlYTOH1I6/X4id9jPxTP+Fg+545euaFT1jJVs4CEdHZAU1vw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.62.2.tgz", + "integrity": "sha512-6nU5F2wCW+qvCBhTn1pdIU3bzsIoF7EUwsCDRxilWGprQR6yd508YnH9+OKFCwpfS8pjZqDUmnCAr7exax0XCg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.62.2.tgz", + "integrity": "sha512-n1GJHPOvpIfhi3TmrCeh6S6URt9BFCt0KQE3qvexyGCTAKpR4Lg+eWvNZEqu7epxwus/8ElT3hacYEucm49SZg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.62.2.tgz", + "integrity": "sha512-JqgflS8wEB+UXV/vS1RpRbifGBeN4D5lz8D8oOFbFZw4vedvdOgCFAjfBmIMdW3yL10XpQQ0Ambepw6MXrhOnA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.62.2.tgz", + "integrity": "sha512-wnFJkogWvN4jm/hQRF2UBaeUmk20j5+DmHvoyWii2b8HJDyvz1MF2OU/6ynXt2KR63rbZLWkFpoytpdc/yBuSA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.62.2.tgz", + "integrity": "sha512-HVu2bp0zhvJ8xHEV9+UUs7S90VadmBSY3LcIMvozbPo4AuMGDWlz3ymHLHZPX4hR67TKTt8Qp5PJ5RBg/i+RMQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.62.2.tgz", + "integrity": "sha512-mQqqAV8QaoSgr9I2fKDLY2BAVvmKjWoGiu/cSYQonsLvtqwEn1E4QYfnCOcp5zoEqNhsDYin1s6jx/VJmrxlZg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.62.2.tgz", + "integrity": "sha512-IxKLoxCQ2IWi6bT2akyDUBGsOImDKB+sPp4EsTmwFQ/fMwpCKm8uLSSgP/Kx/QYUgKis6SEZ5/Nlhup0DIA0PQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.62.2.tgz", + "integrity": "sha512-Mk5ha2RQSgyFfmYYLkBpPnUk8D8FriBxesO1u9O75X0mHgXL1UQcH5Itl2lurWL2tj0RxV9b9tJgipac0hRY9A==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.62.2.tgz", + "integrity": "sha512-CjvEnqJL/0/TQ3TXX3OPIJ/kmBellrWd4heXUmHeJlTnmwjKpSJzoehLaL6Xk0ZnMHBu9dZuFADNOrtjF4v+2w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.62.2.tgz", + "integrity": "sha512-1SiZbzwdkaDURsew/tSOrooKiYy7EQGT6m8ufavAi9NEyQb/6VuIxFXAL1fqa4iZe3g4NbNk4P7J32z2tw5Mgg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.62.2.tgz", + "integrity": "sha512-nQts12zJ3NQRoE6uYljOH89v7szzLDvG2JD/vsX+vGXU8w/At1GowTZ5/7qeFQ8m7L55rpR8Okugnuo5bgjy2Q==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.62.2.tgz", + "integrity": "sha512-E9/ll019jhPIJgpzfZoIkBGhcz+kKNgVWYRY0zr9srBdPPFVpvOKW8VaJKUbeK+eZXyQF9ltME+Kk6affeaPgg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.62.2.tgz", + "integrity": "sha512-5BqxR/pshjey51iliyzTD5Xi3EN0aLmQ2lZ3lvefVV9c82BvrLo2/6OT55iifpWBufs6kdwWbuOKS841DrmK9A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.62.2.tgz", + "integrity": "sha512-uNN83XxQrRAh/w0/pmAfibcwyb6YWt4gP+dpnQKPVJshAloQ785ii8CT8ZCIxkGg9opVsvAlGhFitSm6D1Jjpg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.62.2.tgz", + "integrity": "sha512-srjEIxSH3LRnJN6THczDHWQplqEMFiAJrTab0msUryh9kwNpkICf3Ea6q6MN/2cZwRFUNx5w+h6Hpi4QuHS6Zg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.62.2.tgz", + "integrity": "sha512-8hOJnxgbyObnCm5AlRA3A931xX19xq80RjVTKgJOvEKWqJruP/Uf12IbAOaDjjEXYRewwHLfmF0YRIdK3OwKWA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.62.2.tgz", + "integrity": "sha512-mmF4AY1i0hG/bLWUctUq59gtmgaSIRa3cu/A3JFRp/sCNEme2bgDEiDS22P9FbnJB8NJNF4jPJiSP5RHQpUTDg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.62.2.tgz", + "integrity": "sha512-DZgkknc6jhHrk46V25vbAM0zZkyP0nSDkJB8/dRkLTxv470dOmWDqGoEJl/9A0dFfS7yE3REOwNDxpHwSLSt0Q==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.62.2.tgz", + "integrity": "sha512-T6xr6ucWSFto+VGajA8YH26LdpHRuP4YLHEKAtCWvJDOlnmWcDZVCI2Jmjr+IFHDlt2zRaTAKE4tfjTaWLgJBg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.62.2.tgz", + "integrity": "sha512-BfzEnDJOt9T8M989/lA37EcJgat01wLRnoi5dQf3QzOH7jzpqTAzdDbVfRljVr5r+jzKqpbHeyOfAaXxAd0PAA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "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.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "24.13.3", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.13.3.tgz", + "integrity": "sha512-Dh8vAsV36ig5wa9OX4pXvMc9D3Veibfw2wix0CUwYODLD8nkj9UsLjASr49nPg+2eKzxhBV+v7L8pXvT4e639Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.18.0" + } + }, + "node_modules/@vitest/expect": { + "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": { + "@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": "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": "3.2.7", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.17" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "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": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "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": "3.2.7", + "pathe": "^2.0.3", + "strip-literal": "^3.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "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": "3.2.7", + "magic-string": "^0.30.17", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "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": "^4.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "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": "3.2.7", + "loupe": "^3.1.4", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/chai": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", + "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/check-error": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz", + "integrity": "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-eql": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "dev": true, + "license": "MIT" + }, + "node_modules/esbuild": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/expect-type": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz", + "integrity": "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "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/loupe": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", + "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.16", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.16.tgz", + "integrity": "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/pathval": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz", + "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.16" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.19", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.19.tgz", + "integrity": "sha512-Mz8SaolMd8nB+G13WkORcxQKHZ/NE4xXevtkJHVuG+guo9/wYKlIMTKAqGdEmYOXR2ijPjTYNHssizdaVSUNdQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.12", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/rollup": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.2.tgz", + "integrity": "sha512-RFnrW4lhXA3s3eqHDZvN654g8OTjzRfqpIRJYczCGB6HzphckVAi/Qh4tbPUbRuDi7s1Llv8g/NspLkttY3gTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.9" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.62.2", + "@rollup/rollup-android-arm64": "4.62.2", + "@rollup/rollup-darwin-arm64": "4.62.2", + "@rollup/rollup-darwin-x64": "4.62.2", + "@rollup/rollup-freebsd-arm64": "4.62.2", + "@rollup/rollup-freebsd-x64": "4.62.2", + "@rollup/rollup-linux-arm-gnueabihf": "4.62.2", + "@rollup/rollup-linux-arm-musleabihf": "4.62.2", + "@rollup/rollup-linux-arm64-gnu": "4.62.2", + "@rollup/rollup-linux-arm64-musl": "4.62.2", + "@rollup/rollup-linux-loong64-gnu": "4.62.2", + "@rollup/rollup-linux-loong64-musl": "4.62.2", + "@rollup/rollup-linux-ppc64-gnu": "4.62.2", + "@rollup/rollup-linux-ppc64-musl": "4.62.2", + "@rollup/rollup-linux-riscv64-gnu": "4.62.2", + "@rollup/rollup-linux-riscv64-musl": "4.62.2", + "@rollup/rollup-linux-s390x-gnu": "4.62.2", + "@rollup/rollup-linux-x64-gnu": "4.62.2", + "@rollup/rollup-linux-x64-musl": "4.62.2", + "@rollup/rollup-openbsd-x64": "4.62.2", + "@rollup/rollup-openharmony-arm64": "4.62.2", + "@rollup/rollup-win32-arm64-msvc": "4.62.2", + "@rollup/rollup-win32-ia32-msvc": "4.62.2", + "@rollup/rollup-win32-x64-gnu": "4.62.2", + "@rollup/rollup-win32-x64-msvc": "4.62.2", + "fsevents": "~2.3.2" + } + }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "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==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/std-env": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "dev": true, + "license": "MIT" + }, + "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/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyglobby": { + "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": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinypool": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", + "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + } + }, + "node_modules/tinyrainbow": { + "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": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-4.0.4.tgz", + "integrity": "sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", + "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", + "dev": true, + "license": "MIT" + }, + "node_modules/vite": { + "version": "7.3.6", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.6.tgz", + "integrity": "sha512-4XP60spRGjSZFf1qYH+dJIkK2znL3zQfl9KkOV9MkkRR/3Dls0dxaBsQPTloEc5BLXWPL9vsOxopxyKoMmDueg==", + "dev": true, + "license": "MIT", + "dependencies": { + "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": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.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 + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vite-node": { + "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.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 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vite/node_modules/@esbuild/aix-ppc64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.1.tgz", + "integrity": "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/android-arm": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.1.tgz", + "integrity": "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/android-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.1.tgz", + "integrity": "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/android-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.1.tgz", + "integrity": "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/darwin-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.1.tgz", + "integrity": "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/darwin-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.1.tgz", + "integrity": "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/freebsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.1.tgz", + "integrity": "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/freebsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.1.tgz", + "integrity": "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-arm": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.1.tgz", + "integrity": "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.1.tgz", + "integrity": "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-ia32": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.1.tgz", + "integrity": "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-loong64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.1.tgz", + "integrity": "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-mips64el": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.1.tgz", + "integrity": "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-ppc64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.1.tgz", + "integrity": "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-riscv64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.1.tgz", + "integrity": "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-s390x": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.1.tgz", + "integrity": "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.1.tgz", + "integrity": "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/netbsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.1.tgz", + "integrity": "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/netbsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.1.tgz", + "integrity": "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/openbsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.1.tgz", + "integrity": "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/openbsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.1.tgz", + "integrity": "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/openharmony-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.1.tgz", + "integrity": "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/sunos-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.1.tgz", + "integrity": "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.1.tgz", + "integrity": "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-ia32": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.1.tgz", + "integrity": "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.1.tgz", + "integrity": "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/esbuild": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.1.tgz", + "integrity": "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.28.1", + "@esbuild/android-arm": "0.28.1", + "@esbuild/android-arm64": "0.28.1", + "@esbuild/android-x64": "0.28.1", + "@esbuild/darwin-arm64": "0.28.1", + "@esbuild/darwin-x64": "0.28.1", + "@esbuild/freebsd-arm64": "0.28.1", + "@esbuild/freebsd-x64": "0.28.1", + "@esbuild/linux-arm": "0.28.1", + "@esbuild/linux-arm64": "0.28.1", + "@esbuild/linux-ia32": "0.28.1", + "@esbuild/linux-loong64": "0.28.1", + "@esbuild/linux-mips64el": "0.28.1", + "@esbuild/linux-ppc64": "0.28.1", + "@esbuild/linux-riscv64": "0.28.1", + "@esbuild/linux-s390x": "0.28.1", + "@esbuild/linux-x64": "0.28.1", + "@esbuild/netbsd-arm64": "0.28.1", + "@esbuild/netbsd-x64": "0.28.1", + "@esbuild/openbsd-arm64": "0.28.1", + "@esbuild/openbsd-x64": "0.28.1", + "@esbuild/openharmony-arm64": "0.28.1", + "@esbuild/sunos-x64": "0.28.1", + "@esbuild/win32-arm64": "0.28.1", + "@esbuild/win32-ia32": "0.28.1", + "@esbuild/win32-x64": "0.28.1" + } + }, + "node_modules/vitest": { + "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": { + "@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.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 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@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": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/debug": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + } + } +} diff --git a/package.json b/package.json index 04d8bdd..56123e2 100644 --- a/package.json +++ b/package.json @@ -5,8 +5,18 @@ "type": "module", "description": "Structured git tools for GodMode Intelligence (status, diff, commit, push).", "scripts": { - "build": "echo Use GodMode build_plugin (Bridge esbuild) or npx esbuild locally" + "build": "esbuild src/bridge.ts --bundle --platform=node --format=esm --target=node20 --outfile=dist/bridge.js --external:@godmode/plugin-api --external:@godmode/kernel", + "typecheck": "tsc --noEmit", + "test": "vitest run --no-file-parallelism", + "validate": "npm run typecheck && npm test && npm run build" }, "dependencies": {}, - "devDependencies": {} + "devDependencies": { + "@godmode/kernel": "file:../GodMode/packages/kernel", + "@godmode/plugin-api": "file:../GodMode/packages/plugin-api", + "@types/node": "^24.0.0", + "esbuild": "^0.25.0", + "typescript": "^5.8.0", + "vitest": "^3.2.0" + } } diff --git a/src/bridge.ts b/src/bridge.ts index 458fd08..95dc80e 100644 --- a/src/bridge.ts +++ b/src/bridge.ts @@ -1,44 +1,191 @@ -import type { GodModePluginRegister } from "@godmode/plugin-api"; +import type { + GodModePluginRegister, + PluginKernelClient, + PluginRecordContext, + PluginToolHandler, +} from "@godmode/plugin-api"; +import { KERNEL_CLIENT_API_VERSION } from "@godmode/plugin-api"; import { - rejectDestructiveGitArgs, + parseBoolean, + parseLimit, + parsePath, + parseRevision, resolveWorkingRoot, runGit, } from "./git-util.js"; +import { + GIT_REPOSITORY_DEFINITION, + GIT_REPOSITORY_ID, + GIT_REPOSITORY_TYPE, + gitRepositoryAdapter, +} from "./kernel.js"; + +const cwd = { + cwd: { + type: "string", + minLength: 1, + maxLength: 4_096, + description: "Directory within the tenant coding root", + }, +}; -function str(v: unknown): string { - return typeof v === "string" ? v : ""; +function recordContext( + ctx: Parameters[1], + confirmationId?: string +): PluginRecordContext { + return { + tenantId: ctx.tenantId, + userId: ctx.userId, + activeAgentId: ctx.activeAgentId ?? "godmode-plugin-git", + activeSubtaskCardId: ctx.activeSubtaskCardId, + activeTaskCardId: ctx.activeTaskCardId, + role: "intelligence", + source: "agent", + confirmationId, + }; +} + +function confirmationId(error: unknown): string | undefined { + if (!error || typeof error !== "object") return undefined; + const value = error as { + status?: number; + code?: string; + details?: { confirmationId?: unknown }; + }; + if ( + value.status !== 428 && + value.code !== "KERNEL_CONFIRMATION_REQUIRED" + ) { + return undefined; + } + return typeof value.details?.confirmationId === "string" + ? value.details.confirmationId + : undefined; } -function strList(v: unknown): string[] { - if (Array.isArray(v)) return v.map(String).filter(Boolean); - if (typeof v === "string" && v.trim()) { - return v.split(/\s+/).map((s) => s.trim()).filter(Boolean); +async function delegate( + kernel: PluginKernelClient, + action: string, + args: Record, + ctx: Parameters[1] +): Promise { + try { + return await kernel.runAction( + GIT_REPOSITORY_TYPE, + action, + { ...args }, + recordContext(ctx), + GIT_REPOSITORY_ID + ); + } catch (error) { + // The compatibility tool has already passed its host confirmation gate. + const grant = confirmationId(error); + if (!grant) throw error; + return kernel.runAction( + GIT_REPOSITORY_TYPE, + action, + { ...args }, + recordContext(ctx, grant), + GIT_REPOSITORY_ID + ); } - return []; } +export function assertKernelClientVersion( + kernel: Pick +): void { + if (kernel.apiVersion !== KERNEL_CLIENT_API_VERSION) { + throw new Error( + `godmode-plugin-git requires kernel client API ${KERNEL_CLIENT_API_VERSION}; ` + + `host provided ${String(kernel.apiVersion)}` + ); + } +} + +const mutationTools = [ + { + name: "git_branch", + action: "branch", + description: "Create and/or check out a branch through GitRepository.branch.", + properties: { + ...cwd, + name: { type: "string", minLength: 1, maxLength: 255 }, + create: { type: "boolean", default: false }, + }, + required: ["name"], + }, + { + name: "git_add", + action: "add", + description: "Stage bounded pathspecs through GitRepository.add.", + properties: { + ...cwd, + paths: { + type: "array", + minItems: 1, + maxItems: 500, + items: { type: "string", minLength: 1, maxLength: 1_024 }, + }, + }, + required: ["paths"], + }, + { + name: "git_commit", + action: "commit", + description: "Commit staged changes through GitRepository.commit.", + properties: { + ...cwd, + message: { type: "string", minLength: 1, maxLength: 10_000 }, + skipHooks: { type: "boolean", default: false }, + }, + required: ["message"], + }, + { + name: "git_push", + action: "push", + description: "Push without force through GitRepository.push.", + properties: { + ...cwd, + remote: { type: "string", minLength: 1, maxLength: 256, default: "origin" }, + setUpstream: { type: "boolean", default: true }, + }, + }, + { + name: "git_fetch", + action: "fetch", + description: "Fetch through the confirmed external GitRepository.fetch action.", + properties: { + ...cwd, + remote: { type: "string", minLength: 1, maxLength: 256 }, + }, + }, + { + name: "git_pull", + action: "pull", + description: "Pull through GitRepository.pull with ff-only behavior by default.", + properties: { + ...cwd, + remote: { type: "string", minLength: 1, maxLength: 256 }, + ffOnly: { type: "boolean", default: true }, + }, + }, +] as const; + export const register: GodModePluginRegister = (api) => { + assertKernelClientVersion(api.kernel); + api.objectTypes.register(GIT_REPOSITORY_DEFINITION, gitRepositoryAdapter); api.tools.register([ { name: "git_status", - description: - "Show git branch and porcelain status for the coding root (working tree).", + description: "Show branch and porcelain status inside the tenant coding root.", mode: "auto", - parameters: { - type: "object", - properties: { - cwd: { - type: "string", - description: "Optional absolute working directory override", - }, - }, - }, + parameters: { type: "object", properties: cwd, additionalProperties: false }, handler: async (args, ctx) => { - const cwd = resolveWorkingRoot(ctx, args.cwd); - const branch = await runGit(cwd, ["rev-parse", "--abbrev-ref", "HEAD"]); - const status = await runGit(cwd, ["status", "--porcelain=v1", "-b"]); + const root = resolveWorkingRoot(ctx, args.cwd); + const branch = await runGit(root, ["rev-parse", "--abbrev-ref", "HEAD"]); + const status = await runGit(root, ["status", "--porcelain=v1", "-b"]); return { - cwd, + cwd: root, branch: branch.stdout.trim(), status: status.stdout, code: status.code, @@ -48,225 +195,101 @@ export const register: GodModePluginRegister = (api) => { }, { name: "git_diff", - description: - "Show git diff. Use staged=true for --cached; or commit range via base/head.", + description: "Show staged, revision-range, or path-limited Git diff.", mode: "auto", parameters: { type: "object", properties: { - cwd: { type: "string" }, + ...cwd, staged: { type: "boolean" }, - base: { type: "string", description: "Compare base (e.g. main)" }, - head: { type: "string", description: "Compare head (default HEAD)" }, - pathspec: { type: "string" }, + base: { type: "string", minLength: 1, maxLength: 4_096 }, + head: { type: "string", minLength: 1, maxLength: 4_096 }, + pathspec: { type: "string", minLength: 1, maxLength: 1_024 }, }, + additionalProperties: false, }, handler: async (args, ctx) => { - const cwd = resolveWorkingRoot(ctx, args.cwd); + const root = resolveWorkingRoot(ctx, args.cwd); const gitArgs = ["diff"]; - if (args.staged === true) gitArgs.push("--cached"); - if (str(args.base)) { - gitArgs.push(`${str(args.base)}...${str(args.head) || "HEAD"}`); + if (parseBoolean(args.staged, "staged", false)) gitArgs.push("--cached"); + if (args.base !== undefined) { + const base = parseRevision(args.base, "base"); + const head = + args.head === undefined ? "HEAD" : parseRevision(args.head, "head"); + gitArgs.push(`${base}...${head}`); + } else if (args.head !== undefined) { + throw Object.assign(new Error("head requires base"), { + status: 400, + code: "GIT_INVALID_ARGUMENT", + }); } - if (str(args.pathspec)) gitArgs.push("--", str(args.pathspec)); - const r = await runGit(cwd, gitArgs); - return { cwd, args: gitArgs, diff: r.stdout, code: r.code, stderr: r.stderr || undefined }; + if (args.pathspec !== undefined) gitArgs.push("--", parsePath(args.pathspec)); + const result = await runGit(root, gitArgs); + return { + cwd: root, + args: gitArgs, + diff: result.stdout, + code: result.code, + stderr: result.stderr || undefined, + }; }, }, { name: "git_log", - description: "Recent git log (oneline).", + description: "Show a bounded number of recent commits.", mode: "auto", parameters: { type: "object", properties: { - cwd: { type: "string" }, - limit: { type: "number", description: "Max commits (default 15)" }, + ...cwd, + limit: { type: "integer", minimum: 1, maximum: 50, default: 15 }, }, + additionalProperties: false, }, handler: async (args, ctx) => { - const cwd = resolveWorkingRoot(ctx, args.cwd); - const n = Math.min(50, Math.max(1, Number(args.limit ?? 15))); - const r = await runGit(cwd, ["log", `-n${n}`, "--oneline", "--decorate"]); - return { cwd, log: r.stdout, code: r.code, stderr: r.stderr || undefined }; + const root = resolveWorkingRoot(ctx, args.cwd); + const result = await runGit(root, [ + "log", + `-n${parseLimit(args.limit)}`, + "--oneline", + "--decorate", + ]); + return { + cwd: root, + log: result.stdout, + code: result.code, + stderr: result.stderr || undefined, + }; }, }, { name: "git_branches", - description: "List local git branches (verbose).", + description: "List local Git branches.", mode: "auto", - parameters: { - type: "object", - properties: { - cwd: { type: "string" }, - }, - }, + parameters: { type: "object", properties: cwd, additionalProperties: false }, handler: async (args, ctx) => { - const cwd = resolveWorkingRoot(ctx, args.cwd); - const r = await runGit(cwd, ["branch", "-vv"]); - return { cwd, branches: r.stdout, code: r.code }; - }, - }, - { - name: "git_checkout", - description: - "Create and/or checkout a branch. Use create=true with name to make a new branch.", - mode: "confirm", - parameters: { - type: "object", - properties: { - cwd: { type: "string" }, - name: { type: "string", description: "Branch name" }, - create: { - type: "boolean", - description: "If true, git checkout -b (create)", - }, - }, - required: ["name"], - }, - handler: async (args, ctx) => { - const cwd = resolveWorkingRoot(ctx, args.cwd); - const name = str(args.name); - if (!name) throw new Error("Branch name required"); - const gitArgs = - args.create === true ? ["checkout", "-b", name] : ["checkout", name]; - const r = await runGit(cwd, gitArgs); - if (r.code !== 0) throw new Error(r.stderr || r.stdout || "git checkout failed"); - return { cwd, checkedOut: name, created: args.create === true, ok: true }; - }, - }, - { - name: "git_add", - description: "Stage files (git add). Prefer pathspecs over bare '.' when possible.", - mode: "confirm", - parameters: { - type: "object", - properties: { - cwd: { type: "string" }, - paths: { - type: "array", - items: { type: "string" }, - description: "Paths to stage", - }, - }, - required: ["paths"], - }, - handler: async (args, ctx) => { - const cwd = resolveWorkingRoot(ctx, args.cwd); - const paths = strList(args.paths); - if (!paths.length) throw new Error("paths required"); - const r = await runGit(cwd, ["add", "--", ...paths]); - if (r.code !== 0) throw new Error(r.stderr || "git add failed"); - return { cwd, staged: paths, ok: true }; - }, - }, - { - name: "git_commit", - description: - "Commit staged changes. Does not amend; does not skip hooks unless skipHooks=true (confirm).", - mode: "confirm", - parameters: { - type: "object", - properties: { - cwd: { type: "string" }, - message: { type: "string", description: "Commit message (required)" }, - skipHooks: { - type: "boolean", - description: "Pass --no-verify (discouraged)", - }, - }, - required: ["message"], - }, - handler: async (args, ctx) => { - const cwd = resolveWorkingRoot(ctx, args.cwd); - const message = str(args.message).trim(); - if (!message) throw new Error("message required"); - const gitArgs = ["commit", "-m", message]; - if (args.skipHooks === true) gitArgs.push("--no-verify"); - rejectDestructiveGitArgs(gitArgs); - const r = await runGit(cwd, gitArgs); - if (r.code !== 0) throw new Error(r.stderr || r.stdout || "git commit failed"); - const head = await runGit(cwd, ["rev-parse", "HEAD"]); + const root = resolveWorkingRoot(ctx, args.cwd); + const result = await runGit(root, ["branch", "-vv"]); return { - cwd, - ok: true, - sha: head.stdout.trim(), - stdout: r.stdout, + cwd: root, + branches: result.stdout, + code: result.code, + stderr: result.stderr || undefined, }; }, }, - { - name: "git_push", - description: - "Push current branch to remote (default origin, -u when needed). Force push is blocked.", - mode: "confirm", + ...mutationTools.map((tool) => ({ + name: tool.name, + description: tool.description, + mode: "confirm" as const, parameters: { type: "object", - properties: { - cwd: { type: "string" }, - remote: { type: "string", description: "Default origin" }, - setUpstream: { - type: "boolean", - description: "Use -u (default true for new branches)", - }, - }, - }, - handler: async (args, ctx) => { - const cwd = resolveWorkingRoot(ctx, args.cwd); - const remote = str(args.remote) || "origin"; - const branch = await runGit(cwd, ["rev-parse", "--abbrev-ref", "HEAD"]); - const name = branch.stdout.trim(); - if (!name || name === "HEAD") throw new Error("Detached HEAD — checkout a branch first"); - const gitArgs = ["push"]; - if (args.setUpstream !== false) gitArgs.push("-u"); - gitArgs.push(remote, `HEAD:refs/heads/${name}`); - rejectDestructiveGitArgs(gitArgs); - const r = await runGit(cwd, gitArgs, { timeoutMs: 300_000 }); - if (r.code !== 0) throw new Error(r.stderr || r.stdout || "git push failed"); - return { cwd, ok: true, branch: name, remote, stdout: r.stdout, stderr: r.stderr }; + properties: tool.properties, + required: "required" in tool ? [...tool.required] : undefined, + additionalProperties: false, }, - }, - { - name: "git_fetch", - description: "Fetch from remote(s).", - mode: "auto", - parameters: { - type: "object", - properties: { - cwd: { type: "string" }, - remote: { type: "string" }, - }, - }, - handler: async (args, ctx) => { - const cwd = resolveWorkingRoot(ctx, args.cwd); - const gitArgs = ["fetch"]; - if (str(args.remote)) gitArgs.push(str(args.remote)); - const r = await runGit(cwd, gitArgs, { timeoutMs: 300_000 }); - return { cwd, ok: r.code === 0, stdout: r.stdout, stderr: r.stderr, code: r.code }; - }, - }, - { - name: "git_pull", - description: "Pull with --ff-only by default (safe). Confirm required.", - mode: "confirm", - parameters: { - type: "object", - properties: { - cwd: { type: "string" }, - remote: { type: "string" }, - ffOnly: { type: "boolean", description: "Default true" }, - }, - }, - handler: async (args, ctx) => { - const cwd = resolveWorkingRoot(ctx, args.cwd); - const gitArgs = ["pull"]; - if (args.ffOnly !== false) gitArgs.push("--ff-only"); - if (str(args.remote)) gitArgs.push(str(args.remote)); - rejectDestructiveGitArgs(gitArgs); - const r = await runGit(cwd, gitArgs, { timeoutMs: 300_000 }); - if (r.code !== 0) throw new Error(r.stderr || r.stdout || "git pull failed"); - return { cwd, ok: true, stdout: r.stdout }; - }, - }, + handler: ((args, ctx) => + delegate(api.kernel, tool.action, args, ctx)) satisfies PluginToolHandler, + })), ]); }; diff --git a/src/git-util.ts b/src/git-util.ts index 6c19b1a..c1395e1 100644 --- a/src/git-util.ts +++ b/src/git-util.ts @@ -4,16 +4,46 @@ import os from "node:os"; import path from "node:path"; const MAX_OUT = 200_000; +const MAX_ARGUMENT = 4_096; +const TENANT_ID = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/; +const SAFE_REMOTE = /^[A-Za-z0-9][A-Za-z0-9._/-]{0,255}$/; -export function resolveWorkingRoot( - ctx: { tenantId?: string }, - cwdArg?: unknown -): string { - if (typeof cwdArg === "string" && cwdArg.trim()) { - return path.resolve(cwdArg.trim()); +function badRequest(message: string): Error { + return Object.assign(new Error(message), { + status: 400, + code: "GIT_INVALID_ARGUMENT", + retryable: false, + }); +} + +function text(value: unknown, name: string, required = false): string { + if (value == null && !required) return ""; + if (typeof value !== "string") throw badRequest(`${name} must be a string`); + const result = value.trim(); + if (required && !result) throw badRequest(`${name} is required`); + if (result.length > MAX_ARGUMENT || /[\0\r\n]/.test(result)) { + throw badRequest(`${name} is invalid`); } + return result; +} + +export function codingRoot(ctx: { tenantId?: string }): string { const mode = (process.env.DEPLOYMENT_MODE ?? "local").toLowerCase(); - if ((mode === "hub" || mode === "client") && ctx.tenantId) { + if (mode === "hub" || mode === "client") { + if (!ctx.tenantId) { + throw Object.assign(new Error("Tenant coding root requires tenantId"), { + status: 403, + code: "GIT_TENANT_REQUIRED", + retryable: false, + }); + } + if ( + !TENANT_ID.test(ctx.tenantId) || + ctx.tenantId === "." || + ctx.tenantId === ".." + ) { + throw badRequest("tenantId is invalid"); + } const data = process.env.PLATFORM_DATA_DIR?.trim() || path.join( @@ -22,19 +52,119 @@ export function resolveWorkingRoot( ); return path.join(data, "tenant-workspaces", ctx.tenantId); } - return process.env.PLATFORM_REPO_ROOT?.trim() || process.cwd(); + return path.resolve(process.env.PLATFORM_REPO_ROOT?.trim() || process.cwd()); } export function assertInsideRoot(root: string, target: string): string { const base = path.resolve(root); - const abs = path.resolve(base, target); + const abs = path.isAbsolute(target) + ? path.resolve(target) + : path.resolve(base, target); const rel = path.relative(base, abs); - if (rel.startsWith("..") || path.isAbsolute(rel)) { - throw new Error(`Path escapes coding root: ${target}`); + if (rel === ".." || rel.startsWith(`..${path.sep}`) || path.isAbsolute(rel)) { + throw badRequest(`Path escapes coding root: ${target}`); } return abs; } +export function resolveWorkingRoot( + ctx: { tenantId?: string }, + cwdArg?: unknown +): string { + const root = codingRoot(ctx); + const requested = text(cwdArg, "cwd") || root; + const contained = assertInsideRoot(root, requested); + if (!fs.existsSync(root) || !fs.statSync(root).isDirectory()) { + throw Object.assign(new Error(`Coding root not found: ${root}`), { + status: 404, + code: "GIT_CODING_ROOT_NOT_FOUND", + retryable: false, + }); + } + if (!fs.existsSync(contained) || !fs.statSync(contained).isDirectory()) { + throw Object.assign(new Error(`Working directory not found: ${contained}`), { + status: 404, + code: "GIT_WORKING_DIRECTORY_NOT_FOUND", + retryable: false, + }); + } + const realRoot = fs.realpathSync.native(root); + const realTarget = fs.realpathSync.native(contained); + return assertInsideRoot(realRoot, realTarget); +} + +export function parseBoolean( + value: unknown, + name: string, + fallback: boolean +): boolean { + if (value === undefined) return fallback; + if (typeof value !== "boolean") throw badRequest(`${name} must be a boolean`); + return value; +} + +export function parseLimit(value: unknown, fallback = 15): number { + if (value === undefined) return fallback; + if (!Number.isInteger(value) || Number(value) < 1 || Number(value) > 50) { + throw badRequest("limit must be an integer between 1 and 50"); + } + return Number(value); +} + +export function parseRemote(value: unknown): string { + const remote = text(value, "remote") || "origin"; + if (remote.startsWith("-") || !SAFE_REMOTE.test(remote) || remote.includes("..")) { + throw badRequest("remote is invalid"); + } + return remote; +} + +export function parseRevision(value: unknown, name: string): string { + const revision = text(value, name, true); + if ( + revision.startsWith("-") || + /[\s~^:?*[\\]/.test(revision) || + revision.includes("..") || + revision.includes("@{") + ) { + throw badRequest(`${name} is not a safe revision`); + } + return revision; +} + +export function parseBranch(value: unknown): string { + const branch = parseRevision(value, "name"); + if ( + branch.startsWith("/") || + branch.endsWith("/") || + branch.endsWith(".") || + branch.includes("//") || + branch.includes("/.") + ) { + throw badRequest("name is not a valid branch name"); + } + return branch; +} + +export function parsePath(value: unknown, name = "pathspec"): string { + const result = text(value, name, true); + if (result.length > 1_024) throw badRequest(`${name} is too long`); + return result; +} + +export function parsePaths(value: unknown): string[] { + if (!Array.isArray(value) || value.length === 0 || value.length > 500) { + throw badRequest("paths must contain between 1 and 500 pathspecs"); + } + return value.map((item, index) => parsePath(item, `paths[${index}]`)); +} + +export function parseMessage(value: unknown): string { + const message = text(value, "message", true); + if (message.length > 10_000) throw badRequest("message is too long"); + return message; +} + function truncate(text: string): string { const buf = Buffer.from(text, "utf8"); if (buf.length <= MAX_OUT) return text; @@ -44,7 +174,7 @@ function truncate(text: string): string { export async function runGit( cwd: string, args: string[], - opts?: { timeoutMs?: number } + opts?: { timeoutMs?: number; signal?: AbortSignal } ): Promise<{ code: number; stdout: string; stderr: string }> { if (!fs.existsSync(cwd)) { throw new Error(`Working directory not found: ${cwd}`); @@ -52,27 +182,54 @@ export async function runGit( return new Promise((resolve, reject) => { const child = spawn("git", args, { cwd, - shell: process.platform === "win32", + shell: false, env: { ...process.env }, + windowsHide: true, }); let stdout = ""; let stderr = ""; + let settled = false; + const finishError = (error: Error) => { + if (settled) return; + settled = true; + clearTimeout(timer); + opts?.signal?.removeEventListener("abort", abort); + reject(error); + }; + const abort = () => { + child.kill(); + finishError( + Object.assign(new Error(`git ${args[0]} was cancelled`), { + status: 499, + code: "GIT_CANCELLED", + retryable: false, + }) + ); + }; const timer = setTimeout(() => { - child.kill("SIGTERM"); - reject(new Error(`git ${args[0]} timed out`)); + child.kill(); + finishError( + Object.assign(new Error(`git ${args[0]} timed out`), { + status: 504, + code: "GIT_TIMEOUT", + retryable: true, + }) + ); }, opts?.timeoutMs ?? 120_000); + opts?.signal?.addEventListener("abort", abort, { once: true }); + if (opts?.signal?.aborted) abort(); child.stdout?.on("data", (c: Buffer) => { - stdout += c.toString(); + if (Buffer.byteLength(stdout) <= MAX_OUT) stdout += c.toString(); }); child.stderr?.on("data", (c: Buffer) => { - stderr += c.toString(); - }); - child.on("error", (err) => { - clearTimeout(timer); - reject(err); + if (Buffer.byteLength(stderr) <= MAX_OUT) stderr += c.toString(); }); + child.on("error", finishError); child.on("close", (code) => { + if (settled) return; + settled = true; clearTimeout(timer); + opts?.signal?.removeEventListener("abort", abort); resolve({ code: code ?? 1, stdout: truncate(stdout), diff --git a/src/kernel.ts b/src/kernel.ts new file mode 100644 index 0000000..4aaa6eb --- /dev/null +++ b/src/kernel.ts @@ -0,0 +1,335 @@ +import type { + PluginRecordAdapter, + PluginRecordContext, +} from "@godmode/plugin-api"; +import type { ActionDef, ObjectTypeDef, RecordData, RecordRow } from "@godmode/kernel"; +import { + codingRoot, + parseBoolean, + parseBranch, + parseMessage, + parsePaths, + parseRemote, + rejectDestructiveGitArgs, + resolveWorkingRoot, + runGit, +} from "./git-util.js"; + +export const GIT_REPOSITORY_TYPE = "GitRepository"; +export const GIT_REPOSITORY_ID = "coding-root"; + +const cwdProperty = { + cwd: { + type: "string", + minLength: 1, + maxLength: 4_096, + description: "Directory within the tenant coding root", + }, +}; + +const outputSchema = { + type: "object", + required: ["ok", "cwd"], + properties: { + ok: { type: "boolean" }, + cwd: { type: "string" }, + code: { type: "integer" }, + stdout: { type: "string" }, + stderr: { type: "string" }, + branch: { type: "string" }, + remote: { type: "string" }, + sha: { type: "string" }, + created: { type: "boolean" }, + staged: { type: "array", items: { type: "string" } }, + }, + additionalProperties: false, +}; + +const errorSchema = { + type: "object", + required: ["code", "message", "retryable"], + properties: { + code: { type: "string" }, + message: { type: "string" }, + retryable: { type: "boolean" }, + details: {}, + }, + additionalProperties: false, +}; + +type GitActionName = "branch" | "add" | "commit" | "push" | "fetch" | "pull"; + +function action( + name: GitActionName, + metadata: { + label: string; + description: string; + effect: "write" | "external"; + timeoutMs: number; + properties: Record; + required?: string[]; + } +): ActionDef { + return { + name, + label: metadata.label, + description: metadata.description, + target: "record", + effect: metadata.effect, + execution: "sync", + contractVersion: 1, + roles: ["editor", "owner", "intelligence"], + confirmation: { required: true, ttlSeconds: 300 }, + inputSchema: { + type: "object", + properties: { ...cwdProperty, ...metadata.properties }, + required: metadata.required, + additionalProperties: false, + }, + outputSchema, + errorSchema, + idempotency: { required: false, ttlSeconds: 300 }, + retry: { maxAttempts: 1, retryableErrorCodes: ["GIT_TIMEOUT"] }, + timeoutMs: metadata.timeoutMs, + events: [{ type: `git.repository.${name}` }], + }; +} + +export const GIT_REPOSITORY_ACTIONS: ActionDef[] = [ + action("branch", { + label: "Change branch", + description: "Create and/or check out a validated local branch.", + effect: "write", + timeoutMs: 120_000, + properties: { + name: { type: "string", minLength: 1, maxLength: 255 }, + create: { type: "boolean", default: false }, + }, + required: ["name"], + }), + action("add", { + label: "Stage paths", + description: "Stage bounded pathspecs after an explicit option terminator.", + effect: "write", + timeoutMs: 120_000, + properties: { + paths: { + type: "array", + minItems: 1, + maxItems: 500, + items: { type: "string", minLength: 1, maxLength: 1_024 }, + }, + }, + required: ["paths"], + }), + action("commit", { + label: "Commit staged changes", + description: "Create a commit from staged changes; hooks run unless explicitly disabled.", + effect: "write", + timeoutMs: 180_000, + properties: { + message: { type: "string", minLength: 1, maxLength: 10_000 }, + skipHooks: { type: "boolean", default: false }, + }, + required: ["message"], + }), + action("push", { + label: "Push branch", + description: "Push the current branch without force flags.", + effect: "external", + timeoutMs: 300_000, + properties: { + remote: { type: "string", minLength: 1, maxLength: 256, default: "origin" }, + setUpstream: { type: "boolean", default: true }, + }, + }), + action("fetch", { + label: "Fetch remote", + description: "Fetch remote refs. This external network mutation requires confirmation.", + effect: "external", + timeoutMs: 300_000, + properties: { + remote: { type: "string", minLength: 1, maxLength: 256 }, + }, + }), + action("pull", { + label: "Pull branch", + description: "Pull remote changes with fast-forward-only behavior by default.", + effect: "external", + timeoutMs: 300_000, + properties: { + remote: { type: "string", minLength: 1, maxLength: 256 }, + ffOnly: { type: "boolean", default: true }, + }, + }), +]; + +export const GIT_REPOSITORY_DEFINITION: ObjectTypeDef = { + name: GIT_REPOSITORY_TYPE, + label: "Git repository", + labelPlural: "Git repositories", + description: "The single Git checkout contained by the active tenant coding root.", + storage: { kind: "adapter", adapterId: "git_repository" }, + database: "tenant", + accessPolicy: "tenant-local", + contractVersion: 1, + fields: [ + { name: "id", label: "ID", fieldType: "Data", inForm: false }, + { name: "cwd", label: "Coding root", fieldType: "Data", inForm: false }, + { name: "tenant_id", label: "Tenant", fieldType: "Data", inForm: false }, + ], + permissions: [ + { role: "viewer", read: true }, + { role: "editor", read: true }, + { role: "owner", read: true }, + { role: "intelligence", read: true }, + ], + operations: ["list", "get"], + actions: GIT_REPOSITORY_ACTIONS, +}; + +function repositoryRecord(ctx: PluginRecordContext): RecordRow { + const cwd = codingRoot(ctx); + return { + id: GIT_REPOSITORY_ID, + objectType: GIT_REPOSITORY_TYPE, + data: { + id: GIT_REPOSITORY_ID, + cwd, + tenant_id: ctx.tenantId, + }, + }; +} + +function requireRepository(id: string): void { + if (id !== GIT_REPOSITORY_ID) { + throw Object.assign(new Error("Git repository not found"), { + status: 404, + code: "GIT_REPOSITORY_NOT_FOUND", + retryable: false, + }); + } +} + +function optionalRemote(value: unknown): string | undefined { + return value === undefined ? undefined : parseRemote(value); +} + +function failed(result: { code: number; stdout: string; stderr: string }, command: string): never { + throw Object.assign( + new Error(result.stderr || result.stdout || `git ${command} failed`), + { status: 422, code: "GIT_COMMAND_FAILED", retryable: false } + ); +} + +export const gitRepositoryAdapter: PluginRecordAdapter = { + list(query, ctx) { + const offset = Math.max(Number(query.offset) || 0, 0); + const limit = Math.min(Math.max(Number(query.limit) || 100, 1), 500); + const rows = offset === 0 && limit > 0 ? [repositoryRecord(ctx)] : []; + return { objectType: GIT_REPOSITORY_TYPE, records: rows, total: 1 }; + }, + get(id, ctx) { + return id === GIT_REPOSITORY_ID ? repositoryRecord(ctx) : null; + }, + actions: { + async branch(id, input, ctx) { + requireRepository(id); + const cwd = resolveWorkingRoot(ctx, input.cwd); + const name = parseBranch(input.name); + const create = parseBoolean(input.create, "create", false); + const args = create ? ["checkout", "-b", name] : ["checkout", name]; + const result = await runGit(cwd, args, { signal: ctx.signal }); + if (result.code !== 0) failed(result, "checkout"); + return { ok: true, cwd, branch: name, created: create }; + }, + async add(id, input, ctx) { + requireRepository(id); + const cwd = resolveWorkingRoot(ctx, input.cwd); + const paths = parsePaths(input.paths); + const result = await runGit(cwd, ["add", "--", ...paths], { signal: ctx.signal }); + if (result.code !== 0) failed(result, "add"); + return { ok: true, cwd, staged: paths }; + }, + async commit(id, input, ctx) { + requireRepository(id); + const cwd = resolveWorkingRoot(ctx, input.cwd); + const args = ["commit", "-m", parseMessage(input.message)]; + if (parseBoolean(input.skipHooks, "skipHooks", false)) args.push("--no-verify"); + rejectDestructiveGitArgs(args); + const result = await runGit(cwd, args, { timeoutMs: 180_000, signal: ctx.signal }); + if (result.code !== 0) failed(result, "commit"); + const head = await runGit(cwd, ["rev-parse", "HEAD"], { signal: ctx.signal }); + if (head.code !== 0) failed(head, "rev-parse"); + return { ok: true, cwd, sha: head.stdout.trim(), stdout: result.stdout }; + }, + async push(id, input, ctx) { + requireRepository(id); + const cwd = resolveWorkingRoot(ctx, input.cwd); + const remote = parseRemote(input.remote); + const current = await runGit(cwd, ["rev-parse", "--abbrev-ref", "HEAD"], { + signal: ctx.signal, + }); + if (current.code !== 0) failed(current, "rev-parse"); + const branch = parseBranch(current.stdout.trim()); + if (branch === "HEAD") { + throw Object.assign(new Error("Detached HEAD; check out a branch first"), { + status: 409, + code: "GIT_DETACHED_HEAD", + retryable: false, + }); + } + const args = ["push"]; + if (parseBoolean(input.setUpstream, "setUpstream", true)) args.push("-u"); + args.push(remote, `HEAD:refs/heads/${branch}`); + rejectDestructiveGitArgs(args); + const result = await runGit(cwd, args, { timeoutMs: 300_000, signal: ctx.signal }); + if (result.code !== 0) failed(result, "push"); + return { + ok: true, + cwd, + branch, + remote, + stdout: result.stdout, + stderr: result.stderr, + }; + }, + async fetch(id, input, ctx) { + requireRepository(id); + const cwd = resolveWorkingRoot(ctx, input.cwd); + const remote = optionalRemote(input.remote); + const args = remote ? ["fetch", remote] : ["fetch"]; + const result = await runGit(cwd, args, { timeoutMs: 300_000, signal: ctx.signal }); + if (result.code !== 0) failed(result, "fetch"); + return { + ok: true, + cwd, + ...(remote ? { remote } : {}), + stdout: result.stdout, + stderr: result.stderr, + code: result.code, + }; + }, + async pull(id, input, ctx) { + requireRepository(id); + const cwd = resolveWorkingRoot(ctx, input.cwd); + const remote = optionalRemote(input.remote); + const args = ["pull"]; + if (parseBoolean(input.ffOnly, "ffOnly", true)) args.push("--ff-only"); + if (remote) args.push(remote); + rejectDestructiveGitArgs(args); + const result = await runGit(cwd, args, { timeoutMs: 300_000, signal: ctx.signal }); + if (result.code !== 0) failed(result, "pull"); + return { + ok: true, + cwd, + ...(remote ? { remote } : {}), + stdout: result.stdout, + }; + }, + }, +}; + +export function actionInput(args: Record): RecordData { + return { ...args }; +} diff --git a/tests/argument-injection.test.ts b/tests/argument-injection.test.ts new file mode 100644 index 0000000..f4c6528 --- /dev/null +++ b/tests/argument-injection.test.ts @@ -0,0 +1,65 @@ +import { spawnSync } from "node:child_process"; +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import { afterEach, beforeEach, describe, expect, it } from "vitest"; +import { parseBranch, parseRemote, runGit } from "../src/git-util.js"; +import { GIT_REPOSITORY_ID, gitRepositoryAdapter } from "../src/kernel.js"; + +describe("Git argument injection resistance", () => { + let root = ""; + const previous = { ...process.env }; + + beforeEach(() => { + root = fs.mkdtempSync(path.join(os.tmpdir(), "godmode-git-injection-")); + process.env.DEPLOYMENT_MODE = "local"; + process.env.PLATFORM_REPO_ROOT = root; + const initialized = spawnSync("git", ["init"], { + cwd: root, + shell: false, + windowsHide: true, + }); + if (initialized.status !== 0) { + throw new Error(initialized.stderr.toString()); + } + }); + + afterEach(() => { + process.env = { ...previous }; + fs.rmSync(root, { recursive: true, force: true }); + }); + + it("never interprets Git arguments through a Windows shell", async () => { + const marker = path.join(root, "shell-injection-marker"); + await runGit(root, [ + "status", + "--", + "&", + "powershell", + "-NoProfile", + "-Command", + `New-Item -ItemType File -Path '${marker}'`, + ]); + expect(fs.existsSync(marker)).toBe(false); + }); + + it("uses an option terminator for option-shaped pathspecs", async () => { + fs.writeFileSync(path.join(root, "--help"), "safe pathspec"); + const add = gitRepositoryAdapter.actions?.add; + expect(add).toBeDefined(); + await add?.( + GIT_REPOSITORY_ID, + { paths: ["--help"] }, + { tenantId: "tenant-a", role: "owner", source: "plugin" } + ); + const status = await runGit(root, ["status", "--porcelain=v1"]); + expect(status.stdout).toContain("--help"); + }); + + it("rejects branch and remote command fragments before spawn", () => { + expect(() => parseBranch("safe & powershell")).toThrow(); + expect(() => parseBranch("--upload-pack=evil")).toThrow(); + expect(() => parseRemote("origin;whoami")).toThrow(); + expect(() => parseRemote("--exec=evil")).toThrow(); + }); +}); diff --git a/tests/contract.test.ts b/tests/contract.test.ts new file mode 100644 index 0000000..8c9a4da --- /dev/null +++ b/tests/contract.test.ts @@ -0,0 +1,150 @@ +import { validateObjectTypeDef } from "@godmode/kernel"; +import { + KERNEL_CLIENT_API_VERSION, + readGodmodePluginManifest, +} from "@godmode/plugin-api"; +import { describe, expect, it, vi } from "vitest"; +import { assertKernelClientVersion, register } from "../src/bridge.js"; +import { + GIT_REPOSITORY_ACTIONS, + GIT_REPOSITORY_DEFINITION, +} from "../src/kernel.js"; + +describe("GitRepository action contract", () => { + it("declares and requires kernel client API version 1", () => { + const manifest = readGodmodePluginManifest(process.cwd()); + expect(manifest.kernelApiVersion).toBe(KERNEL_CLIENT_API_VERSION); + expect(KERNEL_CLIENT_API_VERSION).toBe(1); + expect(() => + assertKernelClientVersion({ apiVersion: 2 } as never) + ).toThrow( + "godmode-plugin-git requires kernel client API 1; host provided 2" + ); + }); + + it("fails before registration when the host client version mismatches", () => { + const registerObjectType = vi.fn(); + const registerTools = vi.fn(); + expect(() => + register({ + kernel: { apiVersion: 2 }, + objectTypes: { register: registerObjectType }, + tools: { register: registerTools }, + } as never) + ).toThrow(/requires kernel client API 1; host provided 2/); + expect(registerObjectType).not.toHaveBeenCalled(); + expect(registerTools).not.toHaveBeenCalled(); + }); + + it("is valid and fully declares operational policy", () => { + expect(validateObjectTypeDef(GIT_REPOSITORY_DEFINITION)).toEqual([]); + expect(GIT_REPOSITORY_ACTIONS.map((action) => action.name)).toEqual([ + "branch", + "add", + "commit", + "push", + "fetch", + "pull", + ]); + for (const action of GIT_REPOSITORY_ACTIONS) { + expect(action.roles).toEqual(["editor", "owner", "intelligence"]); + expect(action.confirmation).toEqual({ required: true, ttlSeconds: 300 }); + expect(action.idempotency).toBeDefined(); + expect(action.timeoutMs).toBeGreaterThan(0); + expect(action.inputSchema).toMatchObject({ additionalProperties: false }); + expect(action.outputSchema).toBeDefined(); + expect(action.errorSchema).toBeDefined(); + expect(action.events).toHaveLength(1); + } + expect( + GIT_REPOSITORY_ACTIONS.find((action) => action.name === "fetch") + ).toMatchObject({ + effect: "external", + confirmation: { required: true }, + }); + }); + + it("registers measured semantic wrappers and fixes branch naming", async () => { + const runAction = vi.fn().mockResolvedValue({ ok: true }); + let tools: Array> = []; + let registeredDefinition: unknown; + const api = { + objectTypes: { + register(definition: unknown) { + registeredDefinition = definition; + return { dispose() {} }; + }, + }, + tools: { + register(value: Array>) { + tools = value; + }, + }, + kernel: { apiVersion: KERNEL_CLIENT_API_VERSION, runAction }, + }; + register(api as never); + + expect(registeredDefinition).toBe(GIT_REPOSITORY_DEFINITION); + expect(tools.some((tool) => tool.name === "git_branch")).toBe(true); + expect(tools.some((tool) => tool.name === "git_checkout")).toBe(false); + expect(tools.find((tool) => tool.name === "git_fetch")).toMatchObject({ + mode: "confirm", + }); + + const branch = tools.find((tool) => tool.name === "git_branch") as { + handler: ( + input: Record, + ctx: Record + ) => Promise; + }; + await branch.handler( + { name: "feat/safe", create: true }, + { tenantId: "tenant-a", activeAgentId: "agent-a" } + ); + expect(runAction).toHaveBeenCalledWith( + "GitRepository", + "branch", + { name: "feat/safe", create: true }, + expect.objectContaining({ + tenantId: "tenant-a", + activeAgentId: "agent-a", + role: "intelligence", + }), + "coding-root" + ); + }); + + it("converts confirmed tool approval into a kernel confirmation grant", async () => { + const confirmation = Object.assign(new Error("confirmation required"), { + status: 428, + code: "KERNEL_CONFIRMATION_REQUIRED", + details: { confirmationId: "grant-1" }, + }); + const runAction = vi + .fn() + .mockRejectedValueOnce(confirmation) + .mockResolvedValueOnce({ ok: true }); + let tools: Array> = []; + register({ + objectTypes: { register: () => ({ dispose() {} }) }, + tools: { register: (value: Array>) => (tools = value) }, + kernel: { apiVersion: KERNEL_CLIENT_API_VERSION, runAction }, + } as never); + const fetch = tools.find((tool) => tool.name === "git_fetch") as { + handler: ( + input: Record, + ctx: Record + ) => Promise; + }; + await expect(fetch.handler({}, { tenantId: "tenant-a" })).resolves.toEqual({ + ok: true, + }); + expect(runAction).toHaveBeenLastCalledWith( + "GitRepository", + "fetch", + {}, + expect.objectContaining({ confirmationId: "grant-1" }), + "coding-root" + ); + }); +}); diff --git a/tests/git-util.test.ts b/tests/git-util.test.ts new file mode 100644 index 0000000..f32f255 --- /dev/null +++ b/tests/git-util.test.ts @@ -0,0 +1,70 @@ +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import { afterEach, beforeEach, describe, expect, it } from "vitest"; +import { + parseBoolean, + parseBranch, + parseLimit, + parsePaths, + parseRemote, + parseRevision, + resolveWorkingRoot, +} from "../src/git-util.js"; + +describe("Git option validation", () => { + it("accepts bounded typed options", () => { + expect(parseBoolean(undefined, "flag", true)).toBe(true); + expect(parseBoolean(false, "flag", true)).toBe(false); + expect(parseLimit(50)).toBe(50); + expect(parseBranch("feat/kernel-migration")).toBe("feat/kernel-migration"); + expect(parseRemote("upstream/team")).toBe("upstream/team"); + expect(parsePaths(["--help", "src/file.ts"])).toEqual([ + "--help", + "src/file.ts", + ]); + }); + + it("rejects coercion and option-shaped revisions", () => { + expect(() => parseBoolean("false", "flag", true)).toThrow( + "flag must be a boolean" + ); + expect(() => parseLimit(1.5)).toThrow(); + expect(() => parseLimit(51)).toThrow(); + expect(() => parseRevision("--output=x", "base")).toThrow(); + expect(() => parseBranch("../main")).toThrow(); + expect(() => parseRemote("-c")).toThrow(); + }); +}); + +describe("coding-root containment", () => { + let root = ""; + let outside = ""; + const previous = { ...process.env }; + + beforeEach(() => { + root = fs.mkdtempSync(path.join(os.tmpdir(), "godmode-git-root-")); + outside = fs.mkdtempSync(path.join(os.tmpdir(), "godmode-git-outside-")); + fs.mkdirSync(path.join(root, "nested")); + process.env.DEPLOYMENT_MODE = "local"; + process.env.PLATFORM_REPO_ROOT = root; + }); + + afterEach(() => { + process.env = { ...previous }; + fs.rmSync(root, { recursive: true, force: true }); + fs.rmSync(outside, { recursive: true, force: true }); + }); + + it("allows only existing directories beneath the root", () => { + expect(resolveWorkingRoot({ tenantId: "tenant-a" }, "nested")).toBe( + fs.realpathSync.native(path.join(root, "nested")) + ); + expect(() => + resolveWorkingRoot({ tenantId: "tenant-a" }, outside) + ).toThrow("Path escapes coding root"); + expect(() => + resolveWorkingRoot({ tenantId: "tenant-a" }, "missing") + ).toThrow("Working directory not found"); + }); +}); diff --git a/tests/two-tenant.test.ts b/tests/two-tenant.test.ts new file mode 100644 index 0000000..292a706 --- /dev/null +++ b/tests/two-tenant.test.ts @@ -0,0 +1,61 @@ +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import { afterEach, beforeEach, describe, expect, it } from "vitest"; +import { resolveWorkingRoot } from "../src/git-util.js"; +import { GIT_REPOSITORY_ID, gitRepositoryAdapter } from "../src/kernel.js"; + +describe("two-tenant coding roots", () => { + let dataDir = ""; + const previous = { ...process.env }; + + beforeEach(() => { + dataDir = fs.mkdtempSync(path.join(os.tmpdir(), "godmode-git-tenants-")); + process.env.DEPLOYMENT_MODE = "hub"; + process.env.PLATFORM_DATA_DIR = dataDir; + fs.mkdirSync(path.join(dataDir, "tenant-workspaces", "tenant-a"), { + recursive: true, + }); + fs.mkdirSync(path.join(dataDir, "tenant-workspaces", "tenant-b"), { + recursive: true, + }); + }); + + afterEach(() => { + process.env = { ...previous }; + fs.rmSync(dataDir, { recursive: true, force: true }); + }); + + it("binds records and cwd overrides to the active tenant", () => { + const rootA = resolveWorkingRoot({ tenantId: "tenant-a" }); + const rootB = resolveWorkingRoot({ tenantId: "tenant-b" }); + expect(rootA).not.toBe(rootB); + + const rowA = gitRepositoryAdapter.get?.(GIT_REPOSITORY_ID, { + tenantId: "tenant-a", + role: "owner", + source: "plugin", + }); + const rowB = gitRepositoryAdapter.get?.(GIT_REPOSITORY_ID, { + tenantId: "tenant-b", + role: "owner", + source: "plugin", + }); + expect(rowA?.data.cwd).not.toBe(rowB?.data.cwd); + expect(() => + resolveWorkingRoot({ tenantId: "tenant-a" }, rootB) + ).toThrow("Path escapes coding root"); + }); + + it("rejects tenant path traversal before root construction", () => { + expect(() => resolveWorkingRoot({})).toThrow( + "Tenant coding root requires tenantId" + ); + expect(() => resolveWorkingRoot({ tenantId: "../tenant-b" })).toThrow( + "tenantId is invalid" + ); + expect(() => resolveWorkingRoot({ tenantId: ".." })).toThrow( + "tenantId is invalid" + ); + }); +}); diff --git a/tsconfig.json b/tsconfig.json index c0e6291..8d8dd37 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -3,11 +3,10 @@ "target": "ES2022", "module": "NodeNext", "moduleResolution": "NodeNext", - "outDir": "dist", - "rootDir": "src", "strict": true, "skipLibCheck": true, - "noEmit": true + "noEmit": true, + "types": ["node", "vitest/globals"] }, - "include": ["src"] + "include": ["src", "tests"] }