Compare commits
12
Commits
v1.0.1
...
7708df699b
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7708df699b | ||
|
|
4532af33fb | ||
|
|
5b6d5fc301 | ||
|
|
62b56b6a8a | ||
|
|
99a0090a92 | ||
|
|
75e08a5c63 | ||
|
|
dfe9926c7c | ||
|
|
a6cb0beade | ||
|
|
c48fe99b96 | ||
|
|
411cdd13ff | ||
|
|
4ea9b4a695 | ||
|
|
fb3ad11f3a |
@@ -0,0 +1,29 @@
|
||||
name: CI
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
|
||||
permissions:
|
||||
code: read
|
||||
|
||||
jobs:
|
||||
verify:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: https://github.com/actions/checkout@v4
|
||||
with:
|
||||
github-server-url: ${{ gitea.server_url }}
|
||||
persist-credentials: false
|
||||
- name: Install Bun
|
||||
uses: https://github.com/oven-sh/setup-bun@v2
|
||||
with:
|
||||
bun-version: "1.3.14"
|
||||
- name: Verify and build
|
||||
run: |
|
||||
bun install --frozen-lockfile
|
||||
bun run check
|
||||
bun run lint
|
||||
bun run format:check
|
||||
bun run test
|
||||
bun run build
|
||||
@@ -1,10 +1,10 @@
|
||||
# stanp-opencode
|
||||
|
||||
OpenCode V2 plugin providing a `coordinate` agent and fast/good/deep worker pairs, with GPT routing or explicit custom model assignments. Your default agent and the built-in `build` agent remain unchanged.
|
||||
stanp-opencode is an OpenCode V2 coordinator with specialist workers for fast, good, and deep tasks.
|
||||
|
||||
## Install the latest release
|
||||
|
||||
Requires **OpenCode V2** (`opencode2`), **Bun 1.3.14+**, Bash, curl, jq, and `sha256sum` (Linux/WSL). The native plugin API is pinned to `@opencode/plugin@0.0.0-beta-19378`; use the matching OpenCode beta.
|
||||
Requires OpenCode V2 2.0.2 (`opencode2`), Bun 1.3.14+, Bash, curl, jq, and `sha256sum` on Linux or WSL.
|
||||
|
||||
Run the installer without sudo:
|
||||
|
||||
@@ -12,43 +12,34 @@ Run the installer without sudo:
|
||||
curl -fsSL https://git.krtss.de/StanPonomarev/stanp-opencode/raw/branch/main/install.sh | bash
|
||||
```
|
||||
|
||||
From a checkout, run `bash install.sh` directly. Rerun the same script to upgrade to the latest published, non-prerelease Gitea release.
|
||||
Rerun the command to upgrade to the latest published release.
|
||||
|
||||
The installer downloads `stanp-opencode.tgz` and its SHA-256 checksum, verifies and extracts the self-contained archive, then activates the plugin only after importing and checking the native V2 export. Download, extraction, and validation failures leave the previous active plugin untouched. The checksum detects corruption; it is not a signature independent of the release server.
|
||||
## Install from a local checkout
|
||||
|
||||
Installation defaults to `~/.config/opencode` and respects `XDG_CONFIG_HOME`. Override it explicitly when needed:
|
||||
This requires Bash, jq, and Bun 1.3.14+:
|
||||
|
||||
```sh
|
||||
OPENCODE_CONFIG_DIR=/path/to/opencode-config bash install.sh
|
||||
bash /path/to/stanp-opencode/install-local.sh
|
||||
```
|
||||
|
||||
The installer atomically replaces `plugins/stanp-opencode.js` for [V2 automatic plugin discovery](https://opencode.ai/v2/docs/plugins). It does not create symlinks, install package dependencies, edit `opencode.json` or `opencode.jsonc`, install OpenCode itself, or configure provider credentials.
|
||||
The script installs dependencies and builds the checkout before installing it.
|
||||
|
||||
Restart `opencode2` after installation (or `opencode2 service restart` for an existing service). Connect your provider, then select `coordinate` in the agent picker. The default configuration uses OpenAI GPT models; agents are only enabled when the required models are available.
|
||||
After either installation, restart `opencode2` or run `opencode2 service restart` for an existing service. Connect OpenAI and select `coordinate`. The required models must be available for the workers to run.
|
||||
|
||||
## Configure
|
||||
Both installers use `~/.config/opencode`, respect `XDG_CONFIG_HOME`, and accept `OPENCODE_CONFIG_DIR` as an override.
|
||||
|
||||
See [GPT configuration](examples/gpt.opencode.jsonc) and [custom model assignments](examples/custom.opencode.jsonc). When using the installer, point the example's plugin package entry at the installed package directory rather than the unpublished npm package name:
|
||||
## Configuration
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"plugins": [
|
||||
{
|
||||
"package": "./plugins/stanp-opencode.js",
|
||||
"options": {
|
||||
"mode": "gpt",
|
||||
"provider": "openai",
|
||||
"lunaPriority": true,
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
```
|
||||
See [GPT configuration](examples/gpt.opencode.jsonc) and [custom model assignments](examples/custom.opencode.jsonc).
|
||||
|
||||
This relative path assumes the configuration file is in the selected global config directory. Use an absolute path in project configuration. Remove any old V1 `plugin` entry when migrating; V2 uses `plugins`. `lunaPriority` requests priority service where supported and can increase cost; set it to `false` to use the provider default.
|
||||
For an installed plugin, use `./plugins/stanp-opencode` as the plugin package path. Set `lunaPriority` to `false` to avoid requesting priority pricing.
|
||||
|
||||
The plugin supplies session-local `todoread` and `todowrite` tools when no other plugin provides them. In this pinned V2 beta, custom tools cannot open permission prompts: an `ask` or `deny` rule blocks these todo tools rather than bypassing the rule. Explicitly allow them if you want to use them.
|
||||
## Playwright
|
||||
|
||||
The plugin prefers a local Playwright MCP installation. If ordinary Playwright is installed, it can download and run the MCP package with `npx`; otherwise Docker is required for the automatic fallback. Automatically configured servers allow five minutes for startup because the first launch may need to download a package or container image. Docker reuses its cached image on later launches. Existing `playwright` configuration, including its timeout, is preserved.
|
||||
|
||||
If `opencode mcp list` reports `MCP error -32001: Request timed out`, prefetch the Docker fallback with `docker pull mcr.microsoft.com/playwright/mcp`, or install `@playwright/mcp` in the project so the plugin can launch it directly. Then reconnect the server from `/mcps` or restart OpenCode. A custom `playwright` entry under `mcp.servers` can also set its own `timeout.startup` in milliseconds.
|
||||
|
||||
## Uninstall
|
||||
|
||||
Remove `plugins/stanp-opencode.js` from the selected config directory and remove any explicit configuration entry for it, then restart OpenCode.
|
||||
Remove `plugins/stanp-opencode` from the selected configuration directory and remove its configuration entry, then restart OpenCode.
|
||||
|
||||
@@ -8,22 +8,31 @@
|
||||
"zod": "4.1.8",
|
||||
},
|
||||
"devDependencies": {
|
||||
"@opencode/client": "0.0.0-beta-19378",
|
||||
"@opencode/plugin": "0.0.0-beta-19378",
|
||||
"@opencode/client": "2.0.2",
|
||||
"@opencode/plugin": "2.0.2",
|
||||
"@opencode/schema": "2.0.2",
|
||||
"@opentui/core": "0.5.10",
|
||||
"@opentui/solid": "0.5.10",
|
||||
"@oxlint/plugins": "1.82.0",
|
||||
"@types/bun": "1.3.14",
|
||||
"oxfmt": "0.67.0",
|
||||
"oxlint": "1.82.0",
|
||||
"solid-js": "1.9.15",
|
||||
"typescript": "7.0.2",
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@opencode/plugin": "0.0.0-beta-19378",
|
||||
"@opencode/plugin": "2.0.2",
|
||||
"@opentui/core": ">=0.5.10",
|
||||
"@opentui/solid": ">=0.5.10",
|
||||
"solid-js": ">=1.9.0",
|
||||
},
|
||||
},
|
||||
},
|
||||
"packages": {
|
||||
"@ai-sdk/provider": ["@ai-sdk/provider@3.0.8", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-oGMAgGoQdBXbZqNG0Ze56CHjDZ1IDYOwGYxYjO5KLSlz5HiNQ9udIXsPZ61VWaHGZ5XW/jyjmr6t2xz2jGVwbQ=="],
|
||||
|
||||
"@ampproject/remapping": ["@ampproject/remapping@2.3.0", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw=="],
|
||||
|
||||
"@aws-crypto/crc32": ["@aws-crypto/crc32@5.2.0", "", { "dependencies": { "@aws-crypto/util": "^5.2.0", "@aws-sdk/types": "^3.222.0", "tslib": "^2.6.2" } }, "sha512-nLbCWqQNgUiwwtFsen1AdzAtvuLRsQS8rYgMuxCrdKf9kOssamGLuPwyTY9wyYblNr9+1XM8v6zoDTPPSIeANg=="],
|
||||
|
||||
"@aws-crypto/sha256-browser": ["@aws-crypto/sha256-browser@5.2.0", "", { "dependencies": { "@aws-crypto/sha256-js": "^5.2.0", "@aws-crypto/supports-web-crypto": "^5.2.0", "@aws-crypto/util": "^5.2.0", "@aws-sdk/types": "^3.222.0", "@aws-sdk/util-locate-window": "^3.0.0", "@smithy/util-utf8": "^2.0.0", "tslib": "^2.6.2" } }, "sha512-AXfN/lGotSQwu6HNcEsIASo7kWXZ5HYWvfOmSNKDsEqC4OashTp8alTmaz+F7TC2L083SFv5RdB+qU3Vs1kZqw=="],
|
||||
@@ -72,6 +81,62 @@
|
||||
|
||||
"@aws/lambda-invoke-store": ["@aws/lambda-invoke-store@0.3.0", "", {}, "sha512-sl4Bm6yiMNYrZKkqqDFWN0UfnWhlS8ivKxrYl+6t0gCLrqr8y3B2IqZZbFRkfaVVp7C/baApyh71P+LeE1A2sQ=="],
|
||||
|
||||
"@babel/code-frame": ["@babel/code-frame@7.29.7", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.29.7", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw=="],
|
||||
|
||||
"@babel/compat-data": ["@babel/compat-data@7.29.7", "", {}, "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg=="],
|
||||
|
||||
"@babel/core": ["@babel/core@7.28.0", "", { "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.0", "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-module-transforms": "^7.27.3", "@babel/helpers": "^7.27.6", "@babel/parser": "^7.28.0", "@babel/template": "^7.27.2", "@babel/traverse": "^7.28.0", "@babel/types": "^7.28.0", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-UlLAnTPrFdNGoFtbSXwcGFQBtQZJCNjaN6hQNP3UPvuNXT1i82N26KL3dZeIpNalWywr9IuQuncaAfUaS1g6sQ=="],
|
||||
|
||||
"@babel/generator": ["@babel/generator@7.29.8", "", { "dependencies": { "@babel/parser": "^7.29.8", "@babel/types": "^7.29.8", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-gZbepsdh3WDtgZKWL+vTPh71LSBrm/Y4/QDZBVCcYfmeTEEuoOYwlSy+G1StfJg+/Zy550u/3TATbm7qDbbMtg=="],
|
||||
|
||||
"@babel/helper-annotate-as-pure": ["@babel/helper-annotate-as-pure@7.29.7", "", { "dependencies": { "@babel/types": "^7.29.7" } }, "sha512-OoK6239jHPuSQOoS0kfTVKn0b/rVTk0seKq4Gd2UMLtmOVLjDC0ki3e+c90Trqv2gMfvJFqkiljrr568+qddiw=="],
|
||||
|
||||
"@babel/helper-compilation-targets": ["@babel/helper-compilation-targets@7.29.7", "", { "dependencies": { "@babel/compat-data": "^7.29.7", "@babel/helper-validator-option": "^7.29.7", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" } }, "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g=="],
|
||||
|
||||
"@babel/helper-create-class-features-plugin": ["@babel/helper-create-class-features-plugin@7.29.7", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.29.7", "@babel/helper-member-expression-to-functions": "^7.29.7", "@babel/helper-optimise-call-expression": "^7.29.7", "@babel/helper-replace-supers": "^7.29.7", "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7", "@babel/traverse": "^7.29.7", "semver": "^6.3.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-IY3ZD9Tmooqr3TUhc3DUWxiuo8xx1DWLhd5M7hQ+ZWJamqM2BbalrBJb2MisSLoYorOj75U03qULCxQTY9r3hg=="],
|
||||
|
||||
"@babel/helper-globals": ["@babel/helper-globals@7.29.7", "", {}, "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA=="],
|
||||
|
||||
"@babel/helper-member-expression-to-functions": ["@babel/helper-member-expression-to-functions@7.29.7", "", { "dependencies": { "@babel/traverse": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-j+7JYmk1JYDtACIGj0QJqqWZjoUpMoEikQGADMaHgCMCSDqd2+P32rfcibUNrGOMWrlzK1WJBdxrB3JJQZwWtg=="],
|
||||
|
||||
"@babel/helper-module-imports": ["@babel/helper-module-imports@7.29.7", "", { "dependencies": { "@babel/traverse": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g=="],
|
||||
|
||||
"@babel/helper-module-transforms": ["@babel/helper-module-transforms@7.29.7", "", { "dependencies": { "@babel/helper-module-imports": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7", "@babel/traverse": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg=="],
|
||||
|
||||
"@babel/helper-optimise-call-expression": ["@babel/helper-optimise-call-expression@7.29.7", "", { "dependencies": { "@babel/types": "^7.29.7" } }, "sha512-+kmGVjcT9RGYzoDwdwEqEvGgKe3BYq+O1iGzjFubaNgZHwYHP6lsF2Yghf4kEuv9BV7tYDZ913aBW9am6YKong=="],
|
||||
|
||||
"@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.29.7", "", {}, "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw=="],
|
||||
|
||||
"@babel/helper-replace-supers": ["@babel/helper-replace-supers@7.29.7", "", { "dependencies": { "@babel/helper-member-expression-to-functions": "^7.29.7", "@babel/helper-optimise-call-expression": "^7.29.7", "@babel/traverse": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-atfGXWSeCiF4DnKZIfmJfQRkSw9b9gNNXR1kqKjbhG4pGYCOnkp8OcTB8E3NXjBu8NpheSnOeNKz8KT7UNFTmQ=="],
|
||||
|
||||
"@babel/helper-skip-transparent-expression-wrappers": ["@babel/helper-skip-transparent-expression-wrappers@7.29.7", "", { "dependencies": { "@babel/traverse": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-brcMGQaVzIeUb+6/bs1Av0f8YuNNjKY2JyvfRCsFuFsdKccEQ5Ges2y74D74NZ1Rz8lKJ9ksJkfqwQFJ/iNEyQ=="],
|
||||
|
||||
"@babel/helper-string-parser": ["@babel/helper-string-parser@7.29.7", "", {}, "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw=="],
|
||||
|
||||
"@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.29.7", "", {}, "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg=="],
|
||||
|
||||
"@babel/helper-validator-option": ["@babel/helper-validator-option@7.29.7", "", {}, "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw=="],
|
||||
|
||||
"@babel/helpers": ["@babel/helpers@7.29.7", "", { "dependencies": { "@babel/template": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg=="],
|
||||
|
||||
"@babel/parser": ["@babel/parser@7.29.8", "", { "dependencies": { "@babel/types": "^7.29.8" }, "bin": "./bin/babel-parser.js" }, "sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA=="],
|
||||
|
||||
"@babel/plugin-syntax-jsx": ["@babel/plugin-syntax-jsx@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-TSu8+mHCoEaaCDEZ0I3+6mvTBYR4PCxQwf2z9/r5Tbztv6NaLR3B9thGTTxX2WGuGHJqRiAbKPeGTJ5XWXVg6A=="],
|
||||
|
||||
"@babel/plugin-syntax-typescript": ["@babel/plugin-syntax-typescript@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-ngr+82Sh0xMz25TPCZi+nC2iTzjfCdWS2ONXTp/PtSCHCgaCNBpdMqgvJ2ccdLlClVZ7sisIgB914j/JFe+RZA=="],
|
||||
|
||||
"@babel/plugin-transform-modules-commonjs": ["@babel/plugin-transform-modules-commonjs@7.29.7", "", { "dependencies": { "@babel/helper-module-transforms": "^7.29.7", "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-j0vCldybPC5b5dwCQOJ21uKtHzt7hxLygJTg9eF1ScfaikEDNfzn94XoW5Fi+seBR0nCyL23xaBFFkq7dTM8XQ=="],
|
||||
|
||||
"@babel/plugin-transform-typescript": ["@babel/plugin-transform-typescript@7.29.7", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.29.7", "@babel/helper-create-class-features-plugin": "^7.29.7", "@babel/helper-plugin-utils": "^7.29.7", "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7", "@babel/plugin-syntax-typescript": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-jK52h8LaLc7JarhQV2ofeFMts4H7vnOXnqZNA6fYglBTZewRBE51KWt3BUltW1P+KoPsYkHoJeXePuz4zo2LMw=="],
|
||||
|
||||
"@babel/preset-typescript": ["@babel/preset-typescript@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-validator-option": "^7.27.1", "@babel/plugin-syntax-jsx": "^7.27.1", "@babel/plugin-transform-modules-commonjs": "^7.27.1", "@babel/plugin-transform-typescript": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-l7WfQfX0WK4M0v2RudjuQK4u99BS6yLHYEmdtVPP7lKV013zr9DygFuWNlnbvQ9LR+LS0Egz/XAvGx5U9MX0fQ=="],
|
||||
|
||||
"@babel/template": ["@babel/template@7.29.7", "", { "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/parser": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg=="],
|
||||
|
||||
"@babel/traverse": ["@babel/traverse@7.29.8", "", { "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/generator": "^7.29.8", "@babel/helper-globals": "^7.29.7", "@babel/parser": "^7.29.8", "@babel/template": "^7.29.7", "@babel/types": "^7.29.8", "debug": "^4.3.1" } }, "sha512-I5z7H3bf/41ktsNVLtpN0wAa336HkqIHQ5BuPLEhTkt1jVSyZpeNKIzTgEWmlxjdg81R0IgUCcaE+Ok3NvrfZg=="],
|
||||
|
||||
"@babel/types": ["@babel/types@7.29.8", "", { "dependencies": { "@babel/helper-string-parser": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7" } }, "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg=="],
|
||||
|
||||
"@effect/opentelemetry": ["@effect/opentelemetry@4.0.0-rc.112", "", { "peerDependencies": { "@opentelemetry/api": ">=1.9.0 <2.0.0", "@opentelemetry/api-logs": ">=0.203.0 <0.300.0", "@opentelemetry/resources": ">=2.0.0 <3.0.0", "@opentelemetry/sdk-logs": ">=0.203.0 <0.300.0", "@opentelemetry/sdk-metrics": ">=2.0.0 <3.0.0", "@opentelemetry/sdk-trace-base": ">=2.0.0 <3.0.0", "@opentelemetry/sdk-trace-node": ">=2.0.0 <3.0.0", "@opentelemetry/sdk-trace-web": ">=2.0.0 <3.0.0", "@opentelemetry/semantic-conventions": ">=1.33.0 <2.0.0", "effect": "^4.0.0-rc.112" }, "optionalPeers": ["@opentelemetry/api", "@opentelemetry/api-logs", "@opentelemetry/resources", "@opentelemetry/sdk-logs", "@opentelemetry/sdk-metrics", "@opentelemetry/sdk-trace-base", "@opentelemetry/sdk-trace-node", "@opentelemetry/sdk-trace-web"] }, "sha512-OTRv1DxTHUmnakgJ6XVM8wVgF1KgZH4UXnOemwSLUwUjXO+RCikzF8oR/rlVmOGq81KtQzj1URM4M4nchlQOuQ=="],
|
||||
|
||||
"@effect/platform-node": ["@effect/platform-node@4.0.0-rc.112", "", { "dependencies": { "@effect/platform-node-shared": "^4.0.0-rc.112", "mime": "^4.1.0", "undici": "^8.10.0" }, "peerDependencies": { "effect": "^4.0.0-rc.112", "redis": ">=5.0.0 <7.0.0" } }, "sha512-/BMAcdNGQQskLmI0Zoa95KfTZkr9HV9N4NSxaSrusG6GeW6Ulp9KvZ+Rlaiw8lnOt43CXjFLdfll5/k5rxL4hQ=="],
|
||||
@@ -86,6 +151,14 @@
|
||||
|
||||
"@isaacs/string-locale-compare": ["@isaacs/string-locale-compare@1.1.0", "", {}, "sha512-SQ7Kzhh9+D+ZW9MA0zkYv3VXhIDNx+LzM6EJ+/65I3QY+enU6Itte7E5XX7EWrqLW2FN4n06GWzBnPoC3th2aQ=="],
|
||||
|
||||
"@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="],
|
||||
|
||||
"@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="],
|
||||
|
||||
"@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.6.0", "", {}, "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw=="],
|
||||
|
||||
"@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="],
|
||||
|
||||
"@msgpackr-extract/msgpackr-extract-darwin-arm64": ["@msgpackr-extract/msgpackr-extract-darwin-arm64@3.0.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-LCkGo6JDfaBhgST7UpPWgNgLINpcpabaHfyz5OBx75nUYxBsaEPxjnyNjWpeb/xBup/682QnBfRBy2/LvPutZQ=="],
|
||||
|
||||
"@msgpackr-extract/msgpackr-extract-darwin-x64": ["@msgpackr-extract/msgpackr-extract-darwin-x64@3.0.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-zExlW9zUJKZH/tOtVMttwjKa4Xm/3KcNjnE3dPN92uCktwavMxpgCA3MoJK/DOnTWsQgo224OaST27/mPNAf+w=="],
|
||||
@@ -128,17 +201,17 @@
|
||||
|
||||
"@npmcli/run-script": ["@npmcli/run-script@10.0.4", "", { "dependencies": { "@npmcli/node-gyp": "^5.0.0", "@npmcli/package-json": "^7.0.0", "@npmcli/promise-spawn": "^9.0.0", "node-gyp": "^12.1.0", "proc-log": "^6.0.0" } }, "sha512-mGUWr1uMnf0le2TwfOZY4SFxZGXGfm4Jtay/nwAa2FLNAKXUoUwaGwBMNH36UHPtinWfTSJ3nqFQr0091CxVGg=="],
|
||||
|
||||
"@opencode/ai": ["@opencode/ai@0.0.0-beta-19378", "", { "dependencies": { "@aws-sdk/credential-providers": "3.1057.0", "@opencode/schema": "0.0.0-beta-19378", "@smithy/eventstream-codec": "4.2.14", "@smithy/util-utf8": "4.2.2", "aws4fetch": "1.0.20", "effect": "4.0.0-rc.112", "google-auth-library": "10.5.0" } }, "sha512-mk20W9+HH82jT8VLowhu9weV6Qkch+ydbfPRp2FJFh53RP9up1YaHLDu1o913biSJQNa36Q/9shln7YIOJ2PNA=="],
|
||||
"@opencode/ai": ["@opencode/ai@2.0.2", "", { "dependencies": { "@aws-sdk/credential-providers": "3.1057.0", "@opencode/schema": "2.0.2", "@smithy/eventstream-codec": "4.2.14", "@smithy/util-utf8": "4.2.2", "aws4fetch": "1.0.20", "effect": "4.0.0-rc.112", "google-auth-library": "10.5.0" } }, "sha512-Byq9c3kRdIl+AfGidPkd3JUC6DnjaNXGem77sAFRSxYwA0AMJMImPm+ua6ibnRqJ+6HlEq9JSh+Z1nQ8s8xzrQ=="],
|
||||
|
||||
"@opencode/client": ["@opencode/client@0.0.0-beta-19378", "", { "dependencies": { "@opencode/protocol": "0.0.0-beta-19378", "@opencode/schema": "0.0.0-beta-19378" }, "peerDependencies": { "effect": "4.0.0-rc.112", "solid-js": ">=1.9.0" }, "optionalPeers": ["effect", "solid-js"] }, "sha512-tYhOVbCt4RFIxw3DYvX9NREomExR4QCE/K/mFoUxkU8dlhQNCDDT8ztztDkLyLycYwwvAJ+wKt4La6vH0/s8Pg=="],
|
||||
"@opencode/client": ["@opencode/client@2.0.2", "", { "dependencies": { "@opencode/protocol": "2.0.2", "@opencode/schema": "2.0.2" }, "peerDependencies": { "effect": "4.0.0-rc.112", "solid-js": ">=1.9.0" }, "optionalPeers": ["effect", "solid-js"] }, "sha512-w8rrSsxY9qnuFrsQAHdLDE6mrEqUKQeo7fNsw2i/tFtimgspT53/8/3ckEVRuRUM5k6v68KiU2deG0W2gEIVDQ=="],
|
||||
|
||||
"@opencode/plugin": ["@opencode/plugin@0.0.0-beta-19378", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@opencode/ai": "0.0.0-beta-19378", "@opencode/client": "0.0.0-beta-19378", "@opencode/protocol": "0.0.0-beta-19378", "@opencode/schema": "0.0.0-beta-19378", "@opencode/util": "0.0.0-beta-19378", "@standard-schema/spec": "1.1.0", "effect": "4.0.0-rc.112", "zod": "4.1.8" }, "peerDependencies": { "@opencode/theme": "0.0.0-beta-19378", "@opentui/core": ">=0.5.10", "@opentui/solid": ">=0.5.10", "solid-js": ">=1.9.0" }, "optionalPeers": ["@opencode/theme", "@opentui/core", "@opentui/solid", "solid-js"] }, "sha512-Kzlp3gpem5QSzmEkrUOZPABnFy6a+m5wRGskXqt1KrJbXMIjrW9xnluFn81iHpiHEWRkQZOVZssw6VKPgyXQtA=="],
|
||||
"@opencode/plugin": ["@opencode/plugin@2.0.2", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@opencode/ai": "2.0.2", "@opencode/client": "2.0.2", "@opencode/protocol": "2.0.2", "@opencode/schema": "2.0.2", "@opencode/util": "2.0.2", "@standard-schema/spec": "1.1.0", "effect": "4.0.0-rc.112", "zod": "4.1.8" }, "peerDependencies": { "@opencode/theme": "2.0.2", "@opentui/core": ">=0.5.10", "@opentui/solid": ">=0.5.10", "solid-js": ">=1.9.0" }, "optionalPeers": ["@opencode/theme", "@opentui/core", "@opentui/solid", "solid-js"] }, "sha512-lFXcsyg1fhzCpzuMqBhb2yNddMeynXZwXL3aHs1T9ftbFu4buFAdtE9ox09Q+CL/r7yGbbX4mdtjKYdnt0dn5Q=="],
|
||||
|
||||
"@opencode/protocol": ["@opencode/protocol@0.0.0-beta-19378", "", { "dependencies": { "@opencode/schema": "0.0.0-beta-19378", "effect": "4.0.0-rc.112" } }, "sha512-BXh+ygV/xah3LYD12i0eVH69JW6m+7vVK7MiSUnKm+T88G8DT0ytJmqFcLYtoa1EcDNnujcmHMrLtHheTn4b4g=="],
|
||||
"@opencode/protocol": ["@opencode/protocol@2.0.2", "", { "dependencies": { "@opencode/schema": "2.0.2", "effect": "4.0.0-rc.112" } }, "sha512-FxTxdHPNz46NkG8ZBB2S60QzXbZDlVEyZ6KeYTt2G7hcWB5JlMYBh8jeqHlsLbquKinvMKA2iuEsA1NaccN+YQ=="],
|
||||
|
||||
"@opencode/schema": ["@opencode/schema@0.0.0-beta-19378", "", { "dependencies": { "@standard-schema/spec": "1.1.0", "effect": "4.0.0-rc.112" } }, "sha512-aBilB58tyzWEE44gNmZXZpu5Xw2W3mNV3vrdKCX19z2VRoaZGY8BRhjDwFNGGBoZ9U5KXp9e0C0GKydUuKxT/w=="],
|
||||
"@opencode/schema": ["@opencode/schema@2.0.2", "", { "dependencies": { "@standard-schema/spec": "1.1.0", "effect": "4.0.0-rc.112" } }, "sha512-jF23H41j6zM2Az1pYLD6NFfzpoQ79CX0AxRkfNxTZFbPfjGGc+EPSOzVQP/r4Y/5bkSSqVga0Vn7MyoP8QhwDQ=="],
|
||||
|
||||
"@opencode/util": ["@opencode/util@0.0.0-beta-19378", "", { "dependencies": { "@effect/opentelemetry": "4.0.0-rc.112", "@effect/platform-node": "4.0.0-rc.112", "@effect/platform-node-shared": "4.0.0-rc.112", "@npmcli/arborist": "9.4.0", "@npmcli/config": "10.8.1", "@opentelemetry/api": "1.9.0", "@opentelemetry/context-async-hooks": "2.6.1", "@opentelemetry/exporter-trace-otlp-http": "0.214.0", "@opentelemetry/sdk-trace-base": "2.6.1", "@opentelemetry/sdk-trace-node": "2.6.1", "cross-spawn": "7.0.6", "effect": "4.0.0-rc.112", "glob": "13.0.5", "mime-types": "3.0.2", "minimatch": "10.2.5", "npm-package-arg": "13.0.2", "pacote": "21.5.1" } }, "sha512-YNAWKFt348p9UZWQJ5wyazTq8TOPrStaLnd7bA8m5E17CaULtKnxeiKBIcG095cpeqIZ+/JCQBurcqOZ7UbDrA=="],
|
||||
"@opencode/util": ["@opencode/util@2.0.2", "", { "dependencies": { "@effect/opentelemetry": "4.0.0-rc.112", "@effect/platform-node": "4.0.0-rc.112", "@effect/platform-node-shared": "4.0.0-rc.112", "@npmcli/arborist": "9.4.0", "@npmcli/config": "10.8.1", "@opentelemetry/api": "1.9.0", "@opentelemetry/context-async-hooks": "2.6.1", "@opentelemetry/exporter-trace-otlp-http": "0.214.0", "@opentelemetry/sdk-trace-base": "2.6.1", "@opentelemetry/sdk-trace-node": "2.6.1", "cross-spawn": "7.0.6", "effect": "4.0.0-rc.112", "glob": "13.0.5", "mime-types": "3.0.2", "minimatch": "10.2.5", "npm-package-arg": "13.0.2", "pacote": "21.5.1" } }, "sha512-AKnFr44s1Q7/Ud+Pl4QMCEJoIp33xyDiTi693BofXn+HBDHdJmKrwq87YpG+Kwt9t48gVv6Cx1glmqJ4lVJpHg=="],
|
||||
|
||||
"@opentelemetry/api": ["@opentelemetry/api@1.9.0", "", {}, "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg=="],
|
||||
|
||||
@@ -166,6 +239,26 @@
|
||||
|
||||
"@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.43.0", "", {}, "sha512-eSYWTm620tTk45EKSedaUL8MFYI8hW164hIXsgIHyxu3VobUB3fFCu5t0hQby6OoWRPsG1KkKUG2M5UadiLiVg=="],
|
||||
|
||||
"@opentui/core": ["@opentui/core@0.5.10", "", { "dependencies": { "bun-ffi-structs": "0.3.1", "diff": "9.0.0", "marked": "17.0.1", "string-width": "7.2.0", "strip-ansi": "7.1.2" }, "optionalDependencies": { "@opentui/core-darwin-arm64": "0.5.10", "@opentui/core-darwin-x64": "0.5.10", "@opentui/core-linux-arm64": "0.5.10", "@opentui/core-linux-arm64-musl": "0.5.10", "@opentui/core-linux-x64": "0.5.10", "@opentui/core-linux-x64-musl": "0.5.10", "@opentui/core-win32-arm64": "0.5.10", "@opentui/core-win32-x64": "0.5.10" }, "peerDependencies": { "web-tree-sitter": "0.25.10" } }, "sha512-C3a2UbmefeAjIxAgm4BqjuSxKT4oqutfvYFwVvUgMxmGRHkNbBc/s7sukV0JgwcxFcV3uMFrXxo+E+BQtvuOiw=="],
|
||||
|
||||
"@opentui/core-darwin-arm64": ["@opentui/core-darwin-arm64@0.5.10", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Vyb+nTbhab8ZcRy5gg1loEEGwRcIbjAeVRIBfHBcbFDqmITBOg7x2gqJ+x/TnoOy4uwMhCmICUN2wiyREw3r1Q=="],
|
||||
|
||||
"@opentui/core-darwin-x64": ["@opentui/core-darwin-x64@0.5.10", "", { "os": "darwin", "cpu": "x64" }, "sha512-tTFLcM7Oj1gTyhm/bUdAt3C6grZdCxPk6+/g2azcZBUlI3/62LwbeRS6HbQKFFmm+1fUmX8cq6kWrtul885mVg=="],
|
||||
|
||||
"@opentui/core-linux-arm64": ["@opentui/core-linux-arm64@0.5.10", "", { "os": "linux", "cpu": "arm64" }, "sha512-ncJXcgudhBf2GdJyF3xVQN/Ec+1F7GOL+pRrURmgBYSj2v1w6EyoDQFAACtPTK2c3R38W6fvZwL4JSLlm4EFXQ=="],
|
||||
|
||||
"@opentui/core-linux-arm64-musl": ["@opentui/core-linux-arm64-musl@0.5.10", "", { "os": "linux", "cpu": "arm64" }, "sha512-dGMphDKexSdeYqwl0wgoFBP88Ta/cdi1Zc1mk29/ENkSCGz+74zlCHgqTHRNGLmI8W5TfuUtCyktQH11/Z+TBQ=="],
|
||||
|
||||
"@opentui/core-linux-x64": ["@opentui/core-linux-x64@0.5.10", "", { "os": "linux", "cpu": "x64" }, "sha512-5qtYaOgwVycZD1GaGshTRsi0rXPAmVExO03N1JQaHu+NYxK/vXSOc7Bu4QW0sPXx3Sp0SpzpP+FHjXABfoK66g=="],
|
||||
|
||||
"@opentui/core-linux-x64-musl": ["@opentui/core-linux-x64-musl@0.5.10", "", { "os": "linux", "cpu": "x64" }, "sha512-Oj4H9hApuvuTKPWxh4SoZAgGJorR7vbvnrZA/cAkSMAk2VGSoHRRcqeXQbcH8IcdjVZ0KFpv8Zkl/D5Ye+2mew=="],
|
||||
|
||||
"@opentui/core-win32-arm64": ["@opentui/core-win32-arm64@0.5.10", "", { "os": "win32", "cpu": "arm64" }, "sha512-A9VhgvTxQoUdZ+8LmUumEng1sQNbj9QQQT3NYG9mSxI54qTANi7vOWNSphMiY6RMVsr22pgm6nUvSSvJXv7Jog=="],
|
||||
|
||||
"@opentui/core-win32-x64": ["@opentui/core-win32-x64@0.5.10", "", { "os": "win32", "cpu": "x64" }, "sha512-u3KHa7kEeWrmKVDRJYpxSGO+g5E9cMGlrmTsPN3GVPHUmQMiREUawLXUvsU8+IHaQnqG3Q5nuE1yf4fPBzS+Qw=="],
|
||||
|
||||
"@opentui/solid": ["@opentui/solid@0.5.10", "", { "dependencies": { "@babel/core": "7.28.0", "@babel/preset-typescript": "7.27.1", "@opentui/core": "0.5.10", "babel-plugin-module-resolver": "5.0.2", "babel-preset-solid": "1.9.12", "entities": "7.0.1", "s-js": "^0.4.9" }, "peerDependencies": { "solid-js": "1.9.12" } }, "sha512-KrmMIsHiKBHOABTC0brOwqWm+sGq1ZX2sGCAx6WgtBbE3STMup9n8TAy/6gUYhwcjC9zugT53ytfSVwCwVWZUg=="],
|
||||
|
||||
"@oxfmt/binding-android-arm-eabi": ["@oxfmt/binding-android-arm-eabi@0.67.0", "", { "os": "android", "cpu": "arm" }, "sha512-2olh3ioEmc4gRzQm7jxyB1b/PFBoFvTq8KdgYySeNpysDtA6DEg2Mvya4/I6flhL7G0eOrE8RD7JCNCIMhE16Q=="],
|
||||
|
||||
"@oxfmt/binding-android-arm64": ["@oxfmt/binding-android-arm64@0.67.0", "", { "os": "android", "cpu": "arm64" }, "sha512-ulfw8EHN1MBq/MFFDXw2/M1VAFu5mRUcnuZ8Hqbv9viAnFzO9t1jKSAsDqKYYDGMlytF/uj6Z5z5n/tHupnKhw=="],
|
||||
@@ -370,10 +463,18 @@
|
||||
|
||||
"aws4fetch": ["aws4fetch@1.0.20", "", {}, "sha512-/djoAN709iY65ETD6LKCtyyEI04XIBP5xVvfmNxsEP0uJB5tyaGBztSryRr4HqMStr9R06PisQE7m9zDTXKu6g=="],
|
||||
|
||||
"babel-plugin-jsx-dom-expressions": ["babel-plugin-jsx-dom-expressions@0.40.10", "", { "dependencies": { "@babel/helper-module-imports": "7.18.6", "@babel/plugin-syntax-jsx": "^7.18.6", "@babel/types": "^7.20.7", "html-entities": "2.3.3", "parse5": "^7.1.2" }, "peerDependencies": { "@babel/core": "^7.20.12" } }, "sha512-lxve6Y02YiZTldB7efKpnbf1BH00XCFZNYYW235jSGsYaJNFtHrYlKV6/O+miHbjqpIr9FTe5+0no4hofAMbfA=="],
|
||||
|
||||
"babel-plugin-module-resolver": ["babel-plugin-module-resolver@5.0.2", "", { "dependencies": { "find-babel-config": "^2.1.1", "glob": "^9.3.3", "pkg-up": "^3.1.0", "reselect": "^4.1.7", "resolve": "^1.22.8" } }, "sha512-9KtaCazHee2xc0ibfqsDeamwDps6FZNo5S0Q81dUqEuFzVwPhcT4J5jOqIVvgCA3Q/wO9hKYxN/Ds3tIsp5ygg=="],
|
||||
|
||||
"babel-preset-solid": ["babel-preset-solid@1.9.12", "", { "dependencies": { "babel-plugin-jsx-dom-expressions": "^0.40.6" }, "peerDependencies": { "@babel/core": "^7.0.0", "solid-js": "^1.9.12" }, "optionalPeers": ["solid-js"] }, "sha512-LLqnuKVDlKpyBlMPcH6qEvs/wmS9a+NczppxJ3ryS/c0O5IiSFOIBQi9GzyiGDSbcJpx4Gr87jyFTos1MyEuWg=="],
|
||||
|
||||
"balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="],
|
||||
|
||||
"base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="],
|
||||
|
||||
"baseline-browser-mapping": ["baseline-browser-mapping@2.11.22", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-pWc4w51fBFd7mav43/zKRC+RI6f4yfzQoVlfvE8dECePyfkn1bzLp01Fj0QACcyCZyFhiEMyD2qScfKRWgWibA=="],
|
||||
|
||||
"bignumber.js": ["bignumber.js@9.3.1", "", {}, "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ=="],
|
||||
|
||||
"bin-links": ["bin-links@6.0.2", "", { "dependencies": { "cmd-shim": "^8.0.0", "npm-normalize-package-bin": "^5.0.0", "proc-log": "^6.0.0", "read-cmd-shim": "^6.0.0", "write-file-atomic": "^7.0.0" } }, "sha512-frE1t78WOwJ45PKV2cF2tNPjTcs9L1J9s6VkrV59wanRP4GlaomuxYPVma7BwthMg8WnfSory4w5PTE6FZZ81w=="],
|
||||
@@ -382,12 +483,18 @@
|
||||
|
||||
"brace-expansion": ["brace-expansion@5.0.9", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg=="],
|
||||
|
||||
"browserslist": ["browserslist@4.28.9", "", { "dependencies": { "baseline-browser-mapping": "^2.11.20", "caniuse-lite": "^1.0.30001810", "electron-to-chromium": "^1.5.420", "node-releases": "^2.0.54", "update-browserslist-db": "^1.3.2" }, "bin": { "browserslist": "cli.js" } }, "sha512-EWazOblFYUvlGZcfGhPUPmYh3nikUxBVb+y9MJun5f3hBi812X+8MSQTujLBtgK3cf51fJWbWfOjyeO954d+Eg=="],
|
||||
|
||||
"buffer-equal-constant-time": ["buffer-equal-constant-time@1.0.1", "", {}, "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA=="],
|
||||
|
||||
"bun-ffi-structs": ["bun-ffi-structs@0.3.1", "", { "peerDependencies": { "typescript": "^5" } }, "sha512-3gM7PpVWLyrwxWjcilSiGuhWanhZivvo6l0u573NziPH6f/gwk6McbaYgn7oJWov6pKGRTDbrg94W5DcJsKTtQ=="],
|
||||
|
||||
"bun-types": ["bun-types@1.3.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-4N0ig0fEomHt5R0KCFWjovxow98rIoRwKolrYdCcknNwMekCXRnWEUvgu5soYV8QXtVsrUD8B95MBOZGPvr6KQ=="],
|
||||
|
||||
"cacache": ["cacache@20.0.4", "", { "dependencies": { "@npmcli/fs": "^5.0.0", "fs-minipass": "^3.0.0", "glob": "^13.0.0", "lru-cache": "^11.1.0", "minipass": "^7.0.3", "minipass-collect": "^2.0.1", "minipass-flush": "^1.0.5", "minipass-pipeline": "^1.2.4", "p-map": "^7.0.2", "ssri": "^13.0.0" } }, "sha512-M3Lab8NPYlZU2exsL3bMVvMrMqgwCnMWfdZbK28bn3pK6APT/Te/I8hjRPNu1uwORY9a1eEQoifXbKPQMfMTOA=="],
|
||||
|
||||
"caniuse-lite": ["caniuse-lite@1.0.30001810", "", {}, "sha512-TITQPUkaz+aVk5GL6NhOdwk1aEaNTSDPsGFWrTuhKGtjTF70jL/Oht2W4c6rXUe5fu7Ie19VIahAXHIIiWWNeg=="],
|
||||
|
||||
"chownr": ["chownr@3.0.0", "", {}, "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g=="],
|
||||
|
||||
"ci-info": ["ci-info@4.4.0", "", {}, "sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg=="],
|
||||
@@ -404,26 +511,40 @@
|
||||
|
||||
"content-type": ["content-type@2.1.0", "", {}, "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag=="],
|
||||
|
||||
"convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="],
|
||||
|
||||
"cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="],
|
||||
|
||||
"cssesc": ["cssesc@3.0.0", "", { "bin": { "cssesc": "bin/cssesc" } }, "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg=="],
|
||||
|
||||
"csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="],
|
||||
|
||||
"data-uri-to-buffer": ["data-uri-to-buffer@4.0.1", "", {}, "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A=="],
|
||||
|
||||
"debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" }, "peerDependencies": { "supports-color": "*" }, "optionalPeers": ["supports-color"] }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
|
||||
|
||||
"detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="],
|
||||
|
||||
"diff": ["diff@9.0.0", "", {}, "sha512-svtcdpS8CgJyqAjEQIXdb3OjhFVVYjzGAPO8WGCmRbrml64SPw/jJD4GoE98aR7r25A0XcgrK3F02yw9R/vhQw=="],
|
||||
|
||||
"eastasianwidth": ["eastasianwidth@0.2.0", "", {}, "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA=="],
|
||||
|
||||
"ecdsa-sig-formatter": ["ecdsa-sig-formatter@1.0.11", "", { "dependencies": { "safe-buffer": "^5.0.1" } }, "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ=="],
|
||||
|
||||
"effect": ["effect@4.0.0-rc.112", "", { "dependencies": { "fast-check": "^4.9.0", "msgpackr": "^2.0.5" } }, "sha512-wXxwuh1Ywnv4cPRM3Wfa0vDwuOHnZ1TsTgHJkG9XgzND6inhBH9n1vBxhg3iIXOia/OrpmvVmd3lrD4vq6bF3A=="],
|
||||
|
||||
"emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="],
|
||||
"electron-to-chromium": ["electron-to-chromium@1.5.427", "", {}, "sha512-n14zb3FdsChZ2BNobqNHAJMcP3ifFv4paox2LvCrfVAQcqGiSURgbJl+PfMpHVCNFkStnNc+RRVtPBTVW5PDgw=="],
|
||||
|
||||
"emoji-regex": ["emoji-regex@10.6.0", "", {}, "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A=="],
|
||||
|
||||
"entities": ["entities@7.0.1", "", {}, "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA=="],
|
||||
|
||||
"env-paths": ["env-paths@2.2.1", "", {}, "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A=="],
|
||||
|
||||
"es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="],
|
||||
|
||||
"escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="],
|
||||
|
||||
"exponential-backoff": ["exponential-backoff@3.1.3", "", {}, "sha512-ZgEeZXj30q+I0EN+CbSSpIyPaJ5HVQD18Z1m+u1FXbAeT94mr1zw50q4q6jiiC447Nl/YTcIYSAftiGqetwXCA=="],
|
||||
|
||||
"extend": ["extend@3.0.2", "", {}, "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="],
|
||||
@@ -434,16 +555,28 @@
|
||||
|
||||
"fetch-blob": ["fetch-blob@3.2.0", "", { "dependencies": { "node-domexception": "^1.0.0", "web-streams-polyfill": "^3.0.3" } }, "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ=="],
|
||||
|
||||
"find-babel-config": ["find-babel-config@2.1.2", "", { "dependencies": { "json5": "^2.2.3" } }, "sha512-ZfZp1rQyp4gyuxqt1ZqjFGVeVBvmpURMqdIWXbPRfB97Bf6BzdK/xSIbylEINzQ0kB5tlDQfn9HkNXXWsqTqLg=="],
|
||||
|
||||
"find-up": ["find-up@3.0.0", "", { "dependencies": { "locate-path": "^3.0.0" } }, "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg=="],
|
||||
|
||||
"foreground-child": ["foreground-child@3.3.1", "", { "dependencies": { "cross-spawn": "^7.0.6", "signal-exit": "^4.0.1" } }, "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw=="],
|
||||
|
||||
"formdata-polyfill": ["formdata-polyfill@4.0.10", "", { "dependencies": { "fetch-blob": "^3.1.2" } }, "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g=="],
|
||||
|
||||
"fs-minipass": ["fs-minipass@3.0.3", "", { "dependencies": { "minipass": "^7.0.3" } }, "sha512-XUBA9XClHbnJWSfBzjkm6RvPsyg3sryZt06BEQoXcF7EK/xpGaQYJgQKDJSUH5SGZ76Y7pFx1QBnXz09rU5Fbw=="],
|
||||
|
||||
"fs.realpath": ["fs.realpath@1.0.0", "", {}, "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw=="],
|
||||
|
||||
"function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="],
|
||||
|
||||
"gaxios": ["gaxios@7.3.1", "", { "dependencies": { "extend": "^3.0.2", "https-proxy-agent": "^7.0.1", "node-fetch": "^3.3.2" } }, "sha512-kB3rzJV7d9juLZh8/56QTXCwQfxyhdOMdyYk1HdQKFtF8TJTDTZQJtixWIwXdE9Jji91mC41DUNpjleo4L4eAQ=="],
|
||||
|
||||
"gcp-metadata": ["gcp-metadata@8.1.4", "", { "dependencies": { "gaxios": "7.1.3", "google-logging-utils": "1.1.3", "json-bigint": "^1.0.0" } }, "sha512-iJ9KMsiu+xKtNRX0PmGLSaIU3bUBAyzWTyqKemKPzNPsmmsBCQYmlNg+brEbES7IHSXtdVwzBPzx1vz3FAaipw=="],
|
||||
|
||||
"gensync": ["gensync@1.0.0-beta.2", "", {}, "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg=="],
|
||||
|
||||
"get-east-asian-width": ["get-east-asian-width@1.6.0", "", {}, "sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA=="],
|
||||
|
||||
"glob": ["glob@13.0.5", "", { "dependencies": { "minimatch": "^10.2.1", "minipass": "^7.1.2", "path-scurry": "^2.0.0" } }, "sha512-BzXxZg24Ibra1pbQ/zE7Kys4Ua1ks7Bn6pKLkVPZ9FZe4JQS6/Q7ef3LG1H+k7lUf5l4T3PLSyYyYJVYUvfgTw=="],
|
||||
|
||||
"google-auth-library": ["google-auth-library@10.5.0", "", { "dependencies": { "base64-js": "^1.3.0", "ecdsa-sig-formatter": "^1.0.11", "gaxios": "^7.0.0", "gcp-metadata": "^8.0.0", "google-logging-utils": "^1.0.0", "gtoken": "^8.0.0", "jws": "^4.0.0" } }, "sha512-7ABviyMOlX5hIVD60YOfHw4/CxOfBhyduaYB+wbFWCWoni4N7SLcV46hrVRktuBbZjFC9ONyqamZITN7q3n32w=="],
|
||||
@@ -454,8 +587,12 @@
|
||||
|
||||
"gtoken": ["gtoken@8.0.0", "", { "dependencies": { "gaxios": "^7.0.0", "jws": "^4.0.0" } }, "sha512-+CqsMbHPiSTdtSO14O51eMNlrp9N79gmeqmXeouJOhfucAedHw9noVe/n5uJk3tbKE6a+6ZCQg3RPhVhHByAIw=="],
|
||||
|
||||
"hasown": ["hasown@2.0.4", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A=="],
|
||||
|
||||
"hosted-git-info": ["hosted-git-info@9.0.3", "", { "dependencies": { "lru-cache": "^11.1.0" } }, "sha512-Hc+ghLoSt6QaYZUv0WBiIvmMDZuZZ7oaDvdH8MbfOO4lOsxdXLEvuC6ePoGs9H1X9oCLyq6+NVN0MKqD+ydxyg=="],
|
||||
|
||||
"html-entities": ["html-entities@2.3.3", "", {}, "sha512-DV5Ln36z34NNTDgnz0EWGBLZENelNAtkiFA4kyNOG2tDI6Mz1uSWiq1wAKdyjnJwyDiDO7Fa2SO1CTxPXL8VxA=="],
|
||||
|
||||
"http-cache-semantics": ["http-cache-semantics@4.2.0", "", {}, "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ=="],
|
||||
|
||||
"http-proxy-agent": ["http-proxy-agent@7.0.2", "", { "dependencies": { "agent-base": "^7.1.0", "debug": "^4.3.4" } }, "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig=="],
|
||||
@@ -470,12 +607,18 @@
|
||||
|
||||
"ip-address": ["ip-address@10.7.0", "", {}, "sha512-BGFsyJd5mpXp3rK6jIdADLNgpJUK1jnjzvYF8lK+VyDab9JAmqN0YOKDdP17HlgKb2+ehPgDc8EtnRLbGCAMhA=="],
|
||||
|
||||
"is-core-module": ["is-core-module@2.16.2", "", { "dependencies": { "hasown": "^2.0.3" } }, "sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA=="],
|
||||
|
||||
"is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="],
|
||||
|
||||
"isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="],
|
||||
|
||||
"jackspeak": ["jackspeak@3.4.3", "", { "dependencies": { "@isaacs/cliui": "^8.0.2" }, "optionalDependencies": { "@pkgjs/parseargs": "^0.11.0" } }, "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw=="],
|
||||
|
||||
"js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="],
|
||||
|
||||
"jsesc": ["jsesc@3.1.0", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="],
|
||||
|
||||
"json-bigint": ["json-bigint@1.0.0", "", { "dependencies": { "bignumber.js": "^9.0.0" } }, "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ=="],
|
||||
|
||||
"json-parse-even-better-errors": ["json-parse-even-better-errors@5.0.0", "", {}, "sha512-ZF1nxZ28VhQouRWhUcVlUIN3qwSgPuswK05s/HIaoetAoE/9tngVmCHjSxmSQPav1nd+lPtTL0YZ/2AFdR/iYQ=="],
|
||||
@@ -484,6 +627,8 @@
|
||||
|
||||
"json-stringify-nice": ["json-stringify-nice@1.1.4", "", {}, "sha512-5Z5RFW63yxReJ7vANgW6eZFGWaQvnPE3WNmZoOJrSkGju2etKA2L5rrOa1sm877TVTFt57A80BH1bArcmlLfPw=="],
|
||||
|
||||
"json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="],
|
||||
|
||||
"jsonparse": ["jsonparse@1.3.1", "", {}, "sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg=="],
|
||||
|
||||
"just-diff": ["just-diff@6.0.2", "", {}, "sha512-S59eriX5u3/QhMNq3v/gm8Kd0w8OS6Tz2FS1NG4blv+z0MuQcBRJyFWjdovM0Rad4/P4aUPFtnkNjMjyMlMSYA=="],
|
||||
@@ -494,12 +639,16 @@
|
||||
|
||||
"jws": ["jws@4.0.1", "", { "dependencies": { "jwa": "^2.0.1", "safe-buffer": "^5.0.1" } }, "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA=="],
|
||||
|
||||
"locate-path": ["locate-path@3.0.0", "", { "dependencies": { "p-locate": "^3.0.0", "path-exists": "^3.0.0" } }, "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A=="],
|
||||
|
||||
"long": ["long@5.3.2", "", {}, "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA=="],
|
||||
|
||||
"lru-cache": ["lru-cache@11.5.2", "", {}, "sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g=="],
|
||||
|
||||
"make-fetch-happen": ["make-fetch-happen@15.0.6", "", { "dependencies": { "@gar/promise-retry": "^1.0.0", "@npmcli/agent": "^4.0.0", "@npmcli/redact": "^4.0.0", "cacache": "^20.0.1", "http-cache-semantics": "^4.1.1", "minipass": "^7.0.2", "minipass-fetch": "^5.0.0", "minipass-flush": "^1.0.5", "minipass-pipeline": "^1.2.4", "negotiator": "^1.0.0", "proc-log": "^6.0.0", "ssri": "^13.0.0" } }, "sha512-Je0fLJ0F5atA7F+eIlLzk+Wkcl57JDf4kf+EW8xiP5E31xOQxkIxTbgf1Oi1Lw9tRI9UEMRdI5Vz2xTzoNU1Jw=="],
|
||||
|
||||
"marked": ["marked@17.0.1", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-boeBdiS0ghpWcSwoNm/jJBwdpFaMnZWRzjA6SkUMYb40SVaN1x7mmfGKp0jvexGcx+7y2La5zRZsYFZI6Qpypg=="],
|
||||
|
||||
"mime": ["mime@4.1.0", "", { "bin": { "mime": "bin/cli.js" } }, "sha512-X5ju04+cAzsojXKes0B/S4tcYtFAJ6tTMuSPBEn9CPGlrWr8Fiw7qYeLT0XyH80HSoAoqWCaz+MWKh22P7G1cw=="],
|
||||
|
||||
"mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="],
|
||||
@@ -538,6 +687,8 @@
|
||||
|
||||
"node-gyp-build-optional-packages": ["node-gyp-build-optional-packages@5.2.2", "", { "dependencies": { "detect-libc": "^2.0.1" }, "bin": { "node-gyp-build-optional-packages": "bin.js", "node-gyp-build-optional-packages-optional": "optional.js", "node-gyp-build-optional-packages-test": "build-test.js" } }, "sha512-s+w+rBWnpTMwSFbaE0UXsRlg7hU4FjekKU4eyAih5T8nJuNZT1nNsskXpxmeqSK9UzkBl6UgRlnKc8hz8IEqOw=="],
|
||||
|
||||
"node-releases": ["node-releases@2.0.55", "", {}, "sha512-mIrE/Cw9y+9Au6dS5vDKDhQza9YvG6w+ZrS6X+ZzA7yFW/soAeaups4Qzn1bL6g5FVy8WtP79+0j82oPIbqRjQ=="],
|
||||
|
||||
"nopt": ["nopt@9.0.0", "", { "dependencies": { "abbrev": "^4.0.0" }, "bin": { "nopt": "bin/nopt.js" } }, "sha512-Zhq3a+yFKrYwSBluL4H9XP3m3y5uvQkB/09CwDruCiRmR/UJYnn9W4R48ry0uGC70aeTPKLynBtscP9efFFcPw=="],
|
||||
|
||||
"npm-bundled": ["npm-bundled@5.0.0", "", { "dependencies": { "npm-normalize-package-bin": "^5.0.0" } }, "sha512-JLSpbzh6UUXIEoqPsYBvVNVmyrjVZ1fzEFbqxKkTJQkWBO3xFzFT+KDnSKQWwOQNbuWRwt5LSD6HOTLGIWzfrw=="],
|
||||
@@ -558,20 +709,36 @@
|
||||
|
||||
"oxlint": ["oxlint@1.82.0", "", { "optionalDependencies": { "@oxlint/binding-android-arm-eabi": "1.82.0", "@oxlint/binding-android-arm64": "1.82.0", "@oxlint/binding-darwin-arm64": "1.82.0", "@oxlint/binding-darwin-x64": "1.82.0", "@oxlint/binding-freebsd-x64": "1.82.0", "@oxlint/binding-linux-arm-gnueabihf": "1.82.0", "@oxlint/binding-linux-arm-musleabihf": "1.82.0", "@oxlint/binding-linux-arm64-gnu": "1.82.0", "@oxlint/binding-linux-arm64-musl": "1.82.0", "@oxlint/binding-linux-ppc64-gnu": "1.82.0", "@oxlint/binding-linux-riscv64-gnu": "1.82.0", "@oxlint/binding-linux-riscv64-musl": "1.82.0", "@oxlint/binding-linux-s390x-gnu": "1.82.0", "@oxlint/binding-linux-x64-gnu": "1.82.0", "@oxlint/binding-linux-x64-musl": "1.82.0", "@oxlint/binding-openharmony-arm64": "1.82.0", "@oxlint/binding-win32-arm64-msvc": "1.82.0", "@oxlint/binding-win32-ia32-msvc": "1.82.0", "@oxlint/binding-win32-x64-msvc": "1.82.0" }, "peerDependencies": { "oxlint-tsgolint": ">=7.0.2001", "vite-plus": "*" }, "optionalPeers": ["oxlint-tsgolint", "vite-plus"], "bin": { "oxlint": "bin/oxlint" } }, "sha512-+iFM1BGw1ntYJt3QngbJmjbrGxPaKMUADOXOijpWGnYcBPq8YZnQftSS1C+pVcDYy9YxqDVJKQqQkTazTQMboQ=="],
|
||||
|
||||
"p-limit": ["p-limit@2.3.0", "", { "dependencies": { "p-try": "^2.0.0" } }, "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w=="],
|
||||
|
||||
"p-locate": ["p-locate@3.0.0", "", { "dependencies": { "p-limit": "^2.0.0" } }, "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ=="],
|
||||
|
||||
"p-map": ["p-map@7.0.7", "", {}, "sha512-VaWRu2i4FJNRtiRWCuuQRgfQ1B7a6+gMSrO+3j0EQi/k0ULfS9kosRxGoiqwzIjZTDI02tGfk5mXXltLg6QtfQ=="],
|
||||
|
||||
"p-try": ["p-try@2.2.0", "", {}, "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ=="],
|
||||
|
||||
"package-json-from-dist": ["package-json-from-dist@1.0.1", "", {}, "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw=="],
|
||||
|
||||
"pacote": ["pacote@21.5.1", "", { "dependencies": { "@gar/promise-retry": "^1.0.0", "@npmcli/git": "^7.0.0", "@npmcli/installed-package-contents": "^4.0.0", "@npmcli/package-json": "^7.0.0", "@npmcli/promise-spawn": "^9.0.0", "@npmcli/run-script": "^10.0.0", "cacache": "^20.0.0", "fs-minipass": "^3.0.0", "minipass": "^7.0.2", "npm-package-arg": "^13.0.0", "npm-packlist": "^10.0.1", "npm-pick-manifest": "^11.0.1", "npm-registry-fetch": "^19.0.0", "proc-log": "^6.0.0", "sigstore": "^4.0.0", "ssri": "^13.0.0", "tar": "^7.4.3" }, "bin": { "pacote": "bin/index.js" } }, "sha512-KvcJ9iy3crysCsgqc4+PknH/w6jkrp8JN36mpZBPwNaDRwTfMZD37YzRazNstiZUOhuF5pno9f78n9mEJBavwg=="],
|
||||
|
||||
"parse-conflict-json": ["parse-conflict-json@5.0.1", "", { "dependencies": { "json-parse-even-better-errors": "^5.0.0", "just-diff": "^6.0.0", "just-diff-apply": "^5.2.0" } }, "sha512-ZHEmNKMq1wyJXNwLxyHnluPfRAFSIliBvbK/UiOceROt4Xh9Pz0fq49NytIaeaCUf5VR86hwQ/34FCcNU5/LKQ=="],
|
||||
|
||||
"parse5": ["parse5@7.3.0", "", { "dependencies": { "entities": "^6.0.0" } }, "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw=="],
|
||||
|
||||
"path-exists": ["path-exists@3.0.0", "", {}, "sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ=="],
|
||||
|
||||
"path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="],
|
||||
|
||||
"path-parse": ["path-parse@1.0.7", "", {}, "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="],
|
||||
|
||||
"path-scurry": ["path-scurry@2.0.2", "", { "dependencies": { "lru-cache": "^11.0.0", "minipass": "^7.1.2" } }, "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg=="],
|
||||
|
||||
"picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
|
||||
|
||||
"picomatch": ["picomatch@4.0.7", "", {}, "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA=="],
|
||||
|
||||
"pkg-up": ["pkg-up@3.1.0", "", { "dependencies": { "find-up": "^3.0.0" } }, "sha512-nDywThFk1i4BQK4twPQ6TA4RT8bDY96yeuCVBWL3ePARCiEKDRSrNGbFIgUJpLp+XeIR65v8ra7WuJOFUBtkMA=="],
|
||||
|
||||
"postcss-selector-parser": ["postcss-selector-parser@7.1.6", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-7qASPzhKF2l2KLboRZux8CCTRMdGiV08vWmyKzPz22qZ7ZjQBOeY7rNzNoCLSUiftJ7HUq0GERHmxw/t0dCdMw=="],
|
||||
|
||||
"proc-log": ["proc-log@6.1.0", "", {}, "sha512-iG+GYldRf2BQ0UDUAd6JQ/RwzaQy6mXmsk/IzlYyal4A4SNFw54MeH4/tLkF4I5WoWG9SQwuqWzS99jaFQHBuQ=="],
|
||||
@@ -590,13 +757,23 @@
|
||||
|
||||
"redis": ["redis@6.2.1", "", { "dependencies": { "@redis/bloom": "6.2.1", "@redis/client": "6.2.1", "@redis/json": "6.2.1", "@redis/search": "6.2.1", "@redis/time-series": "6.2.1" } }, "sha512-Z9VHtgYs48PiQC77X9O2Er8Hj4T+5BtFjT91/vi5Is1D04N72cA946ZslM1ImJw8ZctFBZWAVjM7S5wJNeHMpg=="],
|
||||
|
||||
"reselect": ["reselect@4.1.8", "", {}, "sha512-ab9EmR80F/zQTMNeneUr4cv+jSwPJgIlvEmVwLerwrWVbpLlBuls9XHzIeTFy4cegU2NHBp3va0LKOzU5qFEYQ=="],
|
||||
|
||||
"resolve": ["resolve@1.22.12", "", { "dependencies": { "es-errors": "^1.3.0", "is-core-module": "^2.16.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA=="],
|
||||
|
||||
"rimraf": ["rimraf@5.0.10", "", { "dependencies": { "glob": "^10.3.7" }, "bin": { "rimraf": "dist/esm/bin.mjs" } }, "sha512-l0OE8wL34P4nJH/H2ffoaniAokM2qSmrtXHmlpvYr5AVVX8msAyW0l8NVJFDxlSK4u3Uh/f41cQheDVdnYijwQ=="],
|
||||
|
||||
"s-js": ["s-js@0.4.9", "", {}, "sha512-RtpOm+cM6O0sHg6IA70wH+UC3FZcND+rccBZpBAHzlUgNO2Bm5BN+FnM8+OBxzXdwpKWFwX11JGF0MFRkhSoIQ=="],
|
||||
|
||||
"safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="],
|
||||
|
||||
"safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="],
|
||||
|
||||
"semver": ["semver@7.8.5", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA=="],
|
||||
"semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
|
||||
|
||||
"seroval": ["seroval@1.5.6", "", {}, "sha512-rVQVWjjSvlINzaQPZH5JFqsqEsIWdTxY3iJZCnTL/5gQbXIRooVZKI60tVCkOVfzcRPejboxO2t0P89dg5mQaA=="],
|
||||
|
||||
"seroval-plugins": ["seroval-plugins@1.5.6", "", { "peerDependencies": { "seroval": "^1.0" } }, "sha512-HXuLAX2pu/UByPpaeo/TaMfvMIi+1QqIoPJYCcAtU8QkVNwgR6MPlGuCQTErV1JwraaMbYaWVIBX7mppzGLATQ=="],
|
||||
|
||||
"shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="],
|
||||
|
||||
@@ -612,6 +789,8 @@
|
||||
|
||||
"socks-proxy-agent": ["socks-proxy-agent@8.0.5", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "^4.3.4", "socks": "^2.8.3" } }, "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw=="],
|
||||
|
||||
"solid-js": ["solid-js@1.9.15", "", { "dependencies": { "csstype": "^3.1.0", "seroval": "~1.5.4", "seroval-plugins": "~1.5.4" } }, "sha512-EeiY2xfpZJqPLjXspVEKjAII4yv8NyG//NxZ3IpOFHdUNnnTyL0uJOeS9LWGvA7cFCz5y94cjFwYlmw5Luncsg=="],
|
||||
|
||||
"spdx-exceptions": ["spdx-exceptions@2.5.0", "", {}, "sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w=="],
|
||||
|
||||
"spdx-expression-parse": ["spdx-expression-parse@4.0.0", "", { "dependencies": { "spdx-exceptions": "^2.1.0", "spdx-license-ids": "^3.0.0" } }, "sha512-Clya5JIij/7C6bRR22+tnGXbc4VKlibKSVj2iHvVeX5iMW7s1SIQlqu699JkODJJIhh/pUu8L0/VLh8xflD+LQ=="],
|
||||
@@ -620,14 +799,16 @@
|
||||
|
||||
"ssri": ["ssri@13.0.1", "", { "dependencies": { "minipass": "^7.0.3" } }, "sha512-QUiRf1+u9wPTL/76GTYlKttDEBWV1ga9ZXW8BG6kfdeyyM8LGPix9gROyg9V2+P0xNyF3X2Go526xKFdMZrHSQ=="],
|
||||
|
||||
"string-width": ["string-width@5.1.2", "", { "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", "strip-ansi": "^7.0.1" } }, "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA=="],
|
||||
"string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="],
|
||||
|
||||
"string-width-cjs": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
|
||||
|
||||
"strip-ansi": ["strip-ansi@7.2.0", "", { "dependencies": { "ansi-regex": "^6.2.2" } }, "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w=="],
|
||||
"strip-ansi": ["strip-ansi@7.1.2", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA=="],
|
||||
|
||||
"strip-ansi-cjs": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
|
||||
|
||||
"supports-preserve-symlinks-flag": ["supports-preserve-symlinks-flag@1.0.0", "", {}, "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="],
|
||||
|
||||
"tar": ["tar@7.5.22", "", { "dependencies": { "@isaacs/fs-minipass": "^4.0.0", "chownr": "^3.0.0", "minipass": "^7.1.2", "minizlib": "^3.1.0", "yallist": "^5.0.0" } }, "sha512-MFO/QzvtAOmJbkhOaCTvbGcFN9L9b+JunIsDwaKljSOdcLMea3NJ1k9Usz/rjdfSXTq4dfzfeS7W4p4YOAAHeA=="],
|
||||
|
||||
"tinyglobby": ["tinyglobby@0.2.17", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g=="],
|
||||
@@ -646,6 +827,8 @@
|
||||
|
||||
"undici-types": ["undici-types@8.9.0", "", {}, "sha512-KTDyRTYX8sWmKXAikPHHSyc63CRPETMctyjKFupcC6OBLXT3xsN0e9aF7m+mIXutFWpUXuedtowG7iLOzp0kQg=="],
|
||||
|
||||
"update-browserslist-db": ["update-browserslist-db@1.3.3", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-pJ2sYawQS0R/WI928Gj5GlPhTGzbMelq0+4INtSYNDV9ErKJcX6xjGWkoG/VnB3dpUm00zALaqkrUD77pO5TDQ=="],
|
||||
|
||||
"util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="],
|
||||
|
||||
"validate-npm-package-name": ["validate-npm-package-name@7.0.2", "", {}, "sha512-hVDIBwsRruT73PbK7uP5ebUt+ezEtCmzZz3F59BSr2F6OVFnJ/6h8liuvdLrQ88Xmnk6/+xGGuq+pG9WwTuy3A=="],
|
||||
@@ -654,6 +837,8 @@
|
||||
|
||||
"web-streams-polyfill": ["web-streams-polyfill@3.3.3", "", {}, "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw=="],
|
||||
|
||||
"web-tree-sitter": ["web-tree-sitter@0.25.10", "", { "peerDependencies": { "@types/emscripten": "^1.40.0" }, "optionalPeers": ["@types/emscripten"] }, "sha512-Y09sF44/13XvgVKgO2cNDw5rGk6s26MgoZPXLESvMXeefBf7i6/73eFurre0IsTW6E14Y0ArIzhUMmjoc7xyzA=="],
|
||||
|
||||
"which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
|
||||
|
||||
"wrap-ansi": ["wrap-ansi@8.1.0", "", { "dependencies": { "ansi-styles": "^6.1.0", "string-width": "^5.0.1", "strip-ansi": "^7.0.1" } }, "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ=="],
|
||||
@@ -672,10 +857,32 @@
|
||||
|
||||
"@aws-crypto/util/@smithy/util-utf8": ["@smithy/util-utf8@2.3.0", "", { "dependencies": { "@smithy/util-buffer-from": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A=="],
|
||||
|
||||
"@babel/helper-compilation-targets/lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="],
|
||||
|
||||
"@isaacs/cliui/string-width": ["string-width@5.1.2", "", { "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", "strip-ansi": "^7.0.1" } }, "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA=="],
|
||||
|
||||
"@isaacs/cliui/strip-ansi": ["strip-ansi@7.2.0", "", { "dependencies": { "ansi-regex": "^6.2.2" } }, "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w=="],
|
||||
|
||||
"@npmcli/arborist/semver": ["semver@7.8.5", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA=="],
|
||||
|
||||
"@npmcli/config/semver": ["semver@7.8.5", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA=="],
|
||||
|
||||
"@npmcli/fs/semver": ["semver@7.8.5", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA=="],
|
||||
|
||||
"@npmcli/git/semver": ["semver@7.8.5", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA=="],
|
||||
|
||||
"@npmcli/git/which": ["which@6.0.1", "", { "dependencies": { "isexe": "^4.0.0" }, "bin": { "node-which": "bin/which.js" } }, "sha512-oGLe46MIrCRqX7ytPUf66EAYvdeMIZYn3WaocqqKZAxrBpkqHfL/qvTyJ/bTk5+AqHCjXmrv3CEWgy368zhRUg=="],
|
||||
|
||||
"@npmcli/metavuln-calculator/semver": ["semver@7.8.5", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA=="],
|
||||
|
||||
"@npmcli/package-json/semver": ["semver@7.8.5", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA=="],
|
||||
|
||||
"@npmcli/promise-spawn/which": ["which@6.0.1", "", { "dependencies": { "isexe": "^4.0.0" }, "bin": { "node-which": "bin/which.js" } }, "sha512-oGLe46MIrCRqX7ytPUf66EAYvdeMIZYn3WaocqqKZAxrBpkqHfL/qvTyJ/bTk5+AqHCjXmrv3CEWgy368zhRUg=="],
|
||||
|
||||
"babel-plugin-jsx-dom-expressions/@babel/helper-module-imports": ["@babel/helper-module-imports@7.18.6", "", { "dependencies": { "@babel/types": "^7.18.6" } }, "sha512-0NFvs3VkuSYbFi1x2Vd6tKrywq+z/cLeYC/RJNFrIX/30Bf5aiGYbtvGXolEktzJH8o5E5KJ3tT+nkxuuZFVlA=="],
|
||||
|
||||
"babel-plugin-module-resolver/glob": ["glob@9.3.5", "", { "dependencies": { "fs.realpath": "^1.0.0", "minimatch": "^8.0.2", "minipass": "^4.2.4", "path-scurry": "^1.6.1" } }, "sha512-e1LleDykUz2Iu+MTYdkSsuWX8lvAjAcs0Xef0lNIu0S2wOAzuTxCJtcd9S3cijlwYF18EsU3rzb8jPVobxDh9Q=="],
|
||||
|
||||
"gcp-metadata/gaxios": ["gaxios@7.1.3", "", { "dependencies": { "extend": "^3.0.2", "https-proxy-agent": "^7.0.1", "node-fetch": "^3.3.2", "rimraf": "^5.0.1" } }, "sha512-YGGyuEdVIjqxkxVH1pUTMY/XtmmsApXrCVv5EU25iX6inEPbV+VakJfLealkBtJN69AQmh1eGOdCl9Sm1UP6XQ=="],
|
||||
|
||||
"gcp-metadata/google-logging-utils": ["google-logging-utils@1.1.3", "", {}, "sha512-eAmLkjDjAFCVXg7A1unxHsLf961m6y17QFqXqAXGj/gVkKFrEICfStRfwUlGNfeCEjNRa32JEWOUTlYXPyyKvA=="],
|
||||
@@ -684,18 +891,34 @@
|
||||
|
||||
"minipass-pipeline/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="],
|
||||
|
||||
"node-gyp/semver": ["semver@7.8.5", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA=="],
|
||||
|
||||
"node-gyp/undici": ["undici@6.28.1", "", {}, "sha512-zWpdTVD54H48CIybL0rWQ3ukpb9d23wM7eH5RtfdmeP70cWHNjtfo7P4vZX+5CoDcO53J4Pu5uXp7lNfjc6DRA=="],
|
||||
|
||||
"node-gyp/which": ["which@6.0.1", "", { "dependencies": { "isexe": "^4.0.0" }, "bin": { "node-which": "bin/which.js" } }, "sha512-oGLe46MIrCRqX7ytPUf66EAYvdeMIZYn3WaocqqKZAxrBpkqHfL/qvTyJ/bTk5+AqHCjXmrv3CEWgy368zhRUg=="],
|
||||
|
||||
"npm-install-checks/semver": ["semver@7.8.5", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA=="],
|
||||
|
||||
"npm-package-arg/semver": ["semver@7.8.5", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA=="],
|
||||
|
||||
"npm-pick-manifest/semver": ["semver@7.8.5", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA=="],
|
||||
|
||||
"parse5/entities": ["entities@6.0.1", "", {}, "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g=="],
|
||||
|
||||
"rimraf/glob": ["glob@10.5.0", "", { "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", "minimatch": "^9.0.4", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^1.11.1" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg=="],
|
||||
|
||||
"string-width/strip-ansi": ["strip-ansi@7.2.0", "", { "dependencies": { "ansi-regex": "^6.2.2" } }, "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w=="],
|
||||
|
||||
"string-width-cjs/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
|
||||
|
||||
"string-width-cjs/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
|
||||
|
||||
"strip-ansi-cjs/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
|
||||
|
||||
"wrap-ansi/string-width": ["string-width@5.1.2", "", { "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", "strip-ansi": "^7.0.1" } }, "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA=="],
|
||||
|
||||
"wrap-ansi/strip-ansi": ["strip-ansi@7.2.0", "", { "dependencies": { "ansi-regex": "^6.2.2" } }, "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w=="],
|
||||
|
||||
"wrap-ansi-cjs/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
|
||||
|
||||
"wrap-ansi-cjs/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
|
||||
@@ -706,10 +929,20 @@
|
||||
|
||||
"@aws-crypto/util/@smithy/util-utf8/@smithy/util-buffer-from": ["@smithy/util-buffer-from@2.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA=="],
|
||||
|
||||
"@babel/helper-compilation-targets/lru-cache/yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="],
|
||||
|
||||
"@isaacs/cliui/string-width/emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="],
|
||||
|
||||
"@npmcli/git/which/isexe": ["isexe@4.0.0", "", {}, "sha512-FFUtZMpoZ8RqHS3XeXEmHWLA4thH+ZxCv2lOiPIn1Xc7CxrqhWzNSDzD+/chS/zbYezmiwWLdQC09JdQKmthOw=="],
|
||||
|
||||
"@npmcli/promise-spawn/which/isexe": ["isexe@4.0.0", "", {}, "sha512-FFUtZMpoZ8RqHS3XeXEmHWLA4thH+ZxCv2lOiPIn1Xc7CxrqhWzNSDzD+/chS/zbYezmiwWLdQC09JdQKmthOw=="],
|
||||
|
||||
"babel-plugin-module-resolver/glob/minimatch": ["minimatch@8.0.7", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-V+1uQNdzybxa14e/p00HZnQNNcTjnRJjDxg2V8wtkjFctq4M7hXFws4oekyTP0Jebeq7QYtpFyOeBAjc88zvYg=="],
|
||||
|
||||
"babel-plugin-module-resolver/glob/minipass": ["minipass@4.2.8", "", {}, "sha512-fNzuVyifolSLFL4NzpF+wEF4qrgqaaKX0haXPQEdQ7NKAN+WecoKMHV09YcuL/DHxrUsYQOK3MiuDf7Ip2OXfQ=="],
|
||||
|
||||
"babel-plugin-module-resolver/glob/path-scurry": ["path-scurry@1.11.1", "", { "dependencies": { "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" } }, "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA=="],
|
||||
|
||||
"minipass-flush/minipass/yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="],
|
||||
|
||||
"minipass-pipeline/minipass/yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="],
|
||||
@@ -726,10 +959,20 @@
|
||||
|
||||
"wrap-ansi-cjs/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
|
||||
|
||||
"wrap-ansi/string-width/emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="],
|
||||
|
||||
"babel-plugin-module-resolver/glob/minimatch/brace-expansion": ["brace-expansion@2.1.4", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-hGfVzPxthbf3+2yjg/RBs60cB0FhqBS/zvdV/4wn4/BmN0bNMMHPc4V/BbFieqf1TKAGGAHnY4eSjajCl0f2Xg=="],
|
||||
|
||||
"babel-plugin-module-resolver/glob/path-scurry/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="],
|
||||
|
||||
"babel-plugin-module-resolver/glob/path-scurry/minipass": ["minipass@7.1.3", "", {}, "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A=="],
|
||||
|
||||
"rimraf/glob/minimatch/brace-expansion": ["brace-expansion@2.1.4", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-hGfVzPxthbf3+2yjg/RBs60cB0FhqBS/zvdV/4wn4/BmN0bNMMHPc4V/BbFieqf1TKAGGAHnY4eSjajCl0f2Xg=="],
|
||||
|
||||
"rimraf/glob/path-scurry/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="],
|
||||
|
||||
"babel-plugin-module-resolver/glob/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
|
||||
|
||||
"rimraf/glob/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
|
||||
}
|
||||
}
|
||||
|
||||
Executable
+79
@@ -0,0 +1,79 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
for command in bun jq; do
|
||||
command -v "$command" >/dev/null || { printf 'Required command not found: %s\n' "$command" >&2; exit 1; }
|
||||
done
|
||||
|
||||
checkout=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd -P)
|
||||
config="${OPENCODE_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/opencode}"
|
||||
mkdir -p "$config"
|
||||
config=$(cd "$config" && pwd -P)
|
||||
mkdir -p "$config/plugins"
|
||||
install=$(mktemp -d "$config/install.stanp-opencode.XXXXXXXX")
|
||||
staged=""
|
||||
backup=""
|
||||
cleanup() {
|
||||
[[ -z "$install" ]] || rm -rf "$install"
|
||||
[[ -z "$staged" ]] || rm -rf "$staged"
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
(cd "$checkout" && bun install --frozen-lockfile && bun run build)
|
||||
|
||||
bun -e '
|
||||
if (!await Bun.file(process.argv[1]).exists()) {
|
||||
throw new Error("Local build lacks the OpenCode V2 directory entrypoint.");
|
||||
}
|
||||
const { default: plugin } = await import(process.argv[1]);
|
||||
if (!plugin || plugin.id !== "stanp-opencode" || typeof plugin.setup !== "function") {
|
||||
throw new Error("Local build is not a native OpenCode V2 plugin.");
|
||||
}
|
||||
const tuiFile = Bun.file(process.argv[2]);
|
||||
const manifest = await Bun.file(process.argv[3]).json();
|
||||
const exportsTui = manifest?.exports?.["./tui"];
|
||||
if (!await tuiFile.exists() || !new Bun.Transpiler({ loader: "js" }).scan(await tuiFile.text()).exports.includes("default") || (typeof exportsTui !== "string" && typeof exportsTui?.import !== "string")) {
|
||||
throw new Error("Local build lacks the ChatGPT quota TUI plugin.");
|
||||
}
|
||||
' "$checkout/dist/index.js" "$checkout/dist/tui.js" "$checkout/package.json"
|
||||
|
||||
config_file="$config/opencode.json"
|
||||
config_source="$config_file"
|
||||
if [[ ! -e "$config_source" ]]; then
|
||||
config_source="$install/empty-config.json"
|
||||
printf '{}\n' > "$config_source"
|
||||
fi
|
||||
jq -e '
|
||||
if type != "object" then error("opencode.json must contain an object") else . end
|
||||
| if (.plugins // []) | type != "array" then error("opencode.json plugins must be an array") else . end
|
||||
| .["$schema"] = (.["$schema"] // "https://opencode.ai/config.json")
|
||||
| .plugins = (.plugins // [])
|
||||
| if any(.plugins[]?; .package == "./plugins/stanp-opencode") then . else
|
||||
.plugins += [{
|
||||
package: "./plugins/stanp-opencode",
|
||||
options: { mode: "gpt", provider: "openai", lunaPriority: true }
|
||||
}]
|
||||
end
|
||||
' "$config_source" > "$install/opencode.json"
|
||||
|
||||
target="$config/plugins/stanp-opencode"
|
||||
staged="$config/plugins/.stanp-opencode.new.$$"
|
||||
backup="$config/plugins/.stanp-opencode.old.$$"
|
||||
mkdir "$staged"
|
||||
cp "$checkout/dist/index.js" "$staged/index.js"
|
||||
cp "$checkout/dist/tui.js" "$staged/tui.js"
|
||||
cat > "$staged/package.json" <<'EOF'
|
||||
{"name":"stanp-opencode","type":"module","main":"./index.js","exports":{".":"./index.js","./tui":"./tui.js"}}
|
||||
EOF
|
||||
if [[ -e "$target" || -L "$target" ]]; then mv "$target" "$backup"; fi
|
||||
if ! mv "$staged" "$target"; then
|
||||
if [[ -e "$backup" || -L "$backup" ]]; then mv "$backup" "$target"; fi
|
||||
printf 'Could not activate plugin; installation unchanged\n' >&2
|
||||
exit 1
|
||||
fi
|
||||
staged=""
|
||||
rm -rf "$backup"
|
||||
backup=""
|
||||
rm -f "$config/plugins/stanp-opencode.js"
|
||||
mv "$install/opencode.json" "$config_file"
|
||||
printf 'Built and installed stanp-opencode from %s into %s/plugins/stanp-opencode and updated %s\nRestart opencode2 to load it.\n' "$checkout" "$config" "$config_file"
|
||||
+41
-6
@@ -42,14 +42,49 @@ bun -e '
|
||||
if (!plugin || plugin.id !== "stanp-opencode" || typeof plugin.setup !== "function") {
|
||||
throw new Error("Latest release is not a native OpenCode V2 plugin. Publish a V2-compatible release first.");
|
||||
}
|
||||
' "$install/package/dist/index.js"
|
||||
const tuiFile = Bun.file(process.argv[2]);
|
||||
const manifest = await Bun.file(process.argv[3]).json();
|
||||
const exportsTui = manifest?.exports?.["./tui"];
|
||||
if (!await tuiFile.exists() || !new Bun.Transpiler({ loader: "js" }).scan(await tuiFile.text()).exports.includes("default") || (typeof exportsTui !== "string" && typeof exportsTui?.import !== "string")) {
|
||||
throw new Error("Latest release lacks the ChatGPT quota TUI plugin.");
|
||||
}
|
||||
' "$install/package/dist/index.js" "$install/package/dist/tui.js" "$install/package/package.json"
|
||||
|
||||
tag=$(jq -r '.tag_name' "$install/release.json")
|
||||
if ! mv -f "$install/package/dist/index.js" "$config/plugins/stanp-opencode.js"; then
|
||||
config_file="$config/opencode.json"
|
||||
config_source="$config_file"
|
||||
if [[ ! -e "$config_source" ]]; then
|
||||
config_source="$install/empty-config.json"
|
||||
printf '{}\n' > "$config_source"
|
||||
fi
|
||||
jq -e '
|
||||
if type != "object" then error("opencode.json must contain an object") else . end
|
||||
| if (.plugins // []) | type != "array" then error("opencode.json plugins must be an array") else . end
|
||||
| .["$schema"] = (.["$schema"] // "https://opencode.ai/config.json")
|
||||
| .plugins = (.plugins // [])
|
||||
| if any(.plugins[]?; .package == "./plugins/stanp-opencode") then . else
|
||||
.plugins += [{
|
||||
package: "./plugins/stanp-opencode",
|
||||
options: { mode: "gpt", provider: "openai", lunaPriority: true }
|
||||
}]
|
||||
end
|
||||
' "$config_source" > "$install/opencode.json"
|
||||
target="$config/plugins/stanp-opencode"
|
||||
staged="$config/plugins/.stanp-opencode.new.$$"
|
||||
backup="$config/plugins/.stanp-opencode.old.$$"
|
||||
mkdir "$staged"
|
||||
mv "$install/package/dist/index.js" "$staged/index.js"
|
||||
mv "$install/package/dist/tui.js" "$staged/tui.js"
|
||||
cat > "$staged/package.json" <<'EOF'
|
||||
{"name":"stanp-opencode","type":"module","main":"./index.js","exports":{".":"./index.js","./tui":"./tui.js"}}
|
||||
EOF
|
||||
if [[ -e "$target" || -L "$target" ]]; then mv "$target" "$backup"; fi
|
||||
if ! mv "$staged" "$target"; then
|
||||
if [[ -e "$backup" || -L "$backup" ]]; then mv "$backup" "$target"; fi
|
||||
printf 'Could not activate plugin; installation unchanged\n' >&2
|
||||
exit 1
|
||||
fi
|
||||
if [[ -L "$config/plugins/stanp-opencode" ]]; then
|
||||
rm -f "$config/plugins/stanp-opencode"
|
||||
fi
|
||||
printf 'Installed stanp-opencode %s into %s/plugins/stanp-opencode.js\nRestart opencode2 to load it.\n' "$tag" "$config"
|
||||
rm -rf "$backup"
|
||||
rm -f "$config/plugins/stanp-opencode.js"
|
||||
mv "$install/opencode.json" "$config_file"
|
||||
printf 'Installed stanp-opencode %s into %s/plugins/stanp-opencode and updated %s\nRestart opencode2 to load it.\n' "$tag" "$config" "$config_file"
|
||||
|
||||
+20
-5
@@ -9,6 +9,14 @@
|
||||
".": {
|
||||
"types": "./dist/index.d.ts",
|
||||
"import": "./dist/index.js"
|
||||
},
|
||||
"./tui": {
|
||||
"types": "./dist/tui.d.ts",
|
||||
"import": "./dist/tui.js"
|
||||
},
|
||||
"./quota-rpc": {
|
||||
"types": "./dist/quota-rpc.d.ts",
|
||||
"import": "./dist/quota-rpc.js"
|
||||
}
|
||||
},
|
||||
"files": [
|
||||
@@ -25,20 +33,27 @@
|
||||
"lint": "oxlint --deny-warnings .",
|
||||
"format": "oxfmt --write .",
|
||||
"format:check": "oxfmt --check .",
|
||||
"test": "bun test",
|
||||
"build": "bun build src/index.ts --outdir dist --target bun --format esm && tsc --project tsconfig.build.json --emitDeclarationOnly"
|
||||
"test": "bun test --conditions=browser",
|
||||
"build": "bun scripts/build.ts && tsc --project tsconfig.build.json --emitDeclarationOnly"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@opencode/client": "0.0.0-beta-19378",
|
||||
"@opencode/plugin": "0.0.0-beta-19378",
|
||||
"@opencode/client": "2.0.2",
|
||||
"@opencode/plugin": "2.0.2",
|
||||
"@opencode/schema": "2.0.2",
|
||||
"@opentui/core": "0.5.10",
|
||||
"@opentui/solid": "0.5.10",
|
||||
"@oxlint/plugins": "1.82.0",
|
||||
"@types/bun": "1.3.14",
|
||||
"oxfmt": "0.67.0",
|
||||
"oxlint": "1.82.0",
|
||||
"solid-js": "1.9.15",
|
||||
"typescript": "7.0.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@opencode/plugin": "0.0.0-beta-19378"
|
||||
"@opencode/plugin": "2.0.2",
|
||||
"@opentui/core": ">=0.5.10",
|
||||
"@opentui/solid": ">=0.5.10",
|
||||
"solid-js": ">=1.9.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"zod": "4.1.8"
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
import solidPlugin from "@opentui/solid/bun-plugin";
|
||||
|
||||
export async function build() {
|
||||
const results = await Promise.all([
|
||||
Bun.build({
|
||||
entrypoints: ["src/index.ts", "src/quota-rpc.ts"],
|
||||
outdir: "dist",
|
||||
target: "bun",
|
||||
format: "esm",
|
||||
}),
|
||||
Bun.build({
|
||||
entrypoints: ["src/tui.tsx"],
|
||||
outdir: "dist",
|
||||
target: "bun",
|
||||
format: "esm",
|
||||
external: ["@opencode/plugin/tui", "@opentui/solid", "solid-js"],
|
||||
plugins: [solidPlugin],
|
||||
}),
|
||||
]);
|
||||
|
||||
for (const result of results) {
|
||||
if (result.success) continue;
|
||||
for (const log of result.logs) console.error(log);
|
||||
throw new Error("Build failed");
|
||||
}
|
||||
}
|
||||
|
||||
if (import.meta.main) await build();
|
||||
+79
-1
@@ -1,9 +1,13 @@
|
||||
import { Agent, Model, Plugin, Provider } from "@opencode/plugin";
|
||||
import type { RpcRegistration } from "@opencode/plugin/promise/rpc";
|
||||
import type { ModelInfo } from "@opencode/client";
|
||||
import { z } from "zod";
|
||||
import { gptAgents, settings, workerNames } from "./config.js";
|
||||
import { discover } from "./provider.js";
|
||||
import { descriptions, routingGuide } from "./routing.js";
|
||||
import { QuotaTracker } from "./quota.js";
|
||||
import { QuotaRpc } from "./quota-rpc.js";
|
||||
import { playwrightMcpCommand } from "./playwright.js";
|
||||
|
||||
// Permission actions use wildcards, not filesystem globs.
|
||||
function matches(value: string, pattern: string) {
|
||||
@@ -46,11 +50,28 @@ export default Plugin.define({
|
||||
Agent.ID.make("build"),
|
||||
).permissions;
|
||||
let permissionKey = JSON.stringify(inherited);
|
||||
let hasCodegraph = false;
|
||||
let codegraphTools: string[] = [];
|
||||
let agentTransformReady = false;
|
||||
const controller = new AbortController();
|
||||
let quotaRegistration: RpcRegistration<typeof QuotaRpc> | undefined;
|
||||
const quota = new QuotaTracker(context, async () => {
|
||||
await quotaRegistration?.events.emit("updated", {});
|
||||
});
|
||||
quotaRegistration = await context.rpc.register(QuotaRpc, {
|
||||
status: async ({ sessionID }) => (await quota.status(sessionID)) ?? null,
|
||||
});
|
||||
await quota.init();
|
||||
// Subscribe before the first read: configuration can publish its catalog while
|
||||
// asynchronous setup is running.
|
||||
const updates = (async () => {
|
||||
for await (const event of context.event.subscribe({ signal: controller.signal })) {
|
||||
if (
|
||||
event.location &&
|
||||
(event.location.directory !== context.location.directory ||
|
||||
event.location.workspaceID !== context.location.workspaceID)
|
||||
)
|
||||
continue;
|
||||
if (event.type === "catalog.updated") {
|
||||
models = await discover(context, selected.provider);
|
||||
await context.agent.reload();
|
||||
@@ -66,6 +87,20 @@ export default Plugin.define({
|
||||
inherited = permissions;
|
||||
permissionKey = key;
|
||||
await context.agent.reload();
|
||||
} else if (
|
||||
event.type === "session.execution.succeeded" ||
|
||||
event.type === "session.execution.failed" ||
|
||||
event.type === "session.execution.interrupted" ||
|
||||
event.type === "session.idle"
|
||||
) {
|
||||
await quota.complete(event.data.sessionID);
|
||||
} else if (event.type === "credential.updated") {
|
||||
await quota.authChanged();
|
||||
} else if (
|
||||
event.type === "credential.switched" &&
|
||||
String(event.data.integrationID) === "openai"
|
||||
) {
|
||||
await quota.authChanged(true);
|
||||
}
|
||||
}
|
||||
})().catch((error) => {
|
||||
@@ -79,6 +114,38 @@ export default Plugin.define({
|
||||
const assignments = selected.mode === "gpt" ? gptAgents : selected.agents;
|
||||
let workers: (typeof workerNames)[number][] = [];
|
||||
const active = new Set<string>();
|
||||
const playwrightCommand = await playwrightMcpCommand(context.location.directory);
|
||||
|
||||
await context.mcp.transform((editor) => {
|
||||
if (!editor.get("playwright"))
|
||||
editor.set("playwright", {
|
||||
type: "local",
|
||||
command: playwrightCommand,
|
||||
timeout: { startup: 300_000 },
|
||||
});
|
||||
hasCodegraph = editor.get("codegraph") !== undefined;
|
||||
if (hasCodegraph)
|
||||
editor.update("codegraph", (server) => {
|
||||
server.codemode = false;
|
||||
});
|
||||
});
|
||||
await context.tool.transform((editor) => {
|
||||
const next = editor
|
||||
.list()
|
||||
.map((tool) => tool.id)
|
||||
.filter((id) => id.startsWith("codegraph_"))
|
||||
.sort();
|
||||
if (JSON.stringify(next) === JSON.stringify(codegraphTools)) return;
|
||||
codegraphTools = next;
|
||||
if (agentTransformReady)
|
||||
queueMicrotask(() => {
|
||||
void context.agent
|
||||
.reload()
|
||||
.catch((error) =>
|
||||
console.error("stanp-opencode agent reload failed", error),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
await context.agent.transform((editor) => {
|
||||
const deep = selected.mode === "custom" || models.has("gpt-6-astra");
|
||||
@@ -146,6 +213,13 @@ export default Plugin.define({
|
||||
.filter((rule) => matches(action, rule.action))
|
||||
.map((rule) => ({ ...rule, action })),
|
||||
),
|
||||
...(hasCodegraph
|
||||
? codegraphTools.flatMap((action) =>
|
||||
inherited
|
||||
.filter((rule) => matches(action, rule.action))
|
||||
.map((rule) => ({ ...rule, action })),
|
||||
)
|
||||
: []),
|
||||
...workers.map((resource) => ({
|
||||
action: "subagent",
|
||||
resource,
|
||||
@@ -154,6 +228,7 @@ export default Plugin.define({
|
||||
];
|
||||
});
|
||||
});
|
||||
agentTransformReady = true;
|
||||
async function assertTodo(agentID: Agent.ID, action: string) {
|
||||
const { data: agent } = await context.agent.get({ agentID });
|
||||
const effect =
|
||||
@@ -162,7 +237,7 @@ export default Plugin.define({
|
||||
)?.effect ?? "ask";
|
||||
if (effect !== "allow")
|
||||
throw new Error(
|
||||
`${action} requires an allow permission: OpenCode beta's public plugin API cannot request custom-tool permission approval (${effect}).`,
|
||||
`${action} requires an allow permission: OpenCode's public plugin API cannot request custom-tool permission approval (${effect}).`,
|
||||
);
|
||||
}
|
||||
await context.tool.transform((editor) => {
|
||||
@@ -215,6 +290,7 @@ export default Plugin.define({
|
||||
properties: { ...properties, agent: { type: "string", enum: workers } },
|
||||
};
|
||||
});
|
||||
await context.session.hook("model.request", async (event) => quota.start(event.sessionID));
|
||||
await context.tool.hook("execute.before", (event) => {
|
||||
if (event.tool !== "subagent") return;
|
||||
const { agent } = delegation.parse(event.input);
|
||||
@@ -242,6 +318,8 @@ export default Plugin.define({
|
||||
return async () => {
|
||||
controller.abort();
|
||||
await updates;
|
||||
await quota.dispose();
|
||||
await quotaRegistration?.dispose();
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
@@ -0,0 +1,137 @@
|
||||
import { access, readFile, realpath } from "node:fs/promises";
|
||||
import { constants } from "node:fs";
|
||||
import { dirname, join, parse, resolve } from "node:path";
|
||||
import { z } from "zod";
|
||||
|
||||
const dockerCommand = ["docker", "run", "-i", "--rm", "--init", "mcr.microsoft.com/playwright/mcp"];
|
||||
const directPackageManifest = z.object({
|
||||
name: z.literal("@playwright/mcp"),
|
||||
bin: z.string(),
|
||||
});
|
||||
const namedPackageManifest = z.object({
|
||||
name: z.literal("@playwright/mcp"),
|
||||
bin: z.object({ "playwright-mcp": z.string() }),
|
||||
});
|
||||
const playwrightPackageManifest = z.object({
|
||||
name: z.literal("playwright"),
|
||||
bin: z.object({ playwright: z.string() }),
|
||||
});
|
||||
const playwrightTestPackageManifest = z.object({
|
||||
name: z.literal("@playwright/test"),
|
||||
bin: z.object({ playwright: z.string() }),
|
||||
dependencies: z.object({ playwright: z.string() }),
|
||||
});
|
||||
|
||||
async function packageExecutable(packageRoot: string) {
|
||||
try {
|
||||
const input = JSON.parse(await readFile(join(packageRoot, "package.json"), "utf8"));
|
||||
const direct = directPackageManifest.safeParse(input);
|
||||
const bin = direct.success
|
||||
? direct.data.bin
|
||||
: namedPackageManifest.parse(input).bin["playwright-mcp"];
|
||||
const executable = resolve(packageRoot, bin);
|
||||
await access(executable, constants.X_OK);
|
||||
return executable;
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
async function ordinaryPackageExecutable(packageRoot: string) {
|
||||
try {
|
||||
const manifest = playwrightPackageManifest.parse(
|
||||
JSON.parse(await readFile(join(packageRoot, "package.json"), "utf8")),
|
||||
);
|
||||
const executable = resolve(packageRoot, manifest.bin.playwright);
|
||||
await access(executable, constants.X_OK);
|
||||
return executable;
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
async function playwrightDependency(packageRoot: string) {
|
||||
let current = await realpath(packageRoot);
|
||||
const root = parse(current).root;
|
||||
while (true) {
|
||||
const executable = await ordinaryPackageExecutable(
|
||||
join(current, "node_modules/playwright"),
|
||||
);
|
||||
if (executable) return executable;
|
||||
if (current === root) return;
|
||||
current = dirname(current);
|
||||
}
|
||||
}
|
||||
|
||||
async function testPackageExecutable(packageRoot: string) {
|
||||
try {
|
||||
const manifest = playwrightTestPackageManifest.parse(
|
||||
JSON.parse(await readFile(join(packageRoot, "package.json"), "utf8")),
|
||||
);
|
||||
if (!(await playwrightDependency(packageRoot))) return;
|
||||
const executable = resolve(packageRoot, manifest.bin.playwright);
|
||||
await access(executable, constants.X_OK);
|
||||
return executable;
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
async function verifiedPathExecutable(candidate: string, kind: "mcp" | "playwright") {
|
||||
try {
|
||||
await access(candidate, constants.X_OK);
|
||||
const target = await realpath(candidate);
|
||||
let directory = dirname(target);
|
||||
const root = parse(directory).root;
|
||||
while (true) {
|
||||
const executable =
|
||||
kind === "mcp"
|
||||
? await packageExecutable(directory)
|
||||
: ((await ordinaryPackageExecutable(directory)) ??
|
||||
(await testPackageExecutable(directory)));
|
||||
if (executable && (await realpath(executable)) === target) return candidate;
|
||||
if (directory === root) return;
|
||||
directory = dirname(directory);
|
||||
}
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
export async function playwrightMcpCommand(
|
||||
directory: string,
|
||||
path = process.env.PATH ?? "",
|
||||
): Promise<string[]> {
|
||||
let current = resolve(directory);
|
||||
const root = parse(current).root;
|
||||
while (true) {
|
||||
const executable = await packageExecutable(join(current, "node_modules/@playwright/mcp"));
|
||||
if (executable) return [executable];
|
||||
if (current === root) break;
|
||||
current = dirname(current);
|
||||
}
|
||||
|
||||
for (const entry of path.split(process.platform === "win32" ? ";" : ":")) {
|
||||
if (!entry) continue;
|
||||
const executable = await verifiedPathExecutable(join(entry, "playwright-mcp"), "mcp");
|
||||
if (executable) return [executable];
|
||||
}
|
||||
|
||||
current = resolve(directory);
|
||||
while (true) {
|
||||
const playwright = await ordinaryPackageExecutable(
|
||||
join(current, "node_modules/playwright"),
|
||||
);
|
||||
const test = await testPackageExecutable(join(current, "node_modules/@playwright/test"));
|
||||
if (playwright || test) return ["npx", "-y", "@playwright/mcp@latest"];
|
||||
if (current === root) break;
|
||||
current = dirname(current);
|
||||
}
|
||||
|
||||
for (const entry of path.split(process.platform === "win32" ? ";" : ":")) {
|
||||
if (!entry) continue;
|
||||
const executable = await verifiedPathExecutable(join(entry, "playwright"), "playwright");
|
||||
if (executable) return ["npx", "-y", "@playwright/mcp@latest"];
|
||||
}
|
||||
return [...dockerCommand];
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import { Rpc } from "@opencode/plugin/rpc";
|
||||
import { z } from "zod";
|
||||
|
||||
const window = z.object({
|
||||
id: z.string(),
|
||||
label: z.string().nullable(),
|
||||
seconds: z.number(),
|
||||
used: z.number(),
|
||||
resetsAt: z.number(),
|
||||
remaining: z.number(),
|
||||
sessionConsumed: z.number(),
|
||||
});
|
||||
|
||||
export const QuotaRpc = Rpc.define({
|
||||
id: "stanp-opencode.quota",
|
||||
methods: {
|
||||
status: {
|
||||
input: z.object({ sessionID: z.string() }),
|
||||
output: z
|
||||
.object({ plan: z.string(), fetchedAt: z.number(), windows: z.array(window) })
|
||||
.nullable(),
|
||||
},
|
||||
},
|
||||
events: { updated: { schema: z.object({}) } },
|
||||
});
|
||||
+494
@@ -0,0 +1,494 @@
|
||||
import type { Plugin } from "@opencode/plugin";
|
||||
import type { Credential } from "@opencode/schema/credential";
|
||||
import type { Schema } from "effect";
|
||||
import { z } from "zod";
|
||||
|
||||
const INTEGRATION_ID = "openai";
|
||||
const USAGE_URL = "https://chatgpt.com/backend-api/wham/usage";
|
||||
const POLL_MS = 60_000;
|
||||
const TIMEOUT_MS = 5_000;
|
||||
const STORAGE_KEY = "chatgpt-quota";
|
||||
|
||||
function compactLimitIdentity(value: string) {
|
||||
return value.toLowerCase().replace(/[^a-z0-9]+/g, "");
|
||||
}
|
||||
|
||||
function isSparkLimit(meteredFeature?: string, limitName?: string | null) {
|
||||
return (
|
||||
(meteredFeature !== undefined &&
|
||||
compactLimitIdentity(meteredFeature) === "codexbengalfox") ||
|
||||
(limitName != null && compactLimitIdentity(limitName) === "gpt53codexspark")
|
||||
);
|
||||
}
|
||||
|
||||
function isReserveLimit(meteredFeature?: string, limitName?: string | null) {
|
||||
const hasReserveToken = (value: string) => /(^|[^a-z0-9])reserve($|[^a-z0-9])/i.test(value);
|
||||
return (
|
||||
(meteredFeature !== undefined && hasReserveToken(meteredFeature)) ||
|
||||
(limitName != null && hasReserveToken(limitName))
|
||||
);
|
||||
}
|
||||
|
||||
function isExcludedLimit(meteredFeature?: string, limitName?: string | null) {
|
||||
return isSparkLimit(meteredFeature, limitName) || isReserveLimit(meteredFeature, limitName);
|
||||
}
|
||||
|
||||
function isExcludedWindow(window: Pick<QuotaWindow, "id" | "label">) {
|
||||
return isExcludedLimit(window.id.split(":", 1)[0], window.label);
|
||||
}
|
||||
|
||||
export interface QuotaWindow {
|
||||
id: string;
|
||||
label: string | null;
|
||||
seconds: number;
|
||||
used: number;
|
||||
resetsAt: number;
|
||||
}
|
||||
|
||||
export interface QuotaSnapshot {
|
||||
plan: string;
|
||||
fetchedAt: number;
|
||||
windows: QuotaWindow[];
|
||||
}
|
||||
|
||||
export interface QuotaStatus {
|
||||
plan: string;
|
||||
fetchedAt: number;
|
||||
windows: Array<QuotaWindow & { remaining: number; sessionConsumed: number }>;
|
||||
}
|
||||
|
||||
const storedState = z.object({
|
||||
account: z.string(),
|
||||
latest: z
|
||||
.object({
|
||||
plan: z.string(),
|
||||
fetchedAt: z.number(),
|
||||
windows: z.array(
|
||||
z.object({
|
||||
id: z.string(),
|
||||
label: z.string().nullable(),
|
||||
seconds: z.number(),
|
||||
used: z.number(),
|
||||
resetsAt: z.number(),
|
||||
}),
|
||||
),
|
||||
})
|
||||
.optional(),
|
||||
sessions: z.record(
|
||||
z.string(),
|
||||
z.record(
|
||||
z.string(),
|
||||
z.object({ resetsAt: z.number(), lastUsed: z.number(), consumed: z.number() }),
|
||||
),
|
||||
),
|
||||
});
|
||||
type StoredState = z.infer<typeof storedState>;
|
||||
|
||||
function withoutExcludedState(state: StoredState): StoredState {
|
||||
const windows = state.latest?.windows.filter((window) => !isExcludedWindow(window));
|
||||
return {
|
||||
...state,
|
||||
latest: state.latest && windows?.length ? { ...state.latest, windows } : undefined,
|
||||
sessions: Object.fromEntries(
|
||||
Object.entries(state.sessions).map(([session, tracked]) => [
|
||||
session,
|
||||
Object.fromEntries(
|
||||
Object.entries(tracked).filter(
|
||||
([id]) => !isExcludedLimit(id.split(":", 1)[0], undefined),
|
||||
),
|
||||
),
|
||||
]),
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
type QuotaContext = Pick<Plugin.Context, "integration" | "session" | "storage">;
|
||||
type QuotaFetch = (input: string | URL | Request, init?: RequestInit) => Promise<Response>;
|
||||
|
||||
const quotaWindow = z.object({
|
||||
limit_window_seconds: z.number().finite().positive(),
|
||||
used_percent: z.number().finite(),
|
||||
reset_at: z.number().finite(),
|
||||
});
|
||||
const additionalRateLimitIdentity = z.object({
|
||||
limit_name: z.string().optional(),
|
||||
metered_feature: z.string().optional(),
|
||||
});
|
||||
const quotaPayload = z.object({
|
||||
plan_type: z.string(),
|
||||
account_id: z.string().optional(),
|
||||
rate_limit: z
|
||||
.object({
|
||||
allowed: z.boolean().optional(),
|
||||
primary_window: quotaWindow.nullish(),
|
||||
secondary_window: quotaWindow.nullish(),
|
||||
})
|
||||
.nullish(),
|
||||
additional_rate_limits: z.preprocess(
|
||||
(value) =>
|
||||
Array.isArray(value)
|
||||
? value.filter((item) => {
|
||||
const identity = additionalRateLimitIdentity.safeParse(item);
|
||||
return (
|
||||
!identity.success ||
|
||||
!isExcludedLimit(identity.data.metered_feature, identity.data.limit_name)
|
||||
);
|
||||
})
|
||||
: value,
|
||||
z
|
||||
.array(
|
||||
z.object({
|
||||
limit_name: z.string(),
|
||||
metered_feature: z.string(),
|
||||
rate_limit: z
|
||||
.object({
|
||||
allowed: z.boolean().optional(),
|
||||
primary_window: quotaWindow.nullish(),
|
||||
secondary_window: quotaWindow.nullish(),
|
||||
})
|
||||
.nullish(),
|
||||
}),
|
||||
)
|
||||
.nullish(),
|
||||
),
|
||||
});
|
||||
|
||||
function normalizeWindow(
|
||||
id: string,
|
||||
item: z.infer<typeof quotaWindow>,
|
||||
label?: string,
|
||||
): QuotaWindow {
|
||||
return {
|
||||
id,
|
||||
label: label ?? null,
|
||||
seconds: item.limit_window_seconds,
|
||||
used: Math.max(0, Math.min(100, item.used_percent)),
|
||||
resetsAt: item.reset_at < 10_000_000_000 ? item.reset_at * 1000 : item.reset_at,
|
||||
};
|
||||
}
|
||||
|
||||
/** Normalize valid windows from the documented Codex usage payload. */
|
||||
export function normalizeQuota(
|
||||
input: Schema.Json,
|
||||
fetchedAt = Date.now(),
|
||||
): QuotaSnapshot | undefined {
|
||||
const parsed = quotaPayload.safeParse(input);
|
||||
if (!parsed.success) return;
|
||||
const plan = parsed.data.plan_type.toLowerCase();
|
||||
const windows: QuotaWindow[] = [];
|
||||
if (parsed.data.rate_limit)
|
||||
for (const [position, window] of [
|
||||
["primary", parsed.data.rate_limit.primary_window],
|
||||
["secondary", parsed.data.rate_limit.secondary_window],
|
||||
] as const)
|
||||
if (window) windows.push(normalizeWindow(`codex:${position}`, window));
|
||||
for (const additional of parsed.data.additional_rate_limits ?? []) {
|
||||
if (!additional.rate_limit) continue;
|
||||
for (const [position, window] of [
|
||||
["primary", additional.rate_limit.primary_window],
|
||||
["secondary", additional.rate_limit.secondary_window],
|
||||
] as const)
|
||||
if (window)
|
||||
windows.push(
|
||||
normalizeWindow(
|
||||
`${additional.metered_feature}:${position}`,
|
||||
window,
|
||||
additional.limit_name,
|
||||
),
|
||||
);
|
||||
}
|
||||
if (windows.length === 0) return;
|
||||
return { plan, fetchedAt, windows };
|
||||
}
|
||||
|
||||
function accountID(credential: Credential.OAuth) {
|
||||
const metadataID = z
|
||||
.string()
|
||||
.safeParse(credential.metadata?.accountID ?? credential.metadata?.accountId);
|
||||
if (metadataID.success) return metadataID.data;
|
||||
const parts = credential.access.split(".");
|
||||
if (parts.length !== 3) return;
|
||||
try {
|
||||
const claims = z
|
||||
.object({
|
||||
chatgpt_account_id: z.string().optional(),
|
||||
"https://api.openai.com/auth": z
|
||||
.object({ chatgpt_account_id: z.string().optional() })
|
||||
.optional(),
|
||||
organizations: z.array(z.object({ id: z.string() })).optional(),
|
||||
})
|
||||
.safeParse(JSON.parse(Buffer.from(parts[1]!, "base64url").toString()));
|
||||
if (!claims.success) return;
|
||||
return (
|
||||
claims.data.chatgpt_account_id ??
|
||||
claims.data["https://api.openai.com/auth"]?.chatgpt_account_id ??
|
||||
claims.data.organizations?.[0]?.id
|
||||
);
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
async function fingerprint(value: string) {
|
||||
const bytes = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(value));
|
||||
return Buffer.from(bytes).toString("hex");
|
||||
}
|
||||
|
||||
export class QuotaTracker {
|
||||
private state: StoredState | undefined;
|
||||
private timer: ReturnType<typeof setInterval> | undefined;
|
||||
private request: Promise<QuotaSnapshot | undefined> | undefined;
|
||||
private readonly requests = new Set<Promise<QuotaSnapshot | undefined>>();
|
||||
private readonly active = new Map<string, Set<string>>();
|
||||
private controller: AbortController | undefined;
|
||||
private generation = 0;
|
||||
private disposed = false;
|
||||
|
||||
constructor(
|
||||
private readonly context: QuotaContext,
|
||||
private readonly changed: () => Promise<void>,
|
||||
private readonly fetcher: QuotaFetch = fetch,
|
||||
private readonly pollMs = POLL_MS,
|
||||
) {}
|
||||
|
||||
async init() {
|
||||
try {
|
||||
await this.resolveCredential();
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
private async resolveCredential(generation = this.generation) {
|
||||
const connection = await this.context.integration.connection.active(INTEGRATION_ID);
|
||||
if (generation !== this.generation) return;
|
||||
const credential = connection
|
||||
? await this.context.integration.connection.resolve(connection)
|
||||
: undefined;
|
||||
if (generation !== this.generation) return;
|
||||
if (credential?.type !== "oauth") {
|
||||
if (this.state) await this.clear();
|
||||
return;
|
||||
}
|
||||
const id = accountID(credential);
|
||||
const account = id
|
||||
? await fingerprint(`account:${id}`)
|
||||
: (this.state?.account ?? (await fingerprint(`credential:${credential.refresh}`)));
|
||||
if (generation !== this.generation) return;
|
||||
if (this.state?.account !== account) {
|
||||
const parsed = storedState.safeParse(await this.context.storage.get(STORAGE_KEY));
|
||||
if (generation !== this.generation) return;
|
||||
const stored = parsed.success ? parsed.data : undefined;
|
||||
if (generation !== this.generation) return;
|
||||
this.state =
|
||||
stored?.account === account
|
||||
? withoutExcludedState(stored)
|
||||
: { account, sessions: {} };
|
||||
}
|
||||
if (generation !== this.generation) return;
|
||||
if (!this.timer) this.timer = setInterval(() => void this.refresh(), this.pollMs);
|
||||
return credential;
|
||||
}
|
||||
|
||||
private async clear() {
|
||||
this.state = undefined;
|
||||
if (this.timer) clearInterval(this.timer);
|
||||
this.timer = undefined;
|
||||
await this.context.storage.remove(STORAGE_KEY);
|
||||
await this.changed();
|
||||
}
|
||||
|
||||
async refresh(): Promise<QuotaSnapshot | undefined> {
|
||||
const generation = this.generation;
|
||||
if (this.disposed) return;
|
||||
if (this.request) return this.request;
|
||||
const request = this.query();
|
||||
this.request = request;
|
||||
this.requests.add(request);
|
||||
try {
|
||||
const result = await request;
|
||||
if (generation !== this.generation) return;
|
||||
return result ?? this.state?.latest;
|
||||
} finally {
|
||||
this.requests.delete(request);
|
||||
if (this.request === request) this.request = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
private async query() {
|
||||
const generation = this.generation;
|
||||
try {
|
||||
const credential = await this.resolveCredential(generation);
|
||||
if (!credential || !this.state || generation !== this.generation) return;
|
||||
const controller = new AbortController();
|
||||
this.controller = controller;
|
||||
const timeout = setTimeout(() => controller.abort(), TIMEOUT_MS);
|
||||
let response: Response;
|
||||
try {
|
||||
const headers = new Headers({
|
||||
Authorization: `Bearer ${credential.access}`,
|
||||
Accept: "application/json",
|
||||
});
|
||||
const id = accountID(credential);
|
||||
if (id) headers.set("ChatGPT-Account-Id", id);
|
||||
response = await this.fetcher(USAGE_URL, { headers, signal: controller.signal });
|
||||
} finally {
|
||||
clearTimeout(timeout);
|
||||
if (this.controller === controller) this.controller = undefined;
|
||||
}
|
||||
if (!response.ok || generation !== this.generation) return;
|
||||
// SAFETY: JSON.parse semantics from Response.json produce only JSON values;
|
||||
// quotaPayload validates all fields used below.
|
||||
const payload = (await response.json()) as Schema.Json;
|
||||
if (generation !== this.generation || !this.state) return;
|
||||
const parsed = quotaPayload.safeParse(payload);
|
||||
if (parsed.success && parsed.data.account_id) {
|
||||
const account = await fingerprint(`account:${parsed.data.account_id}`);
|
||||
if (generation !== this.generation || !this.state) return;
|
||||
if (account !== this.state.account) this.state = { account, sessions: {} };
|
||||
}
|
||||
const snapshot = normalizeQuota(payload);
|
||||
if (!snapshot) return;
|
||||
this.apply(snapshot);
|
||||
await this.persist();
|
||||
await this.changed();
|
||||
return snapshot;
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
private apply(snapshot: QuotaSnapshot) {
|
||||
if (!this.state) return;
|
||||
for (const root of this.active.keys()) {
|
||||
const tracked = (this.state.sessions[root] ??= {});
|
||||
for (const window of snapshot.windows) {
|
||||
const previous = tracked[window.id];
|
||||
if (!previous) {
|
||||
tracked[window.id] = {
|
||||
resetsAt: window.resetsAt,
|
||||
lastUsed: window.used,
|
||||
consumed: 0,
|
||||
};
|
||||
continue;
|
||||
}
|
||||
if (previous.resetsAt !== window.resetsAt) {
|
||||
previous.resetsAt = window.resetsAt;
|
||||
previous.lastUsed = window.used;
|
||||
previous.consumed += window.used;
|
||||
} else if (window.used >= previous.lastUsed) {
|
||||
previous.consumed += window.used - previous.lastUsed;
|
||||
previous.lastUsed = window.used;
|
||||
}
|
||||
}
|
||||
}
|
||||
this.state.latest = snapshot;
|
||||
}
|
||||
|
||||
private async root(sessionID: string) {
|
||||
let current = sessionID;
|
||||
const seen = new Set<string>();
|
||||
while (!seen.has(current)) {
|
||||
seen.add(current);
|
||||
const session = await this.context.session.get({ sessionID: current });
|
||||
if (!session.parentID) return current;
|
||||
current = session.parentID;
|
||||
}
|
||||
return sessionID;
|
||||
}
|
||||
|
||||
async start(sessionID: string) {
|
||||
const generation = this.generation;
|
||||
try {
|
||||
const root = await this.root(sessionID);
|
||||
if (generation !== this.generation) return;
|
||||
const family = this.active.get(root);
|
||||
if (family?.has(sessionID)) return;
|
||||
const snapshot = await this.refresh();
|
||||
if (!snapshot || !this.state || generation !== this.generation) return;
|
||||
if (!family) {
|
||||
const previous = this.state.sessions[root] ?? {};
|
||||
this.state.sessions[root] = Object.fromEntries(
|
||||
snapshot.windows.map((window) => [
|
||||
window.id,
|
||||
{
|
||||
resetsAt: window.resetsAt,
|
||||
lastUsed: window.used,
|
||||
consumed: previous[window.id]?.consumed ?? 0,
|
||||
},
|
||||
]),
|
||||
);
|
||||
}
|
||||
await this.persist();
|
||||
if (generation !== this.generation) return;
|
||||
await this.changed();
|
||||
if (generation !== this.generation) return;
|
||||
const active = family ?? new Set<string>();
|
||||
active.add(sessionID);
|
||||
this.active.set(root, active);
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
async complete(sessionID: string) {
|
||||
try {
|
||||
const root = await this.root(sessionID);
|
||||
const family = this.active.get(root);
|
||||
if (!family?.has(sessionID)) return;
|
||||
await this.refresh();
|
||||
family.delete(sessionID);
|
||||
if (family.size === 0) this.active.delete(root);
|
||||
await this.persist();
|
||||
await this.changed();
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
async status(sessionID: string): Promise<QuotaStatus | undefined> {
|
||||
if (!this.state?.latest) return;
|
||||
const tracked = this.state.sessions[await this.root(sessionID)];
|
||||
return {
|
||||
plan: this.state.latest.plan,
|
||||
fetchedAt: this.state.latest.fetchedAt,
|
||||
windows: this.state.latest.windows.map((window) => ({
|
||||
...window,
|
||||
remaining: Math.max(0, 100 - window.used),
|
||||
sessionConsumed: tracked?.[window.id]?.consumed ?? 0,
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
private async persist() {
|
||||
if (!this.state) return;
|
||||
const value: Schema.Json = this.state.latest
|
||||
? {
|
||||
account: this.state.account,
|
||||
sessions: this.state.sessions,
|
||||
latest: this.state.latest,
|
||||
}
|
||||
: { account: this.state.account, sessions: this.state.sessions };
|
||||
await this.context.storage.set(STORAGE_KEY, value);
|
||||
}
|
||||
|
||||
async authChanged(switched = false) {
|
||||
if (switched) {
|
||||
this.generation++;
|
||||
this.controller?.abort();
|
||||
this.controller = undefined;
|
||||
this.request = undefined;
|
||||
if (this.state) await this.clear();
|
||||
}
|
||||
await this.refresh();
|
||||
}
|
||||
|
||||
async dispose() {
|
||||
this.disposed = true;
|
||||
this.generation++;
|
||||
this.controller?.abort();
|
||||
if (this.timer) clearInterval(this.timer);
|
||||
this.timer = undefined;
|
||||
await Promise.all(this.requests);
|
||||
}
|
||||
}
|
||||
+78
@@ -0,0 +1,78 @@
|
||||
import { Plugin } from "@opencode/plugin/tui";
|
||||
import { createResource, createSignal, For, Show } from "solid-js";
|
||||
import type { QuotaStatus, QuotaWindow } from "./quota.js";
|
||||
import { QuotaRpc } from "./quota-rpc.js";
|
||||
|
||||
function duration(seconds: number) {
|
||||
if (seconds % 86_400 === 0) return `${seconds / 86_400}d`;
|
||||
if (seconds % 3_600 === 0) return `${seconds / 3_600}h`;
|
||||
return `${Math.round(seconds / 60)}m`;
|
||||
}
|
||||
|
||||
function percent(value: number) {
|
||||
return value < 0.1 && value > 0 ? "<0.1" : value.toFixed(value < 10 ? 1 : 0);
|
||||
}
|
||||
|
||||
export function formatQuotaWindow(
|
||||
window: QuotaWindow & { remaining: number; sessionConsumed: number },
|
||||
) {
|
||||
const label = window.label
|
||||
? `${window.label} ${duration(window.seconds)}`
|
||||
: duration(window.seconds);
|
||||
return `${label}: ${percent(window.remaining)}% left · ~${percent(window.sessionConsumed)}pp this session`;
|
||||
}
|
||||
|
||||
export async function loadQuotaStatus(load: () => Promise<QuotaStatus | null>) {
|
||||
try {
|
||||
return await load();
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export default Plugin.define({
|
||||
id: "stanp-opencode.quota-tui",
|
||||
setup(context) {
|
||||
const quota = context.client.rpc(QuotaRpc);
|
||||
const [revision, setRevision] = createSignal(0);
|
||||
const retry = setInterval(() => {
|
||||
setRevision((value) => value + 1);
|
||||
}, 60_000);
|
||||
const unsubscribe = quota.events.on("updated", () => {
|
||||
setRevision((value) => value + 1);
|
||||
});
|
||||
|
||||
const Slot = (props: { sessionID: string }) => {
|
||||
const [status] = createResource(
|
||||
() => `${props.sessionID}:${revision()}`,
|
||||
() => loadQuotaStatus(() => quota.status({ sessionID: props.sessionID })),
|
||||
);
|
||||
return (
|
||||
<Show when={status()}>
|
||||
<box flexDirection="column">
|
||||
<text fg={context.theme.text.default}>
|
||||
<b>ChatGPT quota ({status()!.plan})</b>
|
||||
</text>
|
||||
<For each={status()!.windows}>
|
||||
{(window) => (
|
||||
<text fg={context.theme.text.muted}>
|
||||
{formatQuotaWindow(window)}
|
||||
</text>
|
||||
)}
|
||||
</For>
|
||||
</box>
|
||||
</Show>
|
||||
);
|
||||
};
|
||||
|
||||
const remove = context.ui.slot({
|
||||
append: "sidebar.footer",
|
||||
render: (input) => <Slot sessionID={input.sessionID} />,
|
||||
});
|
||||
return () => {
|
||||
remove();
|
||||
unsubscribe();
|
||||
clearInterval(retry);
|
||||
};
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,224 @@
|
||||
import { afterEach, expect, test } from "bun:test";
|
||||
import { chmod, mkdir, mkdtemp, readFile, readdir, rm, writeFile } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { dirname, join } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const installer = fileURLToPath(new URL("../install-local.sh", import.meta.url));
|
||||
const directories: string[] = [];
|
||||
const jsonc = '{\n // This file must remain untouched.\n "theme": "dark"\n}\n';
|
||||
const oldPlugin = 'export default { id: "old", setup() { return "old"; } };\n';
|
||||
type FixtureConfig =
|
||||
| { permissions: { shell: string }; plugins: { package: string }[] }
|
||||
| {
|
||||
theme: string;
|
||||
plugins: {
|
||||
package: string;
|
||||
options: { mode: string; coordinate: string; lunaPriority: boolean };
|
||||
}[];
|
||||
}
|
||||
| { plugins: string };
|
||||
|
||||
afterEach(async () => {
|
||||
await Promise.all(
|
||||
directories.splice(0).map((path) => rm(path, { recursive: true, force: true })),
|
||||
);
|
||||
});
|
||||
|
||||
async function run(command: string[], cwd: string, env: NodeJS.ProcessEnv) {
|
||||
const child = Bun.spawn(command, {
|
||||
cwd,
|
||||
env,
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
timeout: 20_000,
|
||||
});
|
||||
const [exitCode, stdout, stderr] = await Promise.all([
|
||||
child.exited,
|
||||
new Response(child.stdout).text(),
|
||||
new Response(child.stderr).text(),
|
||||
]);
|
||||
return { exitCode, stdout, stderr };
|
||||
}
|
||||
|
||||
async function fixture(configValue?: FixtureConfig) {
|
||||
const directory = await mkdtemp(join(tmpdir(), "stanp-local-install-"));
|
||||
directories.push(directory);
|
||||
const checkout = join(directory, "checkout with spaces");
|
||||
const cwd = join(directory, "different cwd");
|
||||
const config = join(directory, "config with spaces");
|
||||
const bin = join(directory, "bin");
|
||||
const installed = join(config, "plugins", "stanp-opencode");
|
||||
await Promise.all([
|
||||
mkdir(checkout),
|
||||
mkdir(cwd),
|
||||
mkdir(bin),
|
||||
mkdir(installed, { recursive: true }),
|
||||
]);
|
||||
await Promise.all([
|
||||
Bun.write(join(checkout, "install-local.sh"), Bun.file(installer)),
|
||||
writeFile(
|
||||
join(checkout, "package.json"),
|
||||
JSON.stringify({
|
||||
name: "stanp-opencode",
|
||||
version: "1.0.0",
|
||||
type: "module",
|
||||
exports: { ".": "./dist/index.js", "./tui": { import: "./dist/tui.js" } },
|
||||
}),
|
||||
),
|
||||
writeFile(join(checkout, "bun.lock"), ""),
|
||||
writeFile(join(config, "opencode.jsonc"), jsonc),
|
||||
writeFile(
|
||||
join(config, "opencode.json"),
|
||||
JSON.stringify(
|
||||
configValue ?? {
|
||||
permissions: { shell: "ask" },
|
||||
plugins: [{ package: "existing-plugin" }],
|
||||
},
|
||||
),
|
||||
),
|
||||
writeFile(join(installed, "index.js"), oldPlugin),
|
||||
writeFile(
|
||||
join(installed, "package.json"),
|
||||
'{"name":"stanp-opencode","type":"module","main":"./index.js"}\n',
|
||||
),
|
||||
writeFile(join(config, "plugins", "stanp-opencode.js"), "legacy\n"),
|
||||
writeFile(
|
||||
join(bin, "bun"),
|
||||
`#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
if [[ "\${1:-}" == "install" ]]; then
|
||||
[[ "\${2:-}" == "--frozen-lockfile" ]]
|
||||
printf '%s\n' "$PWD" >> "$CALLS"
|
||||
[[ "$MODE" != "dependency" ]]
|
||||
exit
|
||||
fi
|
||||
if [[ "\${1:-}" == "run" && "\${2:-}" == "build" ]]; then
|
||||
printf '%s\n' "$PWD" >> "$CALLS"
|
||||
[[ "$MODE" != "build" ]]
|
||||
mkdir -p dist
|
||||
if [[ "$MODE" == "validation" ]]; then
|
||||
printf '%s\n' 'export default { id: "stanp-opencode", server() {} };' > dist/index.js
|
||||
else
|
||||
printf 'export default { id: "stanp-opencode", setup() { return "%s"; } };\n' "$GENERATION" > dist/index.js
|
||||
fi
|
||||
printf '%s\n' 'export default { id: "stanp-opencode.quota-tui", setup() {} };' > dist/tui.js
|
||||
exit
|
||||
fi
|
||||
exec "$REAL_BUN" "$@"
|
||||
`,
|
||||
),
|
||||
writeFile(
|
||||
join(bin, "curl"),
|
||||
`#!/usr/bin/env bash
|
||||
touch "$FETCH_MARKER"
|
||||
printf 'release fetching is forbidden\n' >&2
|
||||
exit 99
|
||||
`,
|
||||
),
|
||||
]);
|
||||
await Promise.all([chmod(join(bin, "bun"), 0o755), chmod(join(bin, "curl"), 0o755)]);
|
||||
const env = {
|
||||
...Bun.env,
|
||||
PATH: `${bin}:${dirname(process.execPath)}:${Bun.env.PATH ?? "/usr/bin:/bin"}`,
|
||||
HOME: join(directory, "home"),
|
||||
XDG_CONFIG_HOME: join(directory, "ignored-xdg"),
|
||||
OPENCODE_CONFIG_DIR: config,
|
||||
REAL_BUN: process.execPath,
|
||||
MODE: "success",
|
||||
GENERATION: "first",
|
||||
CALLS: join(directory, "calls"),
|
||||
FETCH_MARKER: join(directory, "release-fetch-attempted"),
|
||||
};
|
||||
return { directory, checkout, cwd, config, installed, env };
|
||||
}
|
||||
|
||||
test("builds from its checkout and installs defaults when invoked elsewhere", async () => {
|
||||
const setup = await fixture();
|
||||
const result = await run(
|
||||
["bash", join(setup.checkout, "install-local.sh")],
|
||||
setup.cwd,
|
||||
setup.env,
|
||||
);
|
||||
expect(result, result.stderr).toMatchObject({ exitCode: 0 });
|
||||
expect(await readFile(setup.env.CALLS, "utf8")).toBe(`${setup.checkout}\n${setup.checkout}\n`);
|
||||
expect(await Bun.file(setup.env.FETCH_MARKER).exists()).toBe(false);
|
||||
expect(await readFile(join(setup.config, "opencode.jsonc"), "utf8")).toBe(jsonc);
|
||||
expect(JSON.parse(await readFile(join(setup.config, "opencode.json"), "utf8"))).toEqual({
|
||||
$schema: "https://opencode.ai/config.json",
|
||||
permissions: { shell: "ask" },
|
||||
plugins: [
|
||||
{ package: "existing-plugin" },
|
||||
{
|
||||
package: "./plugins/stanp-opencode",
|
||||
options: { mode: "gpt", provider: "openai", lunaPriority: true },
|
||||
},
|
||||
],
|
||||
});
|
||||
const imported = await run(
|
||||
[
|
||||
process.execPath,
|
||||
"-e",
|
||||
"const { default: plugin } = await import(process.argv[1]); console.log(plugin.setup());",
|
||||
setup.installed,
|
||||
],
|
||||
setup.cwd,
|
||||
setup.env,
|
||||
);
|
||||
expect(imported, imported.stderr).toMatchObject({ exitCode: 0, stdout: "first\n" });
|
||||
expect((await readdir(setup.installed)).sort()).toEqual(["index.js", "package.json", "tui.js"]);
|
||||
expect(await Bun.file(join(setup.config, "plugins", "stanp-opencode.js")).exists()).toBe(false);
|
||||
});
|
||||
|
||||
test("reinstall replaces bundles while preserving existing settings and plugin options", async () => {
|
||||
const configured = {
|
||||
theme: "dark",
|
||||
plugins: [
|
||||
{
|
||||
package: "./plugins/stanp-opencode",
|
||||
options: { mode: "custom", coordinate: "provider/model", lunaPriority: false },
|
||||
},
|
||||
],
|
||||
};
|
||||
const setup = await fixture(configured);
|
||||
const first = await run(
|
||||
["bash", join(setup.checkout, "install-local.sh")],
|
||||
setup.cwd,
|
||||
setup.env,
|
||||
);
|
||||
expect(first, first.stderr).toMatchObject({ exitCode: 0 });
|
||||
const second = await run(["bash", join(setup.checkout, "install-local.sh")], setup.cwd, {
|
||||
...setup.env,
|
||||
GENERATION: "second",
|
||||
});
|
||||
expect(second, second.stderr).toMatchObject({ exitCode: 0 });
|
||||
expect(JSON.parse(await readFile(join(setup.config, "opencode.json"), "utf8"))).toEqual({
|
||||
$schema: "https://opencode.ai/config.json",
|
||||
...configured,
|
||||
});
|
||||
expect(await readFile(join(setup.installed, "index.js"), "utf8")).toContain('return "second"');
|
||||
});
|
||||
|
||||
for (const mode of ["dependency", "build", "validation", "config"] as const) {
|
||||
test(`${mode} failure leaves the previous installation and configuration untouched`, async () => {
|
||||
const invalidConfig = mode === "config" ? { plugins: "not-an-array" } : undefined;
|
||||
const setup = await fixture(invalidConfig);
|
||||
const beforeConfig = await readFile(join(setup.config, "opencode.json"), "utf8");
|
||||
const result = await run(["bash", join(setup.checkout, "install-local.sh")], setup.cwd, {
|
||||
...setup.env,
|
||||
MODE: mode === "config" ? "success" : mode,
|
||||
});
|
||||
expect(result.exitCode, result.stderr).not.toBe(0);
|
||||
expect(await readFile(join(setup.installed, "index.js"), "utf8")).toBe(oldPlugin);
|
||||
expect(await readFile(join(setup.config, "opencode.json"), "utf8")).toBe(beforeConfig);
|
||||
expect(await readFile(join(setup.config, "opencode.jsonc"), "utf8")).toBe(jsonc);
|
||||
expect(await readFile(join(setup.config, "plugins", "stanp-opencode.js"), "utf8")).toBe(
|
||||
"legacy\n",
|
||||
);
|
||||
expect((await readdir(join(setup.config, "plugins"))).sort()).toEqual([
|
||||
"stanp-opencode",
|
||||
"stanp-opencode.js",
|
||||
]);
|
||||
expect(await Bun.file(setup.env.FETCH_MARKER).exists()).toBe(false);
|
||||
});
|
||||
}
|
||||
+66
-5
@@ -18,6 +18,7 @@ import { fileURLToPath } from "node:url";
|
||||
const installer = fileURLToPath(new URL("../install.sh", import.meta.url));
|
||||
const directories: string[] = [];
|
||||
const configContents = '{\n // Keep user settings and comments.\n "theme": "dark"\n}\n';
|
||||
const jsonConfig = { permissions: { shell: "ask" }, plugins: [{ package: "existing-plugin" }] };
|
||||
const previousPlugin = 'export default { id: "previous", setup() { return "preserved"; } };\n';
|
||||
const previousTarget = "../stanp-opencode/install.previous";
|
||||
|
||||
@@ -43,7 +44,7 @@ async function run(command: string[], cwd: string, env: NodeJS.ProcessEnv) {
|
||||
return { exitCode, stdout, stderr };
|
||||
}
|
||||
|
||||
async function install(mode: "native" | "checksum" | "legacy") {
|
||||
async function install(mode: "native" | "checksum" | "legacy", configured = false) {
|
||||
const directory = await mkdtemp(join(tmpdir(), "stanp-install-"));
|
||||
directories.push(directory);
|
||||
const config =
|
||||
@@ -60,6 +61,27 @@ async function install(mode: "native" | "checksum" | "legacy") {
|
||||
]);
|
||||
await Promise.all([
|
||||
writeFile(join(config, "opencode.jsonc"), configContents),
|
||||
writeFile(
|
||||
join(config, "opencode.json"),
|
||||
JSON.stringify(
|
||||
configured
|
||||
? {
|
||||
...jsonConfig,
|
||||
plugins: [
|
||||
...jsonConfig.plugins,
|
||||
{
|
||||
package: "./plugins/stanp-opencode",
|
||||
options: {
|
||||
mode: "gpt",
|
||||
provider: "existing-provider",
|
||||
lunaPriority: false,
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
: jsonConfig,
|
||||
),
|
||||
),
|
||||
symlink(previousTarget, join(config, "plugins", "stanp-opencode")),
|
||||
writeFile(join(config, "stanp-opencode", "install.previous", "index.js"), previousPlugin),
|
||||
writeFile(
|
||||
@@ -74,7 +96,7 @@ async function install(mode: "native" | "checksum" | "legacy") {
|
||||
version: "1.0.0",
|
||||
type: "module",
|
||||
main: "./dist/index.js",
|
||||
exports: "./dist/index.js",
|
||||
exports: { ".": "./dist/index.js", "./tui": "./dist/tui.js" },
|
||||
scripts: { postinstall: "bun ./postinstall.js" },
|
||||
}),
|
||||
),
|
||||
@@ -89,6 +111,10 @@ async function install(mode: "native" | "checksum" | "legacy") {
|
||||
? 'export default { id: "stanp-opencode", async server() { return {}; } };\n'
|
||||
: 'export default { id: "stanp-opencode", setup() { return "native-ready"; } };\n'),
|
||||
),
|
||||
writeFile(
|
||||
join(fixture, "dist", "tui.js"),
|
||||
'export default { id: "stanp-opencode.quota-tui", setup() { return "tui-ready"; } };\n',
|
||||
),
|
||||
]);
|
||||
const env = {
|
||||
...Bun.env,
|
||||
@@ -165,7 +191,7 @@ exec sha256sum "$@"
|
||||
return { directory, config, env, result };
|
||||
}
|
||||
|
||||
test("installs an importable native plugin under XDG without changing user configuration or running scripts", async () => {
|
||||
test("installs an importable native plugin and default config under XDG without running scripts", async () => {
|
||||
const { directory, config, env, result } = await install("native");
|
||||
expect(result, result.stderr).toMatchObject({ exitCode: 0 });
|
||||
// The generated package path is only known at runtime; exercise its module-loading boundary.
|
||||
@@ -174,7 +200,7 @@ test("installs an importable native plugin under XDG without changing user confi
|
||||
process.execPath,
|
||||
"-e",
|
||||
"const { default: plugin } = await import(process.argv[1]); console.log(JSON.stringify([plugin.id, plugin.setup()]));",
|
||||
join(config, "plugins", "stanp-opencode.js"),
|
||||
join(config, "plugins", "stanp-opencode"),
|
||||
],
|
||||
directory,
|
||||
env,
|
||||
@@ -184,19 +210,54 @@ test("installs an importable native plugin under XDG without changing user confi
|
||||
stdout: '["stanp-opencode","native-ready"]\n',
|
||||
});
|
||||
expect(await readFile(join(config, "opencode.jsonc"), "utf8")).toBe(configContents);
|
||||
expect(await Bun.file(join(config, "plugins", "stanp-opencode")).exists()).toBe(false);
|
||||
expect(JSON.parse(await readFile(join(config, "opencode.json"), "utf8"))).toEqual({
|
||||
$schema: "https://opencode.ai/config.json",
|
||||
plugins: [
|
||||
{ package: "existing-plugin" },
|
||||
{
|
||||
package: "./plugins/stanp-opencode",
|
||||
options: { mode: "gpt", provider: "openai", lunaPriority: true },
|
||||
},
|
||||
],
|
||||
permissions: { shell: "ask" },
|
||||
});
|
||||
expect(await Bun.file(join(config, "plugins", "stanp-opencode", "package.json")).exists()).toBe(
|
||||
true,
|
||||
);
|
||||
expect(await Bun.file(join(config, "plugins", "stanp-opencode", "tui.js")).exists()).toBe(true);
|
||||
expect(
|
||||
await readFile(join(config, "stanp-opencode", "install.previous", "index.js"), "utf8"),
|
||||
).toBe(previousPlugin);
|
||||
expect(await Bun.file(env.POSTINSTALL_MARKER).exists()).toBe(false);
|
||||
}, 60_000);
|
||||
|
||||
test("preserves an existing stanp-opencode configuration", async () => {
|
||||
const { config, result } = await install("native", true);
|
||||
expect(result, result.stderr).toMatchObject({ exitCode: 0 });
|
||||
const installed = JSON.parse(await readFile(join(config, "opencode.json"), "utf8"));
|
||||
expect(installed.plugins).toEqual([
|
||||
{ package: "existing-plugin" },
|
||||
{
|
||||
package: "./plugins/stanp-opencode",
|
||||
options: {
|
||||
mode: "gpt",
|
||||
provider: "existing-provider",
|
||||
lunaPriority: false,
|
||||
},
|
||||
},
|
||||
]);
|
||||
expect(installed.permissions).toEqual(jsonConfig.permissions);
|
||||
}, 60_000);
|
||||
|
||||
for (const mode of ["checksum", "legacy"] as const) {
|
||||
test(`${mode} failure preserves the installed plugin in a custom path containing spaces`, async () => {
|
||||
const { directory, config, env, result } = await install(mode);
|
||||
expect(result.exitCode, result.stderr).not.toBe(0);
|
||||
expect(await readlink(join(config, "plugins", "stanp-opencode"))).toBe(previousTarget);
|
||||
expect(await readFile(join(config, "opencode.jsonc"), "utf8")).toBe(configContents);
|
||||
expect(JSON.parse(await readFile(join(config, "opencode.json"), "utf8"))).toEqual(
|
||||
jsonConfig,
|
||||
);
|
||||
expect(
|
||||
await readFile(join(config, "stanp-opencode", "install.previous", "index.js"), "utf8"),
|
||||
).toBe(previousPlugin);
|
||||
|
||||
+350
-24
@@ -1,12 +1,17 @@
|
||||
import { afterEach, expect, test } from "bun:test";
|
||||
import { chmod, mkdir, mkdtemp, rm, symlink, writeFile } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { Agent, Model, Plugin, Provider } from "@opencode/plugin";
|
||||
import type { AgentEditor } from "@opencode/plugin/promise/agent";
|
||||
import type { DeepMutable } from "@opencode/plugin/promise/types";
|
||||
import type { Info, ToolContext, ToolEditor } from "@opencode/plugin/promise/tool";
|
||||
import type { JsonValue, ModelInfo, SessionGetInput } from "@opencode/client";
|
||||
import type { SessionContext } from "@opencode/plugin/promise/session";
|
||||
import { z } from "zod";
|
||||
import { gptAgents } from "../src/config.js";
|
||||
import plugin from "../src/index.js";
|
||||
import { playwrightMcpCommand } from "../src/playwright.js";
|
||||
|
||||
const providerID = Provider.ID.make("test");
|
||||
function model(id: string): ModelInfo {
|
||||
@@ -26,13 +31,25 @@ type FixtureSetup = (
|
||||
async function host(
|
||||
models: ModelInfo[] = allModels,
|
||||
options: Plugin.Context["options"] = { provider: "test" },
|
||||
configuredMcp: Record<
|
||||
string,
|
||||
{
|
||||
type: "local";
|
||||
command: string[];
|
||||
codemode?: boolean;
|
||||
disabled?: boolean;
|
||||
timeout?: { startup?: number; catalog?: number; execution?: number };
|
||||
}
|
||||
> = {},
|
||||
configuredMcpTools: string[] = [],
|
||||
directory = "/test",
|
||||
) {
|
||||
const agents = new Map<string, DeepMutable<Agent.Info>>();
|
||||
const build: DeepMutable<Agent.Info> = Agent.Info.default(Agent.ID.make("build"));
|
||||
build.permissions.push({ action: "shell", resource: "git push *", effect: "ask" });
|
||||
agents.set("build", build);
|
||||
const originalBuild = structuredClone(build);
|
||||
const sessionHooks: Record<string, (event: HookEvent) => Promise<void> | void> = {};
|
||||
const sessionHooks: Record<string, ((event: HookEvent) => Promise<void> | void)[]> = {};
|
||||
let before!: (event: { tool: string; agent: string; input: unknown }) => Promise<void> | void;
|
||||
let parent = "build";
|
||||
let child = "fast-lo";
|
||||
@@ -40,17 +57,72 @@ async function host(
|
||||
const registration = { async dispose() {} };
|
||||
let transform!: (editor: AgentEditor) => void;
|
||||
let reloaded = Promise.withResolvers<void>();
|
||||
let events!: ReadableStreamDefaultController<{ type: string }>;
|
||||
const stream = new ReadableStream<{ type: string }>({
|
||||
let events!: ReadableStreamDefaultController<{
|
||||
type: string;
|
||||
location?: { directory: string; workspaceID?: string };
|
||||
}>;
|
||||
const stream = new ReadableStream<{
|
||||
type: string;
|
||||
location?: { directory: string; workspaceID?: string };
|
||||
}>({
|
||||
start(controller) {
|
||||
events = controller;
|
||||
},
|
||||
});
|
||||
const storage = new Map<string, JsonValue>();
|
||||
const tools = new Map<string, Info>();
|
||||
const mcpTools = new Set(configuredMcpTools);
|
||||
const mcpServers = new Map(Object.entries(structuredClone(configuredMcp)));
|
||||
let mcpTransform!: (editor: {
|
||||
get(name: string): (typeof configuredMcp)[string] | undefined;
|
||||
set(name: string, server: (typeof configuredMcp)[string]): void;
|
||||
update(name: string, update: (server: (typeof configuredMcp)[string]) => void): void;
|
||||
}) => void;
|
||||
const toolTransforms: ((editor: ToolEditor) => void)[] = [];
|
||||
const editMcp = () => ({
|
||||
get: (name: string) => mcpServers.get(name),
|
||||
set(name: string, server: (typeof configuredMcp)[string]) {
|
||||
mcpServers.set(name, structuredClone(server));
|
||||
},
|
||||
update(name: string, update: (server: (typeof configuredMcp)[string]) => void) {
|
||||
const server = mcpServers.get(name);
|
||||
if (server) update(server);
|
||||
},
|
||||
});
|
||||
const editTools = (): ToolEditor => ({
|
||||
get(id) {
|
||||
const tool = tools.get(id);
|
||||
return tool ? { ...tool, id } : undefined;
|
||||
},
|
||||
list: () => [
|
||||
...[...tools].map(([id, tool]) => ({ ...tool, id })),
|
||||
...[...mcpTools].map((id) => ({
|
||||
id,
|
||||
name: id,
|
||||
description: id,
|
||||
input: z.object({}),
|
||||
options: { codemode: false },
|
||||
async execute() {
|
||||
return { content: "" };
|
||||
},
|
||||
})),
|
||||
],
|
||||
namespace() {},
|
||||
add(tool) {
|
||||
tools.set(tool.name, tool);
|
||||
},
|
||||
update(id, update) {
|
||||
const tool = tools.get(id);
|
||||
if (tool) update(tool);
|
||||
},
|
||||
remove(id) {
|
||||
tools.delete(id);
|
||||
},
|
||||
});
|
||||
// SAFETY: This test host implements the native APIs used by setup; unrelated context
|
||||
// services are deliberately absent so unexpected new dependencies fail the test.
|
||||
const context = {
|
||||
location: { directory },
|
||||
options,
|
||||
catalog: {
|
||||
model: {
|
||||
@@ -72,6 +144,31 @@ async function host(
|
||||
async set(key: string, value: JsonValue) {
|
||||
storage.set(key, value);
|
||||
},
|
||||
async remove(key: string) {
|
||||
storage.delete(key);
|
||||
},
|
||||
},
|
||||
integration: {
|
||||
connection: {
|
||||
async active() {
|
||||
return undefined;
|
||||
},
|
||||
async resolve() {
|
||||
return undefined;
|
||||
},
|
||||
},
|
||||
},
|
||||
rpc: {
|
||||
async register() {
|
||||
return { ...registration, events: { async emit() {} } };
|
||||
},
|
||||
},
|
||||
mcp: {
|
||||
async transform(callback: typeof mcpTransform) {
|
||||
mcpTransform = callback;
|
||||
callback(editMcp());
|
||||
return registration;
|
||||
},
|
||||
},
|
||||
agent: {
|
||||
async list() {
|
||||
@@ -108,7 +205,7 @@ async function host(
|
||||
},
|
||||
session: {
|
||||
async hook(name: string, callback: (event: HookEvent) => Promise<void> | void) {
|
||||
sessionHooks[name] = callback;
|
||||
(sessionHooks[name] ??= []).push(callback);
|
||||
return registration;
|
||||
},
|
||||
async get({ sessionID }: { sessionID: string }) {
|
||||
@@ -119,24 +216,8 @@ async function host(
|
||||
},
|
||||
tool: {
|
||||
async transform(callback: (editor: ToolEditor) => void) {
|
||||
callback({
|
||||
get(id) {
|
||||
const tool = tools.get(id);
|
||||
return tool ? { ...tool, id } : undefined;
|
||||
},
|
||||
list: () => [...tools].map(([id, tool]) => ({ ...tool, id })),
|
||||
namespace() {},
|
||||
add(tool) {
|
||||
tools.set(tool.name, tool);
|
||||
},
|
||||
update(id, update) {
|
||||
const tool = tools.get(id);
|
||||
if (tool) update(tool);
|
||||
},
|
||||
remove(id) {
|
||||
tools.delete(id);
|
||||
},
|
||||
});
|
||||
toolTransforms.push(callback);
|
||||
callback(editTools());
|
||||
return registration;
|
||||
},
|
||||
async hook(_name: string, callback: typeof before) {
|
||||
@@ -153,6 +234,7 @@ async function host(
|
||||
return {
|
||||
agents,
|
||||
build,
|
||||
mcpServers,
|
||||
defaultAgent,
|
||||
originalBuild,
|
||||
async refresh(next: ModelInfo[]) {
|
||||
@@ -167,6 +249,25 @@ async function host(
|
||||
events.enqueue({ type: "agent.updated" });
|
||||
await reloaded.promise;
|
||||
},
|
||||
changeBuildPermissionsElsewhere(permissions: Agent.Info["permissions"]) {
|
||||
build.permissions = [...permissions];
|
||||
events.enqueue({ type: "agent.updated", location: { directory: "/other" } });
|
||||
},
|
||||
async changeCodegraph(
|
||||
server:
|
||||
| { type: "local"; command: string[]; codemode?: boolean; disabled?: boolean }
|
||||
| undefined,
|
||||
toolNames: string[],
|
||||
) {
|
||||
if (server) mcpServers.set("codegraph", server);
|
||||
else mcpServers.delete("codegraph");
|
||||
mcpTools.clear();
|
||||
for (const name of toolNames) mcpTools.add(name);
|
||||
reloaded = Promise.withResolvers<void>();
|
||||
mcpTransform(editMcp());
|
||||
for (const callback of toolTransforms) callback(editTools());
|
||||
await reloaded.promise;
|
||||
},
|
||||
async todo(name: string, sessionID: string, input: JsonValue = {}) {
|
||||
// SAFETY: Todo executors use only sessionID and agent from the native context.
|
||||
return tools
|
||||
@@ -179,7 +280,7 @@ async function host(
|
||||
async command(caller: string, target: string) {
|
||||
parent = caller;
|
||||
child = target;
|
||||
await sessionHooks.prompt!({ sessionID: "child" });
|
||||
for (const hook of sessionHooks.prompt ?? []) await hook({ sessionID: "child" });
|
||||
},
|
||||
async tools(caller: string) {
|
||||
const event: ModelContext = {
|
||||
@@ -194,7 +295,7 @@ async function host(
|
||||
},
|
||||
},
|
||||
};
|
||||
await sessionHooks.context!(event);
|
||||
for (const hook of sessionHooks.context ?? []) await hook(event);
|
||||
return event.tools;
|
||||
},
|
||||
};
|
||||
@@ -257,6 +358,216 @@ test("worker inheritance and coordinator isolation preserve build and default",
|
||||
});
|
||||
});
|
||||
|
||||
test("coordinator inherits only codegraph MCP permissions when that server exists", async () => {
|
||||
const absent = await host();
|
||||
expect(
|
||||
absent.agents
|
||||
.get("coordinate")
|
||||
?.permissions.some((rule) => rule.action.startsWith("codegraph_")),
|
||||
).toBe(false);
|
||||
expect(absent.mcpServers.has("codegraph")).toBe(false);
|
||||
|
||||
const result = await host(
|
||||
allModels,
|
||||
{ provider: "test" },
|
||||
{
|
||||
codegraph: { type: "local", command: ["codegraph"], codemode: true },
|
||||
docs: { type: "local", command: ["docs"], codemode: true },
|
||||
},
|
||||
["codegraph_codegraph_search", "codegraph_future_query", "docs_search"],
|
||||
);
|
||||
expect(result.mcpServers.get("codegraph")?.codemode).toBe(false);
|
||||
expect(result.mcpServers.get("docs")?.codemode).toBe(true);
|
||||
let coordinate = result.agents.get("coordinate")!;
|
||||
expect(coordinate.permissions).toContainEqual({
|
||||
action: "codegraph_codegraph_search",
|
||||
resource: "*",
|
||||
effect: "allow",
|
||||
});
|
||||
expect(coordinate.permissions.some((rule) => rule.action.startsWith("docs_"))).toBe(false);
|
||||
expect(coordinate.permissions.some((rule) => rule.action === "shell")).toBe(false);
|
||||
expect(coordinate.permissions.some((rule) => rule.action === "execute")).toBe(false);
|
||||
|
||||
await result.changeBuildPermissions([
|
||||
{ action: "*", resource: "*", effect: "allow" },
|
||||
{ action: "code*", resource: "*", effect: "ask" },
|
||||
{ action: "*search", resource: "*", effect: "deny" },
|
||||
{ action: "docs_*", resource: "*", effect: "allow" },
|
||||
{ action: "shell", resource: "*", effect: "allow" },
|
||||
]);
|
||||
coordinate = result.agents.get("coordinate")!;
|
||||
expect(coordinate.permissions).toContainEqual({
|
||||
action: "codegraph_future_query",
|
||||
resource: "*",
|
||||
effect: "ask",
|
||||
});
|
||||
expect(coordinate.permissions).toContainEqual({
|
||||
action: "codegraph_codegraph_search",
|
||||
resource: "*",
|
||||
effect: "deny",
|
||||
});
|
||||
expect(coordinate.permissions.some((rule) => rule.action === "docs_*")).toBe(false);
|
||||
expect(coordinate.permissions.some((rule) => rule.action === "shell")).toBe(false);
|
||||
|
||||
await result.changeCodegraph(undefined, []);
|
||||
coordinate = result.agents.get("coordinate")!;
|
||||
expect(coordinate.permissions.some((rule) => rule.action.startsWith("codegraph_"))).toBe(false);
|
||||
await result.changeCodegraph({ type: "local", command: ["codegraph"], codemode: true }, [
|
||||
"codegraph_added_later",
|
||||
]);
|
||||
expect(result.mcpServers.get("codegraph")?.codemode).toBe(false);
|
||||
expect(result.agents.get("coordinate")?.permissions).toContainEqual({
|
||||
action: "codegraph_added_later",
|
||||
resource: "*",
|
||||
effect: "ask",
|
||||
});
|
||||
});
|
||||
|
||||
test("playwright MCP prefers direct MCP, uses npx for Playwright, and preserves fallback behavior", async () => {
|
||||
const originalPath = process.env.PATH;
|
||||
const root = await mkdtemp(join(tmpdir(), "stanp-playwright-"));
|
||||
try {
|
||||
process.env.PATH = "";
|
||||
const packageRoot = join(root, "local", "node_modules", "@playwright", "mcp");
|
||||
const executable = join(packageRoot, "cli.js");
|
||||
const localProject = join(root, "local", "project");
|
||||
const globalProject = join(root, "global-project");
|
||||
await mkdir(localProject, { recursive: true });
|
||||
await mkdir(globalProject);
|
||||
await mkdir(packageRoot, { recursive: true });
|
||||
await writeFile(
|
||||
join(packageRoot, "package.json"),
|
||||
JSON.stringify({ name: "@playwright/mcp", bin: { "playwright-mcp": "cli.js" } }),
|
||||
);
|
||||
await writeFile(executable, "#!/usr/bin/env node\n");
|
||||
await chmod(executable, 0o755);
|
||||
|
||||
const local = await host(allModels, { provider: "test" }, {}, [], localProject);
|
||||
expect(local.mcpServers.get("playwright")).toEqual({
|
||||
type: "local",
|
||||
command: [executable],
|
||||
timeout: { startup: 300_000 },
|
||||
});
|
||||
|
||||
const globalPackage = join(root, "global", "@playwright", "mcp");
|
||||
const globalExecutable = join(globalPackage, "cli.js");
|
||||
const globalBin = join(root, "bin");
|
||||
const pathExecutable = join(globalBin, "playwright-mcp");
|
||||
await mkdir(globalPackage, { recursive: true });
|
||||
await mkdir(globalBin, { recursive: true });
|
||||
await writeFile(
|
||||
join(globalPackage, "package.json"),
|
||||
JSON.stringify({ name: "@playwright/mcp", bin: { "playwright-mcp": "cli.js" } }),
|
||||
);
|
||||
await writeFile(globalExecutable, "#!/usr/bin/env node\n");
|
||||
await chmod(globalExecutable, 0o755);
|
||||
await symlink(globalExecutable, pathExecutable);
|
||||
expect(await playwrightMcpCommand(globalProject, globalBin)).toEqual([pathExecutable]);
|
||||
|
||||
const ordinaryPackage = join(root, "ordinary-project", "node_modules", "playwright");
|
||||
const ordinaryProject = join(root, "ordinary-project", "project");
|
||||
const ordinaryExecutable = join(ordinaryPackage, "cli.js");
|
||||
await mkdir(ordinaryPackage, { recursive: true });
|
||||
await mkdir(ordinaryProject);
|
||||
await writeFile(
|
||||
join(ordinaryPackage, "package.json"),
|
||||
JSON.stringify({ name: "playwright", bin: { playwright: "cli.js" } }),
|
||||
);
|
||||
await writeFile(ordinaryExecutable, "#!/usr/bin/env node\n");
|
||||
await chmod(ordinaryExecutable, 0o755);
|
||||
const ordinary = await host(allModels, { provider: "test" }, {}, [], ordinaryProject);
|
||||
expect(ordinary.mcpServers.get("playwright")?.command).toEqual([
|
||||
"npx",
|
||||
"-y",
|
||||
"@playwright/mcp@latest",
|
||||
]);
|
||||
|
||||
const testPackage = join(root, "test-project", "node_modules", "@playwright", "test");
|
||||
const testDependency = join(testPackage, "node_modules", "playwright");
|
||||
const testProject = join(root, "test-project", "project");
|
||||
await mkdir(testDependency, { recursive: true });
|
||||
await mkdir(testProject);
|
||||
await writeFile(
|
||||
join(testPackage, "package.json"),
|
||||
JSON.stringify({
|
||||
name: "@playwright/test",
|
||||
bin: { playwright: "cli.js" },
|
||||
dependencies: { playwright: "1.0.0" },
|
||||
}),
|
||||
);
|
||||
await writeFile(join(testPackage, "cli.js"), "#!/usr/bin/env node\n");
|
||||
await chmod(join(testPackage, "cli.js"), 0o755);
|
||||
await writeFile(
|
||||
join(testDependency, "package.json"),
|
||||
JSON.stringify({ name: "playwright", bin: { playwright: "cli.js" } }),
|
||||
);
|
||||
await writeFile(join(testDependency, "cli.js"), "#!/usr/bin/env node\n");
|
||||
await chmod(join(testDependency, "cli.js"), 0o755);
|
||||
expect(await playwrightMcpCommand(testProject, "")).toEqual([
|
||||
"npx",
|
||||
"-y",
|
||||
"@playwright/mcp@latest",
|
||||
]);
|
||||
|
||||
const pathPackage = join(root, "path-playwright", "playwright");
|
||||
const pathBin = join(root, "playwright-bin");
|
||||
const pathPlaywright = join(pathBin, "playwright");
|
||||
await mkdir(pathPackage, { recursive: true });
|
||||
await mkdir(pathBin);
|
||||
await writeFile(
|
||||
join(pathPackage, "package.json"),
|
||||
JSON.stringify({ name: "playwright", bin: { playwright: "cli.js" } }),
|
||||
);
|
||||
await writeFile(join(pathPackage, "cli.js"), "#!/usr/bin/env node\n");
|
||||
await chmod(join(pathPackage, "cli.js"), 0o755);
|
||||
await symlink(join(pathPackage, "cli.js"), pathPlaywright);
|
||||
expect(await playwrightMcpCommand(globalProject, pathBin)).toEqual([
|
||||
"npx",
|
||||
"-y",
|
||||
"@playwright/mcp@latest",
|
||||
]);
|
||||
|
||||
const ordinaryBin = join(root, "ordinary-bin");
|
||||
const fakePlaywright = join(ordinaryBin, "playwright");
|
||||
await mkdir(ordinaryBin);
|
||||
await writeFile(fakePlaywright, "#!/usr/bin/env node\n");
|
||||
await chmod(fakePlaywright, 0o755);
|
||||
expect((await playwrightMcpCommand(globalProject, ordinaryBin))[0]).toBe("docker");
|
||||
|
||||
const fallback = await host();
|
||||
expect(fallback.mcpServers.get("playwright")).toEqual({
|
||||
type: "local",
|
||||
command: ["docker", "run", "-i", "--rm", "--init", "mcr.microsoft.com/playwright/mcp"],
|
||||
timeout: { startup: 300_000 },
|
||||
});
|
||||
|
||||
const existing = await host(
|
||||
allModels,
|
||||
{ provider: "test" },
|
||||
{
|
||||
playwright: {
|
||||
type: "local",
|
||||
command: ["custom-playwright-mcp"],
|
||||
disabled: true,
|
||||
timeout: { startup: 12_345 },
|
||||
},
|
||||
docs: { type: "local", command: ["docs"], codemode: true },
|
||||
},
|
||||
);
|
||||
expect(existing.mcpServers.get("playwright")).toEqual({
|
||||
type: "local",
|
||||
command: ["custom-playwright-mcp"],
|
||||
disabled: true,
|
||||
timeout: { startup: 12_345 },
|
||||
});
|
||||
expect(existing.mcpServers.get("docs")?.codemode).toBe(true);
|
||||
} finally {
|
||||
if (originalPath === undefined) delete process.env.PATH;
|
||||
else process.env.PATH = originalPath;
|
||||
await rm(root, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("manual calls and command-created child sessions cannot bypass delegation restrictions", async () => {
|
||||
const result = await host(allModels.slice(0, 2));
|
||||
for (const invoke of [result.delegate, result.command]) {
|
||||
@@ -337,6 +648,21 @@ test("late build permission overrides remain effective for workers and coordinat
|
||||
).toBe("deny");
|
||||
});
|
||||
|
||||
test("events from another location do not reload this location's agents", async () => {
|
||||
const result = await host();
|
||||
result.changeBuildPermissionsElsewhere([
|
||||
...result.build.permissions,
|
||||
{ action: "read", resource: "*.env", effect: "deny" },
|
||||
]);
|
||||
await result.refresh(allModels);
|
||||
expect(
|
||||
result.agents
|
||||
.get("coordinate")
|
||||
?.permissions.filter((rule) => rule.action === "read" && rule.resource === "*.env")
|
||||
.at(-1)?.effect,
|
||||
).toBe("ask");
|
||||
});
|
||||
|
||||
test("todo lists persist within one session without leaking to another", async () => {
|
||||
const result = await host();
|
||||
const todos = [{ id: "1", content: "Implement", status: "in_progress", priority: "high" }];
|
||||
|
||||
@@ -0,0 +1,548 @@
|
||||
import { expect, test } from "bun:test";
|
||||
import type { Schema } from "effect";
|
||||
import { normalizeQuota, QuotaTracker } from "../src/quota.js";
|
||||
import { formatQuotaWindow, loadQuotaStatus } from "../src/tui.js";
|
||||
|
||||
const plus = {
|
||||
plan_type: "plus",
|
||||
account_id: "account-one",
|
||||
rate_limit: {
|
||||
allowed: true,
|
||||
limit_reached: false,
|
||||
primary_window: {
|
||||
used_percent: 12.5,
|
||||
limit_window_seconds: 18_000,
|
||||
reset_after_seconds: 1_000,
|
||||
reset_at: 2_000_000_000,
|
||||
},
|
||||
secondary_window: {
|
||||
used_percent: 41,
|
||||
limit_window_seconds: 604_800,
|
||||
reset_after_seconds: 2_000,
|
||||
reset_at: 2_000_500_000,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
test("normalizes representative Plus and Pro payloads and hides absent windows", () => {
|
||||
expect(normalizeQuota(plus, 123)).toEqual({
|
||||
plan: "plus",
|
||||
fetchedAt: 123,
|
||||
windows: [
|
||||
{
|
||||
id: "codex:primary",
|
||||
label: null,
|
||||
seconds: 18_000,
|
||||
used: 12.5,
|
||||
resetsAt: 2_000_000_000_000,
|
||||
},
|
||||
{
|
||||
id: "codex:secondary",
|
||||
label: null,
|
||||
seconds: 604_800,
|
||||
used: 41,
|
||||
resetsAt: 2_000_500_000_000,
|
||||
},
|
||||
],
|
||||
});
|
||||
expect(normalizeQuota({ ...plus, plan_type: "pro" }, 123)?.windows).toHaveLength(2);
|
||||
expect(
|
||||
normalizeQuota({
|
||||
plan_type: "pro",
|
||||
rate_limit: { primary_window: plus.rate_limit.primary_window },
|
||||
})?.windows,
|
||||
).toEqual([
|
||||
{
|
||||
id: "codex:primary",
|
||||
label: null,
|
||||
seconds: 18_000,
|
||||
used: 12.5,
|
||||
resetsAt: 2_000_000_000_000,
|
||||
},
|
||||
]);
|
||||
expect(normalizeQuota({ plan_type: "plus", rate_limit: null })).toBeUndefined();
|
||||
});
|
||||
|
||||
test("normalizes additional named limits with stable IDs distinct from equal durations", () => {
|
||||
const normalized = normalizeQuota({
|
||||
...plus,
|
||||
additional_rate_limits: [
|
||||
{
|
||||
limit_name: "GPT-5.6 Luna",
|
||||
metered_feature: "codex_luna",
|
||||
rate_limit: { primary_window: plus.rate_limit.primary_window },
|
||||
},
|
||||
],
|
||||
});
|
||||
expect(normalized?.windows.map((window) => window.id)).toEqual([
|
||||
"codex:primary",
|
||||
"codex:secondary",
|
||||
"codex_luna:primary",
|
||||
]);
|
||||
expect(normalized?.windows[2]?.label).toBe("GPT-5.6 Luna");
|
||||
expect(
|
||||
normalizeQuota({
|
||||
plan_type: "pro",
|
||||
rate_limit: null,
|
||||
additional_rate_limits: [
|
||||
{
|
||||
limit_name: "GPT-5.6 Luna",
|
||||
metered_feature: "codex_luna",
|
||||
rate_limit: { primary_window: plus.rate_limit.primary_window },
|
||||
},
|
||||
],
|
||||
})?.windows.map((window) => window.id),
|
||||
).toEqual(["codex_luna:primary"]);
|
||||
});
|
||||
|
||||
test("ignores Spark and representative reserve limits while preserving other quotas", () => {
|
||||
const normalized = normalizeQuota({
|
||||
...plus,
|
||||
additional_rate_limits: [
|
||||
{
|
||||
limit_name: "gpt 5_3 codex spark",
|
||||
metered_feature: "CODEX-BENGALFOX",
|
||||
rate_limit: {
|
||||
primary_window: { used_percent: "invalid" },
|
||||
},
|
||||
},
|
||||
{
|
||||
limit_name: "Luna Reserve",
|
||||
metered_feature: "codex_luna",
|
||||
rate_limit: { primary_window: { used_percent: "invalid" } },
|
||||
},
|
||||
{
|
||||
limit_name: "Luna overflow",
|
||||
metered_feature: "codex_luna_reserve",
|
||||
rate_limit: { primary_window: { used_percent: "invalid" } },
|
||||
},
|
||||
{
|
||||
limit_name: "GPT-5.6 Luna",
|
||||
metered_feature: "codex_luna",
|
||||
rate_limit: { primary_window: plus.rate_limit.primary_window },
|
||||
},
|
||||
{
|
||||
limit_name: "Preserved capacity",
|
||||
metered_feature: "codex_preserved",
|
||||
rate_limit: { primary_window: plus.rate_limit.primary_window },
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
expect(normalized?.windows.map((window) => window.id)).toEqual([
|
||||
"codex:primary",
|
||||
"codex:secondary",
|
||||
"codex_luna:primary",
|
||||
"codex_preserved:primary",
|
||||
]);
|
||||
expect(normalized?.windows.slice(2).map((window) => window.label)).toEqual([
|
||||
"GPT-5.6 Luna",
|
||||
"Preserved capacity",
|
||||
]);
|
||||
});
|
||||
|
||||
test("returns no quota for a Spark-only payload", () => {
|
||||
expect(
|
||||
normalizeQuota({
|
||||
plan_type: "pro",
|
||||
additional_rate_limits: [
|
||||
{
|
||||
limit_name: "GPT-5.3-Codex-Spark",
|
||||
metered_feature: "codex_bengalfox",
|
||||
rate_limit: { primary_window: plus.rate_limit.primary_window },
|
||||
},
|
||||
],
|
||||
}),
|
||||
).toBeUndefined();
|
||||
});
|
||||
|
||||
function fixture(
|
||||
credential:
|
||||
| {
|
||||
type: "oauth";
|
||||
access: string;
|
||||
refresh: string;
|
||||
metadata?: { accountID?: string; accountId?: string };
|
||||
}
|
||||
| {
|
||||
type: "key";
|
||||
key: string;
|
||||
},
|
||||
payloads: unknown[],
|
||||
pollMs = 1_000_000,
|
||||
storageReads: Promise<Schema.Json | undefined>[] = [],
|
||||
) {
|
||||
const stored = new Map<string, Schema.Json>();
|
||||
const sessions = new Map([
|
||||
["root", {}],
|
||||
["child", { parentID: "root" }],
|
||||
]);
|
||||
let calls = 0;
|
||||
let storageCalls = 0;
|
||||
let changes = 0;
|
||||
let current: typeof credential | undefined = credential;
|
||||
const headers: Headers[] = [];
|
||||
const tracker = new QuotaTracker(
|
||||
// SAFETY: This focused fixture implements only the native calls exercised by
|
||||
// QuotaTracker; branded connection and session fields are irrelevant here.
|
||||
{
|
||||
integration: {
|
||||
connection: {
|
||||
async active() {
|
||||
// SAFETY: Only presence is observed; QuotaTracker passes this value back unchanged.
|
||||
return current ? ({ id: "connection" } as never) : undefined;
|
||||
},
|
||||
async resolve() {
|
||||
// SAFETY: The fixture covers the credential discriminant and fields QuotaTracker reads.
|
||||
return current as never;
|
||||
},
|
||||
},
|
||||
} as never,
|
||||
session: {
|
||||
async get({ sessionID }: { sessionID: string }) {
|
||||
// SAFETY: QuotaTracker reads only parentID from this focused fixture.
|
||||
return sessions.get(sessionID) as never;
|
||||
},
|
||||
} as never,
|
||||
storage: {
|
||||
async get(key: string) {
|
||||
storageCalls++;
|
||||
const deferred = storageReads.shift();
|
||||
if (deferred) return deferred;
|
||||
return stored.get(key);
|
||||
},
|
||||
async set(key: string, value: Schema.Json) {
|
||||
stored.set(key, value);
|
||||
},
|
||||
async remove(key: string) {
|
||||
stored.delete(key);
|
||||
},
|
||||
async scan() {
|
||||
return { entries: [] };
|
||||
},
|
||||
},
|
||||
},
|
||||
async () => {
|
||||
changes++;
|
||||
},
|
||||
async (_url, init) => {
|
||||
calls++;
|
||||
const requestHeaders = new Headers(init?.headers);
|
||||
headers.push(requestHeaders);
|
||||
expect(requestHeaders.get("authorization")).toStartWith("Bearer ");
|
||||
const payload = await (payloads.shift() ?? plus);
|
||||
if (payload instanceof Error) throw payload;
|
||||
return Response.json(payload);
|
||||
},
|
||||
pollMs,
|
||||
);
|
||||
return {
|
||||
tracker,
|
||||
calls: () => calls,
|
||||
storageCalls: () => storageCalls,
|
||||
changes: () => changes,
|
||||
headers,
|
||||
switch(credential: typeof current) {
|
||||
current = credential;
|
||||
},
|
||||
logout: () => {
|
||||
current = undefined;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async function accountFingerprint(id: string) {
|
||||
const bytes = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(`account:${id}`));
|
||||
return Buffer.from(bytes).toString("hex");
|
||||
}
|
||||
|
||||
test("removes cached Spark and representative reserve windows and consumption while offline", async () => {
|
||||
const cached = {
|
||||
account: await accountFingerprint("account-one"),
|
||||
latest: {
|
||||
plan: "pro",
|
||||
fetchedAt: 123,
|
||||
windows: [
|
||||
{
|
||||
id: "codex:primary",
|
||||
label: null,
|
||||
seconds: 18_000,
|
||||
used: 12.5,
|
||||
resetsAt: 2_000_000_000_000,
|
||||
},
|
||||
{
|
||||
id: "CODEX-BENGALFOX:primary",
|
||||
label: "GPT 5.3 Codex Spark",
|
||||
seconds: 18_000,
|
||||
used: 80,
|
||||
resetsAt: 2_000_000_000_000,
|
||||
},
|
||||
{
|
||||
id: "codex_luna_reserve:primary",
|
||||
label: "Luna Reserve",
|
||||
seconds: 18_000,
|
||||
used: 60,
|
||||
resetsAt: 2_000_000_000_000,
|
||||
},
|
||||
{
|
||||
id: "codex_luna:primary",
|
||||
label: "GPT-5.6 Luna",
|
||||
seconds: 18_000,
|
||||
used: 20,
|
||||
resetsAt: 2_000_000_000_000,
|
||||
},
|
||||
],
|
||||
},
|
||||
sessions: {
|
||||
root: {
|
||||
"codex:primary": {
|
||||
resetsAt: 2_000_000_000_000,
|
||||
lastUsed: 12.5,
|
||||
consumed: 2,
|
||||
},
|
||||
"CODEX-BENGALFOX:primary": {
|
||||
resetsAt: 2_000_000_000_000,
|
||||
lastUsed: 80,
|
||||
consumed: 40,
|
||||
},
|
||||
"codex_luna_reserve:primary": {
|
||||
resetsAt: 2_000_000_000_000,
|
||||
lastUsed: 60,
|
||||
consumed: 30,
|
||||
},
|
||||
"codex_luna:primary": {
|
||||
resetsAt: 2_000_000_000_000,
|
||||
lastUsed: 20,
|
||||
consumed: 3,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
const result = fixture(
|
||||
{
|
||||
type: "oauth",
|
||||
access: "opaque",
|
||||
refresh: "refresh",
|
||||
metadata: { accountID: "account-one" },
|
||||
},
|
||||
[new Error("offline")],
|
||||
1_000_000,
|
||||
[Promise.resolve(cached)],
|
||||
);
|
||||
|
||||
await result.tracker.init();
|
||||
const status = await result.tracker.status("root");
|
||||
expect(status?.windows.map((window) => window.id)).toEqual([
|
||||
"codex:primary",
|
||||
"codex_luna:primary",
|
||||
]);
|
||||
expect(status?.windows[0]?.sessionConsumed).toBe(2);
|
||||
expect(status?.windows[1]?.sessionConsumed).toBe(3);
|
||||
await result.tracker.dispose();
|
||||
});
|
||||
|
||||
test("activates only for OAuth and establishes an awaited root baseline shared by children", async () => {
|
||||
const key = fixture({ type: "key", key: "secret" }, [plus]);
|
||||
await key.tracker.init();
|
||||
await key.tracker.start("root");
|
||||
expect(key.calls()).toBe(0);
|
||||
expect(await key.tracker.status("root")).toBeUndefined();
|
||||
await key.tracker.dispose();
|
||||
|
||||
const oauth = fixture(
|
||||
{
|
||||
type: "oauth",
|
||||
access: "opaque",
|
||||
refresh: "refresh",
|
||||
metadata: { accountID: "account-one" },
|
||||
},
|
||||
[
|
||||
plus,
|
||||
{
|
||||
...plus,
|
||||
rate_limit: {
|
||||
...plus.rate_limit,
|
||||
primary_window: { ...plus.rate_limit.primary_window, used_percent: 17.5 },
|
||||
},
|
||||
},
|
||||
],
|
||||
);
|
||||
await oauth.tracker.init();
|
||||
await oauth.tracker.start("root");
|
||||
expect(oauth.calls()).toBe(1);
|
||||
expect(oauth.headers[0]?.get("chatgpt-account-id")).toBe("account-one");
|
||||
await oauth.tracker.refresh();
|
||||
const child = await oauth.tracker.status("child");
|
||||
expect(child?.windows[0]?.sessionConsumed).toBe(5);
|
||||
expect(child?.windows[0]?.remaining).toBe(82.5);
|
||||
await oauth.tracker.dispose();
|
||||
});
|
||||
|
||||
test("polls, carries consumption across quota resets, tolerates errors, and clears on logout", async () => {
|
||||
const reset = {
|
||||
...plus,
|
||||
rate_limit: {
|
||||
...plus.rate_limit,
|
||||
primary_window: {
|
||||
...plus.rate_limit.primary_window,
|
||||
used_percent: 2,
|
||||
reset_at: 2_000_100_000,
|
||||
},
|
||||
},
|
||||
};
|
||||
const result = fixture(
|
||||
{
|
||||
type: "oauth",
|
||||
access: "opaque",
|
||||
refresh: "refresh",
|
||||
metadata: { accountID: "account-one" },
|
||||
},
|
||||
[plus, new Error("offline"), reset, reset, reset, reset],
|
||||
10,
|
||||
);
|
||||
await result.tracker.init();
|
||||
await result.tracker.start("root");
|
||||
await Bun.sleep(35);
|
||||
const status = await result.tracker.status("root");
|
||||
expect(result.calls()).toBeGreaterThanOrEqual(3);
|
||||
expect(status?.windows[0]?.sessionConsumed).toBe(2);
|
||||
result.logout();
|
||||
await result.tracker.authChanged(true);
|
||||
expect(await result.tracker.status("root")).toBeUndefined();
|
||||
const calls = result.calls();
|
||||
await result.tracker.dispose();
|
||||
await Bun.sleep(20);
|
||||
expect(result.calls()).toBe(calls);
|
||||
});
|
||||
|
||||
test("sidebar text places estimated percentage-point use beside latest remaining quota", () => {
|
||||
expect(
|
||||
formatQuotaWindow({
|
||||
seconds: 604_800,
|
||||
id: "codex:secondary",
|
||||
label: null,
|
||||
used: 25,
|
||||
remaining: 75,
|
||||
sessionConsumed: 2.25,
|
||||
resetsAt: 0,
|
||||
}),
|
||||
).toBe("7d: 75% left · ~2.3pp this session");
|
||||
});
|
||||
|
||||
test("inactive roots freeze, resume from a cached baseline on failure, and deactivate by family", async () => {
|
||||
const used = (percent: number) => ({
|
||||
...plus,
|
||||
rate_limit: {
|
||||
...plus.rate_limit,
|
||||
primary_window: { ...plus.rate_limit.primary_window, used_percent: percent },
|
||||
},
|
||||
});
|
||||
const result = fixture(
|
||||
{
|
||||
type: "oauth",
|
||||
access: "opaque",
|
||||
refresh: "refresh",
|
||||
metadata: { accountID: "account-one" },
|
||||
},
|
||||
[used(10), used(11), used(30), new Error("temporary"), used(30), used(33), used(35)],
|
||||
);
|
||||
await result.tracker.init();
|
||||
await result.tracker.start("root");
|
||||
await result.tracker.complete("root");
|
||||
expect((await result.tracker.status("root"))?.windows[0]?.sessionConsumed).toBe(1);
|
||||
await result.tracker.refresh();
|
||||
expect((await result.tracker.status("root"))?.windows[0]?.sessionConsumed).toBe(1);
|
||||
|
||||
await result.tracker.start("root");
|
||||
await result.tracker.start("child");
|
||||
await result.tracker.complete("root");
|
||||
await result.tracker.refresh();
|
||||
await result.tracker.complete("child");
|
||||
expect((await result.tracker.status("root"))?.windows[0]?.sessionConsumed).toBe(6);
|
||||
await result.tracker.dispose();
|
||||
});
|
||||
|
||||
test("an auth switch detaches an in-flight old-account response and immediately fetches the new account", async () => {
|
||||
const pending = Promise.withResolvers<unknown>();
|
||||
const second = {
|
||||
...plus,
|
||||
account_id: "account-two",
|
||||
rate_limit: {
|
||||
...plus.rate_limit,
|
||||
primary_window: { ...plus.rate_limit.primary_window, used_percent: 70 },
|
||||
},
|
||||
};
|
||||
const result = fixture(
|
||||
{ type: "oauth", access: "old", refresh: "old", metadata: { accountID: "account-one" } },
|
||||
[
|
||||
pending.promise,
|
||||
second,
|
||||
{
|
||||
...second,
|
||||
rate_limit: {
|
||||
...second.rate_limit,
|
||||
primary_window: { ...second.rate_limit.primary_window, used_percent: 75 },
|
||||
},
|
||||
},
|
||||
],
|
||||
);
|
||||
await result.tracker.init();
|
||||
const old = result.tracker.start("root");
|
||||
while (result.calls() === 0) await Bun.sleep(1);
|
||||
result.switch({
|
||||
type: "oauth",
|
||||
access: "new",
|
||||
refresh: "new",
|
||||
metadata: { accountID: "account-two" },
|
||||
});
|
||||
await result.tracker.authChanged(true);
|
||||
expect(result.calls()).toBe(2);
|
||||
pending.resolve(plus);
|
||||
await old;
|
||||
expect((await result.tracker.status("root"))?.windows[0]?.used).toBe(70);
|
||||
expect(result.headers[1]?.get("chatgpt-account-id")).toBe("account-two");
|
||||
await result.tracker.refresh();
|
||||
expect((await result.tracker.status("root"))?.windows[0]?.sessionConsumed).toBe(0);
|
||||
await result.tracker.dispose();
|
||||
});
|
||||
|
||||
test("an auth switch during an old account storage read cannot restore stale state or polling", async () => {
|
||||
const pending = Promise.withResolvers<Schema.Json | undefined>();
|
||||
const second = {
|
||||
...plus,
|
||||
account_id: "account-two",
|
||||
rate_limit: {
|
||||
...plus.rate_limit,
|
||||
primary_window: { ...plus.rate_limit.primary_window, used_percent: 64 },
|
||||
},
|
||||
};
|
||||
const result = fixture(
|
||||
{ type: "oauth", access: "old", refresh: "old", metadata: { accountID: "account-one" } },
|
||||
[second],
|
||||
1_000_000,
|
||||
[pending.promise],
|
||||
);
|
||||
const old = result.tracker.init();
|
||||
while (result.storageCalls() === 0) await Bun.sleep(1);
|
||||
result.switch({
|
||||
type: "oauth",
|
||||
access: "new",
|
||||
refresh: "new",
|
||||
metadata: { accountID: "account-two" },
|
||||
});
|
||||
await result.tracker.authChanged(true);
|
||||
expect(result.calls()).toBe(1);
|
||||
pending.resolve(undefined);
|
||||
await old;
|
||||
expect((await result.tracker.status("root"))?.windows[0]?.used).toBe(64);
|
||||
expect(result.headers[0]?.get("chatgpt-account-id")).toBe("account-two");
|
||||
await result.tracker.dispose();
|
||||
});
|
||||
|
||||
test("TUI quota loading converts RPC failures to an empty render state", async () => {
|
||||
expect(
|
||||
await loadQuotaStatus(async () => {
|
||||
throw new Error("disconnected");
|
||||
}),
|
||||
).toBeNull();
|
||||
});
|
||||
@@ -0,0 +1,108 @@
|
||||
import { expect, test } from "bun:test";
|
||||
import type { Plugin } from "@opencode/plugin/tui";
|
||||
import { testRender, type JSX } from "@opentui/solid";
|
||||
import { createSignal } from "solid-js";
|
||||
import { build } from "../scripts/build.js";
|
||||
import type { QuotaStatus } from "../src/quota.js";
|
||||
|
||||
type SlotRender = (input: { readonly sessionID: string }) => JSX.Element;
|
||||
|
||||
const status = (plan: string, remaining: number): QuotaStatus => ({
|
||||
plan,
|
||||
fetchedAt: 1,
|
||||
windows: [
|
||||
{
|
||||
id: "codex:primary",
|
||||
label: null,
|
||||
seconds: 18_000,
|
||||
used: 100 - remaining,
|
||||
resetsAt: 2,
|
||||
remaining,
|
||||
sessionConsumed: 1.5,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
test("built quota slot stays reactive across pending, empty, failure, update, and session states", async () => {
|
||||
await build();
|
||||
// Resolve the built bundle at runtime; tsc runs before build creates dist/tui.js.
|
||||
const tui: typeof import("../src/tui.js") = await import(
|
||||
new URL("../dist/tui.js", import.meta.url).href
|
||||
);
|
||||
const { default: plugin } = tui;
|
||||
const pending = Promise.withResolvers<QuotaStatus | null>();
|
||||
const responses: Array<() => Promise<QuotaStatus | null>> = [() => pending.promise];
|
||||
const sessions: string[] = [];
|
||||
let slotRender: SlotRender | undefined;
|
||||
let notifyUpdated: (() => void) | undefined;
|
||||
|
||||
const quota = {
|
||||
status(input: { sessionID: string }) {
|
||||
sessions.push(input.sessionID);
|
||||
const response = responses.shift();
|
||||
if (!response) throw new Error("Missing test response");
|
||||
return response();
|
||||
},
|
||||
events: {
|
||||
on(_event: "updated", callback: () => void) {
|
||||
notifyUpdated = callback;
|
||||
return () => undefined;
|
||||
},
|
||||
},
|
||||
};
|
||||
const contextFixture = {
|
||||
client: { rpc: () => quota },
|
||||
ui: {
|
||||
slot(claim: { render: SlotRender }) {
|
||||
slotRender = claim.render;
|
||||
return () => undefined;
|
||||
},
|
||||
},
|
||||
theme: { text: { default: "#ffffff", muted: "#888888" } },
|
||||
};
|
||||
// SAFETY: The slot setup only reads the client RPC, slot registration, and theme
|
||||
// members implemented by this focused rendering fixture.
|
||||
const context = Object.assign({} as Plugin.Context, contextFixture);
|
||||
const cleanup = await plugin.setup(context);
|
||||
if (!slotRender || !notifyUpdated) throw new Error("Plugin did not register its slot");
|
||||
|
||||
const [sessionID, setSessionID] = createSignal("session-one");
|
||||
const renderSlot = slotRender;
|
||||
const setup = await testRender(
|
||||
() =>
|
||||
renderSlot({
|
||||
get sessionID() {
|
||||
return sessionID();
|
||||
},
|
||||
}),
|
||||
{ width: 70, height: 5 },
|
||||
);
|
||||
|
||||
try {
|
||||
await setup.renderOnce();
|
||||
expect(setup.captureCharFrame()).not.toContain("ChatGPT quota");
|
||||
|
||||
pending.resolve(status("plus", 75));
|
||||
await setup.waitForFrame((frame) => frame.includes("ChatGPT quota (plus)"));
|
||||
expect(setup.captureCharFrame()).toContain("5h: 75% left · ~1.5pp this session");
|
||||
|
||||
responses.push(async () => null);
|
||||
notifyUpdated();
|
||||
await setup.waitForFrame((frame) => !frame.includes("ChatGPT quota"));
|
||||
|
||||
responses.push(async () => {
|
||||
throw new Error("disconnected");
|
||||
});
|
||||
notifyUpdated();
|
||||
await setup.waitFor(() => sessions.length === 3);
|
||||
expect(setup.captureCharFrame()).not.toContain("ChatGPT quota");
|
||||
|
||||
responses.push(async () => status("pro", 42));
|
||||
setSessionID("session-two");
|
||||
await setup.waitForFrame((frame) => frame.includes("ChatGPT quota (pro)"));
|
||||
expect(sessions).toEqual(["session-one", "session-one", "session-one", "session-two"]);
|
||||
} finally {
|
||||
setup.renderer.destroy();
|
||||
if (cleanup) await cleanup();
|
||||
}
|
||||
});
|
||||
+1
-1
@@ -3,5 +3,5 @@
|
||||
"compilerOptions": {
|
||||
"rootDir": "src"
|
||||
},
|
||||
"include": ["src/**/*.ts"]
|
||||
"include": ["src/**/*.ts", "src/**/*.tsx"]
|
||||
}
|
||||
|
||||
+3
-1
@@ -12,10 +12,12 @@
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
"jsx": "preserve",
|
||||
"jsxImportSource": "@opentui/solid",
|
||||
"skipLibCheck": true,
|
||||
"declaration": true,
|
||||
"rootDir": ".",
|
||||
"outDir": "dist"
|
||||
},
|
||||
"include": ["src/**/*.ts", "tests/**/*.ts"]
|
||||
"include": ["src/**/*.ts", "src/**/*.tsx", "scripts/**/*.ts", "tests/**/*.ts"]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user