From 5d9270335ea7cfaaa5bfa39750d89291bd683546 Mon Sep 17 00:00:00 2001 From: Seth Date: Tue, 1 Sep 2026 00:48:29 -0400 Subject: [PATCH 01/45] build: pin bun and add canonical lockfile --- .bun-version | 1 + bun.lock | 1054 +++++ bunfig.toml | 3 + package-lock.json | 5837 ---------------------------- package.json | 4 +- packages/coding-agent/package.json | 5 +- 6 files changed, 1060 insertions(+), 5844 deletions(-) create mode 100644 .bun-version create mode 100644 bun.lock create mode 100644 bunfig.toml delete mode 100644 package-lock.json diff --git a/.bun-version b/.bun-version new file mode 100644 index 00000000000..085c0f2666a --- /dev/null +++ b/.bun-version @@ -0,0 +1 @@ +1.3.14 diff --git a/bun.lock b/bun.lock new file mode 100644 index 00000000000..aa34d8105e5 --- /dev/null +++ b/bun.lock @@ -0,0 +1,1054 @@ +{ + "lockfileVersion": 1, + "configVersion": 1, + "workspaces": { + "": { + "name": "prime-agent", + "dependencies": { + "@earendil-works/pi-coding-agent": "^0.8.1", + "get-east-asian-width": "^1.6.0", + }, + "devDependencies": { + "@anthropic-ai/sandbox-runtime": "^0.0.55", + "@biomejs/biome": "2.5.5", + "@types/node": "^22.10.5", + "@typescript/native-preview": "7.0.0-dev.20260120.1", + "concurrently": "^9.2.4", + "husky": "^9.1.7", + "jiti": "^2.7.0", + "shx": "^0.4.0", + "tsx": "^4.23.1", + "typescript": "^7.0.2", + }, + }, + "packages/agent": { + "name": "@earendil-works/pi-agent-core", + "version": "0.8.1", + "dependencies": { + "@earendil-works/pi-ai": "^0.8.1", + "typebox": "^1.3.9", + }, + "devDependencies": { + "@types/node": "^24.3.0", + "typescript": "^7.0.2", + "vitest": "^4.1.10", + }, + }, + "packages/ai": { + "name": "@earendil-works/pi-ai", + "version": "0.8.1", + "bin": { + "pi-ai": "./dist/cli.js", + }, + "dependencies": { + "@anthropic-ai/sdk": "^0.91.1", + "@aws-sdk/client-bedrock-runtime": "^3.1095.0", + "@google/genai": "^1.40.0", + "@mistralai/mistralai": "^2.2.0", + "chalk": "^5.6.2", + "openai": "6.47.0", + "partial-json": "^0.1.7", + "proxy-agent": "^6.5.0", + "typebox": "^1.3.9", + "undici": "^7.29.0", + "zod-to-json-schema": "^3.24.6", + }, + "devDependencies": { + "@types/node": "^24.3.0", + "canvas": "^3.2.0", + "vitest": "^4.1.10", + }, + }, + "packages/coding-agent": { + "name": "@earendil-works/pi-coding-agent", + "version": "0.8.1", + "bin": { + "pi": "dist/bundle/cli.js", + }, + "dependencies": { + "@agentclientprotocol/sdk": "^1.3.0", + "@earendil-works/pi-agent-core": "^0.8.1", + "@earendil-works/pi-ai": "^0.8.1", + "@earendil-works/pi-tui": "^0.8.1", + "@silvia-odwyer/photon-node": "^0.3.4", + "chalk": "^5.5.0", + "cli-highlight": "^2.1.11", + "diff": "^9.0.0", + "extract-zip": "^2.0.1", + "file-type": "^21.1.1", + "glob": "^13.0.1", + "grok-mermaid": "0.2.3", + "hosted-git-info": "^9.0.2", + "ignore": "^7.0.5", + "jiti": "^2.7.0", + "marked": "^18.0.7", + "minimatch": "^10.2.3", + "proper-lockfile": "^4.1.2", + "strip-ansi": "^7.1.0", + "typebox": "^1.3.9", + "undici": "^7.29.0", + "uuid": "^14.0.0", + "yaml": "^2.9.0", + }, + "devDependencies": { + "@types/diff": "^8.0.0", + "@types/hosted-git-info": "^3.0.5", + "@types/ms": "^2.1.0", + "@types/node": "^24.3.0", + "@types/proper-lockfile": "^4.1.4", + "esbuild": "^0.28.1", + "shx": "^0.4.0", + "typescript": "^7.0.2", + "vitest": "^4.1.10", + }, + "optionalDependencies": { + "@mariozechner/clipboard": "^0.3.9", + }, + }, + "packages/coding-agent/examples/extensions/custom-provider-anthropic": { + "name": "pi-extension-custom-provider-anthropic", + "version": "0.0.1", + "dependencies": { + "@anthropic-ai/sdk": "^0.52.0", + }, + }, + "packages/coding-agent/examples/extensions/custom-provider-gitlab-duo": { + "name": "pi-extension-custom-provider-gitlab-duo", + "version": "0.0.1", + }, + "packages/coding-agent/examples/extensions/sandbox": { + "name": "pi-extension-sandbox", + "version": "1.4.0", + "dependencies": { + "@anthropic-ai/sandbox-runtime": "^0.0.55", + }, + }, + "packages/coding-agent/examples/extensions/with-deps": { + "name": "pi-extension-with-deps", + "version": "0.0.1", + "dependencies": { + "ms": "^2.1.3", + }, + "devDependencies": { + "@types/ms": "^2.1.0", + }, + }, + "packages/tui": { + "name": "@earendil-works/pi-tui", + "version": "0.8.1", + "dependencies": { + "@types/mime-types": "^3.0.1", + "chalk": "^5.5.0", + "get-east-asian-width": "^1.6.0", + "marked": "^18.0.7", + "mime-types": "^3.0.1", + }, + "devDependencies": { + "@xterm/headless": "^6.0.0", + "@xterm/xterm": "^6.0.0", + }, + "optionalDependencies": { + "koffi": "^2.9.0", + }, + }, + }, + "overrides": { + "rimraf": "6.1.2", + "shell-quote": "^1.10.0", + }, + "packages": { + "@agentclientprotocol/sdk": ["@agentclientprotocol/sdk@1.4.0", "", { "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" } }, "sha512-/eufudw+aFY1LKLolT6yFE6UMmYRl7fMJ/DEONSIyR6wI3slHWITBsANRGqXEY8FRzqUxwh7QEaGiZHcJPVThg=="], + + "@anthropic-ai/sandbox-runtime": ["@anthropic-ai/sandbox-runtime@0.0.55", "", { "dependencies": { "@pondwader/socks5-server": "^1.0.10", "commander": "^12.1.0", "node-forge": "^1.4.0", "shell-quote": "^1.8.4", "zod": "^3.24.1" }, "bin": { "srt": "dist/cli.js" } }, "sha512-XGTLbzitn0y6OCdmqP5aYrBR0HlNi87ue++m9vMPxcNwaQeOj9i3f9CSWmXua8mv+Q8E55k4ZGlzJmpDHzQFBw=="], + + "@anthropic-ai/sdk": ["@anthropic-ai/sdk@0.91.1", "", { "dependencies": { "json-schema-to-ts": "^3.1.1" }, "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" }, "optionalPeers": ["zod"], "bin": { "anthropic-ai-sdk": "bin/cli" } }, "sha512-LAmu761tSN9r66ixvmciswUj/ZC+1Q4iAfpedTfSVLeswRwnY3n2Nb6Tsk+cLPP28aLOPWeMgIuTuCcMC6W/iw=="], + + "@aws-sdk/client-bedrock-runtime": ["@aws-sdk/client-bedrock-runtime@3.1117.0", "", { "dependencies": { "@aws-sdk/core": "^3.977.9", "@aws-sdk/credential-provider-node": "^3.972.81", "@aws-sdk/eventstream-handler-node": "^3.972.34", "@aws-sdk/middleware-eventstream": "^3.972.29", "@aws-sdk/middleware-websocket": "^3.972.52", "@aws-sdk/token-providers": "3.1117.0", "@aws-sdk/types": "^3.974.5", "@smithy/core": "^3.33.3", "@smithy/fetch-http-handler": "^5.7.2", "@smithy/node-http-handler": "^4.11.3", "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-rUc58ZS5GgsvzfM265XsrNno35dsRIwQb0aDfJIZbJyn1VGP+6XVL5Pwm8Rg9uABQSdKpdTayu5R3ZuDlFfIHg=="], + + "@aws-sdk/core": ["@aws-sdk/core@3.977.9", "", { "dependencies": { "@aws-sdk/types": "^3.974.5", "@aws-sdk/xml-builder": "^3.972.40", "@aws/lambda-invoke-store": "^0.3.0", "@smithy/core": "^3.33.3", "@smithy/signature-v4": "^5.6.12", "@smithy/types": "^4.17.2", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, "sha512-reqPFEQrZxDZpeGj4PFMepBeR5LGYHRqq/L0motTzgFkCRBA4rFdaVXDSLYyGHhxVz7sT2PDnPN9CluGSfgyJA=="], + + "@aws-sdk/credential-provider-env": ["@aws-sdk/credential-provider-env@3.972.70", "", { "dependencies": { "@aws-sdk/core": "^3.977.9", "@aws-sdk/types": "^3.974.5", "@smithy/core": "^3.33.3", "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-H404B7dJl2mCrBqahDEYsanB0xhdDp6tXnXcTUnXmmpy2Q3J0Ho0bUajZ2jr/RdwzCyS59Gi8xXIFwPLGBl6Uw=="], + + "@aws-sdk/credential-provider-http": ["@aws-sdk/credential-provider-http@3.972.72", "", { "dependencies": { "@aws-sdk/core": "^3.977.9", "@aws-sdk/types": "^3.974.5", "@smithy/core": "^3.33.3", "@smithy/fetch-http-handler": "^5.7.2", "@smithy/node-http-handler": "^4.11.3", "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-X98zYOrVOeuosCX+6ktf29FC2N2GHPLia7qv6mzPzTc+RPAuHWCDS++Z6JK7eGYqb/v6uaW7bAXaOvDBfol+0w=="], + + "@aws-sdk/credential-provider-ini": ["@aws-sdk/credential-provider-ini@3.973.15", "", { "dependencies": { "@aws-sdk/core": "^3.977.9", "@aws-sdk/credential-provider-env": "^3.972.70", "@aws-sdk/credential-provider-http": "^3.972.72", "@aws-sdk/credential-provider-login": "^3.972.77", "@aws-sdk/credential-provider-process": "^3.972.70", "@aws-sdk/credential-provider-sso": "^3.973.14", "@aws-sdk/credential-provider-web-identity": "^3.972.76", "@aws-sdk/nested-clients": "^3.997.44", "@aws-sdk/types": "^3.974.5", "@smithy/core": "^3.33.3", "@smithy/credential-provider-imds": "^4.4.16", "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-Rykg6s5ceBuynMOGWgoowO4N+27JfnqXAnVaSunZl0hOO1XodSrxGNz6sCEbnmS0lAfQZDKyb3fbr46gSuv6Sg=="], + + "@aws-sdk/credential-provider-login": ["@aws-sdk/credential-provider-login@3.972.77", "", { "dependencies": { "@aws-sdk/core": "^3.977.9", "@aws-sdk/nested-clients": "^3.997.44", "@aws-sdk/types": "^3.974.5", "@smithy/core": "^3.33.3", "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-Jb59xfEISoN5mmbnA+HYqdtrSX3CgCtJoof+V5D8/TgUI56W63GEEd5Y58WijU3Ou6+WEgaLD1feVzaRXV5IDQ=="], + + "@aws-sdk/credential-provider-node": ["@aws-sdk/credential-provider-node@3.972.81", "", { "dependencies": { "@aws-sdk/credential-provider-env": "^3.972.70", "@aws-sdk/credential-provider-http": "^3.972.72", "@aws-sdk/credential-provider-ini": "^3.973.15", "@aws-sdk/credential-provider-process": "^3.972.70", "@aws-sdk/credential-provider-sso": "^3.973.14", "@aws-sdk/credential-provider-web-identity": "^3.972.76", "@aws-sdk/types": "^3.974.5", "@smithy/core": "^3.33.3", "@smithy/credential-provider-imds": "^4.4.16", "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-Rml+WitoFvXmv6JZ18U/xGdGDGGvB/mOin0ya0lTnTrdC0Z1lrVxTYh7iNklZBcvcRMrs4DoEf6xy1KWyrLQQw=="], + + "@aws-sdk/credential-provider-process": ["@aws-sdk/credential-provider-process@3.972.70", "", { "dependencies": { "@aws-sdk/core": "^3.977.9", "@aws-sdk/types": "^3.974.5", "@smithy/core": "^3.33.3", "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-2ry03fGRJr4sV3jI+ocjj5JqALnFD6ymM5KiNCDZMvq8bX2GSbE0vji4aM43TVCl2nXqqLRZaUxdq/KeWRAY4Q=="], + + "@aws-sdk/credential-provider-sso": ["@aws-sdk/credential-provider-sso@3.973.14", "", { "dependencies": { "@aws-sdk/core": "^3.977.9", "@aws-sdk/nested-clients": "^3.997.44", "@aws-sdk/token-providers": "3.1116.0", "@aws-sdk/types": "^3.974.5", "@smithy/core": "^3.33.3", "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-jkhg/8ocAAoc0RFyLMhCw+/zZh7gystQgd4F4hznNa8P4Cc501PQmxd+jGLiMHodPJ+7Zv/3znM62gZojyasmA=="], + + "@aws-sdk/credential-provider-web-identity": ["@aws-sdk/credential-provider-web-identity@3.972.76", "", { "dependencies": { "@aws-sdk/core": "^3.977.9", "@aws-sdk/nested-clients": "^3.997.44", "@aws-sdk/types": "^3.974.5", "@smithy/core": "^3.33.3", "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-d3AGyVu759PGr35mEB2s22xxlNEA5rpdxtSPJthfPFJvoQ8dt357iVPECqWfUxXp1toJAvKmbtcIYVGigaGsCA=="], + + "@aws-sdk/eventstream-handler-node": ["@aws-sdk/eventstream-handler-node@3.972.34", "", { "dependencies": { "@aws-sdk/types": "^3.974.5", "@smithy/core": "^3.33.3", "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-cTeVzpu1xEAkryTZBYhGwnQ6gOGyp8ZYZvmn0Sg/nI/ABmy/CRHHxPDJDUi9PxwxUtGGaatvfRUB3FCgT/rSWw=="], + + "@aws-sdk/middleware-eventstream": ["@aws-sdk/middleware-eventstream@3.972.29", "", { "dependencies": { "@aws-sdk/types": "^3.974.5", "@smithy/core": "^3.33.3", "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-dlRzHCgyB8W6hLuDC5pcT5q+ziPt00n4QGgGBE17ucLVU4zMa6lsbuUdQ2Pm75Z5VA8GF+R/+SgrRcaTdIzSIQ=="], + + "@aws-sdk/middleware-websocket": ["@aws-sdk/middleware-websocket@3.972.52", "", { "dependencies": { "@aws-sdk/core": "^3.977.9", "@aws-sdk/types": "^3.974.5", "@smithy/core": "^3.33.3", "@smithy/fetch-http-handler": "^5.7.2", "@smithy/signature-v4": "^5.6.12", "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-vsPPM+nMbKJlUCFU+eoGZbdxdxDIAX9LbpjSXaR5Ufpmqgp8TdYQnoExhLu4T3umW/JIIPny1ydbhWidZZYokQ=="], + + "@aws-sdk/nested-clients": ["@aws-sdk/nested-clients@3.997.44", "", { "dependencies": { "@aws-sdk/core": "^3.977.9", "@aws-sdk/signature-v4-multi-region": "^3.996.46", "@aws-sdk/types": "^3.974.5", "@smithy/core": "^3.33.3", "@smithy/fetch-http-handler": "^5.7.2", "@smithy/node-http-handler": "^4.11.3", "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-NhEgryjlBF9w38ZXqGymQV28IhkYa1mKhlbYnqIis57AYwWGVYfUPgg/qC2rLRqOUfblxx++irvju10kVTa8Vw=="], + + "@aws-sdk/signature-v4-multi-region": ["@aws-sdk/signature-v4-multi-region@3.996.46", "", { "dependencies": { "@aws-sdk/types": "^3.974.5", "@smithy/signature-v4": "^5.6.12", "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-L+2xZTye/2T96f3lwCws0Zw6GG2JHZW9e8FpVgGBeeExSKyeoZ6CWRpBml/7DNiK/O26jrgPM9F+Ay8VkgzUWQ=="], + + "@aws-sdk/token-providers": ["@aws-sdk/token-providers@3.1117.0", "", { "dependencies": { "@aws-sdk/core": "^3.977.9", "@aws-sdk/nested-clients": "^3.997.44", "@aws-sdk/types": "^3.974.5", "@smithy/core": "^3.33.3", "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-I7QIf9aR0+J4Q1qlLQw006d8hs3Pb/J6mOVSCQ/fnQ4AXHTQ6Y/M45O2dWDRKoYqsQJ2kN8sg6b5709/qCzp9A=="], + + "@aws-sdk/types": ["@aws-sdk/types@3.974.5", "", { "dependencies": { "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-LkwLL2BLbC6wNNm4JaH9mbEqBMdOZCct6VAYqhdN4U1xrWM+fUJQEfbHwQgDypapOWTRtlk25akb5afM0P8CIQ=="], + + "@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.972.40", "", { "dependencies": { "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-wlFmCIGUlwF4zx/kncw+bmxTQh1HeSJq4mYV/V5cZUSJadDP3kXvGW8Rn21cimj/7y9ju+47oYWXi97vF7czaA=="], + + "@aws/lambda-invoke-store": ["@aws/lambda-invoke-store@0.3.0", "", {}, "sha512-sl4Bm6yiMNYrZKkqqDFWN0UfnWhlS8ivKxrYl+6t0gCLrqr8y3B2IqZZbFRkfaVVp7C/baApyh71P+LeE1A2sQ=="], + + "@babel/runtime": ["@babel/runtime@7.29.7", "", {}, "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw=="], + + "@biomejs/biome": ["@biomejs/biome@2.5.5", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "2.5.5", "@biomejs/cli-darwin-x64": "2.5.5", "@biomejs/cli-linux-arm64": "2.5.5", "@biomejs/cli-linux-arm64-musl": "2.5.5", "@biomejs/cli-linux-x64": "2.5.5", "@biomejs/cli-linux-x64-musl": "2.5.5", "@biomejs/cli-win32-arm64": "2.5.5", "@biomejs/cli-win32-x64": "2.5.5" }, "bin": { "biome": "bin/biome" } }, "sha512-r1S8nFsAG1MY+vJFZALzIvwXAJv6ejDQ0mxP21Tgr9YK3ZFtjrvbBwDdNhx1rUqvccEIeNg20cYCNzl6Cr69pQ=="], + + "@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@2.5.5", "", { "os": "darwin", "cpu": "arm64" }, "sha512-kUrAhXVWUrwmAUnV2iXSK7umxKFysTwvqK+Ty6ptUcLY/7T3SnCAjUowE4uvwaEej6nXZ7hu/dTtbokKdsPeag=="], + + "@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@2.5.5", "", { "os": "darwin", "cpu": "x64" }, "sha512-DamiYc5bUYZ2uxlfc+RLEPtz1Abb6PO5eTbOkufLpSGwd/7AMQAdxhFYiXmwwkJL8IsT8S7GvdgwDHqaMFAvKw=="], + + "@biomejs/cli-linux-arm64": ["@biomejs/cli-linux-arm64@2.5.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-lRKF/pH/1RiYiBKExi3TCZVAtvzEm77aifrvcNiDFrR9WxeAnDUjDnseb6y2XV85mjitLs6SILGm2XG77cHtSQ=="], + + "@biomejs/cli-linux-arm64-musl": ["@biomejs/cli-linux-arm64-musl@2.5.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-U4WMl/sy/E/Q73vf15VspakLRRs2LDFcCeBxJnQfXzssb88zpV6PJPaQ3ezhQ7H6Ht2/8bvuZeHgJWzmoxllZg=="], + + "@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@2.5.5", "", { "os": "linux", "cpu": "x64" }, "sha512-H/O39nJEw/2Zm/fm7hrmxxoF8kK/aU1uCoPp70ruXVbomaAdLpJJnCmL11Q2JotT8QVHH06So04Oq53lCSwSwQ=="], + + "@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@2.5.5", "", { "os": "linux", "cpu": "x64" }, "sha512-m7wC7tjX5Lrmo69dc4md8FeKpPU1NTCY1v7xUoQQ2vadWwNnBS0KZOG8471otFPHrTHihQJAjQPgMObpLvDe6A=="], + + "@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@2.5.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-7BryINPuYypLUAH3o/o5ZdgomJ4zn3EDR0ChZJst7n32S6ZhKbgHXuYydLu+YAnx59ehGFR0z/MG6qnzQi3Yyw=="], + + "@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@2.5.5", "", { "os": "win32", "cpu": "x64" }, "sha512-bIBFo+n6MIxdNcVFy5CrurbKiZQiUciK3bt8+O9I4wjFZNTfXLpi+giq47522eXqW5NBc9ulx7dR1SlZKi2J5g=="], + + "@borewit/text-codec": ["@borewit/text-codec@0.2.2", "", {}, "sha512-DDaRehssg1aNrH4+2hnj1B7vnUGEjU6OIlyRdkMd0aUdIUvKXrJfXsy8LVtXAy7DRvYVluWbMspsRhz2lcW0mQ=="], + + "@earendil-works/pi-agent-core": ["@earendil-works/pi-agent-core@workspace:packages/agent"], + + "@earendil-works/pi-ai": ["@earendil-works/pi-ai@workspace:packages/ai"], + + "@earendil-works/pi-coding-agent": ["@earendil-works/pi-coding-agent@workspace:packages/coding-agent"], + + "@earendil-works/pi-tui": ["@earendil-works/pi-tui@workspace:packages/tui"], + + "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.28.2", "", { "os": "aix", "cpu": "ppc64" }, "sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ=="], + + "@esbuild/android-arm": ["@esbuild/android-arm@0.28.2", "", { "os": "android", "cpu": "arm" }, "sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg=="], + + "@esbuild/android-arm64": ["@esbuild/android-arm64@0.28.2", "", { "os": "android", "cpu": "arm64" }, "sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A=="], + + "@esbuild/android-x64": ["@esbuild/android-x64@0.28.2", "", { "os": "android", "cpu": "x64" }, "sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q=="], + + "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.28.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw=="], + + "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.28.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw=="], + + "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.28.2", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw=="], + + "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.28.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg=="], + + "@esbuild/linux-arm": ["@esbuild/linux-arm@0.28.2", "", { "os": "linux", "cpu": "arm" }, "sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w=="], + + "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.28.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug=="], + + "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.28.2", "", { "os": "linux", "cpu": "ia32" }, "sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ=="], + + "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.28.2", "", { "os": "linux", "cpu": "none" }, "sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ=="], + + "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.28.2", "", { "os": "linux", "cpu": "none" }, "sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA=="], + + "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.28.2", "", { "os": "linux", "cpu": "ppc64" }, "sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ=="], + + "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.28.2", "", { "os": "linux", "cpu": "none" }, "sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA=="], + + "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.28.2", "", { "os": "linux", "cpu": "s390x" }, "sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg=="], + + "@esbuild/linux-x64": ["@esbuild/linux-x64@0.28.2", "", { "os": "linux", "cpu": "x64" }, "sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ=="], + + "@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.28.2", "", { "os": "none", "cpu": "arm64" }, "sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw=="], + + "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.28.2", "", { "os": "none", "cpu": "x64" }, "sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw=="], + + "@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.28.2", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ=="], + + "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.28.2", "", { "os": "openbsd", "cpu": "x64" }, "sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw=="], + + "@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.28.2", "", { "os": "none", "cpu": "arm64" }, "sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q=="], + + "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.28.2", "", { "os": "sunos", "cpu": "x64" }, "sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g=="], + + "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.28.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ=="], + + "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.28.2", "", { "os": "win32", "cpu": "ia32" }, "sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA=="], + + "@esbuild/win32-x64": ["@esbuild/win32-x64@0.28.2", "", { "os": "win32", "cpu": "x64" }, "sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g=="], + + "@google/genai": ["@google/genai@1.52.0", "", { "dependencies": { "google-auth-library": "^10.3.0", "p-retry": "^4.6.2", "protobufjs": "^7.5.4", "ws": "^8.18.0" }, "peerDependencies": { "@modelcontextprotocol/sdk": "^1.25.2" }, "optionalPeers": ["@modelcontextprotocol/sdk"] }, "sha512-gwSvbpiN/17O9TbsqSsE/OzZcpv5Fo4RQjdngGgogtuB9RsyJ8ZHhX5KjHj1bp5N9snN2eK8LDGXSaWW2hof8Q=="], + + "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="], + + "@mariozechner/clipboard": ["@mariozechner/clipboard@0.3.9", "", { "optionalDependencies": { "@mariozechner/clipboard-darwin-arm64": "0.3.9", "@mariozechner/clipboard-darwin-universal": "0.3.9", "@mariozechner/clipboard-darwin-x64": "0.3.9", "@mariozechner/clipboard-linux-arm64-gnu": "0.3.9", "@mariozechner/clipboard-linux-arm64-musl": "0.3.9", "@mariozechner/clipboard-linux-riscv64-gnu": "0.3.9", "@mariozechner/clipboard-linux-x64-gnu": "0.3.9", "@mariozechner/clipboard-linux-x64-musl": "0.3.9", "@mariozechner/clipboard-win32-arm64-msvc": "0.3.9", "@mariozechner/clipboard-win32-x64-msvc": "0.3.9" } }, "sha512-ABnA53mdfkGZwOFUdZNv2S0CWGO/EIuPj8Vv9xmBFmSYg/qFc7ihO6q5FcQjvoE67kZpWkEc4AhD6B/os04yuA=="], + + "@mariozechner/clipboard-darwin-arm64": ["@mariozechner/clipboard-darwin-arm64@0.3.9", "", { "os": "darwin", "cpu": "arm64" }, "sha512-BfgV7vCEWZwJwZJw03r6bP5+tf0iI/ANuQYCxi9RNn7FrWB3yzGuMKCrNLRl6V761vXRdL8+OqZ0wd4TqlsNOQ=="], + + "@mariozechner/clipboard-darwin-universal": ["@mariozechner/clipboard-darwin-universal@0.3.9", "", { "os": "darwin" }, "sha512-BGGR4iA9Z2shAjI65eI5xtyb3LYNlDW9X3gxKxDbqtbnREohsrqznov6zpKoIrsRWpzlYVEdKphS7ksJ0/ndSQ=="], + + "@mariozechner/clipboard-darwin-x64": ["@mariozechner/clipboard-darwin-x64@0.3.9", "", { "os": "darwin", "cpu": "x64" }, "sha512-4kURmCbS6nt8uYhtmWpUcJWyPHfmAr5dTpXD1nO3pIfa+TSQ9DbrGOYCKH+aEFW47XhQ4Vp8ZTszie+wfFvDKg=="], + + "@mariozechner/clipboard-linux-arm64-gnu": ["@mariozechner/clipboard-linux-arm64-gnu@0.3.9", "", { "os": "linux", "cpu": "arm64" }, "sha512-g59OkUGP2DDfCOIKypHeYgv2M55u/cKvXa5dSxFbEJ34XvIQMdcVmpKCkGUro3ZgefXiGVdwguvTMQGpHWzIXw=="], + + "@mariozechner/clipboard-linux-arm64-musl": ["@mariozechner/clipboard-linux-arm64-musl@0.3.9", "", { "os": "linux", "cpu": "arm64" }, "sha512-AGuJdgKsmJdm4Pych7kv3sqe591ERRaAHW3xjLooiFzn8J+PxUyof++7YZrB5Y5tpnTO+K18Og3taj2NpluCRQ=="], + + "@mariozechner/clipboard-linux-riscv64-gnu": ["@mariozechner/clipboard-linux-riscv64-gnu@0.3.9", "", { "os": "linux", "cpu": "none" }, "sha512-DXBEAiuMpk7dhS1a9NzNxVAFi1vaKoPu7rQNgY8LIDLGrK3lnIp3nT10DUum+PKVJoJppIP+NAA8IZe4DMNDPw=="], + + "@mariozechner/clipboard-linux-x64-gnu": ["@mariozechner/clipboard-linux-x64-gnu@0.3.9", "", { "os": "linux", "cpu": "x64" }, "sha512-WORrMLd6EpElEME7JRKfSaY34nW1P5LbdgK5YNCS1ncG2LqmITsSMEJ8nh2mpvxb3TxqbOOKgY7k9eMJYlW9Mw=="], + + "@mariozechner/clipboard-linux-x64-musl": ["@mariozechner/clipboard-linux-x64-musl@0.3.9", "", { "os": "linux", "cpu": "x64" }, "sha512-/DHn+1DrfL6oRaPPWXaOKvonFFrni666fxd+zFqiQEfvBH0tsHVWjq9iqBk0oDp0qaPA72lIMy5BptxISBEhZQ=="], + + "@mariozechner/clipboard-win32-arm64-msvc": ["@mariozechner/clipboard-win32-arm64-msvc@0.3.9", "", { "os": "win32", "cpu": "arm64" }, "sha512-O5FHD3ErkMwMhNzAfu3ggy0ug4z7btZuoQgwwxlzPrwV2bxlD6WDpqBY4NCgICAgZdDKdp+loUEKVAVt8aYnhQ=="], + + "@mariozechner/clipboard-win32-x64-msvc": ["@mariozechner/clipboard-win32-x64-msvc@0.3.9", "", { "os": "win32", "cpu": "x64" }, "sha512-ihQC3EufqEY81vhXBgVBtK4prL+wc62zJsSvxrgz7K1hsdt6OObz6v9p3Rn1OG3GJksTTKMJF0u/guMISHPhSA=="], + + "@mistralai/mistralai": ["@mistralai/mistralai@2.6.4", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.40.0", "ws": "^8.18.0", "zod": "^3.25.0 || ^4.0.0", "zod-to-json-schema": "^3.25.0" }, "peerDependencies": { "@opentelemetry/api": "^1.9.0", "@opentelemetry/exporter-trace-otlp-http": "^0.220.0", "@opentelemetry/resources": "^2.9.0", "@opentelemetry/sdk-trace-base": "^2.9.0" }, "optionalPeers": ["@opentelemetry/api", "@opentelemetry/exporter-trace-otlp-http", "@opentelemetry/resources", "@opentelemetry/sdk-trace-base"] }, "sha512-PPt4GyJqs2hEsWrYCJZK5f0ORmT+L2MSm75LVGD7kBLf6ZKsoDpld/FRBQXr8xG6iFCBOFJFYzvGYhUb+UCkbw=="], + + "@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="], + + "@nodelib/fs.stat": ["@nodelib/fs.stat@2.0.5", "", {}, "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A=="], + + "@nodelib/fs.walk": ["@nodelib/fs.walk@1.2.8", "", { "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" } }, "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg=="], + + "@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.43.0", "", {}, "sha512-eSYWTm620tTk45EKSedaUL8MFYI8hW164hIXsgIHyxu3VobUB3fFCu5t0hQby6OoWRPsG1KkKUG2M5UadiLiVg=="], + + "@oxc-project/types": ["@oxc-project/types@0.146.0", "", {}, "sha512-XC0QsnnhVe7sLIWmYmdPw7x5P0h4W8vUU3Nv1ySgWXtvCz8NizoAEpGXA0sOYoJQV2Rl13LgURAHQ5cI5ILCSA=="], + + "@pondwader/socks5-server": ["@pondwader/socks5-server@1.0.10", "", {}, "sha512-bQY06wzzR8D2+vVCUoBsr5QS2U6UgPUQRmErNwtsuI6vLcyRKkafjkr3KxbtGFf9aBBIV2mcvlsKD1UYaIV+sg=="], + + "@protobufjs/aspromise": ["@protobufjs/aspromise@1.1.2", "", {}, "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ=="], + + "@protobufjs/base64": ["@protobufjs/base64@1.1.2", "", {}, "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg=="], + + "@protobufjs/codegen": ["@protobufjs/codegen@2.0.5", "", {}, "sha512-zgXFLzW3Ap33e6d0Wlj4MGIm6Ce8O89n/apUaGNB/jx+hw+ruWEp7EwGUshdLKVRCxZW12fp9r40E1mQrf/34g=="], + + "@protobufjs/eventemitter": ["@protobufjs/eventemitter@1.1.1", "", {}, "sha512-vW1GmwMZNnL+gMRaovlh9yZX74kc+TTU3FObkkurpMaRtBfLP3ldjS9KQWlwZgraRE0+dheEEoAxdzcJQ8eXZg=="], + + "@protobufjs/fetch": ["@protobufjs/fetch@1.1.1", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.1" } }, "sha512-GpptLrs57adMSuHi3VNj0mAF8dwh36LMaYF6XyJ6JMWlVsc+t42tm1HSEDmOs3A8fC9yyeisgLhsTVQokOZ0zw=="], + + "@protobufjs/float": ["@protobufjs/float@1.0.2", "", {}, "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ=="], + + "@protobufjs/path": ["@protobufjs/path@1.1.2", "", {}, "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA=="], + + "@protobufjs/pool": ["@protobufjs/pool@1.1.0", "", {}, "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw=="], + + "@protobufjs/utf8": ["@protobufjs/utf8@1.1.2", "", {}, "sha512-b1UQwcEZ4yCnMCD8DAL1VlbvBJE9/IX4FTIp7BG1xYpf29SLazLSrqUkj4w7Y5y7cCVP6E5tcqqcI0xemPkHug=="], + + "@rolldown/binding-android-arm-eabi": ["@rolldown/binding-android-arm-eabi@1.2.5", "", { "os": "android", "cpu": "arm" }, "sha512-DLe/i+l8ynIBY7XEQ191TeZvCoowIGa18R+dIV30GW7DiOtp74i/xX8hs8GUjW5ARV7VZuie3d6AumSmCwbeRA=="], + + "@rolldown/binding-android-arm64": ["@rolldown/binding-android-arm64@1.2.5", "", { "os": "android", "cpu": "arm64" }, "sha512-zXcwKlQApYAOELHd8PwKDFkagYF9Wy4e0RJ+0qnzl9Pjnpj75TEG8ufv40p2J7kCEfwZAsNiuzRIyNNMWT38ig=="], + + "@rolldown/binding-darwin-arm64": ["@rolldown/binding-darwin-arm64@1.2.5", "", { "os": "darwin", "cpu": "arm64" }, "sha512-dK4QakI42nzWgJT5sm4y4y/O//D4OxM75/cH28RLV+nzIN9AY+YsbuUVrUTjlLjXR6vpyxFbSsbmNuJ6BP9sww=="], + + "@rolldown/binding-darwin-x64": ["@rolldown/binding-darwin-x64@1.2.5", "", { "os": "darwin", "cpu": "x64" }, "sha512-fqSALaUu1Wjd1nK2uW2kJDWdLCc8lx1IcY+MTY26Aurfdx19anlzhqXOgCFbBFQnlFDTn4TC1/7Nz4Bl2mLP3A=="], + + "@rolldown/binding-freebsd-x64": ["@rolldown/binding-freebsd-x64@1.2.5", "", { "os": "freebsd", "cpu": "x64" }, "sha512-/vCnNxlkxs9tKxNDcyWUePpJ/PgTzxIaVhoM5SmG8UV+GR/IcPam4VYxi7GIMo7PSDuNqlJqvprqii9NqqVCMw=="], + + "@rolldown/binding-linux-arm-gnueabihf": ["@rolldown/binding-linux-arm-gnueabihf@1.2.5", "", { "os": "linux", "cpu": "arm" }, "sha512-abk0NLA519LxRCszmbE0jYKuQ9YPocOXTiOXOo6Yr+YAT95VH+PtqYAjOJvGKt3viEd/x4qzabAlwd5bHOOARg=="], + + "@rolldown/binding-linux-arm64-gnu": ["@rolldown/binding-linux-arm64-gnu@1.2.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-Y7eALiJ8lr0M2HH103Js+g7V34wf6snlpZLAsHI90uLhr3PVlNsbFVAXJC9d/V6BnPyKtpSwI+NcB/RLxsQxuA=="], + + "@rolldown/binding-linux-arm64-musl": ["@rolldown/binding-linux-arm64-musl@1.2.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-xMvZgnbZg4YVnR/AX2b3oOPDTFYJvUVaJg5FedA/LuvexAtXibZQej4cnTkw3rjsJ/ggUROB64TdtETiim+FYA=="], + + "@rolldown/binding-linux-ppc64-gnu": ["@rolldown/binding-linux-ppc64-gnu@1.2.5", "", { "os": "linux", "cpu": "ppc64" }, "sha512-GRjeqTUDHTo5GwntsLaAMcBahG3nlpjftXWZLN73HiYQlhwEowvarFgQnRnQZtIp4keXX7quXFbG38uPZBa2EA=="], + + "@rolldown/binding-linux-s390x-gnu": ["@rolldown/binding-linux-s390x-gnu@1.2.5", "", { "os": "linux", "cpu": "s390x" }, "sha512-vLNTR45F2Uwc8AufkNXPmB4VliaXs+FvcheEogIzOXzO4l+LzieXF5A/TWxLy5HtqpsRCHUfd0lPVrrdgXdLHQ=="], + + "@rolldown/binding-linux-x64-gnu": ["@rolldown/binding-linux-x64-gnu@1.2.5", "", { "os": "linux", "cpu": "x64" }, "sha512-Mgj59/HTuYeK9Gz2MA+mBWKnHsAgkBSec15ZMb1st3oIfFbX7gCjOae7GydHhzcyQi9Z/7M1QuN9bR3oFqF0jQ=="], + + "@rolldown/binding-linux-x64-musl": ["@rolldown/binding-linux-x64-musl@1.2.5", "", { "os": "linux", "cpu": "x64" }, "sha512-mY8AP0/ichsbhAxGnLa3d3+MwV0EfgrPND2bplI3Ym8T6R2pJ0N87bvrKVwNXmdy3jnr6eQBecdqx/HMknBmpA=="], + + "@rolldown/binding-openharmony-arm64": ["@rolldown/binding-openharmony-arm64@1.2.5", "", { "os": "none", "cpu": "arm64" }, "sha512-8SLssA2oweAxyRgDp789ACfRb/3P+zNRJpzZxSizxF9m8NUDQ4+3xjo8ttjhVGGw6Qxb70oZiEtIjaKikCO7Yw=="], + + "@rolldown/binding-win32-arm64-msvc": ["@rolldown/binding-win32-arm64-msvc@1.2.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-vGbruD5zquhoc8D9SViXgN2FBJtNdTyQ4DtG+SWiEGlJiAzoKcZ2xp+xuXCffhubVdt0NJlTZqkeRuERy7g8Cw=="], + + "@rolldown/binding-win32-x64-msvc": ["@rolldown/binding-win32-x64-msvc@1.2.5", "", { "os": "win32", "cpu": "x64" }, "sha512-e/SXpgISz+IoqVcSSI0rx/d/he8zqLex+/rCWpnHpmVfmPIUjag9H6P7zotf0gJHwPUhQxZ/mF8tr6acebT9yw=="], + + "@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.1", "", {}, "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw=="], + + "@silvia-odwyer/photon-node": ["@silvia-odwyer/photon-node@0.3.4", "", {}, "sha512-bnly4BKB3KDTFxrUIcgCLbaeVVS8lrAkri1pEzskpmxu9MdfGQTy8b8EgcD83ywD3RPMsIulY8xJH5Awa+t9fA=="], + + "@smithy/core": ["@smithy/core@3.33.3", "", { "dependencies": { "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-CsOeKq/9kA3y6VJHt+/+VTCtBaxJ4OTFpgrjIUhPpDIKxBci1k2bJaQASF2h/ELWrulGp+t97DZ0mevfAD8idg=="], + + "@smithy/credential-provider-imds": ["@smithy/credential-provider-imds@4.5.2", "", { "dependencies": { "@smithy/core": "^3.33.2", "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-A9uSdn72ozbRUSit0eib0TW7nXuNPlaeM0zcGkJ+nE6tFcSDbnmtwoxbTCFBukVQcszDAyvsd7+rTduPTXpygg=="], + + "@smithy/fetch-http-handler": ["@smithy/fetch-http-handler@5.7.2", "", { "dependencies": { "@smithy/core": "^3.33.2", "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-nZyWTmSpJEXl6VtWVMBJve/7x12DZu6sIX1z1a+ZMaHlQQRs9Zpu6NbTe/gmxYXVRpkjxyDYpZ5gx2IM6f/Wkw=="], + + "@smithy/node-http-handler": ["@smithy/node-http-handler@4.11.3", "", { "dependencies": { "@smithy/core": "^3.33.3", "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-2jY1tSpERfPfWqyBV2pH+iGFaghVsIJszJNsT7hxtQYhVJpWDyc0LqOWI+nXOxOAHaEfZ4PXXtp1wW1TGpHhkA=="], + + "@smithy/signature-v4": ["@smithy/signature-v4@5.7.3", "", { "dependencies": { "@smithy/core": "^3.33.3", "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-7ImGm+FkHRLcBaRttIAMZ6bzJZWb2cJGoYjq46F2UjycujWzrL9GEN9h4w7eQyXJYnltrUhxbbieBAIRrdqpow=="], + + "@smithy/types": ["@smithy/types@4.17.2", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-FOKpVZob9MPTn2znRzGrnsMHv7BOsKVw3XiP/cOyYLDVZ9qKp4nifIiSCuUU/fIj5Vu0UOAxCFr+qRAtG0NUkA=="], + + "@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], + + "@tokenizer/inflate": ["@tokenizer/inflate@0.4.1", "", { "dependencies": { "debug": "^4.4.3", "token-types": "^6.1.1" } }, "sha512-2mAv+8pkG6GIZiF1kNg1jAjh27IDxEPKwdGul3snfztFerfPGI1LjDezZp3i7BElXompqEtPmoPx6c2wgtWsOA=="], + + "@tokenizer/token": ["@tokenizer/token@0.3.0", "", {}, "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A=="], + + "@tootallnate/quickjs-emscripten": ["@tootallnate/quickjs-emscripten@0.23.0", "", {}, "sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA=="], + + "@types/chai": ["@types/chai@5.2.3", "", { "dependencies": { "@types/deep-eql": "*", "assertion-error": "^2.0.1" } }, "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA=="], + + "@types/deep-eql": ["@types/deep-eql@4.0.2", "", {}, "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw=="], + + "@types/diff": ["@types/diff@8.0.0", "", { "dependencies": { "diff": "*" } }, "sha512-o7jqJM04gfaYrdCecCVMbZhNdG6T1MHg/oQoRFdERLV+4d+V7FijhiEAbFu0Usww84Yijk9yH58U4Jk4HbtzZw=="], + + "@types/estree": ["@types/estree@1.0.9", "", {}, "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg=="], + + "@types/hosted-git-info": ["@types/hosted-git-info@3.0.5", "", {}, "sha512-Dmngh7U003cOHPhKGyA7LWqrnvcTyILNgNPmNCxlx7j8MIi54iBliiT8XqVLIQ3GchoOjVAyBzNJVyuaJjqokg=="], + + "@types/mime-types": ["@types/mime-types@3.0.1", "", {}, "sha512-xRMsfuQbnRq1Ef+C+RKaENOxXX87Ygl38W1vDfPHRku02TgQr+Qd8iivLtAMcR0KF5/29xlnFihkTlbqFrGOVQ=="], + + "@types/ms": ["@types/ms@2.1.0", "", {}, "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA=="], + + "@types/node": ["@types/node@22.20.1", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-EANqOCF9QFyra+4pfxUcX9STKJpCLjMbObVzljIJomAWSnuSIEAvyzEU53GaajbXJEgdh0iEcPL+DGvpUd4k1Q=="], + + "@types/proper-lockfile": ["@types/proper-lockfile@4.1.4", "", { "dependencies": { "@types/retry": "*" } }, "sha512-uo2ABllncSqg9F1D4nugVl9v93RmjxF6LJzQLMLDdPaXCUIDPeOJ21Gbqi43xNKzBi/WQ0Q0dICqufzQbMjipQ=="], + + "@types/retry": ["@types/retry@0.12.5", "", {}, "sha512-3xSjTp3v03X/lSQLkczaN9UIEwJMoMCA1+Nb5HfbJEQWogdeQIyVtTvxPXDQjZ5zws8rFQfVfRdz03ARihPJgw=="], + + "@types/yauzl": ["@types/yauzl@2.10.3", "", { "dependencies": { "@types/node": "*" } }, "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q=="], + + "@typescript/native-preview": ["@typescript/native-preview@7.0.0-dev.20260120.1", "", { "optionalDependencies": { "@typescript/native-preview-darwin-arm64": "7.0.0-dev.20260120.1", "@typescript/native-preview-darwin-x64": "7.0.0-dev.20260120.1", "@typescript/native-preview-linux-arm": "7.0.0-dev.20260120.1", "@typescript/native-preview-linux-arm64": "7.0.0-dev.20260120.1", "@typescript/native-preview-linux-x64": "7.0.0-dev.20260120.1", "@typescript/native-preview-win32-arm64": "7.0.0-dev.20260120.1", "@typescript/native-preview-win32-x64": "7.0.0-dev.20260120.1" }, "bin": { "tsgo": "bin/tsgo.js" } }, "sha512-nnEf37C9ue7OBRnF2zmV/OCBmV5Y7T/K4mCHa+nxgiXcF/1w8sA0cgdFl+gHQ0mysqUJ+Bu5btAMeWgpLyjrgg=="], + + "@typescript/native-preview-darwin-arm64": ["@typescript/native-preview-darwin-arm64@7.0.0-dev.20260120.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-r3pWFuR2H7mn6ScwpH5jJljKQqKto0npVuJSk6pRwFwexpTyxOGmJTZJ1V0AWiisaNxU2+CNAqWFJSJYIE/QTg=="], + + "@typescript/native-preview-darwin-x64": ["@typescript/native-preview-darwin-x64@7.0.0-dev.20260120.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-cuC1+wLbUP+Ip2UT94G134fqRdp5w3b3dhcCO6/FQ4yXxvRNyv/WK+upHBUFDaeSOeHgDTyO9/QFYUWwC4If1A=="], + + "@typescript/native-preview-linux-arm": ["@typescript/native-preview-linux-arm@7.0.0-dev.20260120.1", "", { "os": "linux", "cpu": "arm" }, "sha512-vN6OYVySol/kQZjJGmAzd6L30SyVlCgmCXS8WjUYtE5clN0YrzQHop16RK29fYZHMxpkOniVBtRPxUYQANZBlQ=="], + + "@typescript/native-preview-linux-arm64": ["@typescript/native-preview-linux-arm64@7.0.0-dev.20260120.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-zZGvEGY7wcHYefMZ87KNmvjN3NLIhsCMHEpHZiGCS3khKf+8z6ZsanrzCjOTodvL01VPyBzHxV1EtkSxAcLiQg=="], + + "@typescript/native-preview-linux-x64": ["@typescript/native-preview-linux-x64@7.0.0-dev.20260120.1", "", { "os": "linux", "cpu": "x64" }, "sha512-JBfNhWd/asd5MDeS3VgRvE24pGKBkmvLub6tsux6ypr+Yhy+o0WaAEzVpmlRYZUqss2ai5tvOu4dzPBXzZAtFw=="], + + "@typescript/native-preview-win32-arm64": ["@typescript/native-preview-win32-arm64@7.0.0-dev.20260120.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-tTndRtYCq2xwgE0VkTi9ACNiJaV43+PqvBqCxk8ceYi3X36Ve+CCnwlZfZJ4k9NxZthtrAwF/kUmpC9iIYbq1w=="], + + "@typescript/native-preview-win32-x64": ["@typescript/native-preview-win32-x64@7.0.0-dev.20260120.1", "", { "os": "win32", "cpu": "x64" }, "sha512-oZia7hFL6k9pVepfonuPI86Jmyz6WlJKR57tWCDwRNmpA7odxuTq1PbvcYgy1z4+wHF1nnKKJY0PMAiq6ac18w=="], + + "@typescript/typescript-aix-ppc64": ["@typescript/typescript-aix-ppc64@7.0.2", "", { "os": "aix", "cpu": "ppc64" }, "sha512-MTKKkWB7p/0E9xi1d1tHtZ5PiLkGEMIq88pK2CubZjOsLtYTLqhgIgi6zepFa+9GHZ6h05NMCkQxGKiPXMxXtQ=="], + + "@typescript/typescript-darwin-arm64": ["@typescript/typescript-darwin-arm64@7.0.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-gowzar9MwS/aRWp6f3a4KUqzRjAZjOsmGNCM6LcTgXum+dBfgsBVMN+AgvOCCbguXyick6LJhpBszxMebJ8syA=="], + + "@typescript/typescript-darwin-x64": ["@typescript/typescript-darwin-x64@7.0.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-SZ9xZInqApNlNGc9s0W1VSsktYSOe9cFqNOIqmN1Gs8SmkjKZYFt017G4VwPxASInODuAdbTW7sXiFUf893RgA=="], + + "@typescript/typescript-freebsd-arm64": ["@typescript/typescript-freebsd-arm64@7.0.2", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-W5NH4y/J0plIIS5b2xvTEkU7JFxyqdMAOgf+Ilhl0vHQXKO5dZoxd+C/jEtq56c4F3wk71RB4BMRQ2XdI+bwYQ=="], + + "@typescript/typescript-freebsd-x64": ["@typescript/typescript-freebsd-x64@7.0.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-UMGDx5sTpzNw3WiPebH7l90IWfJggEd+egHt/q6p7/Cm3zqoV7VxkGXt+3DxPIw8CcmvAB0j3sVVfbhX+M4Tpw=="], + + "@typescript/typescript-linux-arm": ["@typescript/typescript-linux-arm@7.0.2", "", { "os": "linux", "cpu": "arm" }, "sha512-gffT3xPz9sR7j/YJExkyPntrI0P2EP9XbOyWzth2/Gs0RstK+90RBcO0ncXoXy/beYll1SXw846Nf2zdnEz0QQ=="], + + "@typescript/typescript-linux-arm64": ["@typescript/typescript-linux-arm64@7.0.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-Qh4eU4/y3yDjnfjjyPYihMj5/ODIlmt+Bzu17OI+fiSRDW57QmU5SiN63exPRNJPKUzcc1INa1NXdrJ+MqHjUQ=="], + + "@typescript/typescript-linux-loong64": ["@typescript/typescript-linux-loong64@7.0.2", "", { "os": "linux", "cpu": "none" }, "sha512-uEHck9i8hoAzXPiYRib1O7miOnz23SxIeVl6F4LXox+qov1K35jHcEW6VHKvZI+pyvl7fZEP4MCU5LYvIq1GuQ=="], + + "@typescript/typescript-linux-mips64el": ["@typescript/typescript-linux-mips64el@7.0.2", "", { "os": "linux", "cpu": "none" }, "sha512-R4KvAMnE43W5Qeqb0Ly56O3mWMWIAgsMyz36DCaycd5nbg/9kzm0liw3JocfRqyJY0KPmzFjbswozXyW0DnIYA=="], + + "@typescript/typescript-linux-ppc64": ["@typescript/typescript-linux-ppc64@7.0.2", "", { "os": "linux", "cpu": "ppc64" }, "sha512-DORx5b3sd/4S7eayxm4FQv+A7CrkUIGRaHiwI8oiHTAI1fAPWhF4J0vAlkC8biAlHSVVwxMQ3tjZ2/DVbnQiiA=="], + + "@typescript/typescript-linux-riscv64": ["@typescript/typescript-linux-riscv64@7.0.2", "", { "os": "linux", "cpu": "none" }, "sha512-wf0jqEDOjrPRnKwYRyyJDRo11KMbvMFrU+q4zqKyChODBzvlkbhNQfKvLxQCcwTpdDaXSHZTVuh0JoCrKCUMHQ=="], + + "@typescript/typescript-linux-s390x": ["@typescript/typescript-linux-s390x@7.0.2", "", { "os": "linux", "cpu": "s390x" }, "sha512-IkwJc3L7yhytWd/ewjyxNDfOmswCm9GWMJT/ue/dU4aZNbwZeYAetq42VyLmsmSjvoX7z74X6ZaYCtzAr0EuGw=="], + + "@typescript/typescript-linux-x64": ["@typescript/typescript-linux-x64@7.0.2", "", { "os": "linux", "cpu": "x64" }, "sha512-EYdf2cNg7rgCWJnxCdJ+F3V39O8ihb37eHAu1LK8oAFizgTQbPOK7zHHXbPt8rX24COqODXeI3sIf0fCXG7H/A=="], + + "@typescript/typescript-netbsd-arm64": ["@typescript/typescript-netbsd-arm64@7.0.2", "", { "os": "none", "cpu": "arm64" }, "sha512-+polYF4MF04aPpO5FTkHran9yUQDSXqy5GiSDKpsll5jy3l3+g9QLhpf39T+ePtefhXLOGrLl0QIjkQP6VnelA=="], + + "@typescript/typescript-netbsd-x64": ["@typescript/typescript-netbsd-x64@7.0.2", "", { "os": "none", "cpu": "x64" }, "sha512-8YIT0EHM/3dq10ZOVF/A7pc/YSMtbcecct4rWtexrnSCHOPcpC2KTLXfTCR6vDpnSiY12heNb1GiN/wu+T/FyA=="], + + "@typescript/typescript-openbsd-arm64": ["@typescript/typescript-openbsd-arm64@7.0.2", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-APT8+ClYnuYm1u9+kgGXoMj2VzWzcymwh2gNSQVySHfkRDGOTVkoWLjCmOQSaO+PoqQ57B0flRp9SA+7GnnkzQ=="], + + "@typescript/typescript-openbsd-x64": ["@typescript/typescript-openbsd-x64@7.0.2", "", { "os": "openbsd", "cpu": "x64" }, "sha512-yX7s+Q0Dln0Dt9tEzZsAjXXR/+ytBM7AlglaqyeMPxQszJ1JhlJdZ6jLA+IzldHtflX81em7lDao1xXu+aRRkg=="], + + "@typescript/typescript-sunos-x64": ["@typescript/typescript-sunos-x64@7.0.2", "", { "os": "sunos", "cpu": "x64" }, "sha512-dLJDGaLZ1D4HPQn62u1n8mBDkJREwMsAkCdkwd4Ieqw+x3TUyTsqY0YiBCtE6H6OzzgGk3iuZ3vFWRS+E8/d1g=="], + + "@typescript/typescript-win32-arm64": ["@typescript/typescript-win32-arm64@7.0.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-Gyl1Vy6OsWesLzmq+EP0Fb7b4Nid5232AvcA2SFcdYreldpNtYFFofPjnt62y9hQy7VTaZp65ICJjuAQRaVcIQ=="], + + "@typescript/typescript-win32-x64": ["@typescript/typescript-win32-x64@7.0.2", "", { "os": "win32", "cpu": "x64" }, "sha512-0BQ3HkAHHlKLSp1qRvf3SUhGpGsDuhB/jgFw75guyqbxJqEaS0Cw/VFO8i2nHglJUzQCRtMMR/IBAKE3ETMC4g=="], + + "@vitest/expect": ["@vitest/expect@4.1.11", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "@types/chai": "^5.2.2", "@vitest/spy": "4.1.11", "@vitest/utils": "4.1.11", "chai": "^6.2.2", "tinyrainbow": "^3.1.0" } }, "sha512-VX2x5vNJXET47KAFzwERI+KRMtTTCSWTfSMKsW7JsUsXV4psq++e3DvZpuTDOpHcxytiDs6p2nhVb2tVDiiUYw=="], + + "@vitest/mocker": ["@vitest/mocker@4.1.11", "", { "dependencies": { "@vitest/spy": "4.1.11", "estree-walker": "^3.0.3", "magic-string": "^0.30.21" }, "peerDependencies": { "msw": "^2.4.9", "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" }, "optionalPeers": ["msw", "vite"] }, "sha512-2XJVD55d1o5AZous5CCGKS74g/riOj9odEt2bQpCVZeblHyHdnMeFl4jl0XjU21stf4mbjUkew2eXQZt65g5CQ=="], + + "@vitest/pretty-format": ["@vitest/pretty-format@4.1.11", "", { "dependencies": { "tinyrainbow": "^3.1.0" } }, "sha512-yiZzPbGTS9Sr/JpFl8zHrcIkAofNbFV6k21vIgQN/cY/oxZeXhJv5sc/MBJ5jFKWmWs+oJHw0UXLZjmf931+Vw=="], + + "@vitest/runner": ["@vitest/runner@4.1.11", "", { "dependencies": { "@vitest/utils": "4.1.11", "pathe": "^2.0.3" } }, "sha512-LztvUgdwMNJMIkj3hQnnxiC2Xy1zNxq928W/xhjCLaNCzqTZOudjwbQf6v9IntZGPw132i2Lq2rgTRZHD3JHNw=="], + + "@vitest/snapshot": ["@vitest/snapshot@4.1.11", "", { "dependencies": { "@vitest/pretty-format": "4.1.11", "@vitest/utils": "4.1.11", "magic-string": "^0.30.21", "pathe": "^2.0.3" } }, "sha512-pN7ikn1ON7h8ee4gIAp4AzyK+zBtJPzVbqOgu5LCEh4VaJVbPQcgYQYJIMGQPXVeJJq1fnfazis7a5pFNPahog=="], + + "@vitest/spy": ["@vitest/spy@4.1.11", "", {}, "sha512-apNa/prQy2qCeywhnixOHPRCgGNhvg7T4Dapfl1GahLp/R+uhBm5cPyFoNVyqsNd2h1nJxL6BqqdIjiABL60YA=="], + + "@vitest/utils": ["@vitest/utils@4.1.11", "", { "dependencies": { "@vitest/pretty-format": "4.1.11", "convert-source-map": "^2.0.0", "tinyrainbow": "^3.1.0" } }, "sha512-zTCVGpyFsGWBhllOyKlTw/vnr6D9qxsfSDyfbyZmTyjHw5N/VuvzHpHoQjm2ZJzn4RJgx5w4r7V0er69CmLgPQ=="], + + "@xterm/headless": ["@xterm/headless@6.0.0", "", {}, "sha512-5Yj1QINYCyzrZtf8OFIHi47iQtI+0qYFPHmouEfG8dHNxbZ9Tb9YGSuLcsEwj9Z+OL75GJqPyJbyoFer80a2Hw=="], + + "@xterm/xterm": ["@xterm/xterm@6.0.0", "", {}, "sha512-TQwDdQGtwwDt+2cgKDLn0IRaSxYu1tSUjgKarSDkUM0ZNiSRXFpjxEsvc/Zgc5kq5omJ+V0a8/kIM2WD3sMOYg=="], + + "agent-base": ["agent-base@7.1.4", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="], + + "ansi-regex": ["ansi-regex@6.3.0", "", {}, "sha512-WpDfL7NO6j7tH88IDBNVdUJxDh9nmCteAVW9dsep846XdwF4naCBK+/tGLX3KJgcpgMRXCFlTM2hKGoK9FsdrQ=="], + + "ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], + + "any-promise": ["any-promise@1.3.0", "", {}, "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A=="], + + "assertion-error": ["assertion-error@2.0.1", "", {}, "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA=="], + + "ast-types": ["ast-types@0.13.4", "", { "dependencies": { "tslib": "^2.0.1" } }, "sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w=="], + + "balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="], + + "base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="], + + "basic-ftp": ["basic-ftp@5.3.1", "", {}, "sha512-bopVNp6ugyA150DDuZfPFdt1KZ5a94ZDiwX4hMgZDzF+GttD80lEy8kj98kbyhLXnPvhtIo93mdnLIjpCAeeOw=="], + + "bignumber.js": ["bignumber.js@9.3.1", "", {}, "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ=="], + + "bl": ["bl@4.1.0", "", { "dependencies": { "buffer": "^5.5.0", "inherits": "^2.0.4", "readable-stream": "^3.4.0" } }, "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w=="], + + "bowser": ["bowser@2.14.1", "", {}, "sha512-tzPjzCxygAKWFOJP011oxFHs57HzIhOEracIgAePE4pqB3LikALKnSzUyU4MGs9/iCEUuHlAJTjTc5M+u7YEGg=="], + + "brace-expansion": ["brace-expansion@5.0.9", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg=="], + + "braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="], + + "buffer": ["buffer@5.7.1", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.1.13" } }, "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ=="], + + "buffer-crc32": ["buffer-crc32@0.2.13", "", {}, "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ=="], + + "buffer-equal-constant-time": ["buffer-equal-constant-time@1.0.1", "", {}, "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA=="], + + "canvas": ["canvas@3.2.3", "", { "dependencies": { "node-addon-api": "^7.0.0", "prebuild-install": "^7.1.3" } }, "sha512-PzE5nJZPz72YUAfo8oTp0u3fqqY7IzlTubneAihqDYAUcBk7ryeCmBbdJBEdaH0bptSOe2VT2Zwcb3UaFyaSWw=="], + + "chai": ["chai@6.2.2", "", {}, "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg=="], + + "chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="], + + "chownr": ["chownr@1.1.4", "", {}, "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg=="], + + "cli-highlight": ["cli-highlight@2.1.11", "", { "dependencies": { "chalk": "^4.0.0", "highlight.js": "^10.7.1", "mz": "^2.4.0", "parse5": "^5.1.1", "parse5-htmlparser2-tree-adapter": "^6.0.0", "yargs": "^16.0.0" }, "bin": { "highlight": "bin/highlight" } }, "sha512-9KDcoEVwyUXrjcJNvHD0NFc/hiwe/WPVYIleQh2O1N2Zro5gWJZ/K+3DGn8w8P/F6FxOgzyC5bxDyHIgCSPhGg=="], + + "cliui": ["cliui@8.0.1", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ=="], + + "color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], + + "color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], + + "commander": ["commander@12.1.0", "", {}, "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA=="], + + "concurrently": ["concurrently@9.2.4", "", { "dependencies": { "chalk": "4.1.2", "rxjs": "7.8.2", "shell-quote": "1.9.0", "supports-color": "8.1.1", "tree-kill": "1.2.2", "yargs": "17.7.2" }, "bin": { "conc": "dist/bin/concurrently.js", "concurrently": "dist/bin/concurrently.js" } }, "sha512-TZ0CEhyzvFjgtAvHTusDMgj7wNdihCh7LLLrzdUOXIhdlnL2JBBGA9eJxR24rtqgmdjh3OA3hrN1rCHj6HM8qA=="], + + "convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="], + + "cross-spawn": ["cross-spawn@6.0.6", "", { "dependencies": { "nice-try": "^1.0.4", "path-key": "^2.0.1", "semver": "^5.5.0", "shebang-command": "^1.2.0", "which": "^1.2.9" } }, "sha512-VqCUuhcd1iB+dsv8gxPttb5iZh/D0iubSP21g36KXdEuf6I5JiioesUVjpCdHV9MZRUfVFlvwtIUyPfxo5trtw=="], + + "data-uri-to-buffer": ["data-uri-to-buffer@6.0.2", "", {}, "sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw=="], + + "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" }, "peerDependencies": { "supports-color": "*" }, "optionalPeers": ["supports-color"] }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], + + "decompress-response": ["decompress-response@6.0.0", "", { "dependencies": { "mimic-response": "^3.1.0" } }, "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ=="], + + "deep-extend": ["deep-extend@0.6.0", "", {}, "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA=="], + + "degenerator": ["degenerator@5.0.1", "", { "dependencies": { "ast-types": "^0.13.4", "escodegen": "^2.1.0", "esprima": "^4.0.1" } }, "sha512-TllpMR/t0M5sqCXfj85i4XaAzxmS5tVA16dqvdkMwGmzI+dXLXnw3J+3Vdv7VKw+ThlTMboK6i9rnZ6Nntj5CQ=="], + + "detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], + + "diff": ["diff@9.0.0", "", {}, "sha512-svtcdpS8CgJyqAjEQIXdb3OjhFVVYjzGAPO8WGCmRbrml64SPw/jJD4GoE98aR7r25A0XcgrK3F02yw9R/vhQw=="], + + "ecdsa-sig-formatter": ["ecdsa-sig-formatter@1.0.11", "", { "dependencies": { "safe-buffer": "^5.0.1" } }, "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ=="], + + "emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], + + "end-of-stream": ["end-of-stream@1.4.5", "", { "dependencies": { "once": "^1.4.0" } }, "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg=="], + + "es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="], + + "es-module-lexer": ["es-module-lexer@2.3.2", "", {}, "sha512-poHGpORABojJJucnV9KbOavETW8lBVnphkW77ER5/BQ5Fz7oXSoCNek7IH3vR5nRjdsEz926ibFYX8KtLQmdyw=="], + + "esbuild": ["esbuild@0.28.2", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.28.2", "@esbuild/android-arm": "0.28.2", "@esbuild/android-arm64": "0.28.2", "@esbuild/android-x64": "0.28.2", "@esbuild/darwin-arm64": "0.28.2", "@esbuild/darwin-x64": "0.28.2", "@esbuild/freebsd-arm64": "0.28.2", "@esbuild/freebsd-x64": "0.28.2", "@esbuild/linux-arm": "0.28.2", "@esbuild/linux-arm64": "0.28.2", "@esbuild/linux-ia32": "0.28.2", "@esbuild/linux-loong64": "0.28.2", "@esbuild/linux-mips64el": "0.28.2", "@esbuild/linux-ppc64": "0.28.2", "@esbuild/linux-riscv64": "0.28.2", "@esbuild/linux-s390x": "0.28.2", "@esbuild/linux-x64": "0.28.2", "@esbuild/netbsd-arm64": "0.28.2", "@esbuild/netbsd-x64": "0.28.2", "@esbuild/openbsd-arm64": "0.28.2", "@esbuild/openbsd-x64": "0.28.2", "@esbuild/openharmony-arm64": "0.28.2", "@esbuild/sunos-x64": "0.28.2", "@esbuild/win32-arm64": "0.28.2", "@esbuild/win32-ia32": "0.28.2", "@esbuild/win32-x64": "0.28.2" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA=="], + + "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="], + + "escodegen": ["escodegen@2.1.0", "", { "dependencies": { "esprima": "^4.0.1", "estraverse": "^5.2.0", "esutils": "^2.0.2" }, "optionalDependencies": { "source-map": "~0.6.1" }, "bin": { "esgenerate": "bin/esgenerate.js", "escodegen": "bin/escodegen.js" } }, "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w=="], + + "esprima": ["esprima@4.0.1", "", { "bin": { "esparse": "./bin/esparse.js", "esvalidate": "./bin/esvalidate.js" } }, "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A=="], + + "estraverse": ["estraverse@5.3.0", "", {}, "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA=="], + + "estree-walker": ["estree-walker@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g=="], + + "esutils": ["esutils@2.0.3", "", {}, "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="], + + "execa": ["execa@1.0.0", "", { "dependencies": { "cross-spawn": "^6.0.0", "get-stream": "^4.0.0", "is-stream": "^1.1.0", "npm-run-path": "^2.0.0", "p-finally": "^1.0.0", "signal-exit": "^3.0.0", "strip-eof": "^1.0.0" } }, "sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA=="], + + "expand-template": ["expand-template@2.0.3", "", {}, "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg=="], + + "expect-type": ["expect-type@1.4.0", "", {}, "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA=="], + + "extend": ["extend@3.0.2", "", {}, "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="], + + "extract-zip": ["extract-zip@2.0.1", "", { "dependencies": { "debug": "^4.1.1", "get-stream": "^5.1.0", "yauzl": "^2.10.0" }, "optionalDependencies": { "@types/yauzl": "^2.9.1" }, "bin": { "extract-zip": "cli.js" } }, "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg=="], + + "fast-glob": ["fast-glob@3.3.3", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.8" } }, "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg=="], + + "fastq": ["fastq@1.20.1", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw=="], + + "fd-slicer": ["fd-slicer@1.1.0", "", { "dependencies": { "pend": "~1.2.0" } }, "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g=="], + + "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], + + "fetch-blob": ["fetch-blob@3.2.0", "", { "dependencies": { "node-domexception": "^1.0.0", "web-streams-polyfill": "^3.0.3" } }, "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ=="], + + "file-type": ["file-type@21.3.4", "", { "dependencies": { "@tokenizer/inflate": "^0.4.1", "strtok3": "^10.3.4", "token-types": "^6.1.1", "uint8array-extras": "^1.4.0" } }, "sha512-Ievi/yy8DS3ygGvT47PjSfdFoX+2isQueoYP1cntFW1JLYAuS4GD7NUPGg4zv2iZfV52uDyk5w5Z0TdpRS6Q1g=="], + + "fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="], + + "formdata-polyfill": ["formdata-polyfill@4.0.10", "", { "dependencies": { "fetch-blob": "^3.1.2" } }, "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g=="], + + "fs-constants": ["fs-constants@1.0.0", "", {}, "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow=="], + + "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], + + "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.2", "", { "dependencies": { "gaxios": "^7.0.0", "google-logging-utils": "^1.0.0", "json-bigint": "^1.0.0" } }, "sha512-zV/5HKTfCeKWnxG0Dmrw51hEWFGfcF2xiXqcA3+J90WDuP0SvoiSO5ORvcBsifmx/FoIjgQN3oNOGaQ5PhLFkg=="], + + "get-caller-file": ["get-caller-file@2.0.5", "", {}, "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="], + + "get-east-asian-width": ["get-east-asian-width@1.6.0", "", {}, "sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA=="], + + "get-stream": ["get-stream@5.2.0", "", { "dependencies": { "pump": "^3.0.0" } }, "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA=="], + + "get-uri": ["get-uri@6.0.5", "", { "dependencies": { "basic-ftp": "^5.0.2", "data-uri-to-buffer": "^6.0.2", "debug": "^4.3.4" } }, "sha512-b1O07XYq8eRuVzBNgJLstU6FYc1tS6wnMtF1I1D9lE8LxZSOGZ7LhxN54yPP6mGw5f2CkXY2BQUL9Fx41qvcIg=="], + + "github-from-package": ["github-from-package@0.0.0", "", {}, "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw=="], + + "glob": ["glob@13.0.6", "", { "dependencies": { "minimatch": "^10.2.2", "minipass": "^7.1.3", "path-scurry": "^2.0.2" } }, "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw=="], + + "glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], + + "google-auth-library": ["google-auth-library@10.9.1", "", { "dependencies": { "base64-js": "^1.3.0", "ecdsa-sig-formatter": "^1.0.11", "gaxios": "^7.1.4", "gcp-metadata": "8.1.2", "google-logging-utils": "1.1.3", "jws": "^4.0.0" } }, "sha512-i1ydyHrqcIxXkWh/uBmVkzCvIuq5yiK2ATndIe5XxKholrG/MTYP9xGYka4sQhrbIAgGjL2B6NOE7rFaiF3fXw=="], + + "google-logging-utils": ["google-logging-utils@1.1.3", "", {}, "sha512-eAmLkjDjAFCVXg7A1unxHsLf961m6y17QFqXqAXGj/gVkKFrEICfStRfwUlGNfeCEjNRa32JEWOUTlYXPyyKvA=="], + + "graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="], + + "grok-mermaid": ["grok-mermaid@0.2.3", "", {}, "sha512-/4KopAbsjvuRP9MdPtlDjOHUmUVEohOX73JNcsWpzAtFxh+bq5+Dhb6gzvRieLDwIPQIR3/vy8V1NNTuz4Zsmg=="], + + "has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], + + "hasown": ["hasown@2.0.4", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A=="], + + "highlight.js": ["highlight.js@10.7.3", "", {}, "sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A=="], + + "hosted-git-info": ["hosted-git-info@9.0.3", "", { "dependencies": { "lru-cache": "^11.1.0" } }, "sha512-Hc+ghLoSt6QaYZUv0WBiIvmMDZuZZ7oaDvdH8MbfOO4lOsxdXLEvuC6ePoGs9H1X9oCLyq6+NVN0MKqD+ydxyg=="], + + "http-proxy-agent": ["http-proxy-agent@7.0.2", "", { "dependencies": { "agent-base": "^7.1.0", "debug": "^4.3.4" } }, "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig=="], + + "https-proxy-agent": ["https-proxy-agent@7.0.6", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "4" } }, "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw=="], + + "husky": ["husky@9.1.7", "", { "bin": { "husky": "bin.js" } }, "sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA=="], + + "ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="], + + "ignore": ["ignore@7.0.6", "", {}, "sha512-BAg6QkE8W+TuQLrrw0Ugr7HegXduRuuj8/ti2kSOc+jz1dmx8/WNcjr6XGnq5YpDWxFwwaavqD0+jIUOKelTsw=="], + + "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="], + + "ini": ["ini@1.3.8", "", {}, "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew=="], + + "interpret": ["interpret@1.4.0", "", {}, "sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA=="], + + "ip-address": ["ip-address@10.5.0", "", {}, "sha512-R5SnVLJmgYYvf2F2ZgwSBnelz5G4q5AxIC277GDfUaNbrZKNANcBC7RHqYYePlszf4kBolVkJauG0ZjHHFh55g=="], + + "is-core-module": ["is-core-module@2.16.2", "", { "dependencies": { "hasown": "^2.0.3" } }, "sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA=="], + + "is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="], + + "is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="], + + "is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="], + + "is-number": ["is-number@7.0.0", "", {}, "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="], + + "is-stream": ["is-stream@1.1.0", "", {}, "sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ=="], + + "isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], + + "jiti": ["jiti@2.7.0", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ=="], + + "json-bigint": ["json-bigint@1.0.0", "", { "dependencies": { "bignumber.js": "^9.0.0" } }, "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ=="], + + "json-schema-to-ts": ["json-schema-to-ts@3.1.1", "", { "dependencies": { "@babel/runtime": "^7.18.3", "ts-algebra": "^2.0.0" } }, "sha512-+DWg8jCJG2TEnpy7kOm/7/AxaYoaRbjVB4LFZLySZlWn8exGs3A4OLJR966cVvU26N7X9TWxl+Jsw7dzAqKT6g=="], + + "jwa": ["jwa@2.0.1", "", { "dependencies": { "buffer-equal-constant-time": "^1.0.1", "ecdsa-sig-formatter": "1.0.11", "safe-buffer": "^5.0.1" } }, "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg=="], + + "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=="], + + "koffi": ["koffi@2.16.3", "", {}, "sha512-E9y1AsgYGlaxMhcZzHr8y96QF2U5XzA12GGVAfbWqIubTwPNMXQarfBzePNXHe0xtIEtNd6ifAv3GAKYGUeBAQ=="], + + "lightningcss": ["lightningcss@1.33.0", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.33.0", "lightningcss-darwin-arm64": "1.33.0", "lightningcss-darwin-x64": "1.33.0", "lightningcss-freebsd-x64": "1.33.0", "lightningcss-linux-arm-gnueabihf": "1.33.0", "lightningcss-linux-arm64-gnu": "1.33.0", "lightningcss-linux-arm64-musl": "1.33.0", "lightningcss-linux-x64-gnu": "1.33.0", "lightningcss-linux-x64-musl": "1.33.0", "lightningcss-win32-arm64-msvc": "1.33.0", "lightningcss-win32-x64-msvc": "1.33.0" } }, "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA=="], + + "lightningcss-android-arm64": ["lightningcss-android-arm64@1.33.0", "", { "os": "android", "cpu": "arm64" }, "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg=="], + + "lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.33.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg=="], + + "lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.33.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ=="], + + "lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.33.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg=="], + + "lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.33.0", "", { "os": "linux", "cpu": "arm" }, "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ=="], + + "lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.33.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg=="], + + "lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.33.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ=="], + + "lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.33.0", "", { "os": "linux", "cpu": "x64" }, "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg=="], + + "lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.33.0", "", { "os": "linux", "cpu": "x64" }, "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw=="], + + "lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.33.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA=="], + + "lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.33.0", "", { "os": "win32", "cpu": "x64" }, "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA=="], + + "long": ["long@5.3.2", "", {}, "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA=="], + + "lru-cache": ["lru-cache@7.18.3", "", {}, "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA=="], + + "magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="], + + "marked": ["marked@18.0.11", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-HnslJfsZkRPBDJRHvVtAaWlZHEpSu7u8LgQuJCELjRKuWR+hpq4A7sLq3p8HaI9ypVoXDXxV34CsQJEe1+J5Aw=="], + + "merge2": ["merge2@1.4.1", "", {}, "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="], + + "micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="], + + "mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="], + + "mime-types": ["mime-types@3.0.2", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A=="], + + "mimic-response": ["mimic-response@3.1.0", "", {}, "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ=="], + + "minimatch": ["minimatch@10.2.6", "", { "dependencies": { "brace-expansion": "^5.0.8" } }, "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A=="], + + "minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="], + + "minipass": ["minipass@7.1.3", "", {}, "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A=="], + + "mkdirp-classic": ["mkdirp-classic@0.5.3", "", {}, "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A=="], + + "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], + + "mz": ["mz@2.7.0", "", { "dependencies": { "any-promise": "^1.0.0", "object-assign": "^4.0.1", "thenify-all": "^1.0.0" } }, "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q=="], + + "nanoid": ["nanoid@3.3.18", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w=="], + + "napi-build-utils": ["napi-build-utils@2.0.0", "", {}, "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA=="], + + "netmask": ["netmask@2.1.1", "", {}, "sha512-eonl3sLUha+S1GzTPxychyhnUzKyeQkZ7jLjKrBagJgPla13F+uQ71HgpFefyHgqrjEbCPkDArxYsjY8/+gLKA=="], + + "nice-try": ["nice-try@1.0.5", "", {}, "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ=="], + + "node-abi": ["node-abi@3.95.0", "", { "dependencies": { "semver": "^7.3.5" } }, "sha512-T9iGctuocf0qIWFFOTxPzjT5q0SILqaBYXt272tlBHvTKC5+3JnkMirLxNJNkXHtFyBjU2Jx+NL4Zipr0B/c6Q=="], + + "node-addon-api": ["node-addon-api@7.1.1", "", {}, "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ=="], + + "node-domexception": ["node-domexception@1.0.0", "", {}, "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ=="], + + "node-fetch": ["node-fetch@3.3.2", "", { "dependencies": { "data-uri-to-buffer": "^4.0.0", "fetch-blob": "^3.1.4", "formdata-polyfill": "^4.0.10" } }, "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA=="], + + "node-forge": ["node-forge@1.4.0", "", {}, "sha512-LarFH0+6VfriEhqMMcLX2F7SwSXeWwnEAJEsYm5QKWchiVYVvJyV9v7UDvUv+w5HO23ZpQTXDv/GxdDdMyOuoQ=="], + + "npm-run-path": ["npm-run-path@2.0.2", "", { "dependencies": { "path-key": "^2.0.0" } }, "sha512-lJxZYlT4DW/bRUtFh1MQIWqmLwQfAxnqWG4HhEdjMlkrJYnJn0Jrr2u3mgxqaWsdiBc76TYkTG/mhrnYTuzfHw=="], + + "object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="], + + "obug": ["obug@2.1.4", "", {}, "sha512-4a+OsYv9UktOJKE+l1A4OufDgdRF9PifWj+tJnHURo/P+WOxpG4GzUFL9qCalmWauao6ogiG+QvnCovwPoyAWA=="], + + "once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="], + + "openai": ["openai@6.47.0", "", { "peerDependencies": { "@aws-sdk/credential-provider-node": ">=3.972.0 <4", "@smithy/hash-node": ">=4.3.0 <5", "@smithy/signature-v4": ">=5.4.0 <6", "ws": "^8.18.0", "zod": "^3.25 || ^4.0" }, "optionalPeers": ["@aws-sdk/credential-provider-node", "@smithy/hash-node", "@smithy/signature-v4", "ws", "zod"] }, "sha512-xYr+R9woSzWxVxeiqkkNbHhv89tZDEI6eBMbrdPnv3poh+mijHvbhS35a+3o6xHa411/ns8j5ENY3So9DCXWYw=="], + + "p-finally": ["p-finally@1.0.0", "", {}, "sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow=="], + + "p-retry": ["p-retry@4.6.2", "", { "dependencies": { "@types/retry": "0.12.0", "retry": "^0.13.1" } }, "sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ=="], + + "pac-proxy-agent": ["pac-proxy-agent@7.2.0", "", { "dependencies": { "@tootallnate/quickjs-emscripten": "^0.23.0", "agent-base": "^7.1.2", "debug": "^4.3.4", "get-uri": "^6.0.1", "http-proxy-agent": "^7.0.0", "https-proxy-agent": "^7.0.6", "pac-resolver": "^7.0.1", "socks-proxy-agent": "^8.0.5" } }, "sha512-TEB8ESquiLMc0lV8vcd5Ql/JAKAoyzHFXaStwjkzpOpC5Yv+pIzLfHvjTSdf3vpa2bMiUQrg9i6276yn8666aA=="], + + "pac-resolver": ["pac-resolver@7.0.1", "", { "dependencies": { "degenerator": "^5.0.0", "netmask": "^2.0.2" } }, "sha512-5NPgf87AT2STgwa2ntRMr45jTKrYBGkVU36yT0ig/n/GMAa3oPqhZfIQ2kMEimReg0+t9kZViDVZ83qfVUlckg=="], + + "parse5": ["parse5@5.1.1", "", {}, "sha512-ugq4DFI0Ptb+WWjAdOK16+u/nHfiIrcE+sh8kZMaM0WllQKLI9rOUq6c2b7cwPkXdzfQESqvoqK6ug7U/Yyzug=="], + + "parse5-htmlparser2-tree-adapter": ["parse5-htmlparser2-tree-adapter@6.0.1", "", { "dependencies": { "parse5": "^6.0.1" } }, "sha512-qPuWvbLgvDGilKc5BoicRovlT4MtYT6JfJyBOMDsKoiT+GiuP5qyrPCnR9HcPECIJJmZh5jRndyNThnhhb/vlA=="], + + "partial-json": ["partial-json@0.1.7", "", {}, "sha512-Njv/59hHaokb/hRUjce3Hdv12wd60MtM9Z5Olmn+nehe0QDAsRtRbJPvJ0Z91TusF0SuZRIvnM+S4l6EIP8leA=="], + + "path-key": ["path-key@2.0.1", "", {}, "sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw=="], + + "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=="], + + "pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], + + "pend": ["pend@1.2.0", "", {}, "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg=="], + + "pi-extension-custom-provider-anthropic": ["pi-extension-custom-provider-anthropic@workspace:packages/coding-agent/examples/extensions/custom-provider-anthropic"], + + "pi-extension-custom-provider-gitlab-duo": ["pi-extension-custom-provider-gitlab-duo@workspace:packages/coding-agent/examples/extensions/custom-provider-gitlab-duo"], + + "pi-extension-sandbox": ["pi-extension-sandbox@workspace:packages/coding-agent/examples/extensions/sandbox"], + + "pi-extension-with-deps": ["pi-extension-with-deps@workspace:packages/coding-agent/examples/extensions/with-deps"], + + "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], + + "picomatch": ["picomatch@4.0.7", "", {}, "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA=="], + + "postcss": ["postcss@8.5.26", "", { "dependencies": { "nanoid": "^3.3.17", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ=="], + + "prebuild-install": ["prebuild-install@7.1.3", "", { "dependencies": { "detect-libc": "^2.0.0", "expand-template": "^2.0.3", "github-from-package": "0.0.0", "minimist": "^1.2.3", "mkdirp-classic": "^0.5.3", "napi-build-utils": "^2.0.0", "node-abi": "^3.3.0", "pump": "^3.0.0", "rc": "^1.2.7", "simple-get": "^4.0.0", "tar-fs": "^2.0.0", "tunnel-agent": "^0.6.0" }, "bin": { "prebuild-install": "bin.js" } }, "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug=="], + + "proper-lockfile": ["proper-lockfile@4.1.2", "", { "dependencies": { "graceful-fs": "^4.2.4", "retry": "^0.12.0", "signal-exit": "^3.0.2" } }, "sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA=="], + + "protobufjs": ["protobufjs@7.6.5", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", "@protobufjs/codegen": "^2.0.5", "@protobufjs/eventemitter": "^1.1.1", "@protobufjs/fetch": "^1.1.1", "@protobufjs/float": "^1.0.2", "@protobufjs/path": "^1.1.2", "@protobufjs/pool": "^1.1.0", "@protobufjs/utf8": "^1.1.1", "@types/node": ">=13.7.0", "long": "^5.3.2" } }, "sha512-/FPD0nUc9jH6rfFjji9IBqOz4pcSE3CsT1m7Ep6Mdb0LxSUMj8hgl6GomOvZzpNpAqqGaXA0P3VSrZLFzIhQrw=="], + + "proxy-agent": ["proxy-agent@6.5.0", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "^4.3.4", "http-proxy-agent": "^7.0.1", "https-proxy-agent": "^7.0.6", "lru-cache": "^7.14.1", "pac-proxy-agent": "^7.1.0", "proxy-from-env": "^1.1.0", "socks-proxy-agent": "^8.0.5" } }, "sha512-TmatMXdr2KlRiA2CyDu8GqR8EjahTG3aY3nXjdzFyoZbmB8hrBsTyMezhULIXKnC0jpfjlmiZ3+EaCzoInSu/A=="], + + "proxy-from-env": ["proxy-from-env@1.1.0", "", {}, "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="], + + "pump": ["pump@3.0.4", "", { "dependencies": { "end-of-stream": "^1.1.0", "once": "^1.3.1" } }, "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA=="], + + "queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="], + + "rc": ["rc@1.2.8", "", { "dependencies": { "deep-extend": "^0.6.0", "ini": "~1.3.0", "minimist": "^1.2.0", "strip-json-comments": "~2.0.1" }, "bin": { "rc": "./cli.js" } }, "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw=="], + + "readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="], + + "rechoir": ["rechoir@0.6.2", "", { "dependencies": { "resolve": "^1.1.6" } }, "sha512-HFM8rkZ+i3zrV+4LQjwQ0W+ez98pApMGM3HUrN04j3CqzPOzl9nmP15Y8YXNm8QHGv/eacOVEjqhmWpkRV0NAw=="], + + "require-directory": ["require-directory@2.1.1", "", {}, "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q=="], + + "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=="], + + "retry": ["retry@0.12.0", "", {}, "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow=="], + + "reusify": ["reusify@1.1.0", "", {}, "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw=="], + + "rolldown": ["rolldown@1.2.5", "", { "dependencies": { "@oxc-project/types": "=0.146.0", "@rolldown/pluginutils": "^1.0.0" }, "optionalDependencies": { "@rolldown/binding-android-arm-eabi": "1.2.5", "@rolldown/binding-android-arm64": "1.2.5", "@rolldown/binding-darwin-arm64": "1.2.5", "@rolldown/binding-darwin-x64": "1.2.5", "@rolldown/binding-freebsd-x64": "1.2.5", "@rolldown/binding-linux-arm-gnueabihf": "1.2.5", "@rolldown/binding-linux-arm64-gnu": "1.2.5", "@rolldown/binding-linux-arm64-musl": "1.2.5", "@rolldown/binding-linux-ppc64-gnu": "1.2.5", "@rolldown/binding-linux-s390x-gnu": "1.2.5", "@rolldown/binding-linux-x64-gnu": "1.2.5", "@rolldown/binding-linux-x64-musl": "1.2.5", "@rolldown/binding-openharmony-arm64": "1.2.5", "@rolldown/binding-win32-arm64-msvc": "1.2.5", "@rolldown/binding-win32-x64-msvc": "1.2.5" }, "bin": { "rolldown": "./bin/cli.mjs" } }, "sha512-VD2IE5PUG4Oj8zz2VGykiYd5wbnjdIiSsNQb8Qu5B+noEp+A78mu2iVvpp27g8es14Tk9rofNs5Tku9iQCS4fA=="], + + "run-parallel": ["run-parallel@1.2.0", "", { "dependencies": { "queue-microtask": "^1.2.2" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="], + + "rxjs": ["rxjs@7.8.2", "", { "dependencies": { "tslib": "^2.1.0" } }, "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA=="], + + "safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="], + + "semver": ["semver@7.8.5", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA=="], + + "shebang-command": ["shebang-command@1.2.0", "", { "dependencies": { "shebang-regex": "^1.0.0" } }, "sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg=="], + + "shebang-regex": ["shebang-regex@1.0.0", "", {}, "sha512-wpoSFAxys6b2a2wHZ1XpDSgD7N9iVjg29Ph9uV/uaP9Ex/KXlkTZTeddxDPSYQpgvzKLGJke2UU0AzoGCjNIvQ=="], + + "shell-quote": ["shell-quote@1.10.0", "", {}, "sha512-w1aiOKwKuRgtwAReIIj89puqg+I7GvX4IbLrvmhXbzQsj1+Zwi4VO3+fa6ZF91TWSjIxoEkKnMeHcLEODK5ZXA=="], + + "shelljs": ["shelljs@0.9.2", "", { "dependencies": { "execa": "^1.0.0", "fast-glob": "^3.3.2", "interpret": "^1.0.0", "rechoir": "^0.6.2" }, "bin": { "shjs": "bin/shjs" } }, "sha512-S3I64fEiKgTZzKCC46zT/Ib9meqofLrQVbpSswtjFfAVDW+AZ54WTnAM/3/yENoxz/V1Cy6u3kiiEbQ4DNphvw=="], + + "shx": ["shx@0.4.0", "", { "dependencies": { "minimist": "^1.2.8", "shelljs": "^0.9.2" }, "bin": { "shx": "lib/cli.js" } }, "sha512-Z0KixSIlGPpijKgcH6oCMCbltPImvaKy0sGH8AkLRXw1KyzpKtaCTizP2xen+hNDqVF4xxgvA0KXSb9o4Q6hnA=="], + + "siginfo": ["siginfo@2.0.0", "", {}, "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g=="], + + "signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="], + + "simple-concat": ["simple-concat@1.0.1", "", {}, "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q=="], + + "simple-get": ["simple-get@4.0.1", "", { "dependencies": { "decompress-response": "^6.0.0", "once": "^1.3.1", "simple-concat": "^1.0.0" } }, "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA=="], + + "smart-buffer": ["smart-buffer@4.2.0", "", {}, "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg=="], + + "socks": ["socks@2.8.9", "", { "dependencies": { "ip-address": "^10.1.1", "smart-buffer": "^4.2.0" } }, "sha512-LJhUYUvItdQ0LkJTmPeaEObWXAqFyfmP85x0tch/ez9cahmhlBBLbIqDFnvBnUJGagb0JbIQrkBs1wJ+yRYpEw=="], + + "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=="], + + "source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], + + "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], + + "stackback": ["stackback@0.0.2", "", {}, "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw=="], + + "std-env": ["std-env@4.2.0", "", {}, "sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw=="], + + "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=="], + + "string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="], + + "strip-ansi": ["strip-ansi@7.2.0", "", { "dependencies": { "ansi-regex": "^6.2.2" } }, "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w=="], + + "strip-eof": ["strip-eof@1.0.0", "", {}, "sha512-7FCwGGmx8mD5xQd3RPUvnSpUXHM3BWuzjtpD4TXsfcZ9EL4azvVVUscFYwD9nx8Kh+uCBC00XBtAykoMHwTh8Q=="], + + "strip-json-comments": ["strip-json-comments@2.0.1", "", {}, "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ=="], + + "strtok3": ["strtok3@10.3.5", "", { "dependencies": { "@tokenizer/token": "^0.3.0" } }, "sha512-ki4hZQfh5rX0QDLLkOCj+h+CVNkqmp/CMf8v8kZpkNVK6jGQooMytqzLZYUVYIZcFZ6yDB70EfD8POcFXiF5oA=="], + + "supports-color": ["supports-color@8.1.1", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q=="], + + "supports-preserve-symlinks-flag": ["supports-preserve-symlinks-flag@1.0.0", "", {}, "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="], + + "tar-fs": ["tar-fs@2.1.5", "", { "dependencies": { "chownr": "^1.1.1", "mkdirp-classic": "^0.5.2", "pump": "^3.0.0", "tar-stream": "^2.1.4" } }, "sha512-OboTd8mmMhZDNPV+UjQcK9yKAatXu2aJ+r1w4im1Otd4M4fl2hwvdoXUxIYHFTHWK/3y3FarBP70v3vwmGlOxw=="], + + "tar-stream": ["tar-stream@2.2.0", "", { "dependencies": { "bl": "^4.0.3", "end-of-stream": "^1.4.1", "fs-constants": "^1.0.0", "inherits": "^2.0.3", "readable-stream": "^3.1.1" } }, "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ=="], + + "thenify": ["thenify@3.3.1", "", { "dependencies": { "any-promise": "^1.0.0" } }, "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw=="], + + "thenify-all": ["thenify-all@1.6.0", "", { "dependencies": { "thenify": ">= 3.1.0 < 4" } }, "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA=="], + + "tinybench": ["tinybench@2.9.0", "", {}, "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg=="], + + "tinyexec": ["tinyexec@1.3.0", "", {}, "sha512-QKAl9m8gWWGHV8jZcPeym6j+XULi6tOf1mT83WYJ4Lk2ytW/uwAWkrP0uFsdoYMdueVJ0qs26wZ+23xeB4ibNQ=="], + + "tinyglobby": ["tinyglobby@0.2.17", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g=="], + + "tinyrainbow": ["tinyrainbow@3.1.1", "", {}, "sha512-yau8yJdTt989Mm0Bd/236QnzEiPf2xLLTqUZRUJOo/3CB078LSwzei343DgtJVmfJKJE3TMINY1u42SQsP6mXw=="], + + "to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="], + + "token-types": ["token-types@6.1.2", "", { "dependencies": { "@borewit/text-codec": "^0.2.1", "@tokenizer/token": "^0.3.0", "ieee754": "^1.2.1" } }, "sha512-dRXchy+C0IgK8WPC6xvCHFRIWYUbqqdEIKPaKo/AcTUNzwLTK6AH7RjdLWsEZcAN/TBdtfUw3PYEgPr5VPr6ww=="], + + "tree-kill": ["tree-kill@1.2.2", "", { "bin": { "tree-kill": "cli.js" } }, "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A=="], + + "ts-algebra": ["ts-algebra@2.0.0", "", {}, "sha512-FPAhNPFMrkwz76P7cdjdmiShwMynZYN6SgOujD1urY4oNm80Ou9oMdmbR45LotcKOXoy7wSmHkRFE6Mxbrhefw=="], + + "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + + "tsx": ["tsx@4.23.12", "", { "dependencies": { "esbuild": "~0.28.0" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "bin": { "tsx": "dist/cli.mjs" } }, "sha512-FDf4L4sYzKtzWYhU/Xm0AQFdTjdIxNo9ElTf2mxXM6k8YMHXzYUe4yODVaXP4V9uMFbVg8c0qyBccK2OOxb45Q=="], + + "tunnel-agent": ["tunnel-agent@0.6.0", "", { "dependencies": { "safe-buffer": "^5.0.1" } }, "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w=="], + + "typebox": ["typebox@1.3.18", "", {}, "sha512-/wYPoDqxWZSxV/XD8Eskzr3YluXC9CaWJOuUYMkj+lLVLkyeEIQKzHvMuS/IRc3OLTIBC32LAtHgXo/WFEOMHQ=="], + + "typescript": ["typescript@7.0.2", "", { "optionalDependencies": { "@typescript/typescript-aix-ppc64": "7.0.2", "@typescript/typescript-darwin-arm64": "7.0.2", "@typescript/typescript-darwin-x64": "7.0.2", "@typescript/typescript-freebsd-arm64": "7.0.2", "@typescript/typescript-freebsd-x64": "7.0.2", "@typescript/typescript-linux-arm": "7.0.2", "@typescript/typescript-linux-arm64": "7.0.2", "@typescript/typescript-linux-loong64": "7.0.2", "@typescript/typescript-linux-mips64el": "7.0.2", "@typescript/typescript-linux-ppc64": "7.0.2", "@typescript/typescript-linux-riscv64": "7.0.2", "@typescript/typescript-linux-s390x": "7.0.2", "@typescript/typescript-linux-x64": "7.0.2", "@typescript/typescript-netbsd-arm64": "7.0.2", "@typescript/typescript-netbsd-x64": "7.0.2", "@typescript/typescript-openbsd-arm64": "7.0.2", "@typescript/typescript-openbsd-x64": "7.0.2", "@typescript/typescript-sunos-x64": "7.0.2", "@typescript/typescript-win32-arm64": "7.0.2", "@typescript/typescript-win32-x64": "7.0.2" }, "bin": { "tsc": "bin/tsc" } }, "sha512-8FYau96o3NKOhbjKi/qNvG/W5jhzxkbdm5sj9AbZ/5T5sWqn3hJgLfGx27sRKZWTvyzCP8dLRBTf5tBTSRVUNA=="], + + "uint8array-extras": ["uint8array-extras@1.5.0", "", {}, "sha512-rvKSBiC5zqCCiDZ9kAOszZcDvdAHwwIKJG33Ykj43OKcWsnmcBRL09YTU4nOeHZ8Y2a7l1MgTd08SBe9A8Qj6A=="], + + "undici": ["undici@7.29.0", "", {}, "sha512-IDxfleLmmbSskfWSUATiN1nfn2rDuvnMOqb5CWR92iIfojA0Ud+ulOAAEQ57LPr9rWmsreUyf5lwyao+7GNNVw=="], + + "undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], + + "util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="], + + "uuid": ["uuid@14.0.2", "", { "bin": { "uuid": "dist-node/bin/uuid" } }, "sha512-xZe/16rV4aa+HGSOCiY2YeLT1OybRLrrkL/Rqaq7p7GMVXjFh+6wN4oMYgjFmnSnhY8t6Xpdl2l9qmnHYuMHwQ=="], + + "vite": ["vite@8.2.2", "", { "dependencies": { "lightningcss": "^1.33.0", "picomatch": "^4.0.5", "postcss": "^8.5.26", "rolldown": "~1.2.4", "tinyglobby": "^0.2.17" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "@vitejs/devtools": "^0.4.0 || ^0.5.0", "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "@vitejs/devtools", "esbuild", "jiti", "less", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-cFKLV/PRgAUlIRm5WjMjJ86jrftzpqcgH+Us+DS8mI3CDNiH30Whrz8uHL3+MOLPAgqbMBAqWdAHAphOAM+z/Q=="], + + "vitest": ["vitest@4.1.11", "", { "dependencies": { "@vitest/expect": "4.1.11", "@vitest/mocker": "4.1.11", "@vitest/pretty-format": "4.1.11", "@vitest/runner": "4.1.11", "@vitest/snapshot": "4.1.11", "@vitest/spy": "4.1.11", "@vitest/utils": "4.1.11", "es-module-lexer": "^2.0.0", "expect-type": "^1.3.0", "magic-string": "^0.30.21", "obug": "^2.1.1", "pathe": "^2.0.3", "picomatch": "^4.0.3", "std-env": "^4.0.0-rc.1", "tinybench": "^2.9.0", "tinyexec": "^1.0.2", "tinyglobby": "^0.2.15", "tinyrainbow": "^3.1.0", "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", "why-is-node-running": "^2.3.0" }, "peerDependencies": { "@edge-runtime/vm": "*", "@opentelemetry/api": "^1.9.0", "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", "@vitest/browser-playwright": "4.1.11", "@vitest/browser-preview": "4.1.11", "@vitest/browser-webdriverio": "4.1.11", "@vitest/coverage-istanbul": "4.1.11", "@vitest/coverage-v8": "4.1.11", "@vitest/ui": "4.1.11", "happy-dom": "*", "jsdom": "*" }, "optionalPeers": ["@edge-runtime/vm", "@opentelemetry/api", "@types/node", "@vitest/browser-playwright", "@vitest/browser-preview", "@vitest/browser-webdriverio", "@vitest/coverage-istanbul", "@vitest/coverage-v8", "@vitest/ui", "happy-dom", "jsdom"], "bin": { "vitest": "./vitest.mjs" } }, "sha512-fhACrNXUidIbGSBr5FlbuBkO7VWC1ZyLl0DO4CU2DrQoAPxX84Ysxs+HeGQpii5lZWV1Q4gBZTTu49mF+A6Edw=="], + + "web-streams-polyfill": ["web-streams-polyfill@3.3.3", "", {}, "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw=="], + + "which": ["which@1.3.1", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "which": "./bin/which" } }, "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ=="], + + "why-is-node-running": ["why-is-node-running@2.3.0", "", { "dependencies": { "siginfo": "^2.0.0", "stackback": "0.0.2" }, "bin": { "why-is-node-running": "cli.js" } }, "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w=="], + + "wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="], + + "wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="], + + "ws": ["ws@8.21.3", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw=="], + + "y18n": ["y18n@5.0.8", "", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="], + + "yaml": ["yaml@2.9.0", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA=="], + + "yargs": ["yargs@17.7.2", "", { "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.3", "y18n": "^5.0.5", "yargs-parser": "^21.1.1" } }, "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w=="], + + "yargs-parser": ["yargs-parser@21.1.1", "", {}, "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="], + + "yauzl": ["yauzl@2.10.0", "", { "dependencies": { "buffer-crc32": "~0.2.3", "fd-slicer": "~1.1.0" } }, "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g=="], + + "zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], + + "zod-to-json-schema": ["zod-to-json-schema@3.25.2", "", { "peerDependencies": { "zod": "^3.25.28 || ^4" } }, "sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA=="], + + "@aws-sdk/credential-provider-sso/@aws-sdk/token-providers": ["@aws-sdk/token-providers@3.1116.0", "", { "dependencies": { "@aws-sdk/core": "^3.977.9", "@aws-sdk/nested-clients": "^3.997.44", "@aws-sdk/types": "^3.974.5", "@smithy/core": "^3.33.3", "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-ygIivKqh8aHzNkucOCXHyIBgBpLPfrSI0mCqXF+vLBsPTUKqj0VSqAY0GFPe7lQl4HntjOcQ+KSyS7oUV2C54Q=="], + + "@earendil-works/pi-agent-core/@types/node": ["@types/node@24.13.3", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-Dh8vAsV36ig5wa9OX4pXvMc9D3Veibfw2wix0CUwYODLD8nkj9UsLjASr49nPg+2eKzxhBV+v7L8pXvT4e639Q=="], + + "@earendil-works/pi-ai/@types/node": ["@types/node@24.13.3", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-Dh8vAsV36ig5wa9OX4pXvMc9D3Veibfw2wix0CUwYODLD8nkj9UsLjASr49nPg+2eKzxhBV+v7L8pXvT4e639Q=="], + + "@earendil-works/pi-coding-agent/@types/node": ["@types/node@24.13.3", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-Dh8vAsV36ig5wa9OX4pXvMc9D3Veibfw2wix0CUwYODLD8nkj9UsLjASr49nPg+2eKzxhBV+v7L8pXvT4e639Q=="], + + "@mistralai/mistralai/zod": ["zod@4.4.3", "", {}, "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ=="], + + "@types/yauzl/@types/node": ["@types/node@24.13.3", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-Dh8vAsV36ig5wa9OX4pXvMc9D3Veibfw2wix0CUwYODLD8nkj9UsLjASr49nPg+2eKzxhBV+v7L8pXvT4e639Q=="], + + "cli-highlight/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], + + "cli-highlight/yargs": ["yargs@16.2.2", "", { "dependencies": { "cliui": "^7.0.2", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.0", "y18n": "^5.0.5", "yargs-parser": "^20.2.2" } }, "sha512-Nt9ZJjXTv5R8MHbqby/wXQ6Gi0Bb3TcYZkR1bzuL4yB2OxWPkXknz513gEF0GoA6tn00UpbPvERW8rzCuWCA6w=="], + + "cliui/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + + "concurrently/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], + + "cross-spawn/semver": ["semver@5.7.2", "", { "bin": { "semver": "bin/semver" } }, "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g=="], + + "execa/get-stream": ["get-stream@4.1.0", "", { "dependencies": { "pump": "^3.0.0" } }, "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w=="], + + "hosted-git-info/lru-cache": ["lru-cache@11.5.2", "", {}, "sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g=="], + + "micromatch/picomatch": ["picomatch@2.3.2", "", {}, "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA=="], + + "node-fetch/data-uri-to-buffer": ["data-uri-to-buffer@4.0.1", "", {}, "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A=="], + + "p-retry/@types/retry": ["@types/retry@0.12.0", "", {}, "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA=="], + + "p-retry/retry": ["retry@0.13.1", "", {}, "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg=="], + + "parse5-htmlparser2-tree-adapter/parse5": ["parse5@6.0.1", "", {}, "sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw=="], + + "path-scurry/lru-cache": ["lru-cache@11.5.2", "", {}, "sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g=="], + + "pi-extension-custom-provider-anthropic/@anthropic-ai/sdk": ["@anthropic-ai/sdk@0.52.0", "", { "bin": { "anthropic-ai-sdk": "bin/cli" } }, "sha512-d4c+fg+xy9e46c8+YnrrgIQR45CZlAi7PwdzIfDXDM6ACxEZli1/fxhURsq30ZpMZy6LvSkr41jGq5aF5TD7rQ=="], + + "protobufjs/@types/node": ["@types/node@24.13.3", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-Dh8vAsV36ig5wa9OX4pXvMc9D3Veibfw2wix0CUwYODLD8nkj9UsLjASr49nPg+2eKzxhBV+v7L8pXvT4e639Q=="], + + "string-width/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + + "wrap-ansi/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + + "@earendil-works/pi-agent-core/@types/node/undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="], + + "@earendil-works/pi-ai/@types/node/undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="], + + "@earendil-works/pi-coding-agent/@types/node/undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="], + + "@types/yauzl/@types/node/undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="], + + "cli-highlight/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], + + "cli-highlight/yargs/cliui": ["cliui@7.0.4", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.0", "wrap-ansi": "^7.0.0" } }, "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ=="], + + "cli-highlight/yargs/yargs-parser": ["yargs-parser@20.2.9", "", {}, "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w=="], + + "cliui/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + + "concurrently/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], + + "protobufjs/@types/node/undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="], + + "string-width/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + + "wrap-ansi/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + + "cli-highlight/yargs/cliui/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + + "cli-highlight/yargs/cliui/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + } +} diff --git a/bunfig.toml b/bunfig.toml new file mode 100644 index 00000000000..35daac48646 --- /dev/null +++ b/bunfig.toml @@ -0,0 +1,3 @@ +[install] +linker = "hoisted" +minimumReleaseAge = 604800 diff --git a/package-lock.json b/package-lock.json deleted file mode 100644 index b5fb6acd3ea..00000000000 --- a/package-lock.json +++ /dev/null @@ -1,5837 +0,0 @@ -{ - "name": "prime-agent", - "version": "0.9.1", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "prime-agent", - "version": "0.9.1", - "workspaces": [ - "packages/*", - "packages/coding-agent/examples/extensions/with-deps", - "packages/coding-agent/examples/extensions/custom-provider-anthropic", - "packages/coding-agent/examples/extensions/custom-provider-gitlab-duo", - "packages/coding-agent/examples/extensions/sandbox" - ], - "dependencies": { - "@earendil-works/pi-coding-agent": "^0.9.1", - "get-east-asian-width": "^1.6.0" - }, - "devDependencies": { - "@anthropic-ai/sandbox-runtime": "^0.0.55", - "@biomejs/biome": "2.5.5", - "@types/node": "^22.10.5", - "@typescript/native-preview": "7.0.0-dev.20260120.1", - "concurrently": "^9.2.4", - "husky": "^9.1.7", - "jiti": "^2.7.0", - "shx": "^0.4.0", - "tsx": "^4.23.1", - "typescript": "^7.0.2" - }, - "engines": { - "node": ">=22.8.0" - } - }, - "node_modules/@agentclientprotocol/sdk": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/@agentclientprotocol/sdk/-/sdk-1.3.0.tgz", - "integrity": "sha512-i3h/efaeuMUFAO1HSfo97QZQnnvMd7wWBYtBsdL6UMZg3a78sk3Ffya5Xu7C7tYsXomXoDXJBAzQF2PcFKAhIQ==", - "license": "Apache-2.0", - "peerDependencies": { - "zod": "^3.25.0 || ^4.0.0" - } - }, - "node_modules/@anthropic-ai/sandbox-runtime": { - "version": "0.0.55", - "resolved": "https://registry.npmjs.org/@anthropic-ai/sandbox-runtime/-/sandbox-runtime-0.0.55.tgz", - "integrity": "sha512-XGTLbzitn0y6OCdmqP5aYrBR0HlNi87ue++m9vMPxcNwaQeOj9i3f9CSWmXua8mv+Q8E55k4ZGlzJmpDHzQFBw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@pondwader/socks5-server": "^1.0.10", - "commander": "^12.1.0", - "node-forge": "^1.4.0", - "shell-quote": "^1.8.4", - "zod": "^3.24.1" - }, - "bin": { - "srt": "dist/cli.js" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@anthropic-ai/sdk": { - "version": "0.91.1", - "license": "MIT", - "dependencies": { - "json-schema-to-ts": "^3.1.1" - }, - "bin": { - "anthropic-ai-sdk": "bin/cli" - }, - "peerDependencies": { - "zod": "^3.25.0 || ^4.0.0" - }, - "peerDependenciesMeta": { - "zod": { - "optional": true - } - } - }, - "node_modules/@aws-sdk/client-bedrock-runtime": { - "version": "3.1095.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-bedrock-runtime/-/client-bedrock-runtime-3.1095.0.tgz", - "integrity": "sha512-DWcwoQdQPrQJxnG3hz1sG88EjfzGv3SReRy4mtAO+pZXtLX+trriTa20o+qcTmVzwqS43JXtX26ythOzErev9A==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/core": "^3.977.0", - "@aws-sdk/credential-provider-node": "^3.972.72", - "@aws-sdk/eventstream-handler-node": "^3.972.30", - "@aws-sdk/middleware-eventstream": "^3.972.25", - "@aws-sdk/middleware-websocket": "^3.972.43", - "@aws-sdk/token-providers": "3.1095.0", - "@aws-sdk/types": "^3.974.2", - "@smithy/core": "^3.29.8", - "@smithy/fetch-http-handler": "^5.6.10", - "@smithy/node-http-handler": "^4.9.10", - "@smithy/types": "^4.16.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@aws-sdk/core": { - "version": "3.977.1", - "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.977.1.tgz", - "integrity": "sha512-KVtQRtc00ES/y+Sc3vYXeP6pCIcNlBJCZOwvqSy8ZpVGmbM5+IG+AfhuTKQ2oXmIVqZJewaGMMpzPkywC6xg0w==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/types": "^3.974.2", - "@aws-sdk/xml-builder": "^3.972.37", - "@aws/lambda-invoke-store": "^0.3.0", - "@smithy/core": "^3.29.8", - "@smithy/signature-v4": "^5.6.9", - "@smithy/types": "^4.16.1", - "bowser": "^2.11.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@aws-sdk/credential-provider-env": { - "version": "3.972.61", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.972.61.tgz", - "integrity": "sha512-qihs2ekMb89Nxd2JenCgVFhjbkb3EIo7HEBCBzyZACKVJdrLUZBLOmAE3xr0Sayml8n/jZSzwO/IufIiIzO7PQ==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/core": "^3.977.0", - "@aws-sdk/types": "^3.974.2", - "@smithy/core": "^3.29.8", - "@smithy/types": "^4.16.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@aws-sdk/credential-provider-http": { - "version": "3.972.63", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.972.63.tgz", - "integrity": "sha512-yfozsS8wkWZEi/n6IsrodcFKBWZ0iNAezhJbTReMNc0z1Px17qdeAeuL1/wziCAmCZyXiW7QzP75ggJkBQv8jQ==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/core": "^3.977.0", - "@aws-sdk/types": "^3.974.2", - "@smithy/core": "^3.29.8", - "@smithy/fetch-http-handler": "^5.6.10", - "@smithy/node-http-handler": "^4.9.10", - "@smithy/types": "^4.16.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@aws-sdk/credential-provider-ini": { - "version": "3.973.6", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.973.6.tgz", - "integrity": "sha512-jGLTW1bj148GL/6/IMlfY2fMYS9FtHOG+NahkFD4y0qkzYudNUahelxryY68/HGMslYuHClk1XaS/3b3eJzEkg==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/core": "^3.977.0", - "@aws-sdk/credential-provider-env": "^3.972.61", - "@aws-sdk/credential-provider-http": "^3.972.63", - "@aws-sdk/credential-provider-login": "^3.972.68", - "@aws-sdk/credential-provider-process": "^3.972.61", - "@aws-sdk/credential-provider-sso": "^3.973.5", - "@aws-sdk/credential-provider-web-identity": "^3.972.67", - "@aws-sdk/nested-clients": "^3.997.35", - "@aws-sdk/types": "^3.974.2", - "@smithy/core": "^3.29.8", - "@smithy/credential-provider-imds": "^4.4.13", - "@smithy/types": "^4.16.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@aws-sdk/credential-provider-login": { - "version": "3.972.68", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-login/-/credential-provider-login-3.972.68.tgz", - "integrity": "sha512-w6tNci6g7RqFpLhj1f5xseBvaNojb4Pkgp5Jp5apl9hrJtaf2AA+rX9+qlhlWUK6kcyAFYPA7emO+55zj+S98Q==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/core": "^3.977.0", - "@aws-sdk/nested-clients": "^3.997.35", - "@aws-sdk/types": "^3.974.2", - "@smithy/core": "^3.29.8", - "@smithy/types": "^4.16.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@aws-sdk/credential-provider-node": { - "version": "3.972.72", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.972.72.tgz", - "integrity": "sha512-blQ7F5QGzylnzeh5549zQLoCAiMHkXFLjFovEMaVy4b2X8JhUu+u9NXro1hyK95YHdVFNmBHKs2hIHtZchxKlQ==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/credential-provider-env": "^3.972.61", - "@aws-sdk/credential-provider-http": "^3.972.63", - "@aws-sdk/credential-provider-ini": "^3.973.6", - "@aws-sdk/credential-provider-process": "^3.972.61", - "@aws-sdk/credential-provider-sso": "^3.973.5", - "@aws-sdk/credential-provider-web-identity": "^3.972.67", - "@aws-sdk/types": "^3.974.2", - "@smithy/core": "^3.29.8", - "@smithy/credential-provider-imds": "^4.4.13", - "@smithy/types": "^4.16.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@aws-sdk/credential-provider-process": { - "version": "3.972.61", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.972.61.tgz", - "integrity": "sha512-xzRuj+fUVO4nkafKQJVKAF97kGpeQbfjuwmRrtGZNf42/1dkmcz6o7dswBy7alY0htQn5sCL1GWQYEykviWZkA==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/core": "^3.977.0", - "@aws-sdk/types": "^3.974.2", - "@smithy/core": "^3.29.8", - "@smithy/types": "^4.16.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@aws-sdk/credential-provider-sso": { - "version": "3.973.5", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.973.5.tgz", - "integrity": "sha512-fZRjjWhLFelsDoOYjqShQTrIGYC3Pf9Mx9Czf+1ikfQDgktxjze33dVo1q1/ZQ+T0qbtejVoHNHrfD5aJVpv/w==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/core": "^3.977.0", - "@aws-sdk/nested-clients": "^3.997.35", - "@aws-sdk/token-providers": "3.1095.0", - "@aws-sdk/types": "^3.974.2", - "@smithy/core": "^3.29.8", - "@smithy/types": "^4.16.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@aws-sdk/credential-provider-web-identity": { - "version": "3.972.67", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.972.67.tgz", - "integrity": "sha512-FTNZ05gkPBA6CKbU3N4zPgybV+stdazwMOya75CmGdcJL7p8Fw/BdHP8WVxJd0mvzyPK2cg/C3gli58Ir4HgCw==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/core": "^3.977.0", - "@aws-sdk/nested-clients": "^3.997.35", - "@aws-sdk/types": "^3.974.2", - "@smithy/core": "^3.29.8", - "@smithy/types": "^4.16.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@aws-sdk/eventstream-handler-node": { - "version": "3.972.30", - "resolved": "https://registry.npmjs.org/@aws-sdk/eventstream-handler-node/-/eventstream-handler-node-3.972.30.tgz", - "integrity": "sha512-hJboPgIpq5+ADc++/B9TBqn65CXV21cZLGB8V5RBQbxkZ/rQ6qMfcxTnW/SvQlasX4jhaSG8B1wsVjhQyDrsnQ==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/types": "^3.974.2", - "@smithy/core": "^3.29.8", - "@smithy/types": "^4.16.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@aws-sdk/middleware-eventstream": { - "version": "3.972.25", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-eventstream/-/middleware-eventstream-3.972.25.tgz", - "integrity": "sha512-9SFbPzJDHHR5k6Q6KvXVas/veUm/TzNcNTFM2UhdXHZHpyIvI2lS+s4cxljw1BihGpVhsAkQDo/2nW7dHxpf4Q==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/types": "^3.974.2", - "@smithy/core": "^3.29.8", - "@smithy/types": "^4.16.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@aws-sdk/middleware-websocket": { - "version": "3.972.43", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-websocket/-/middleware-websocket-3.972.43.tgz", - "integrity": "sha512-n29++15Vma64Kd0enp9Bo8a6LTm8TvUoMbJEwqXtIksv0oEs+SUCRMm3gozDfPD1Ly0k/sSBughxarlLlRF6Xw==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/core": "^3.977.0", - "@aws-sdk/types": "^3.974.2", - "@smithy/core": "^3.29.8", - "@smithy/fetch-http-handler": "^5.6.10", - "@smithy/signature-v4": "^5.6.9", - "@smithy/types": "^4.16.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">= 14.0.0" - } - }, - "node_modules/@aws-sdk/nested-clients": { - "version": "3.997.35", - "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.997.35.tgz", - "integrity": "sha512-2MJfseVG/aXvIyOIBlYA/Oaf6qFDdsu4D8RKsEUdOQpVuLaor0BdxIBBtJLBNQQEe6Ku3YMvLljwb1MwVUpzRw==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/core": "^3.977.0", - "@aws-sdk/signature-v4-multi-region": "^3.996.42", - "@aws-sdk/types": "^3.974.2", - "@smithy/core": "^3.29.8", - "@smithy/fetch-http-handler": "^5.6.10", - "@smithy/node-http-handler": "^4.9.10", - "@smithy/types": "^4.16.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@aws-sdk/signature-v4-multi-region": { - "version": "3.996.42", - "resolved": "https://registry.npmjs.org/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.996.42.tgz", - "integrity": "sha512-DBV4naZP6HYBlAvPpoQzOP12Wvfou/5rN8yJPXjBTBylU5qwCbh/tXr2MddHoIjgoRkEl/eS+IljiUqvmwey1Q==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/types": "^3.974.2", - "@smithy/signature-v4": "^5.6.9", - "@smithy/types": "^4.16.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@aws-sdk/token-providers": { - "version": "3.1095.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.1095.0.tgz", - "integrity": "sha512-65SudS6y4nzaYHybtqcpm3sHe5jLhdMn68HRKS1nUx690BtQeaAQOoujQ+dpOjBATIVGVgKKjEP8tR+U06QJQA==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/core": "^3.977.0", - "@aws-sdk/nested-clients": "^3.997.35", - "@aws-sdk/types": "^3.974.2", - "@smithy/core": "^3.29.8", - "@smithy/types": "^4.16.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@aws-sdk/types": { - "version": "3.974.2", - "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.974.2.tgz", - "integrity": "sha512-3W6IUtSxFbH6X7Wb7DzGCV5QiFQsd0g8bOfntpmDxQlzBoKWUMBu/JPQR0DwkE+Hpnxd6db1tXbOwdeHddG6cA==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/types": "^4.16.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@aws-sdk/xml-builder": { - "version": "3.972.37", - "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.972.37.tgz", - "integrity": "sha512-zKq4HQum8JwDyEuyfuI4bbiAcU0KxP6qy+9PR/IsR92IyE/DaBAikzAS50tjxip4bqIIANpCcG+Yyj6CVhXupg==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/types": "^4.16.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@aws/lambda-invoke-store": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/@aws/lambda-invoke-store/-/lambda-invoke-store-0.3.0.tgz", - "integrity": "sha512-sl4Bm6yiMNYrZKkqqDFWN0UfnWhlS8ivKxrYl+6t0gCLrqr8y3B2IqZZbFRkfaVVp7C/baApyh71P+LeE1A2sQ==", - "license": "Apache-2.0", - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@babel/runtime": { - "version": "7.29.2", - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@biomejs/biome": { - "version": "2.5.5", - "resolved": "https://registry.npmjs.org/@biomejs/biome/-/biome-2.5.5.tgz", - "integrity": "sha512-r1S8nFsAG1MY+vJFZALzIvwXAJv6ejDQ0mxP21Tgr9YK3ZFtjrvbBwDdNhx1rUqvccEIeNg20cYCNzl6Cr69pQ==", - "dev": true, - "license": "MIT OR Apache-2.0", - "bin": { - "biome": "bin/biome" - }, - "engines": { - "node": ">=14.21.3" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/biome" - }, - "optionalDependencies": { - "@biomejs/cli-darwin-arm64": "2.5.5", - "@biomejs/cli-darwin-x64": "2.5.5", - "@biomejs/cli-linux-arm64": "2.5.5", - "@biomejs/cli-linux-arm64-musl": "2.5.5", - "@biomejs/cli-linux-x64": "2.5.5", - "@biomejs/cli-linux-x64-musl": "2.5.5", - "@biomejs/cli-win32-arm64": "2.5.5", - "@biomejs/cli-win32-x64": "2.5.5" - } - }, - "node_modules/@biomejs/cli-darwin-arm64": { - "version": "2.5.5", - "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-arm64/-/cli-darwin-arm64-2.5.5.tgz", - "integrity": "sha512-kUrAhXVWUrwmAUnV2iXSK7umxKFysTwvqK+Ty6ptUcLY/7T3SnCAjUowE4uvwaEej6nXZ7hu/dTtbokKdsPeag==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT OR Apache-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=14.21.3" - } - }, - "node_modules/@biomejs/cli-darwin-x64": { - "version": "2.5.5", - "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-x64/-/cli-darwin-x64-2.5.5.tgz", - "integrity": "sha512-DamiYc5bUYZ2uxlfc+RLEPtz1Abb6PO5eTbOkufLpSGwd/7AMQAdxhFYiXmwwkJL8IsT8S7GvdgwDHqaMFAvKw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT OR Apache-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=14.21.3" - } - }, - "node_modules/@biomejs/cli-linux-arm64": { - "version": "2.5.5", - "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64/-/cli-linux-arm64-2.5.5.tgz", - "integrity": "sha512-lRKF/pH/1RiYiBKExi3TCZVAtvzEm77aifrvcNiDFrR9WxeAnDUjDnseb6y2XV85mjitLs6SILGm2XG77cHtSQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT OR Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=14.21.3" - } - }, - "node_modules/@biomejs/cli-linux-arm64-musl": { - "version": "2.5.5", - "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.5.5.tgz", - "integrity": "sha512-U4WMl/sy/E/Q73vf15VspakLRRs2LDFcCeBxJnQfXzssb88zpV6PJPaQ3ezhQ7H6Ht2/8bvuZeHgJWzmoxllZg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT OR Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=14.21.3" - } - }, - "node_modules/@biomejs/cli-linux-x64": { - "version": "2.5.5", - "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64/-/cli-linux-x64-2.5.5.tgz", - "integrity": "sha512-H/O39nJEw/2Zm/fm7hrmxxoF8kK/aU1uCoPp70ruXVbomaAdLpJJnCmL11Q2JotT8QVHH06So04Oq53lCSwSwQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT OR Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=14.21.3" - } - }, - "node_modules/@biomejs/cli-linux-x64-musl": { - "version": "2.5.5", - "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64-musl/-/cli-linux-x64-musl-2.5.5.tgz", - "integrity": "sha512-m7wC7tjX5Lrmo69dc4md8FeKpPU1NTCY1v7xUoQQ2vadWwNnBS0KZOG8471otFPHrTHihQJAjQPgMObpLvDe6A==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT OR Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=14.21.3" - } - }, - "node_modules/@biomejs/cli-win32-arm64": { - "version": "2.5.5", - "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-arm64/-/cli-win32-arm64-2.5.5.tgz", - "integrity": "sha512-7BryINPuYypLUAH3o/o5ZdgomJ4zn3EDR0ChZJst7n32S6ZhKbgHXuYydLu+YAnx59ehGFR0z/MG6qnzQi3Yyw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT OR Apache-2.0", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=14.21.3" - } - }, - "node_modules/@biomejs/cli-win32-x64": { - "version": "2.5.5", - "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-x64/-/cli-win32-x64-2.5.5.tgz", - "integrity": "sha512-bIBFo+n6MIxdNcVFy5CrurbKiZQiUciK3bt8+O9I4wjFZNTfXLpi+giq47522eXqW5NBc9ulx7dR1SlZKi2J5g==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT OR Apache-2.0", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=14.21.3" - } - }, - "node_modules/@borewit/text-codec": { - "version": "0.2.2", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/Borewit" - } - }, - "node_modules/@earendil-works/pi-agent-core": { - "resolved": "packages/agent", - "link": true - }, - "node_modules/@earendil-works/pi-ai": { - "resolved": "packages/ai", - "link": true - }, - "node_modules/@earendil-works/pi-coding-agent": { - "resolved": "packages/coding-agent", - "link": true - }, - "node_modules/@earendil-works/pi-tui": { - "resolved": "packages/tui", - "link": true - }, - "node_modules/@esbuild/aix-ppc64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.1.tgz", - "integrity": "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.1.tgz", - "integrity": "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.1.tgz", - "integrity": "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-x64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.1.tgz", - "integrity": "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/darwin-arm64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.1.tgz", - "integrity": "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.1.tgz", - "integrity": "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.1.tgz", - "integrity": "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.1.tgz", - "integrity": "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.1.tgz", - "integrity": "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.1.tgz", - "integrity": "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.1.tgz", - "integrity": "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.1.tgz", - "integrity": "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.1.tgz", - "integrity": "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==", - "cpu": [ - "mips64el" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ppc64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.1.tgz", - "integrity": "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-riscv64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.1.tgz", - "integrity": "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-s390x": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.1.tgz", - "integrity": "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-x64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.1.tgz", - "integrity": "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-arm64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.1.tgz", - "integrity": "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.1.tgz", - "integrity": "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-arm64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.1.tgz", - "integrity": "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.1.tgz", - "integrity": "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openharmony-arm64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.1.tgz", - "integrity": "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.1.tgz", - "integrity": "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-arm64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.1.tgz", - "integrity": "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-ia32": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.1.tgz", - "integrity": "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-x64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.1.tgz", - "integrity": "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@google/genai": { - "version": "1.52.0", - "hasInstallScript": true, - "license": "Apache-2.0", - "dependencies": { - "google-auth-library": "^10.3.0", - "p-retry": "^4.6.2", - "protobufjs": "^7.5.4", - "ws": "^8.18.0" - }, - "engines": { - "node": ">=20.0.0" - }, - "peerDependencies": { - "@modelcontextprotocol/sdk": "^1.25.2" - }, - "peerDependenciesMeta": { - "@modelcontextprotocol/sdk": { - "optional": true - } - } - }, - "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.5", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", - "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", - "dev": true, - "license": "MIT" - }, - "node_modules/@mariozechner/clipboard": { - "version": "0.3.9", - "resolved": "https://registry.npmjs.org/@mariozechner/clipboard/-/clipboard-0.3.9.tgz", - "integrity": "sha512-ABnA53mdfkGZwOFUdZNv2S0CWGO/EIuPj8Vv9xmBFmSYg/qFc7ihO6q5FcQjvoE67kZpWkEc4AhD6B/os04yuA==", - "license": "MIT", - "optional": true, - "engines": { - "node": ">= 10" - }, - "optionalDependencies": { - "@mariozechner/clipboard-darwin-arm64": "0.3.9", - "@mariozechner/clipboard-darwin-universal": "0.3.9", - "@mariozechner/clipboard-darwin-x64": "0.3.9", - "@mariozechner/clipboard-linux-arm64-gnu": "0.3.9", - "@mariozechner/clipboard-linux-arm64-musl": "0.3.9", - "@mariozechner/clipboard-linux-riscv64-gnu": "0.3.9", - "@mariozechner/clipboard-linux-x64-gnu": "0.3.9", - "@mariozechner/clipboard-linux-x64-musl": "0.3.9", - "@mariozechner/clipboard-win32-arm64-msvc": "0.3.9", - "@mariozechner/clipboard-win32-x64-msvc": "0.3.9" - } - }, - "node_modules/@mariozechner/clipboard-darwin-arm64": { - "version": "0.3.9", - "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-darwin-arm64/-/clipboard-darwin-arm64-0.3.9.tgz", - "integrity": "sha512-BfgV7vCEWZwJwZJw03r6bP5+tf0iI/ANuQYCxi9RNn7FrWB3yzGuMKCrNLRl6V761vXRdL8+OqZ0wd4TqlsNOQ==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@mariozechner/clipboard-darwin-universal": { - "version": "0.3.9", - "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-darwin-universal/-/clipboard-darwin-universal-0.3.9.tgz", - "integrity": "sha512-BGGR4iA9Z2shAjI65eI5xtyb3LYNlDW9X3gxKxDbqtbnREohsrqznov6zpKoIrsRWpzlYVEdKphS7ksJ0/ndSQ==", - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@mariozechner/clipboard-darwin-x64": { - "version": "0.3.9", - "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-darwin-x64/-/clipboard-darwin-x64-0.3.9.tgz", - "integrity": "sha512-4kURmCbS6nt8uYhtmWpUcJWyPHfmAr5dTpXD1nO3pIfa+TSQ9DbrGOYCKH+aEFW47XhQ4Vp8ZTszie+wfFvDKg==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@mariozechner/clipboard-linux-arm64-gnu": { - "version": "0.3.9", - "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-linux-arm64-gnu/-/clipboard-linux-arm64-gnu-0.3.9.tgz", - "integrity": "sha512-g59OkUGP2DDfCOIKypHeYgv2M55u/cKvXa5dSxFbEJ34XvIQMdcVmpKCkGUro3ZgefXiGVdwguvTMQGpHWzIXw==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@mariozechner/clipboard-linux-arm64-musl": { - "version": "0.3.9", - "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-linux-arm64-musl/-/clipboard-linux-arm64-musl-0.3.9.tgz", - "integrity": "sha512-AGuJdgKsmJdm4Pych7kv3sqe591ERRaAHW3xjLooiFzn8J+PxUyof++7YZrB5Y5tpnTO+K18Og3taj2NpluCRQ==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@mariozechner/clipboard-linux-riscv64-gnu": { - "version": "0.3.9", - "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-linux-riscv64-gnu/-/clipboard-linux-riscv64-gnu-0.3.9.tgz", - "integrity": "sha512-DXBEAiuMpk7dhS1a9NzNxVAFi1vaKoPu7rQNgY8LIDLGrK3lnIp3nT10DUum+PKVJoJppIP+NAA8IZe4DMNDPw==", - "cpu": [ - "riscv64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@mariozechner/clipboard-linux-x64-gnu": { - "version": "0.3.9", - "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-linux-x64-gnu/-/clipboard-linux-x64-gnu-0.3.9.tgz", - "integrity": "sha512-WORrMLd6EpElEME7JRKfSaY34nW1P5LbdgK5YNCS1ncG2LqmITsSMEJ8nh2mpvxb3TxqbOOKgY7k9eMJYlW9Mw==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@mariozechner/clipboard-linux-x64-musl": { - "version": "0.3.9", - "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-linux-x64-musl/-/clipboard-linux-x64-musl-0.3.9.tgz", - "integrity": "sha512-/DHn+1DrfL6oRaPPWXaOKvonFFrni666fxd+zFqiQEfvBH0tsHVWjq9iqBk0oDp0qaPA72lIMy5BptxISBEhZQ==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@mariozechner/clipboard-win32-arm64-msvc": { - "version": "0.3.9", - "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-win32-arm64-msvc/-/clipboard-win32-arm64-msvc-0.3.9.tgz", - "integrity": "sha512-O5FHD3ErkMwMhNzAfu3ggy0ug4z7btZuoQgwwxlzPrwV2bxlD6WDpqBY4NCgICAgZdDKdp+loUEKVAVt8aYnhQ==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@mariozechner/clipboard-win32-x64-msvc": { - "version": "0.3.9", - "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-win32-x64-msvc/-/clipboard-win32-x64-msvc-0.3.9.tgz", - "integrity": "sha512-ihQC3EufqEY81vhXBgVBtK4prL+wc62zJsSvxrgz7K1hsdt6OObz6v9p3Rn1OG3GJksTTKMJF0u/guMISHPhSA==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@mistralai/mistralai": { - "version": "2.2.1", - "license": "Apache-2.0", - "dependencies": { - "ws": "^8.18.0", - "zod": "^3.25.0 || ^4.0.0", - "zod-to-json-schema": "^3.25.0" - } - }, - "node_modules/@napi-rs/wasm-runtime": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.2.0.tgz", - "integrity": "sha512-kDoONqMa+VnZ4vvvu/ZUurpJ4gkZU57e7g69qpNgWhYcZFPUHZM2CEMKm+cG6ufDVALbjMvfmMjFVqaK7uEMnA==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@tybys/wasm-util": "^0.10.3" - }, - "engines": { - "node": "^20.19.0 || ^22.13.0 || >=23.5.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/Brooooooklyn" - }, - "peerDependencies": { - "@emnapi/core": "^2.0.0-alpha.3", - "@emnapi/runtime": "^2.0.0-alpha.3" - } - }, - "node_modules/@nodelib/fs.scandir": { - "version": "2.1.5", - "dev": true, - "license": "MIT", - "dependencies": { - "@nodelib/fs.stat": "2.0.5", - "run-parallel": "^1.1.9" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.stat": { - "version": "2.0.5", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.walk": { - "version": "1.2.8", - "dev": true, - "license": "MIT", - "dependencies": { - "@nodelib/fs.scandir": "2.1.5", - "fastq": "^1.6.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@oxc-project/types": { - "version": "0.139.0", - "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.139.0.tgz", - "integrity": "sha512-r9gHphtCs+1M7J0pw6Sn/hh/Wpa/iQrOOkrNAlVLF/gHq+/CJmHIWKKUUhdWjcD6CIa8idarspCsASiXCXvFUw==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/Boshen" - } - }, - "node_modules/@pondwader/socks5-server": { - "version": "1.0.10", - "license": "MIT" - }, - "node_modules/@protobufjs/aspromise": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", - "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==", - "license": "BSD-3-Clause" - }, - "node_modules/@protobufjs/base64": { - "version": "1.1.2", - "license": "BSD-3-Clause" - }, - "node_modules/@protobufjs/codegen": { - "version": "2.0.5", - "license": "BSD-3-Clause" - }, - "node_modules/@protobufjs/eventemitter": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.1.tgz", - "integrity": "sha512-vW1GmwMZNnL+gMRaovlh9yZX74kc+TTU3FObkkurpMaRtBfLP3ldjS9KQWlwZgraRE0+dheEEoAxdzcJQ8eXZg==", - "license": "BSD-3-Clause" - }, - "node_modules/@protobufjs/fetch": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.1.tgz", - "integrity": "sha512-GpptLrs57adMSuHi3VNj0mAF8dwh36LMaYF6XyJ6JMWlVsc+t42tm1HSEDmOs3A8fC9yyeisgLhsTVQokOZ0zw==", - "license": "BSD-3-Clause", - "dependencies": { - "@protobufjs/aspromise": "^1.1.1" - } - }, - "node_modules/@protobufjs/float": { - "version": "1.0.2", - "license": "BSD-3-Clause" - }, - "node_modules/@protobufjs/path": { - "version": "1.1.2", - "license": "BSD-3-Clause" - }, - "node_modules/@protobufjs/pool": { - "version": "1.1.0", - "license": "BSD-3-Clause" - }, - "node_modules/@protobufjs/utf8": { - "version": "1.1.1", - "license": "BSD-3-Clause" - }, - "node_modules/@rolldown/binding-android-arm64": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.1.5.tgz", - "integrity": "sha512-lZg8fqIv2v7FF237bwMgzGZEJvGL79/s5knJ/i6FmsGF4XXlzccZ4jb+TrFIxtSSxFtIpdsgrPZeMk1I9AFcyQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-darwin-arm64": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.1.5.tgz", - "integrity": "sha512-51Bnx9pNiMRKSUNtBfySkNJ9vMU9Hh3I1ozDd6gyPPYzaXCfnptUcEZxXGYFn+ul2dtcMUiqGR1Yai2K10uoTw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-darwin-x64": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.1.5.tgz", - "integrity": "sha512-Tm+gbfC0aHu1tBA/JvKQh32S0K6YgCHkiAF4/W6xX0K0RmNuc94VeK419dJoE65R5aRxmo+noZQSWrAMF6yb6g==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-freebsd-x64": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.1.5.tgz", - "integrity": "sha512-JMzDKCCXq93YccG5gz3hvOs1oXRKAf0XYpfOS88e+wZrC8Iugj6j68867vrYZkvpDDpKn/KoKORThmchMpF6TA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-linux-arm-gnueabihf": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.1.5.tgz", - "integrity": "sha512-uML21j2K5TfPGutKxub+M+nLjZIrWjXQ5Grx4lCe/nimTj9B4L63zHpjXLl4y0L3mcm2htEQIb06oCG/szerNw==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-linux-arm64-gnu": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.1.5.tgz", - "integrity": "sha512-navSiuTMogvnQoZoM/v+l3ZWo50/NTwSHSzheABx/RCnmUPaKwq9qSo4Br2OYRs21+Fz8uFqITZM3H4opOB0/Q==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-linux-arm64-musl": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.1.5.tgz", - "integrity": "sha512-lAryqH7IteztmCXQXk0etKj4wBQ7Gx5S6LjKhsgp9zb8I5bsuvU/2llH1hDQcjsFeqIsovMVN339/8pUDDBXxA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-linux-ppc64-gnu": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.1.5.tgz", - "integrity": "sha512-fsK/sNBnxzBlL4O1JNrZakVQxPspqpED5dLtNsZS9oOKmtSpdNIzxH2kkol5HYTWJN47sE20ztMJPxfZ89qGOg==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-linux-s390x-gnu": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.1.5.tgz", - "integrity": "sha512-gLYb4BIadlfTOYT5gO503n8zQjXflgzpD0FcyKh0Mzx3rqCZKnHoJWV9xe1KXUJ5lx2JfcSHr/mhzS0PC/McAA==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-linux-x64-gnu": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.1.5.tgz", - "integrity": "sha512-FjcpEKUyJygHgs1o50VYNvkt5+7Le/VEdYt0AkRpkL33MnyQfwr8l5mXwMmfmTbyMPr5vJLC+8/Gd9gXnwU1QQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-linux-x64-musl": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.1.5.tgz", - "integrity": "sha512-Me+PfPI2TMeOQk0gYWfLQZtTktrmzbr8cDboqX83XKc7UrgAi55gF+2dUkWdxd19n55Essp2yeca+O9N5rBxHg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-openharmony-arm64": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.1.5.tgz", - "integrity": "sha512-yc5WrLzXks6zCQfn9Oxr8pORKyl/pF+QjHmW/Qx3qu0oyrrNC+y2JLTU1E2rcWYAmzlnqngWXHQjy51VzW70Vw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-wasm32-wasi": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.1.5.tgz", - "integrity": "sha512-VbQGPX2b4r48TAMIM2cjgluIM1HYutm4pcTEJsle7iEP7sB1dFqtPLBVbdLAZCxy1txCcPxf4QFf4v8uvltPqA==", - "cpu": [ - "wasm32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/core": "1.11.1", - "@emnapi/runtime": "1.11.1", - "@napi-rs/wasm-runtime": "^1.1.6" - }, - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-wasm32-wasi/node_modules/@emnapi/core": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.1.tgz", - "integrity": "sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/wasi-threads": "1.2.2", - "tslib": "^2.4.0" - } - }, - "node_modules/@rolldown/binding-wasm32-wasi/node_modules/@emnapi/runtime": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.1.tgz", - "integrity": "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@rolldown/binding-wasm32-wasi/node_modules/@emnapi/wasi-threads": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.2.tgz", - "integrity": "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@rolldown/binding-win32-arm64-msvc": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.1.5.tgz", - "integrity": "sha512-gHv82k63z4qpV5+Q1y/12KrK0ltWBukVDI8nZcbT7Tt/ZlOIVwppazneq0F93oDxTo3IgAMEDIoQh3E2n6mVsw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-win32-x64-msvc": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.1.5.tgz", - "integrity": "sha512-tTZuDBPw85tEN5PQi1pnEBzDy0Z49HtScLAbD5t6hyeU92A95pRWaSMw1GZZi/RwgSgUIl0xrSlXIT/9QzvYSA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/pluginutils": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", - "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@silvia-odwyer/photon-node": { - "version": "0.3.4", - "license": "Apache-2.0" - }, - "node_modules/@smithy/core": { - "version": "3.30.0", - "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.30.0.tgz", - "integrity": "sha512-dl2yRglDxfzH9uJ4fSo4zTaAHa0zH7+V7BZMRWy8hEYIKT1BiqMUK/CN6T3ADQ3kbA5N1tmUulroJ2UtONS7Kw==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/types": "^4.16.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/credential-provider-imds": { - "version": "4.4.14", - "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-4.4.14.tgz", - "integrity": "sha512-QgbuahIb2qxQeZQvNK0sw3aF3JH5zwH8j2lLp5DUasVXexGGMWULAR+7z0omPXFolCP/m5wN9M5lm9EGdSviTQ==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/core": "^3.30.0", - "@smithy/types": "^4.16.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/fetch-http-handler": { - "version": "5.6.11", - "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-5.6.11.tgz", - "integrity": "sha512-o0Zkj1nKqJAoq+a+BrkhU39tRftMNjLwpc/z06Frfl43wpbHrJMaSAVZE4vTqlxtVkNaGaT0bIDxOp7tkFTuQQ==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/core": "^3.30.0", - "@smithy/types": "^4.16.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/node-http-handler": { - "version": "4.9.11", - "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.9.11.tgz", - "integrity": "sha512-slbzbz8taEOzoXv/9y34YNBoE+ZHmddLykCgjDAjvMAsu2nM5s2Gzwa5OGF721tD8s+CKeFUBds5lSj9lcbuDg==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/core": "^3.30.0", - "@smithy/types": "^4.16.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/signature-v4": { - "version": "5.6.10", - "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-5.6.10.tgz", - "integrity": "sha512-EXhWePm3SXJAX38npIy4TXL2Aex/OVgCClTjelN2QHw/U+8CQUH8C7AaxsVzQcYieYPseywn48s89DmvuGjiAg==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/core": "^3.30.0", - "@smithy/types": "^4.16.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/types": { - "version": "4.16.1", - "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.16.1.tgz", - "integrity": "sha512-0JFs3V2y2M9tKW5na/qxe69Zv+uxLMO7QBbhxF/FHu/Gp2NFZAAL9tWl9PU02xxo07pb3G9FTyjNc6D5uZrJIg==", - "license": "Apache-2.0", - "dependencies": { - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@standard-schema/spec": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", - "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", - "dev": true, - "license": "MIT" - }, - "node_modules/@tokenizer/inflate": { - "version": "0.4.1", - "license": "MIT", - "dependencies": { - "debug": "^4.4.3", - "token-types": "^6.1.1" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/Borewit" - } - }, - "node_modules/@tokenizer/token": { - "version": "0.3.0", - "license": "MIT" - }, - "node_modules/@tootallnate/quickjs-emscripten": { - "version": "0.23.0", - "license": "MIT" - }, - "node_modules/@tybys/wasm-util": { - "version": "0.10.3", - "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.3.tgz", - "integrity": "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@types/chai": { - "version": "5.2.3", - "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", - "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/deep-eql": "*", - "assertion-error": "^2.0.1" - } - }, - "node_modules/@types/deep-eql": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", - "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/diff": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/@types/diff/-/diff-8.0.0.tgz", - "integrity": "sha512-o7jqJM04gfaYrdCecCVMbZhNdG6T1MHg/oQoRFdERLV+4d+V7FijhiEAbFu0Usww84Yijk9yH58U4Jk4HbtzZw==", - "deprecated": "This is a stub types definition. diff provides its own type definitions, so you do not need this installed.", - "dev": true, - "license": "MIT", - "dependencies": { - "diff": "*" - } - }, - "node_modules/@types/estree": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", - "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/hosted-git-info": { - "version": "3.0.5", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/mime-types": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@types/mime-types/-/mime-types-3.0.1.tgz", - "integrity": "sha512-xRMsfuQbnRq1Ef+C+RKaENOxXX87Ygl38W1vDfPHRku02TgQr+Qd8iivLtAMcR0KF5/29xlnFihkTlbqFrGOVQ==", - "license": "MIT" - }, - "node_modules/@types/ms": { - "version": "2.1.0", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/node": { - "version": "22.20.1", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.20.1.tgz", - "integrity": "sha512-EANqOCF9QFyra+4pfxUcX9STKJpCLjMbObVzljIJomAWSnuSIEAvyzEU53GaajbXJEgdh0iEcPL+DGvpUd4k1Q==", - "license": "MIT", - "dependencies": { - "undici-types": "~6.21.0" - } - }, - "node_modules/@types/proper-lockfile": { - "version": "4.1.4", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/retry": "*" - } - }, - "node_modules/@types/retry": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.0.tgz", - "integrity": "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==", - "license": "MIT" - }, - "node_modules/@types/yauzl": { - "version": "2.10.3", - "license": "MIT", - "optional": true, - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@typescript/native-preview": { - "version": "7.0.0-dev.20260120.1", - "dev": true, - "license": "Apache-2.0", - "bin": { - "tsgo": "bin/tsgo.js" - }, - "optionalDependencies": { - "@typescript/native-preview-darwin-arm64": "7.0.0-dev.20260120.1", - "@typescript/native-preview-darwin-x64": "7.0.0-dev.20260120.1", - "@typescript/native-preview-linux-arm": "7.0.0-dev.20260120.1", - "@typescript/native-preview-linux-arm64": "7.0.0-dev.20260120.1", - "@typescript/native-preview-linux-x64": "7.0.0-dev.20260120.1", - "@typescript/native-preview-win32-arm64": "7.0.0-dev.20260120.1", - "@typescript/native-preview-win32-x64": "7.0.0-dev.20260120.1" - } - }, - "node_modules/@typescript/native-preview-darwin-arm64": { - "version": "7.0.0-dev.20260120.1", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@typescript/native-preview-darwin-x64": { - "version": "7.0.0-dev.20260120.1", - "resolved": "https://registry.npmjs.org/@typescript/native-preview-darwin-x64/-/native-preview-darwin-x64-7.0.0-dev.20260120.1.tgz", - "integrity": "sha512-cuC1+wLbUP+Ip2UT94G134fqRdp5w3b3dhcCO6/FQ4yXxvRNyv/WK+upHBUFDaeSOeHgDTyO9/QFYUWwC4If1A==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@typescript/native-preview-linux-arm": { - "version": "7.0.0-dev.20260120.1", - "resolved": "https://registry.npmjs.org/@typescript/native-preview-linux-arm/-/native-preview-linux-arm-7.0.0-dev.20260120.1.tgz", - "integrity": "sha512-vN6OYVySol/kQZjJGmAzd6L30SyVlCgmCXS8WjUYtE5clN0YrzQHop16RK29fYZHMxpkOniVBtRPxUYQANZBlQ==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@typescript/native-preview-linux-arm64": { - "version": "7.0.0-dev.20260120.1", - "resolved": "https://registry.npmjs.org/@typescript/native-preview-linux-arm64/-/native-preview-linux-arm64-7.0.0-dev.20260120.1.tgz", - "integrity": "sha512-zZGvEGY7wcHYefMZ87KNmvjN3NLIhsCMHEpHZiGCS3khKf+8z6ZsanrzCjOTodvL01VPyBzHxV1EtkSxAcLiQg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@typescript/native-preview-linux-x64": { - "version": "7.0.0-dev.20260120.1", - "resolved": "https://registry.npmjs.org/@typescript/native-preview-linux-x64/-/native-preview-linux-x64-7.0.0-dev.20260120.1.tgz", - "integrity": "sha512-JBfNhWd/asd5MDeS3VgRvE24pGKBkmvLub6tsux6ypr+Yhy+o0WaAEzVpmlRYZUqss2ai5tvOu4dzPBXzZAtFw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@typescript/native-preview-win32-arm64": { - "version": "7.0.0-dev.20260120.1", - "resolved": "https://registry.npmjs.org/@typescript/native-preview-win32-arm64/-/native-preview-win32-arm64-7.0.0-dev.20260120.1.tgz", - "integrity": "sha512-tTndRtYCq2xwgE0VkTi9ACNiJaV43+PqvBqCxk8ceYi3X36Ve+CCnwlZfZJ4k9NxZthtrAwF/kUmpC9iIYbq1w==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@typescript/native-preview-win32-x64": { - "version": "7.0.0-dev.20260120.1", - "resolved": "https://registry.npmjs.org/@typescript/native-preview-win32-x64/-/native-preview-win32-x64-7.0.0-dev.20260120.1.tgz", - "integrity": "sha512-oZia7hFL6k9pVepfonuPI86Jmyz6WlJKR57tWCDwRNmpA7odxuTq1PbvcYgy1z4+wHF1nnKKJY0PMAiq6ac18w==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@typescript/typescript-aix-ppc64": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@typescript/typescript-aix-ppc64/-/typescript-aix-ppc64-7.0.2.tgz", - "integrity": "sha512-MTKKkWB7p/0E9xi1d1tHtZ5PiLkGEMIq88pK2CubZjOsLtYTLqhgIgi6zepFa+9GHZ6h05NMCkQxGKiPXMxXtQ==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=16.20.0" - } - }, - "node_modules/@typescript/typescript-darwin-arm64": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@typescript/typescript-darwin-arm64/-/typescript-darwin-arm64-7.0.2.tgz", - "integrity": "sha512-gowzar9MwS/aRWp6f3a4KUqzRjAZjOsmGNCM6LcTgXum+dBfgsBVMN+AgvOCCbguXyick6LJhpBszxMebJ8syA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=16.20.0" - } - }, - "node_modules/@typescript/typescript-darwin-x64": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@typescript/typescript-darwin-x64/-/typescript-darwin-x64-7.0.2.tgz", - "integrity": "sha512-SZ9xZInqApNlNGc9s0W1VSsktYSOe9cFqNOIqmN1Gs8SmkjKZYFt017G4VwPxASInODuAdbTW7sXiFUf893RgA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=16.20.0" - } - }, - "node_modules/@typescript/typescript-freebsd-arm64": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@typescript/typescript-freebsd-arm64/-/typescript-freebsd-arm64-7.0.2.tgz", - "integrity": "sha512-W5NH4y/J0plIIS5b2xvTEkU7JFxyqdMAOgf+Ilhl0vHQXKO5dZoxd+C/jEtq56c4F3wk71RB4BMRQ2XdI+bwYQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=16.20.0" - } - }, - "node_modules/@typescript/typescript-freebsd-x64": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@typescript/typescript-freebsd-x64/-/typescript-freebsd-x64-7.0.2.tgz", - "integrity": "sha512-UMGDx5sTpzNw3WiPebH7l90IWfJggEd+egHt/q6p7/Cm3zqoV7VxkGXt+3DxPIw8CcmvAB0j3sVVfbhX+M4Tpw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=16.20.0" - } - }, - "node_modules/@typescript/typescript-linux-arm": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-arm/-/typescript-linux-arm-7.0.2.tgz", - "integrity": "sha512-gffT3xPz9sR7j/YJExkyPntrI0P2EP9XbOyWzth2/Gs0RstK+90RBcO0ncXoXy/beYll1SXw846Nf2zdnEz0QQ==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=16.20.0" - } - }, - "node_modules/@typescript/typescript-linux-arm64": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-arm64/-/typescript-linux-arm64-7.0.2.tgz", - "integrity": "sha512-Qh4eU4/y3yDjnfjjyPYihMj5/ODIlmt+Bzu17OI+fiSRDW57QmU5SiN63exPRNJPKUzcc1INa1NXdrJ+MqHjUQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=16.20.0" - } - }, - "node_modules/@typescript/typescript-linux-loong64": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-loong64/-/typescript-linux-loong64-7.0.2.tgz", - "integrity": "sha512-uEHck9i8hoAzXPiYRib1O7miOnz23SxIeVl6F4LXox+qov1K35jHcEW6VHKvZI+pyvl7fZEP4MCU5LYvIq1GuQ==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=16.20.0" - } - }, - "node_modules/@typescript/typescript-linux-mips64el": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-mips64el/-/typescript-linux-mips64el-7.0.2.tgz", - "integrity": "sha512-R4KvAMnE43W5Qeqb0Ly56O3mWMWIAgsMyz36DCaycd5nbg/9kzm0liw3JocfRqyJY0KPmzFjbswozXyW0DnIYA==", - "cpu": [ - "mips64el" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=16.20.0" - } - }, - "node_modules/@typescript/typescript-linux-ppc64": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-ppc64/-/typescript-linux-ppc64-7.0.2.tgz", - "integrity": "sha512-DORx5b3sd/4S7eayxm4FQv+A7CrkUIGRaHiwI8oiHTAI1fAPWhF4J0vAlkC8biAlHSVVwxMQ3tjZ2/DVbnQiiA==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=16.20.0" - } - }, - "node_modules/@typescript/typescript-linux-riscv64": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-riscv64/-/typescript-linux-riscv64-7.0.2.tgz", - "integrity": "sha512-wf0jqEDOjrPRnKwYRyyJDRo11KMbvMFrU+q4zqKyChODBzvlkbhNQfKvLxQCcwTpdDaXSHZTVuh0JoCrKCUMHQ==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=16.20.0" - } - }, - "node_modules/@typescript/typescript-linux-s390x": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-s390x/-/typescript-linux-s390x-7.0.2.tgz", - "integrity": "sha512-IkwJc3L7yhytWd/ewjyxNDfOmswCm9GWMJT/ue/dU4aZNbwZeYAetq42VyLmsmSjvoX7z74X6ZaYCtzAr0EuGw==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=16.20.0" - } - }, - "node_modules/@typescript/typescript-linux-x64": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-x64/-/typescript-linux-x64-7.0.2.tgz", - "integrity": "sha512-EYdf2cNg7rgCWJnxCdJ+F3V39O8ihb37eHAu1LK8oAFizgTQbPOK7zHHXbPt8rX24COqODXeI3sIf0fCXG7H/A==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=16.20.0" - } - }, - "node_modules/@typescript/typescript-netbsd-arm64": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@typescript/typescript-netbsd-arm64/-/typescript-netbsd-arm64-7.0.2.tgz", - "integrity": "sha512-+polYF4MF04aPpO5FTkHran9yUQDSXqy5GiSDKpsll5jy3l3+g9QLhpf39T+ePtefhXLOGrLl0QIjkQP6VnelA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=16.20.0" - } - }, - "node_modules/@typescript/typescript-netbsd-x64": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@typescript/typescript-netbsd-x64/-/typescript-netbsd-x64-7.0.2.tgz", - "integrity": "sha512-8YIT0EHM/3dq10ZOVF/A7pc/YSMtbcecct4rWtexrnSCHOPcpC2KTLXfTCR6vDpnSiY12heNb1GiN/wu+T/FyA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=16.20.0" - } - }, - "node_modules/@typescript/typescript-openbsd-arm64": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@typescript/typescript-openbsd-arm64/-/typescript-openbsd-arm64-7.0.2.tgz", - "integrity": "sha512-APT8+ClYnuYm1u9+kgGXoMj2VzWzcymwh2gNSQVySHfkRDGOTVkoWLjCmOQSaO+PoqQ57B0flRp9SA+7GnnkzQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=16.20.0" - } - }, - "node_modules/@typescript/typescript-openbsd-x64": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@typescript/typescript-openbsd-x64/-/typescript-openbsd-x64-7.0.2.tgz", - "integrity": "sha512-yX7s+Q0Dln0Dt9tEzZsAjXXR/+ytBM7AlglaqyeMPxQszJ1JhlJdZ6jLA+IzldHtflX81em7lDao1xXu+aRRkg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=16.20.0" - } - }, - "node_modules/@typescript/typescript-sunos-x64": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@typescript/typescript-sunos-x64/-/typescript-sunos-x64-7.0.2.tgz", - "integrity": "sha512-dLJDGaLZ1D4HPQn62u1n8mBDkJREwMsAkCdkwd4Ieqw+x3TUyTsqY0YiBCtE6H6OzzgGk3iuZ3vFWRS+E8/d1g==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=16.20.0" - } - }, - "node_modules/@typescript/typescript-win32-arm64": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@typescript/typescript-win32-arm64/-/typescript-win32-arm64-7.0.2.tgz", - "integrity": "sha512-Gyl1Vy6OsWesLzmq+EP0Fb7b4Nid5232AvcA2SFcdYreldpNtYFFofPjnt62y9hQy7VTaZp65ICJjuAQRaVcIQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=16.20.0" - } - }, - "node_modules/@typescript/typescript-win32-x64": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@typescript/typescript-win32-x64/-/typescript-win32-x64-7.0.2.tgz", - "integrity": "sha512-0BQ3HkAHHlKLSp1qRvf3SUhGpGsDuhB/jgFw75guyqbxJqEaS0Cw/VFO8i2nHglJUzQCRtMMR/IBAKE3ETMC4g==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=16.20.0" - } - }, - "node_modules/@vitest/expect": { - "version": "4.1.10", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.10.tgz", - "integrity": "sha512-YsCn+qAk1GWjQOWFEsEcL2gNQ0zmVmQu3T03qP6UyjhtmdtwtbuI+DASn/7iQB3HGTXkdBwGddzxPlmiql5vlA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@standard-schema/spec": "^1.1.0", - "@types/chai": "^5.2.2", - "@vitest/spy": "4.1.10", - "@vitest/utils": "4.1.10", - "chai": "^6.2.2", - "tinyrainbow": "^3.1.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/mocker": { - "version": "4.1.10", - "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.10.tgz", - "integrity": "sha512-v0xaezt+DKEmKfaxg133ldzADrwLGd7Ze1MfQQTYfvs8OqZIwbxyxaYURivwV7sWy5fqn3rH5uOrSp07bp44Ow==", - "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/spy": "4.1.10", - "estree-walker": "^3.0.3", - "magic-string": "^0.30.21" - }, - "funding": { - "url": "https://opencollective.com/vitest" - }, - "peerDependencies": { - "msw": "^2.4.9", - "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" - }, - "peerDependenciesMeta": { - "msw": { - "optional": true - }, - "vite": { - "optional": true - } - } - }, - "node_modules/@vitest/pretty-format": { - "version": "4.1.10", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.10.tgz", - "integrity": "sha512-W1HsjSH4MXQ9YfmmhLAoIYf1HRfekQCGngeIgcei6MP5QQGWUe0gkopdZQaVCFO+JDJMrAJGwa5pRpNpvy4P8Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "tinyrainbow": "^3.1.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/runner": { - "version": "4.1.10", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.10.tgz", - "integrity": "sha512-IKI6kpIH+LmpROplyLwBBaCfMgOZOMsygVa6BARD6ahA04VRuJSa6OaVG7kRvSEMD870Vd91rSSw0eegtWyLGg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/utils": "4.1.10", - "pathe": "^2.0.3" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/snapshot": { - "version": "4.1.10", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.10.tgz", - "integrity": "sha512-xRkfOT1qpTAi/Ti4Y1LtfRc3kEuqxGw59eN2jN9pRWMtS/XDevekhcFSqvQqjUNGksfjMJu3Y+oJ+4Ypn2OaJw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/pretty-format": "4.1.10", - "@vitest/utils": "4.1.10", - "magic-string": "^0.30.21", - "pathe": "^2.0.3" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/spy": { - "version": "4.1.10", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.10.tgz", - "integrity": "sha512-PLf/Ugvoq5wO/b4rwYCR1h2PSIdXz7wnkQFMiUpLdtM7l6pqVFcQIBEHyT1+l+cj7mNwAfZHzqXqDyjvOuwbDw==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/utils": { - "version": "4.1.10", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.10.tgz", - "integrity": "sha512-fy9am/HWxbaGt/Sawrp90vt6Y6jQwf1RX77cz3uwoJwJVMli/e1IEwRPnMNJ7vKfPTwo0diXifkpPvwH9v7nGA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/pretty-format": "4.1.10", - "convert-source-map": "^2.0.0", - "tinyrainbow": "^3.1.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@xterm/headless": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/@xterm/headless/-/headless-6.0.0.tgz", - "integrity": "sha512-5Yj1QINYCyzrZtf8OFIHi47iQtI+0qYFPHmouEfG8dHNxbZ9Tb9YGSuLcsEwj9Z+OL75GJqPyJbyoFer80a2Hw==", - "dev": true, - "license": "MIT", - "workspaces": [ - "addons/*" - ] - }, - "node_modules/@xterm/xterm": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-6.0.0.tgz", - "integrity": "sha512-TQwDdQGtwwDt+2cgKDLn0IRaSxYu1tSUjgKarSDkUM0ZNiSRXFpjxEsvc/Zgc5kq5omJ+V0a8/kIM2WD3sMOYg==", - "dev": true, - "license": "MIT", - "workspaces": [ - "addons/*" - ] - }, - "node_modules/agent-base": { - "version": "7.1.4", - "license": "MIT", - "engines": { - "node": ">= 14" - } - }, - "node_modules/ansi-regex": { - "version": "6.2.2", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, - "node_modules/ansi-styles": { - "version": "4.3.0", - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/any-promise": { - "version": "1.3.0", - "license": "MIT" - }, - "node_modules/assertion-error": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", - "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - } - }, - "node_modules/ast-types": { - "version": "0.13.4", - "license": "MIT", - "dependencies": { - "tslib": "^2.0.1" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/balanced-match": { - "version": "4.0.4", - "license": "MIT", - "engines": { - "node": "18 || 20 || >=22" - } - }, - "node_modules/base64-js": { - "version": "1.5.1", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/basic-ftp": { - "version": "5.3.1", - "license": "MIT", - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/bignumber.js": { - "version": "9.3.1", - "license": "MIT", - "engines": { - "node": "*" - } - }, - "node_modules/bl": { - "version": "4.1.0", - "dev": true, - "license": "MIT", - "dependencies": { - "buffer": "^5.5.0", - "inherits": "^2.0.4", - "readable-stream": "^3.4.0" - } - }, - "node_modules/bowser": { - "version": "2.14.1", - "resolved": "https://registry.npmjs.org/bowser/-/bowser-2.14.1.tgz", - "integrity": "sha512-tzPjzCxygAKWFOJP011oxFHs57HzIhOEracIgAePE4pqB3LikALKnSzUyU4MGs9/iCEUuHlAJTjTc5M+u7YEGg==", - "license": "MIT" - }, - "node_modules/brace-expansion": { - "version": "5.0.9", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz", - "integrity": "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==", - "license": "MIT", - "dependencies": { - "balanced-match": "^4.0.2" - }, - "engines": { - "node": "20 || >=22" - } - }, - "node_modules/braces": { - "version": "3.0.3", - "dev": true, - "license": "MIT", - "dependencies": { - "fill-range": "^7.1.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/buffer": { - "version": "5.7.1", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "dependencies": { - "base64-js": "^1.3.1", - "ieee754": "^1.1.13" - } - }, - "node_modules/buffer-crc32": { - "version": "0.2.13", - "license": "MIT", - "engines": { - "node": "*" - } - }, - "node_modules/buffer-equal-constant-time": { - "version": "1.0.1", - "license": "BSD-3-Clause" - }, - "node_modules/canvas": { - "version": "3.2.3", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "dependencies": { - "node-addon-api": "^7.0.0", - "prebuild-install": "^7.1.3" - }, - "engines": { - "node": "^18.12.0 || >= 20.9.0" - } - }, - "node_modules/chai": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", - "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - } - }, - "node_modules/chalk": { - "version": "5.6.2", - "license": "MIT", - "engines": { - "node": "^12.17.0 || ^14.13 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/chownr": { - "version": "1.1.4", - "dev": true, - "license": "ISC" - }, - "node_modules/cli-highlight": { - "version": "2.1.11", - "license": "ISC", - "dependencies": { - "chalk": "^4.0.0", - "highlight.js": "^10.7.1", - "mz": "^2.4.0", - "parse5": "^5.1.1", - "parse5-htmlparser2-tree-adapter": "^6.0.0", - "yargs": "^16.0.0" - }, - "bin": { - "highlight": "bin/highlight" - }, - "engines": { - "node": ">=8.0.0", - "npm": ">=5.0.0" - } - }, - "node_modules/cli-highlight/node_modules/chalk": { - "version": "4.1.2", - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/cli-highlight/node_modules/supports-color": { - "version": "7.2.0", - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/cliui": { - "version": "7.0.4", - "license": "ISC", - "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.0", - "wrap-ansi": "^7.0.0" - } - }, - "node_modules/cliui/node_modules/ansi-regex": { - "version": "5.0.1", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/cliui/node_modules/strip-ansi": { - "version": "6.0.1", - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/color-convert": { - "version": "2.0.1", - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/color-name": { - "version": "1.1.4", - "license": "MIT" - }, - "node_modules/commander": { - "version": "12.1.0", - "license": "MIT", - "engines": { - "node": ">=18" - } - }, - "node_modules/concurrently": { - "version": "9.2.4", - "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-9.2.4.tgz", - "integrity": "sha512-TZ0CEhyzvFjgtAvHTusDMgj7wNdihCh7LLLrzdUOXIhdlnL2JBBGA9eJxR24rtqgmdjh3OA3hrN1rCHj6HM8qA==", - "dev": true, - "license": "MIT", - "dependencies": { - "chalk": "4.1.2", - "rxjs": "7.8.2", - "shell-quote": "1.9.0", - "supports-color": "8.1.1", - "tree-kill": "1.2.2", - "yargs": "17.7.2" - }, - "bin": { - "conc": "dist/bin/concurrently.js", - "concurrently": "dist/bin/concurrently.js" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/open-cli-tools/concurrently?sponsor=1" - } - }, - "node_modules/concurrently/node_modules/ansi-regex": { - "version": "5.0.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/concurrently/node_modules/chalk": { - "version": "4.1.2", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/concurrently/node_modules/chalk/node_modules/supports-color": { - "version": "7.2.0", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/concurrently/node_modules/cliui": { - "version": "8.0.1", - "dev": true, - "license": "ISC", - "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.1", - "wrap-ansi": "^7.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/concurrently/node_modules/strip-ansi": { - "version": "6.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/concurrently/node_modules/yargs": { - "version": "17.7.2", - "dev": true, - "license": "MIT", - "dependencies": { - "cliui": "^8.0.1", - "escalade": "^3.1.1", - "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", - "string-width": "^4.2.3", - "y18n": "^5.0.5", - "yargs-parser": "^21.1.1" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/concurrently/node_modules/yargs-parser": { - "version": "21.1.1", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=12" - } - }, - "node_modules/convert-source-map": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", - "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", - "dev": true, - "license": "MIT" - }, - "node_modules/cross-spawn": { - "version": "6.0.6", - "dev": true, - "license": "MIT", - "dependencies": { - "nice-try": "^1.0.4", - "path-key": "^2.0.1", - "semver": "^5.5.0", - "shebang-command": "^1.2.0", - "which": "^1.2.9" - }, - "engines": { - "node": ">=4.8" - } - }, - "node_modules/cross-spawn/node_modules/semver": { - "version": "5.7.2", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver" - } - }, - "node_modules/data-uri-to-buffer": { - "version": "4.0.1", - "license": "MIT", - "engines": { - "node": ">= 12" - } - }, - "node_modules/debug": { - "version": "4.4.3", - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/decompress-response": { - "version": "6.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "mimic-response": "^3.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/deep-extend": { - "version": "0.6.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4.0.0" - } - }, - "node_modules/degenerator": { - "version": "5.0.1", - "license": "MIT", - "dependencies": { - "ast-types": "^0.13.4", - "escodegen": "^2.1.0", - "esprima": "^4.0.1" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/detect-libc": { - "version": "2.1.2", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=8" - } - }, - "node_modules/diff": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/diff/-/diff-9.0.0.tgz", - "integrity": "sha512-svtcdpS8CgJyqAjEQIXdb3OjhFVVYjzGAPO8WGCmRbrml64SPw/jJD4GoE98aR7r25A0XcgrK3F02yw9R/vhQw==", - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.3.1" - } - }, - "node_modules/ecdsa-sig-formatter": { - "version": "1.0.11", - "license": "Apache-2.0", - "dependencies": { - "safe-buffer": "^5.0.1" - } - }, - "node_modules/emoji-regex": { - "version": "8.0.0", - "license": "MIT" - }, - "node_modules/end-of-stream": { - "version": "1.4.5", - "license": "MIT", - "dependencies": { - "once": "^1.4.0" - } - }, - "node_modules/es-errors": { - "version": "1.3.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-module-lexer": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.1.0.tgz", - "integrity": "sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/esbuild": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.1.tgz", - "integrity": "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "peer": true, - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=18" - }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.28.1", - "@esbuild/android-arm": "0.28.1", - "@esbuild/android-arm64": "0.28.1", - "@esbuild/android-x64": "0.28.1", - "@esbuild/darwin-arm64": "0.28.1", - "@esbuild/darwin-x64": "0.28.1", - "@esbuild/freebsd-arm64": "0.28.1", - "@esbuild/freebsd-x64": "0.28.1", - "@esbuild/linux-arm": "0.28.1", - "@esbuild/linux-arm64": "0.28.1", - "@esbuild/linux-ia32": "0.28.1", - "@esbuild/linux-loong64": "0.28.1", - "@esbuild/linux-mips64el": "0.28.1", - "@esbuild/linux-ppc64": "0.28.1", - "@esbuild/linux-riscv64": "0.28.1", - "@esbuild/linux-s390x": "0.28.1", - "@esbuild/linux-x64": "0.28.1", - "@esbuild/netbsd-arm64": "0.28.1", - "@esbuild/netbsd-x64": "0.28.1", - "@esbuild/openbsd-arm64": "0.28.1", - "@esbuild/openbsd-x64": "0.28.1", - "@esbuild/openharmony-arm64": "0.28.1", - "@esbuild/sunos-x64": "0.28.1", - "@esbuild/win32-arm64": "0.28.1", - "@esbuild/win32-ia32": "0.28.1", - "@esbuild/win32-x64": "0.28.1" - } - }, - "node_modules/escalade": { - "version": "3.2.0", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/escodegen": { - "version": "2.1.0", - "license": "BSD-2-Clause", - "dependencies": { - "esprima": "^4.0.1", - "estraverse": "^5.2.0", - "esutils": "^2.0.2" - }, - "bin": { - "escodegen": "bin/escodegen.js", - "esgenerate": "bin/esgenerate.js" - }, - "engines": { - "node": ">=6.0" - }, - "optionalDependencies": { - "source-map": "~0.6.1" - } - }, - "node_modules/esprima": { - "version": "4.0.1", - "license": "BSD-2-Clause", - "bin": { - "esparse": "bin/esparse.js", - "esvalidate": "bin/esvalidate.js" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/estraverse": { - "version": "5.3.0", - "license": "BSD-2-Clause", - "engines": { - "node": ">=4.0" - } - }, - "node_modules/estree-walker": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", - "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/estree": "^1.0.0" - } - }, - "node_modules/esutils": { - "version": "2.0.3", - "license": "BSD-2-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/execa": { - "version": "1.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "cross-spawn": "^6.0.0", - "get-stream": "^4.0.0", - "is-stream": "^1.1.0", - "npm-run-path": "^2.0.0", - "p-finally": "^1.0.0", - "signal-exit": "^3.0.0", - "strip-eof": "^1.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/execa/node_modules/get-stream": { - "version": "4.1.0", - "dev": true, - "license": "MIT", - "dependencies": { - "pump": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/expand-template": { - "version": "2.0.3", - "dev": true, - "license": "(MIT OR WTFPL)", - "engines": { - "node": ">=6" - } - }, - "node_modules/expect-type": { - "version": "1.3.0", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=12.0.0" - } - }, - "node_modules/extend": { - "version": "3.0.2", - "license": "MIT" - }, - "node_modules/extract-zip": { - "version": "2.0.1", - "license": "BSD-2-Clause", - "dependencies": { - "debug": "^4.1.1", - "get-stream": "^5.1.0", - "yauzl": "^2.10.0" - }, - "bin": { - "extract-zip": "cli.js" - }, - "engines": { - "node": ">= 10.17.0" - }, - "optionalDependencies": { - "@types/yauzl": "^2.9.1" - } - }, - "node_modules/fast-glob": { - "version": "3.3.3", - "dev": true, - "license": "MIT", - "dependencies": { - "@nodelib/fs.stat": "^2.0.2", - "@nodelib/fs.walk": "^1.2.3", - "glob-parent": "^5.1.2", - "merge2": "^1.3.0", - "micromatch": "^4.0.8" - }, - "engines": { - "node": ">=8.6.0" - } - }, - "node_modules/fastq": { - "version": "1.20.1", - "dev": true, - "license": "ISC", - "dependencies": { - "reusify": "^1.0.4" - } - }, - "node_modules/fd-slicer": { - "version": "1.1.0", - "license": "MIT", - "dependencies": { - "pend": "~1.2.0" - } - }, - "node_modules/fetch-blob": { - "version": "3.2.0", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/jimmywarting" - }, - { - "type": "paypal", - "url": "https://paypal.me/jimmywarting" - } - ], - "license": "MIT", - "dependencies": { - "node-domexception": "^1.0.0", - "web-streams-polyfill": "^3.0.3" - }, - "engines": { - "node": "^12.20 || >= 14.13" - } - }, - "node_modules/file-type": { - "version": "21.3.4", - "license": "MIT", - "dependencies": { - "@tokenizer/inflate": "^0.4.1", - "strtok3": "^10.3.4", - "token-types": "^6.1.1", - "uint8array-extras": "^1.4.0" - }, - "engines": { - "node": ">=20" - }, - "funding": { - "url": "https://github.com/sindresorhus/file-type?sponsor=1" - } - }, - "node_modules/fill-range": { - "version": "7.1.1", - "dev": true, - "license": "MIT", - "dependencies": { - "to-regex-range": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/formdata-polyfill": { - "version": "4.0.10", - "license": "MIT", - "dependencies": { - "fetch-blob": "^3.1.2" - }, - "engines": { - "node": ">=12.20.0" - } - }, - "node_modules/fs-constants": { - "version": "1.0.0", - "dev": true, - "license": "MIT" - }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, - "node_modules/function-bind": { - "version": "1.1.2", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/gaxios": { - "version": "7.1.4", - "license": "Apache-2.0", - "dependencies": { - "extend": "^3.0.2", - "https-proxy-agent": "^7.0.1", - "node-fetch": "^3.3.2" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/gcp-metadata": { - "version": "8.1.2", - "license": "Apache-2.0", - "dependencies": { - "gaxios": "^7.0.0", - "google-logging-utils": "^1.0.0", - "json-bigint": "^1.0.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/get-caller-file": { - "version": "2.0.5", - "license": "ISC", - "engines": { - "node": "6.* || 8.* || >= 10.*" - } - }, - "node_modules/get-east-asian-width": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.6.0.tgz", - "integrity": "sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA==", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/get-stream": { - "version": "5.2.0", - "license": "MIT", - "dependencies": { - "pump": "^3.0.0" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/get-uri": { - "version": "6.0.5", - "license": "MIT", - "dependencies": { - "basic-ftp": "^5.0.2", - "data-uri-to-buffer": "^6.0.2", - "debug": "^4.3.4" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/get-uri/node_modules/data-uri-to-buffer": { - "version": "6.0.2", - "license": "MIT", - "engines": { - "node": ">= 14" - } - }, - "node_modules/github-from-package": { - "version": "0.0.0", - "dev": true, - "license": "MIT" - }, - "node_modules/glob": { - "version": "13.0.6", - "license": "BlueOak-1.0.0", - "dependencies": { - "minimatch": "^10.2.2", - "minipass": "^7.1.3", - "path-scurry": "^2.0.2" - }, - "engines": { - "node": "18 || 20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/glob-parent": { - "version": "5.1.2", - "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/google-auth-library": { - "version": "10.6.2", - "license": "Apache-2.0", - "dependencies": { - "base64-js": "^1.3.0", - "ecdsa-sig-formatter": "^1.0.11", - "gaxios": "^7.1.4", - "gcp-metadata": "8.1.2", - "google-logging-utils": "1.1.3", - "jws": "^4.0.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/google-logging-utils": { - "version": "1.1.3", - "license": "Apache-2.0", - "engines": { - "node": ">=14" - } - }, - "node_modules/graceful-fs": { - "version": "4.2.11", - "license": "ISC" - }, - "node_modules/grok-mermaid": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/grok-mermaid/-/grok-mermaid-0.2.3.tgz", - "integrity": "sha512-/4KopAbsjvuRP9MdPtlDjOHUmUVEohOX73JNcsWpzAtFxh+bq5+Dhb6gzvRieLDwIPQIR3/vy8V1NNTuz4Zsmg==", - "license": "Apache-2.0", - "engines": { - "node": ">=18" - } - }, - "node_modules/has-flag": { - "version": "4.0.0", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/hasown": { - "version": "2.0.3", - "dev": true, - "license": "MIT", - "dependencies": { - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/highlight.js": { - "version": "10.7.3", - "license": "BSD-3-Clause", - "engines": { - "node": "*" - } - }, - "node_modules/hosted-git-info": { - "version": "9.0.3", - "license": "ISC", - "dependencies": { - "lru-cache": "^11.1.0" - }, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/http-proxy-agent": { - "version": "7.0.2", - "license": "MIT", - "dependencies": { - "agent-base": "^7.1.0", - "debug": "^4.3.4" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/https-proxy-agent": { - "version": "7.0.6", - "license": "MIT", - "dependencies": { - "agent-base": "^7.1.2", - "debug": "4" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/husky": { - "version": "9.1.7", - "dev": true, - "license": "MIT", - "bin": { - "husky": "bin.js" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/typicode" - } - }, - "node_modules/ieee754": { - "version": "1.2.1", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "BSD-3-Clause" - }, - "node_modules/ignore": { - "version": "7.0.5", - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, - "node_modules/inherits": { - "version": "2.0.4", - "dev": true, - "license": "ISC" - }, - "node_modules/ini": { - "version": "1.3.8", - "dev": true, - "license": "ISC" - }, - "node_modules/interpret": { - "version": "1.4.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/ip-address": { - "version": "10.4.0", - "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.4.0.tgz", - "integrity": "sha512-oSK96Grm3aP6OrS263xVxbNDGVL7rzBtYdpGqlDG8iQdoenDoTs/nkki+DflYbAEE8Xl6o5YxhxlrKvI3nqKXQ==", - "license": "MIT", - "engines": { - "node": ">= 12" - } - }, - "node_modules/is-core-module": { - "version": "2.16.2", - "dev": true, - "license": "MIT", - "dependencies": { - "hasown": "^2.0.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-extglob": { - "version": "2.1.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/is-glob": { - "version": "4.0.3", - "dev": true, - "license": "MIT", - "dependencies": { - "is-extglob": "^2.1.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-number": { - "version": "7.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.12.0" - } - }, - "node_modules/is-stream": { - "version": "1.1.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/isexe": { - "version": "2.0.0", - "dev": true, - "license": "ISC" - }, - "node_modules/jiti": { - "version": "2.7.0", - "license": "MIT", - "peer": true, - "bin": { - "jiti": "lib/jiti-cli.mjs" - } - }, - "node_modules/json-bigint": { - "version": "1.0.0", - "license": "MIT", - "dependencies": { - "bignumber.js": "^9.0.0" - } - }, - "node_modules/json-schema-to-ts": { - "version": "3.1.1", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.18.3", - "ts-algebra": "^2.0.0" - }, - "engines": { - "node": ">=16" - } - }, - "node_modules/jwa": { - "version": "2.0.1", - "license": "MIT", - "dependencies": { - "buffer-equal-constant-time": "^1.0.1", - "ecdsa-sig-formatter": "1.0.11", - "safe-buffer": "^5.0.1" - } - }, - "node_modules/jws": { - "version": "4.0.1", - "license": "MIT", - "dependencies": { - "jwa": "^2.0.1", - "safe-buffer": "^5.0.1" - } - }, - "node_modules/koffi": { - "version": "2.16.2", - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "funding": { - "url": "https://liberapay.com/Koromix" - } - }, - "node_modules/lightningcss": { - "version": "1.33.0", - "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.33.0.tgz", - "integrity": "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==", - "dev": true, - "license": "MPL-2.0", - "dependencies": { - "detect-libc": "^2.0.3" - }, - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - }, - "optionalDependencies": { - "lightningcss-android-arm64": "1.33.0", - "lightningcss-darwin-arm64": "1.33.0", - "lightningcss-darwin-x64": "1.33.0", - "lightningcss-freebsd-x64": "1.33.0", - "lightningcss-linux-arm-gnueabihf": "1.33.0", - "lightningcss-linux-arm64-gnu": "1.33.0", - "lightningcss-linux-arm64-musl": "1.33.0", - "lightningcss-linux-x64-gnu": "1.33.0", - "lightningcss-linux-x64-musl": "1.33.0", - "lightningcss-win32-arm64-msvc": "1.33.0", - "lightningcss-win32-x64-msvc": "1.33.0" - } - }, - "node_modules/lightningcss-android-arm64": { - "version": "1.33.0", - "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.33.0.tgz", - "integrity": "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-darwin-arm64": { - "version": "1.33.0", - "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.33.0.tgz", - "integrity": "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-darwin-x64": { - "version": "1.33.0", - "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.33.0.tgz", - "integrity": "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-freebsd-x64": { - "version": "1.33.0", - "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.33.0.tgz", - "integrity": "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-arm-gnueabihf": { - "version": "1.33.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.33.0.tgz", - "integrity": "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-arm64-gnu": { - "version": "1.33.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.33.0.tgz", - "integrity": "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-arm64-musl": { - "version": "1.33.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.33.0.tgz", - "integrity": "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-x64-gnu": { - "version": "1.33.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.33.0.tgz", - "integrity": "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-x64-musl": { - "version": "1.33.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.33.0.tgz", - "integrity": "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-win32-arm64-msvc": { - "version": "1.33.0", - "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.33.0.tgz", - "integrity": "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-win32-x64-msvc": { - "version": "1.33.0", - "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.33.0.tgz", - "integrity": "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/long": { - "version": "5.3.2", - "license": "Apache-2.0" - }, - "node_modules/lru-cache": { - "version": "11.5.2", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.2.tgz", - "integrity": "sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==", - "license": "BlueOak-1.0.0", - "engines": { - "node": "20 || >=22" - } - }, - "node_modules/magic-string": { - "version": "0.30.21", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", - "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.5" - } - }, - "node_modules/marked": { - "version": "18.0.7", - "resolved": "https://registry.npmjs.org/marked/-/marked-18.0.7.tgz", - "integrity": "sha512-iDVQ5ldaiKXn6b2JroX5kgRfmwgqolW7NpaEzTl1k/2Zh1njIEN9yniyLV/mOvWwtsE8OGgkjsCYvijuPk1dtA==", - "license": "MIT", - "bin": { - "marked": "bin/marked.js" - }, - "engines": { - "node": ">= 20" - } - }, - "node_modules/merge2": { - "version": "1.4.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 8" - } - }, - "node_modules/micromatch": { - "version": "4.0.8", - "dev": true, - "license": "MIT", - "dependencies": { - "braces": "^3.0.3", - "picomatch": "^2.3.1" - }, - "engines": { - "node": ">=8.6" - } - }, - "node_modules/mime-db": { - "version": "1.54.0", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime-types": { - "version": "3.0.2", - "license": "MIT", - "dependencies": { - "mime-db": "^1.54.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/mimic-response": { - "version": "3.1.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/minimatch": { - "version": "10.2.5", - "license": "BlueOak-1.0.0", - "dependencies": { - "brace-expansion": "^5.0.5" - }, - "engines": { - "node": "18 || 20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/minimist": { - "version": "1.2.8", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/minipass": { - "version": "7.1.3", - "license": "BlueOak-1.0.0", - "engines": { - "node": ">=16 || 14 >=14.17" - } - }, - "node_modules/mkdirp-classic": { - "version": "0.5.3", - "dev": true, - "license": "MIT" - }, - "node_modules/ms": { - "version": "2.1.3", - "license": "MIT" - }, - "node_modules/mz": { - "version": "2.7.0", - "license": "MIT", - "dependencies": { - "any-promise": "^1.0.0", - "object-assign": "^4.0.1", - "thenify-all": "^1.0.0" - } - }, - "node_modules/nanoid": { - "version": "3.3.17", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.17.tgz", - "integrity": "sha512-xQLf0A3HOMlgHq0n247/LRuAOYmB7dXJ/DvAxGvsSBij45XtBSmQycu+F8ODbHwns/XyFZagyL1+J0Offw1E0g==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "bin": { - "nanoid": "bin/nanoid.cjs" - }, - "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" - } - }, - "node_modules/napi-build-utils": { - "version": "2.0.0", - "dev": true, - "license": "MIT" - }, - "node_modules/netmask": { - "version": "2.1.1", - "license": "MIT", - "engines": { - "node": ">= 0.4.0" - } - }, - "node_modules/nice-try": { - "version": "1.0.5", - "dev": true, - "license": "MIT" - }, - "node_modules/node-abi": { - "version": "3.92.0", - "dev": true, - "license": "MIT", - "dependencies": { - "semver": "^7.3.5" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/node-addon-api": { - "version": "7.1.1", - "dev": true, - "license": "MIT" - }, - "node_modules/node-domexception": { - "version": "1.0.0", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/jimmywarting" - }, - { - "type": "github", - "url": "https://paypal.me/jimmywarting" - } - ], - "license": "MIT", - "engines": { - "node": ">=10.5.0" - } - }, - "node_modules/node-fetch": { - "version": "3.3.2", - "license": "MIT", - "dependencies": { - "data-uri-to-buffer": "^4.0.0", - "fetch-blob": "^3.1.4", - "formdata-polyfill": "^4.0.10" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/node-fetch" - } - }, - "node_modules/node-forge": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.4.0.tgz", - "integrity": "sha512-LarFH0+6VfriEhqMMcLX2F7SwSXeWwnEAJEsYm5QKWchiVYVvJyV9v7UDvUv+w5HO23ZpQTXDv/GxdDdMyOuoQ==", - "dev": true, - "license": "(BSD-3-Clause OR GPL-2.0)", - "engines": { - "node": ">= 6.13.0" - } - }, - "node_modules/npm-run-path": { - "version": "2.0.2", - "dev": true, - "license": "MIT", - "dependencies": { - "path-key": "^2.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/object-assign": { - "version": "4.1.1", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/obug": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.2.tgz", - "integrity": "sha512-AWGB9WFcRXOQs48Z/udjI5ZcZMHXwX8XPByNpOydgcGsDLIzjGizhoMWJyKAWze7AVW/2W1i+/gPX4YtKe5cyg==", - "dev": true, - "funding": [ - "https://github.com/sponsors/sxzz", - "https://opencollective.com/debug" - ], - "license": "MIT", - "engines": { - "node": ">=12.20.0" - } - }, - "node_modules/once": { - "version": "1.4.0", - "license": "ISC", - "dependencies": { - "wrappy": "1" - } - }, - "node_modules/openai": { - "version": "6.47.0", - "resolved": "https://registry.npmjs.org/openai/-/openai-6.47.0.tgz", - "integrity": "sha512-xYr+R9woSzWxVxeiqkkNbHhv89tZDEI6eBMbrdPnv3poh+mijHvbhS35a+3o6xHa411/ns8j5ENY3So9DCXWYw==", - "license": "Apache-2.0", - "peerDependencies": { - "@aws-sdk/credential-provider-node": ">=3.972.0 <4", - "@smithy/hash-node": ">=4.3.0 <5", - "@smithy/signature-v4": ">=5.4.0 <6", - "ws": "^8.18.0", - "zod": "^3.25 || ^4.0" - }, - "peerDependenciesMeta": { - "@aws-sdk/credential-provider-node": { - "optional": true - }, - "@smithy/hash-node": { - "optional": true - }, - "@smithy/signature-v4": { - "optional": true - }, - "ws": { - "optional": true - }, - "zod": { - "optional": true - } - } - }, - "node_modules/p-finally": { - "version": "1.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/p-retry": { - "version": "4.6.2", - "license": "MIT", - "dependencies": { - "@types/retry": "0.12.0", - "retry": "^0.13.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/pac-proxy-agent": { - "version": "7.2.0", - "license": "MIT", - "dependencies": { - "@tootallnate/quickjs-emscripten": "^0.23.0", - "agent-base": "^7.1.2", - "debug": "^4.3.4", - "get-uri": "^6.0.1", - "http-proxy-agent": "^7.0.0", - "https-proxy-agent": "^7.0.6", - "pac-resolver": "^7.0.1", - "socks-proxy-agent": "^8.0.5" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/pac-resolver": { - "version": "7.0.1", - "license": "MIT", - "dependencies": { - "degenerator": "^5.0.0", - "netmask": "^2.0.2" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/parse5": { - "version": "5.1.1", - "license": "MIT" - }, - "node_modules/parse5-htmlparser2-tree-adapter": { - "version": "6.0.1", - "license": "MIT", - "dependencies": { - "parse5": "^6.0.1" - } - }, - "node_modules/parse5-htmlparser2-tree-adapter/node_modules/parse5": { - "version": "6.0.1", - "license": "MIT" - }, - "node_modules/partial-json": { - "version": "0.1.7", - "license": "MIT" - }, - "node_modules/path-key": { - "version": "2.0.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/path-parse": { - "version": "1.0.7", - "dev": true, - "license": "MIT" - }, - "node_modules/path-scurry": { - "version": "2.0.2", - "license": "BlueOak-1.0.0", - "dependencies": { - "lru-cache": "^11.0.0", - "minipass": "^7.1.2" - }, - "engines": { - "node": "18 || 20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/pathe": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", - "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", - "dev": true, - "license": "MIT" - }, - "node_modules/pend": { - "version": "1.2.0", - "license": "MIT" - }, - "node_modules/pi-extension-custom-provider-anthropic": { - "resolved": "packages/coding-agent/examples/extensions/custom-provider-anthropic", - "link": true - }, - "node_modules/pi-extension-custom-provider-gitlab-duo": { - "resolved": "packages/coding-agent/examples/extensions/custom-provider-gitlab-duo", - "link": true - }, - "node_modules/pi-extension-sandbox": { - "resolved": "packages/coding-agent/examples/extensions/sandbox", - "link": true - }, - "node_modules/pi-extension-with-deps": { - "resolved": "packages/coding-agent/examples/extensions/with-deps", - "link": true - }, - "node_modules/picocolors": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", - "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "dev": true, - "license": "ISC" - }, - "node_modules/picomatch": { - "version": "2.3.2", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/postcss": { - "version": "8.5.25", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.25.tgz", - "integrity": "sha512-DTPx3RWSSnWyzLxQnlH0rJP+EW5ekl16ZU4/psbIhA0e53kJfdgaN5vKM+xP7yJtXVu+nfdVFmlgFDEKAe4Pyw==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/postcss" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "nanoid": "^3.3.16", - "picocolors": "^1.1.1", - "source-map-js": "^1.2.1" - }, - "engines": { - "node": "^10 || ^12 || >=14" - } - }, - "node_modules/prebuild-install": { - "version": "7.1.3", - "dev": true, - "license": "MIT", - "dependencies": { - "detect-libc": "^2.0.0", - "expand-template": "^2.0.3", - "github-from-package": "0.0.0", - "minimist": "^1.2.3", - "mkdirp-classic": "^0.5.3", - "napi-build-utils": "^2.0.0", - "node-abi": "^3.3.0", - "pump": "^3.0.0", - "rc": "^1.2.7", - "simple-get": "^4.0.0", - "tar-fs": "^2.0.0", - "tunnel-agent": "^0.6.0" - }, - "bin": { - "prebuild-install": "bin.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/proper-lockfile": { - "version": "4.1.2", - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.2.4", - "retry": "^0.12.0", - "signal-exit": "^3.0.2" - } - }, - "node_modules/proper-lockfile/node_modules/retry": { - "version": "0.12.0", - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, - "node_modules/protobufjs": { - "version": "7.6.4", - "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.6.4.tgz", - "integrity": "sha512-RJJPTTpvFfHcWLkIa2JFWK4XvtSzS0yEWDmunqHXli1h3JlkbcQZXDZdcWxv+JK3Xsl5/UFDPZ0iGm7DAengYw==", - "hasInstallScript": true, - "license": "BSD-3-Clause", - "dependencies": { - "@protobufjs/aspromise": "^1.1.2", - "@protobufjs/base64": "^1.1.2", - "@protobufjs/codegen": "^2.0.5", - "@protobufjs/eventemitter": "^1.1.1", - "@protobufjs/fetch": "^1.1.1", - "@protobufjs/float": "^1.0.2", - "@protobufjs/path": "^1.1.2", - "@protobufjs/pool": "^1.1.0", - "@protobufjs/utf8": "^1.1.1", - "@types/node": ">=13.7.0", - "long": "^5.3.2" - }, - "engines": { - "node": ">=12.0.0" - } - }, - "node_modules/proxy-agent": { - "version": "6.5.0", - "license": "MIT", - "dependencies": { - "agent-base": "^7.1.2", - "debug": "^4.3.4", - "http-proxy-agent": "^7.0.1", - "https-proxy-agent": "^7.0.6", - "lru-cache": "^7.14.1", - "pac-proxy-agent": "^7.1.0", - "proxy-from-env": "^1.1.0", - "socks-proxy-agent": "^8.0.5" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/proxy-agent/node_modules/lru-cache": { - "version": "7.18.3", - "license": "ISC", - "engines": { - "node": ">=12" - } - }, - "node_modules/proxy-from-env": { - "version": "1.1.0", - "license": "MIT" - }, - "node_modules/pump": { - "version": "3.0.4", - "license": "MIT", - "dependencies": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" - } - }, - "node_modules/queue-microtask": { - "version": "1.2.3", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/rc": { - "version": "1.2.8", - "dev": true, - "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", - "dependencies": { - "deep-extend": "^0.6.0", - "ini": "~1.3.0", - "minimist": "^1.2.0", - "strip-json-comments": "~2.0.1" - }, - "bin": { - "rc": "cli.js" - } - }, - "node_modules/readable-stream": { - "version": "3.6.2", - "dev": true, - "license": "MIT", - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/rechoir": { - "version": "0.6.2", - "dev": true, - "dependencies": { - "resolve": "^1.1.6" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/require-directory": { - "version": "2.1.1", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/resolve": { - "version": "1.22.12", - "dev": true, - "license": "MIT", - "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" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/retry": { - "version": "0.13.1", - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, - "node_modules/reusify": { - "version": "1.1.0", - "dev": true, - "license": "MIT", - "engines": { - "iojs": ">=1.0.0", - "node": ">=0.10.0" - } - }, - "node_modules/rolldown": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.1.5.tgz", - "integrity": "sha512-t9z29cJjXf/vxQ8dyhCSpt6H6aSwHTk8cT5I3iy6SMXuFpk5mB6PL6XfC8PCwrPTx93udwKUm9HRteAlTGBLiA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@oxc-project/types": "=0.139.0", - "@rolldown/pluginutils": "^1.0.0" - }, - "bin": { - "rolldown": "bin/cli.mjs" - }, - "engines": { - "node": "^20.19.0 || >=22.12.0" - }, - "optionalDependencies": { - "@rolldown/binding-android-arm64": "1.1.5", - "@rolldown/binding-darwin-arm64": "1.1.5", - "@rolldown/binding-darwin-x64": "1.1.5", - "@rolldown/binding-freebsd-x64": "1.1.5", - "@rolldown/binding-linux-arm-gnueabihf": "1.1.5", - "@rolldown/binding-linux-arm64-gnu": "1.1.5", - "@rolldown/binding-linux-arm64-musl": "1.1.5", - "@rolldown/binding-linux-ppc64-gnu": "1.1.5", - "@rolldown/binding-linux-s390x-gnu": "1.1.5", - "@rolldown/binding-linux-x64-gnu": "1.1.5", - "@rolldown/binding-linux-x64-musl": "1.1.5", - "@rolldown/binding-openharmony-arm64": "1.1.5", - "@rolldown/binding-wasm32-wasi": "1.1.5", - "@rolldown/binding-win32-arm64-msvc": "1.1.5", - "@rolldown/binding-win32-x64-msvc": "1.1.5" - } - }, - "node_modules/run-parallel": { - "version": "1.2.0", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "dependencies": { - "queue-microtask": "^1.2.2" - } - }, - "node_modules/rxjs": { - "version": "7.8.2", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "tslib": "^2.1.0" - } - }, - "node_modules/safe-buffer": { - "version": "5.2.1", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/semver": { - "version": "7.8.5", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", - "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/shebang-command": { - "version": "1.2.0", - "dev": true, - "license": "MIT", - "dependencies": { - "shebang-regex": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/shebang-regex": { - "version": "1.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/shell-quote": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.10.0.tgz", - "integrity": "sha512-w1aiOKwKuRgtwAReIIj89puqg+I7GvX4IbLrvmhXbzQsj1+Zwi4VO3+fa6ZF91TWSjIxoEkKnMeHcLEODK5ZXA==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/shelljs": { - "version": "0.9.2", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "execa": "^1.0.0", - "fast-glob": "^3.3.2", - "interpret": "^1.0.0", - "rechoir": "^0.6.2" - }, - "bin": { - "shjs": "bin/shjs" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/shx": { - "version": "0.4.0", - "dev": true, - "license": "MIT", - "dependencies": { - "minimist": "^1.2.8", - "shelljs": "^0.9.2" - }, - "bin": { - "shx": "lib/cli.js" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/siginfo": { - "version": "2.0.0", - "dev": true, - "license": "ISC" - }, - "node_modules/signal-exit": { - "version": "3.0.7", - "license": "ISC" - }, - "node_modules/simple-concat": { - "version": "1.0.1", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/simple-get": { - "version": "4.0.1", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "dependencies": { - "decompress-response": "^6.0.0", - "once": "^1.3.1", - "simple-concat": "^1.0.0" - } - }, - "node_modules/smart-buffer": { - "version": "4.2.0", - "license": "MIT", - "engines": { - "node": ">= 6.0.0", - "npm": ">= 3.0.0" - } - }, - "node_modules/socks": { - "version": "2.8.8", - "license": "MIT", - "dependencies": { - "ip-address": "^10.1.1", - "smart-buffer": "^4.2.0" - }, - "engines": { - "node": ">= 10.0.0", - "npm": ">= 3.0.0" - } - }, - "node_modules/socks-proxy-agent": { - "version": "8.0.5", - "license": "MIT", - "dependencies": { - "agent-base": "^7.1.2", - "debug": "^4.3.4", - "socks": "^2.8.3" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/source-map": { - "version": "0.6.1", - "license": "BSD-3-Clause", - "optional": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/source-map-js": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", - "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/stackback": { - "version": "0.0.2", - "dev": true, - "license": "MIT" - }, - "node_modules/std-env": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.1.0.tgz", - "integrity": "sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/string_decoder": { - "version": "1.3.0", - "dev": true, - "license": "MIT", - "dependencies": { - "safe-buffer": "~5.2.0" - } - }, - "node_modules/string-width": { - "version": "4.2.3", - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/string-width/node_modules/ansi-regex": { - "version": "5.0.1", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/string-width/node_modules/strip-ansi": { - "version": "6.0.1", - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-ansi": { - "version": "7.2.0", - "license": "MIT", - "dependencies": { - "ansi-regex": "^6.2.2" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, - "node_modules/strip-eof": { - "version": "1.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/strip-json-comments": { - "version": "2.0.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/strtok3": { - "version": "10.3.5", - "license": "MIT", - "dependencies": { - "@tokenizer/token": "^0.3.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/Borewit" - } - }, - "node_modules/supports-color": { - "version": "8.1.1", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/supports-color?sponsor=1" - } - }, - "node_modules/supports-preserve-symlinks-flag": { - "version": "1.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/tar-fs": { - "version": "2.1.4", - "dev": true, - "license": "MIT", - "dependencies": { - "chownr": "^1.1.1", - "mkdirp-classic": "^0.5.2", - "pump": "^3.0.0", - "tar-stream": "^2.1.4" - } - }, - "node_modules/tar-stream": { - "version": "2.2.0", - "dev": true, - "license": "MIT", - "dependencies": { - "bl": "^4.0.3", - "end-of-stream": "^1.4.1", - "fs-constants": "^1.0.0", - "inherits": "^2.0.3", - "readable-stream": "^3.1.1" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/thenify": { - "version": "3.3.1", - "license": "MIT", - "dependencies": { - "any-promise": "^1.0.0" - } - }, - "node_modules/thenify-all": { - "version": "1.6.0", - "license": "MIT", - "dependencies": { - "thenify": ">= 3.1.0 < 4" - }, - "engines": { - "node": ">=0.8" - } - }, - "node_modules/tinybench": { - "version": "2.9.0", - "dev": true, - "license": "MIT" - }, - "node_modules/tinyexec": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.2.4.tgz", - "integrity": "sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - } - }, - "node_modules/tinyglobby": { - "version": "0.2.17", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", - "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", - "dev": true, - "license": "MIT", - "dependencies": { - "fdir": "^6.5.0", - "picomatch": "^4.0.4" - }, - "engines": { - "node": ">=12.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/SuperchupuDev" - } - }, - "node_modules/tinyglobby/node_modules/fdir": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", - "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12.0.0" - }, - "peerDependencies": { - "picomatch": "^3 || ^4" - }, - "peerDependenciesMeta": { - "picomatch": { - "optional": true - } - } - }, - "node_modules/tinyglobby/node_modules/picomatch": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", - "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/tinyrainbow": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.1.tgz", - "integrity": "sha512-yau8yJdTt989Mm0Bd/236QnzEiPf2xLLTqUZRUJOo/3CB078LSwzei343DgtJVmfJKJE3TMINY1u42SQsP6mXw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/to-regex-range": { - "version": "5.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "is-number": "^7.0.0" - }, - "engines": { - "node": ">=8.0" - } - }, - "node_modules/token-types": { - "version": "6.1.2", - "license": "MIT", - "dependencies": { - "@borewit/text-codec": "^0.2.1", - "@tokenizer/token": "^0.3.0", - "ieee754": "^1.2.1" - }, - "engines": { - "node": ">=14.16" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/Borewit" - } - }, - "node_modules/tree-kill": { - "version": "1.2.2", - "dev": true, - "license": "MIT", - "bin": { - "tree-kill": "cli.js" - } - }, - "node_modules/ts-algebra": { - "version": "2.0.0", - "license": "MIT" - }, - "node_modules/tslib": { - "version": "2.8.1", - "license": "0BSD" - }, - "node_modules/tsx": { - "version": "4.23.1", - "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.23.1.tgz", - "integrity": "sha512-GQHnkIfxyx1wYCOS/wonik5MVRZU9hi1TEZmzGZSCJB1y9YgoZ8H6itNE/u4suE+yLmOzuE4E5S4TZ/ZX2wcWQ==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "esbuild": "~0.28.0" - }, - "bin": { - "tsx": "dist/cli.mjs" - }, - "engines": { - "node": ">=18.0.0" - }, - "optionalDependencies": { - "fsevents": "~2.3.3" - } - }, - "node_modules/tunnel-agent": { - "version": "0.6.0", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "safe-buffer": "^5.0.1" - }, - "engines": { - "node": "*" - } - }, - "node_modules/typebox": { - "version": "1.3.10", - "resolved": "https://registry.npmjs.org/typebox/-/typebox-1.3.10.tgz", - "integrity": "sha512-L0MT00X96q0P30f1NrGULgaSbmu8hfTjWbLULeVoM+j5u0jR5wyefoDquX2NmRa39f0KiNIBo1wWSaVvHJTZlA==", - "license": "MIT" - }, - "node_modules/typescript": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-7.0.2.tgz", - "integrity": "sha512-8FYau96o3NKOhbjKi/qNvG/W5jhzxkbdm5sj9AbZ/5T5sWqn3hJgLfGx27sRKZWTvyzCP8dLRBTf5tBTSRVUNA==", - "dev": true, - "license": "Apache-2.0", - "bin": { - "tsc": "bin/tsc" - }, - "engines": { - "node": ">=16.20.0" - }, - "optionalDependencies": { - "@typescript/typescript-aix-ppc64": "7.0.2", - "@typescript/typescript-darwin-arm64": "7.0.2", - "@typescript/typescript-darwin-x64": "7.0.2", - "@typescript/typescript-freebsd-arm64": "7.0.2", - "@typescript/typescript-freebsd-x64": "7.0.2", - "@typescript/typescript-linux-arm": "7.0.2", - "@typescript/typescript-linux-arm64": "7.0.2", - "@typescript/typescript-linux-loong64": "7.0.2", - "@typescript/typescript-linux-mips64el": "7.0.2", - "@typescript/typescript-linux-ppc64": "7.0.2", - "@typescript/typescript-linux-riscv64": "7.0.2", - "@typescript/typescript-linux-s390x": "7.0.2", - "@typescript/typescript-linux-x64": "7.0.2", - "@typescript/typescript-netbsd-arm64": "7.0.2", - "@typescript/typescript-netbsd-x64": "7.0.2", - "@typescript/typescript-openbsd-arm64": "7.0.2", - "@typescript/typescript-openbsd-x64": "7.0.2", - "@typescript/typescript-sunos-x64": "7.0.2", - "@typescript/typescript-win32-arm64": "7.0.2", - "@typescript/typescript-win32-x64": "7.0.2" - } - }, - "node_modules/uint8array-extras": { - "version": "1.5.0", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/undici": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/undici/-/undici-7.29.0.tgz", - "integrity": "sha512-IDxfleLmmbSskfWSUATiN1nfn2rDuvnMOqb5CWR92iIfojA0Ud+ulOAAEQ57LPr9rWmsreUyf5lwyao+7GNNVw==", - "license": "MIT", - "engines": { - "node": ">=20.18.1" - } - }, - "node_modules/undici-types": { - "version": "6.21.0", - "license": "MIT" - }, - "node_modules/util-deprecate": { - "version": "1.0.2", - "dev": true, - "license": "MIT" - }, - "node_modules/uuid": { - "version": "14.0.0", - "funding": [ - "https://github.com/sponsors/broofa", - "https://github.com/sponsors/ctavan" - ], - "license": "MIT", - "bin": { - "uuid": "dist-node/bin/uuid" - } - }, - "node_modules/vite": { - "version": "8.1.5", - "resolved": "https://registry.npmjs.org/vite/-/vite-8.1.5.tgz", - "integrity": "sha512-7ULLwsCdYx/nRyrpiEwvqb5TFHrMVZyBt+rg/OAXT7rgj/z+DtTDyKFeLAdDkubDVDKD8jOsndmy7m55XcfUsw==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "lightningcss": "^1.32.0", - "picomatch": "^4.0.5", - "postcss": "^8.5.17", - "rolldown": "~1.1.5", - "tinyglobby": "^0.2.17" - }, - "bin": { - "vite": "bin/vite.js" - }, - "engines": { - "node": "^20.19.0 || >=22.12.0" - }, - "funding": { - "url": "https://github.com/vitejs/vite?sponsor=1" - }, - "optionalDependencies": { - "fsevents": "~2.3.3" - }, - "peerDependencies": { - "@types/node": "^20.19.0 || >=22.12.0", - "@vitejs/devtools": "^0.3.0", - "esbuild": "^0.27.0 || ^0.28.0", - "jiti": ">=1.21.0", - "less": "^4.0.0", - "sass": "^1.70.0", - "sass-embedded": "^1.70.0", - "stylus": ">=0.54.8", - "sugarss": "^5.0.0", - "terser": "^5.16.0", - "tsx": "^4.8.1", - "yaml": "^2.4.2" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - }, - "@vitejs/devtools": { - "optional": true - }, - "esbuild": { - "optional": true - }, - "jiti": { - "optional": true - }, - "less": { - "optional": true - }, - "sass": { - "optional": true - }, - "sass-embedded": { - "optional": true - }, - "stylus": { - "optional": true - }, - "sugarss": { - "optional": true - }, - "terser": { - "optional": true - }, - "tsx": { - "optional": true - }, - "yaml": { - "optional": true - } - } - }, - "node_modules/vite/node_modules/picomatch": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", - "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/vitest": { - "version": "4.1.10", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.10.tgz", - "integrity": "sha512-R9jUTe5S4Qb0HCd4TNqpC7oGcrMssMRGXLW80ubjWsW9VH5GF8y1Y0SFLY9AbqSk6nt0PnOx4H4WNJYZ13GUPw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/expect": "4.1.10", - "@vitest/mocker": "4.1.10", - "@vitest/pretty-format": "4.1.10", - "@vitest/runner": "4.1.10", - "@vitest/snapshot": "4.1.10", - "@vitest/spy": "4.1.10", - "@vitest/utils": "4.1.10", - "es-module-lexer": "^2.0.0", - "expect-type": "^1.3.0", - "magic-string": "^0.30.21", - "obug": "^2.1.1", - "pathe": "^2.0.3", - "picomatch": "^4.0.3", - "std-env": "^4.0.0-rc.1", - "tinybench": "^2.9.0", - "tinyexec": "^1.0.2", - "tinyglobby": "^0.2.15", - "tinyrainbow": "^3.1.0", - "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", - "why-is-node-running": "^2.3.0" - }, - "bin": { - "vitest": "vitest.mjs" - }, - "engines": { - "node": "^20.0.0 || ^22.0.0 || >=24.0.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - }, - "peerDependencies": { - "@edge-runtime/vm": "*", - "@opentelemetry/api": "^1.9.0", - "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", - "@vitest/browser-playwright": "4.1.10", - "@vitest/browser-preview": "4.1.10", - "@vitest/browser-webdriverio": "4.1.10", - "@vitest/coverage-istanbul": "4.1.10", - "@vitest/coverage-v8": "4.1.10", - "@vitest/ui": "4.1.10", - "happy-dom": "*", - "jsdom": "*", - "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" - }, - "peerDependenciesMeta": { - "@edge-runtime/vm": { - "optional": true - }, - "@opentelemetry/api": { - "optional": true - }, - "@types/node": { - "optional": true - }, - "@vitest/browser-playwright": { - "optional": true - }, - "@vitest/browser-preview": { - "optional": true - }, - "@vitest/browser-webdriverio": { - "optional": true - }, - "@vitest/coverage-istanbul": { - "optional": true - }, - "@vitest/coverage-v8": { - "optional": true - }, - "@vitest/ui": { - "optional": true - }, - "happy-dom": { - "optional": true - }, - "jsdom": { - "optional": true - }, - "vite": { - "optional": false - } - } - }, - "node_modules/vitest/node_modules/picomatch": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", - "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/web-streams-polyfill": { - "version": "3.3.3", - "license": "MIT", - "engines": { - "node": ">= 8" - } - }, - "node_modules/which": { - "version": "1.3.1", - "dev": true, - "license": "ISC", - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "which": "bin/which" - } - }, - "node_modules/why-is-node-running": { - "version": "2.3.0", - "dev": true, - "license": "MIT", - "dependencies": { - "siginfo": "^2.0.0", - "stackback": "0.0.2" - }, - "bin": { - "why-is-node-running": "cli.js" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/wrap-ansi": { - "version": "7.0.0", - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/wrap-ansi/node_modules/ansi-regex": { - "version": "5.0.1", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/wrap-ansi/node_modules/strip-ansi": { - "version": "6.0.1", - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/wrappy": { - "version": "1.0.2", - "license": "ISC" - }, - "node_modules/ws": { - "version": "8.21.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz", - "integrity": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==", - "license": "MIT", - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": ">=5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - }, - "node_modules/y18n": { - "version": "5.0.8", - "license": "ISC", - "engines": { - "node": ">=10" - } - }, - "node_modules/yaml": { - "version": "2.9.0", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.0.tgz", - "integrity": "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==", - "license": "ISC", - "peer": true, - "bin": { - "yaml": "bin.mjs" - }, - "engines": { - "node": ">= 14.6" - }, - "funding": { - "url": "https://github.com/sponsors/eemeli" - } - }, - "node_modules/yargs": { - "version": "16.2.2", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.2.tgz", - "integrity": "sha512-Nt9ZJjXTv5R8MHbqby/wXQ6Gi0Bb3TcYZkR1bzuL4yB2OxWPkXknz513gEF0GoA6tn00UpbPvERW8rzCuWCA6w==", - "license": "MIT", - "dependencies": { - "cliui": "^7.0.2", - "escalade": "^3.1.1", - "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", - "string-width": "^4.2.0", - "y18n": "^5.0.5", - "yargs-parser": "^20.2.2" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/yargs-parser": { - "version": "20.2.9", - "license": "ISC", - "engines": { - "node": ">=10" - } - }, - "node_modules/yauzl": { - "version": "2.10.0", - "license": "MIT", - "dependencies": { - "buffer-crc32": "~0.2.3", - "fd-slicer": "~1.1.0" - } - }, - "node_modules/zod": { - "version": "3.25.76", - "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", - "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", - "license": "MIT", - "peer": true, - "funding": { - "url": "https://github.com/sponsors/colinhacks" - } - }, - "node_modules/zod-to-json-schema": { - "version": "3.25.2", - "license": "ISC", - "peerDependencies": { - "zod": "^3.25.28 || ^4" - } - }, - "packages/agent": { - "name": "@earendil-works/pi-agent-core", - "version": "0.9.1", - "license": "MIT", - "dependencies": { - "@earendil-works/pi-ai": "^0.9.1", - "typebox": "^1.3.9" - }, - "devDependencies": { - "@types/node": "^24.3.0", - "typescript": "^7.0.2", - "vitest": "^4.1.10" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "packages/agent/node_modules/@types/node": { - "version": "24.13.3", - "resolved": "https://registry.npmjs.org/@types/node/-/node-24.13.3.tgz", - "integrity": "sha512-Dh8vAsV36ig5wa9OX4pXvMc9D3Veibfw2wix0CUwYODLD8nkj9UsLjASr49nPg+2eKzxhBV+v7L8pXvT4e639Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "undici-types": "~7.18.0" - } - }, - "packages/agent/node_modules/undici-types": { - "version": "7.18.2", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", - "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", - "dev": true, - "license": "MIT" - }, - "packages/ai": { - "name": "@earendil-works/pi-ai", - "version": "0.9.1", - "license": "MIT", - "dependencies": { - "@anthropic-ai/sdk": "^0.91.1", - "@aws-sdk/client-bedrock-runtime": "^3.1095.0", - "@google/genai": "^1.40.0", - "@mistralai/mistralai": "^2.2.0", - "chalk": "^5.6.2", - "openai": "6.47.0", - "partial-json": "^0.1.7", - "proxy-agent": "^6.5.0", - "typebox": "^1.3.9", - "undici": "^7.29.0", - "zod-to-json-schema": "^3.24.6" - }, - "bin": { - "pi-ai": "dist/cli.js" - }, - "devDependencies": { - "@types/node": "^24.3.0", - "canvas": "^3.2.0", - "vitest": "^4.1.10" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "packages/ai/node_modules/@types/node": { - "version": "24.13.3", - "resolved": "https://registry.npmjs.org/@types/node/-/node-24.13.3.tgz", - "integrity": "sha512-Dh8vAsV36ig5wa9OX4pXvMc9D3Veibfw2wix0CUwYODLD8nkj9UsLjASr49nPg+2eKzxhBV+v7L8pXvT4e639Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "undici-types": "~7.18.0" - } - }, - "packages/ai/node_modules/undici-types": { - "version": "7.18.2", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", - "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", - "dev": true, - "license": "MIT" - }, - "packages/coding-agent": { - "name": "@earendil-works/pi-coding-agent", - "version": "0.9.1", - "hasInstallScript": true, - "license": "MIT", - "dependencies": { - "@agentclientprotocol/sdk": "^1.3.0", - "@earendil-works/pi-agent-core": "^0.9.1", - "@earendil-works/pi-ai": "^0.9.1", - "@earendil-works/pi-tui": "^0.9.1", - "@silvia-odwyer/photon-node": "^0.3.4", - "chalk": "^5.5.0", - "cli-highlight": "^2.1.11", - "diff": "^9.0.0", - "extract-zip": "^2.0.1", - "file-type": "^21.1.1", - "glob": "^13.0.1", - "grok-mermaid": "0.2.3", - "hosted-git-info": "^9.0.2", - "ignore": "^7.0.5", - "jiti": "^2.7.0", - "marked": "^18.0.7", - "minimatch": "^10.2.3", - "proper-lockfile": "^4.1.2", - "strip-ansi": "^7.1.0", - "typebox": "^1.3.9", - "undici": "^7.29.0", - "uuid": "^14.0.0", - "yaml": "^2.9.0" - }, - "bin": { - "pi": "dist/bundle/cli.js" - }, - "devDependencies": { - "@types/diff": "^8.0.0", - "@types/hosted-git-info": "^3.0.5", - "@types/ms": "^2.1.0", - "@types/node": "^24.3.0", - "@types/proper-lockfile": "^4.1.4", - "esbuild": "^0.28.1", - "shx": "^0.4.0", - "typescript": "^7.0.2", - "vitest": "^4.1.10" - }, - "engines": { - "node": ">=22.8.0" - }, - "optionalDependencies": { - "@mariozechner/clipboard": "^0.3.9" - } - }, - "packages/coding-agent/examples/extensions/custom-provider-anthropic": { - "name": "pi-extension-custom-provider-anthropic", - "version": "0.1.1", - "dependencies": { - "@anthropic-ai/sdk": "^0.52.0" - } - }, - "packages/coding-agent/examples/extensions/custom-provider-gitlab-duo": { - "name": "pi-extension-custom-provider-gitlab-duo", - "version": "0.1.1" - }, - "packages/coding-agent/examples/extensions/sandbox": { - "name": "pi-extension-sandbox", - "version": "1.5.1", - "dependencies": { - "@anthropic-ai/sandbox-runtime": "^0.0.55" - } - }, - "packages/coding-agent/examples/extensions/sandbox/node_modules/@anthropic-ai/sandbox-runtime": { - "version": "0.0.26", - "license": "Apache-2.0", - "dependencies": { - "@pondwader/socks5-server": "^1.0.10", - "@types/lodash-es": "^4.17.12", - "commander": "^12.1.0", - "lodash-es": "^4.17.21", - "shell-quote": "^1.8.3", - "zod": "^3.24.1" - }, - "bin": { - "srt": "dist/cli.js" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "packages/coding-agent/examples/extensions/sandbox/node_modules/@types/lodash": { - "version": "4.17.23", - "license": "MIT" - }, - "packages/coding-agent/examples/extensions/sandbox/node_modules/@types/lodash-es": { - "version": "4.17.12", - "license": "MIT", - "dependencies": { - "@types/lodash": "*" - } - }, - "packages/coding-agent/examples/extensions/sandbox/node_modules/lodash-es": { - "version": "4.18.1", - "license": "MIT" - }, - "packages/coding-agent/examples/extensions/with-deps": { - "name": "pi-extension-with-deps", - "version": "0.1.1", - "dependencies": { - "ms": "^2.1.3" - }, - "devDependencies": { - "@types/ms": "^2.1.0" - } - }, - "packages/coding-agent/node_modules/@anthropic-ai/sdk": { - "version": "0.52.0", - "license": "MIT", - "bin": { - "anthropic-ai-sdk": "bin/cli" - } - }, - "packages/coding-agent/node_modules/@types/node": { - "version": "24.13.3", - "resolved": "https://registry.npmjs.org/@types/node/-/node-24.13.3.tgz", - "integrity": "sha512-Dh8vAsV36ig5wa9OX4pXvMc9D3Veibfw2wix0CUwYODLD8nkj9UsLjASr49nPg+2eKzxhBV+v7L8pXvT4e639Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "undici-types": "~7.18.0" - } - }, - "packages/coding-agent/node_modules/undici-types": { - "version": "7.18.2", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", - "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", - "dev": true, - "license": "MIT" - }, - "packages/tui": { - "name": "@earendil-works/pi-tui", - "version": "0.9.1", - "license": "MIT", - "dependencies": { - "@types/mime-types": "^3.0.1", - "chalk": "^5.5.0", - "get-east-asian-width": "^1.6.0", - "marked": "^18.0.7", - "mime-types": "^3.0.1" - }, - "devDependencies": { - "@xterm/headless": "^6.0.0", - "@xterm/xterm": "^6.0.0" - }, - "engines": { - "node": ">=20.0.0" - }, - "optionalDependencies": { - "koffi": "^2.9.0" - } - } - } -} diff --git a/package.json b/package.json index 6d64610db8e..c406109e150 100644 --- a/package.json +++ b/package.json @@ -2,6 +2,7 @@ "name": "prime-agent", "private": true, "type": "module", + "packageManager": "bun@1.3.14", "workspaces": [ "packages/*", "packages/coding-agent/examples/extensions/with-deps", @@ -55,9 +56,6 @@ }, "overrides": { "rimraf": "6.1.2", - "gaxios": { - "rimraf": "6.1.2" - }, "shell-quote": "^1.10.0" } } diff --git a/packages/coding-agent/package.json b/packages/coding-agent/package.json index b9dafc79904..994c27e8d17 100644 --- a/packages/coding-agent/package.json +++ b/packages/coding-agent/package.json @@ -72,10 +72,7 @@ "yaml": "^2.9.0" }, "overrides": { - "rimraf": "6.1.2", - "gaxios": { - "rimraf": "6.1.2" - } + "rimraf": "6.1.2" }, "optionalDependencies": { "@mariozechner/clipboard": "^0.3.9" From 1eb597fe77684b8ee38ff111a240b3dbbdffb14f Mon Sep 17 00:00:00 2001 From: Seth Date: Tue, 1 Sep 2026 00:55:04 -0400 Subject: [PATCH 02/45] build: make bun the primary development runtime --- .husky/pre-commit | 2 +- bun.lock | 33 ++-------- package.json | 27 +++++---- packages/agent/package.json | 8 +-- packages/ai/package.json | 14 ++--- packages/coding-agent/package.json | 18 +++--- packages/coding-agent/scripts/copy-assets.ts | 63 ++++++++++++++++++++ packages/coding-agent/src/bun/cli.ts | 2 +- packages/tui/package.json | 8 +-- prime-agent.sh | 27 +++++---- scripts/dev.ts | 41 +++++++++++++ scripts/remove-paths.ts | 9 +++ 12 files changed, 176 insertions(+), 76 deletions(-) create mode 100644 packages/coding-agent/scripts/copy-assets.ts create mode 100644 scripts/dev.ts create mode 100644 scripts/remove-paths.ts diff --git a/.husky/pre-commit b/.husky/pre-commit index 64a0169cd05..4453e34be2c 100755 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -4,7 +4,7 @@ STAGED_FILES=$(git diff --cached --name-only) echo "Running formatting, linting, and type checking..." -npm run check +bun run check if [ $? -ne 0 ]; then echo "Checks failed. Please fix the errors before committing." exit 1 diff --git a/bun.lock b/bun.lock index aa34d8105e5..8fafded7e3b 100644 --- a/bun.lock +++ b/bun.lock @@ -13,7 +13,6 @@ "@biomejs/biome": "2.5.5", "@types/node": "^22.10.5", "@typescript/native-preview": "7.0.0-dev.20260120.1", - "concurrently": "^9.2.4", "husky": "^9.1.7", "jiti": "^2.7.0", "shx": "^0.4.0", @@ -44,7 +43,7 @@ "@anthropic-ai/sdk": "^0.91.1", "@aws-sdk/client-bedrock-runtime": "^3.1095.0", "@google/genai": "^1.40.0", - "@mistralai/mistralai": "^2.2.0", + "@mistralai/mistralai": "2.2.1", "chalk": "^5.6.2", "openai": "6.47.0", "partial-json": "^0.1.7", @@ -309,7 +308,7 @@ "@mariozechner/clipboard-win32-x64-msvc": ["@mariozechner/clipboard-win32-x64-msvc@0.3.9", "", { "os": "win32", "cpu": "x64" }, "sha512-ihQC3EufqEY81vhXBgVBtK4prL+wc62zJsSvxrgz7K1hsdt6OObz6v9p3Rn1OG3GJksTTKMJF0u/guMISHPhSA=="], - "@mistralai/mistralai": ["@mistralai/mistralai@2.6.4", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.40.0", "ws": "^8.18.0", "zod": "^3.25.0 || ^4.0.0", "zod-to-json-schema": "^3.25.0" }, "peerDependencies": { "@opentelemetry/api": "^1.9.0", "@opentelemetry/exporter-trace-otlp-http": "^0.220.0", "@opentelemetry/resources": "^2.9.0", "@opentelemetry/sdk-trace-base": "^2.9.0" }, "optionalPeers": ["@opentelemetry/api", "@opentelemetry/exporter-trace-otlp-http", "@opentelemetry/resources", "@opentelemetry/sdk-trace-base"] }, "sha512-PPt4GyJqs2hEsWrYCJZK5f0ORmT+L2MSm75LVGD7kBLf6ZKsoDpld/FRBQXr8xG6iFCBOFJFYzvGYhUb+UCkbw=="], + "@mistralai/mistralai": ["@mistralai/mistralai@2.2.1", "", { "dependencies": { "ws": "^8.18.0", "zod": "^3.25.0 || ^4.0.0", "zod-to-json-schema": "^3.25.0" } }, "sha512-uKU8CZmL2RzYKmplsU01hii4p3pe4HqJefpWNRWXm1Tcm0Sm4xXfwSLIy4k7ZCPlbETCGcp69E7hZs+WOJ5itQ=="], "@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="], @@ -317,8 +316,6 @@ "@nodelib/fs.walk": ["@nodelib/fs.walk@1.2.8", "", { "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" } }, "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg=="], - "@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.43.0", "", {}, "sha512-eSYWTm620tTk45EKSedaUL8MFYI8hW164hIXsgIHyxu3VobUB3fFCu5t0hQby6OoWRPsG1KkKUG2M5UadiLiVg=="], - "@oxc-project/types": ["@oxc-project/types@0.146.0", "", {}, "sha512-XC0QsnnhVe7sLIWmYmdPw7x5P0h4W8vUU3Nv1ySgWXtvCz8NizoAEpGXA0sOYoJQV2Rl13LgURAHQ5cI5ILCSA=="], "@pondwader/socks5-server": ["@pondwader/socks5-server@1.0.10", "", {}, "sha512-bQY06wzzR8D2+vVCUoBsr5QS2U6UgPUQRmErNwtsuI6vLcyRKkafjkr3KxbtGFf9aBBIV2mcvlsKD1UYaIV+sg=="], @@ -535,7 +532,7 @@ "cli-highlight": ["cli-highlight@2.1.11", "", { "dependencies": { "chalk": "^4.0.0", "highlight.js": "^10.7.1", "mz": "^2.4.0", "parse5": "^5.1.1", "parse5-htmlparser2-tree-adapter": "^6.0.0", "yargs": "^16.0.0" }, "bin": { "highlight": "bin/highlight" } }, "sha512-9KDcoEVwyUXrjcJNvHD0NFc/hiwe/WPVYIleQh2O1N2Zro5gWJZ/K+3DGn8w8P/F6FxOgzyC5bxDyHIgCSPhGg=="], - "cliui": ["cliui@8.0.1", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ=="], + "cliui": ["cliui@7.0.4", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.0", "wrap-ansi": "^7.0.0" } }, "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ=="], "color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], @@ -543,8 +540,6 @@ "commander": ["commander@12.1.0", "", {}, "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA=="], - "concurrently": ["concurrently@9.2.4", "", { "dependencies": { "chalk": "4.1.2", "rxjs": "7.8.2", "shell-quote": "1.9.0", "supports-color": "8.1.1", "tree-kill": "1.2.2", "yargs": "17.7.2" }, "bin": { "conc": "dist/bin/concurrently.js", "concurrently": "dist/bin/concurrently.js" } }, "sha512-TZ0CEhyzvFjgtAvHTusDMgj7wNdihCh7LLLrzdUOXIhdlnL2JBBGA9eJxR24rtqgmdjh3OA3hrN1rCHj6HM8qA=="], - "convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="], "cross-spawn": ["cross-spawn@6.0.6", "", { "dependencies": { "nice-try": "^1.0.4", "path-key": "^2.0.1", "semver": "^5.5.0", "shebang-command": "^1.2.0", "which": "^1.2.9" } }, "sha512-VqCUuhcd1iB+dsv8gxPttb5iZh/D0iubSP21g36KXdEuf6I5JiioesUVjpCdHV9MZRUfVFlvwtIUyPfxo5trtw=="], @@ -849,8 +844,6 @@ "run-parallel": ["run-parallel@1.2.0", "", { "dependencies": { "queue-microtask": "^1.2.2" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="], - "rxjs": ["rxjs@7.8.2", "", { "dependencies": { "tslib": "^2.1.0" } }, "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA=="], - "safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="], "semver": ["semver@7.8.5", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA=="], @@ -923,8 +916,6 @@ "token-types": ["token-types@6.1.2", "", { "dependencies": { "@borewit/text-codec": "^0.2.1", "@tokenizer/token": "^0.3.0", "ieee754": "^1.2.1" } }, "sha512-dRXchy+C0IgK8WPC6xvCHFRIWYUbqqdEIKPaKo/AcTUNzwLTK6AH7RjdLWsEZcAN/TBdtfUw3PYEgPr5VPr6ww=="], - "tree-kill": ["tree-kill@1.2.2", "", { "bin": { "tree-kill": "cli.js" } }, "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A=="], - "ts-algebra": ["ts-algebra@2.0.0", "", {}, "sha512-FPAhNPFMrkwz76P7cdjdmiShwMynZYN6SgOujD1urY4oNm80Ou9oMdmbR45LotcKOXoy7wSmHkRFE6Mxbrhefw=="], "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], @@ -967,9 +958,9 @@ "yaml": ["yaml@2.9.0", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA=="], - "yargs": ["yargs@17.7.2", "", { "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.3", "y18n": "^5.0.5", "yargs-parser": "^21.1.1" } }, "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w=="], + "yargs": ["yargs@16.2.2", "", { "dependencies": { "cliui": "^7.0.2", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.0", "y18n": "^5.0.5", "yargs-parser": "^20.2.2" } }, "sha512-Nt9ZJjXTv5R8MHbqby/wXQ6Gi0Bb3TcYZkR1bzuL4yB2OxWPkXknz513gEF0GoA6tn00UpbPvERW8rzCuWCA6w=="], - "yargs-parser": ["yargs-parser@21.1.1", "", {}, "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="], + "yargs-parser": ["yargs-parser@20.2.9", "", {}, "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w=="], "yauzl": ["yauzl@2.10.0", "", { "dependencies": { "buffer-crc32": "~0.2.3", "fd-slicer": "~1.1.0" } }, "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g=="], @@ -991,12 +982,8 @@ "cli-highlight/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], - "cli-highlight/yargs": ["yargs@16.2.2", "", { "dependencies": { "cliui": "^7.0.2", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.0", "y18n": "^5.0.5", "yargs-parser": "^20.2.2" } }, "sha512-Nt9ZJjXTv5R8MHbqby/wXQ6Gi0Bb3TcYZkR1bzuL4yB2OxWPkXknz513gEF0GoA6tn00UpbPvERW8rzCuWCA6w=="], - "cliui/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], - "concurrently/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], - "cross-spawn/semver": ["semver@5.7.2", "", { "bin": { "semver": "bin/semver" } }, "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g=="], "execa/get-stream": ["get-stream@4.1.0", "", { "dependencies": { "pump": "^3.0.0" } }, "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w=="], @@ -1033,22 +1020,12 @@ "cli-highlight/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], - "cli-highlight/yargs/cliui": ["cliui@7.0.4", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.0", "wrap-ansi": "^7.0.0" } }, "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ=="], - - "cli-highlight/yargs/yargs-parser": ["yargs-parser@20.2.9", "", {}, "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w=="], - "cliui/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], - "concurrently/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], - "protobufjs/@types/node/undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="], "string-width/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], "wrap-ansi/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], - - "cli-highlight/yargs/cliui/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], - - "cli-highlight/yargs/cliui/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], } } diff --git a/package.json b/package.json index c406109e150..6d936c8d7eb 100644 --- a/package.json +++ b/package.json @@ -11,15 +11,15 @@ "packages/coding-agent/examples/extensions/sandbox" ], "scripts": { - "clean": "npm run clean --workspaces", - "build": "cd packages/tui && npm run build && cd ../ai && npm run build && cd ../agent && npm run build && cd ../coding-agent && npm run build", - "dev": "concurrently --names \"ai,agent,coding-agent,tui\" --prefix-colors \"cyan,yellow,red,magenta\" \"cd packages/ai && npm run dev\" \"cd packages/agent && npm run dev\" \"cd packages/coding-agent && npm run dev\" \"cd packages/tui && npm run dev\"", - "dev:tsc": "cd packages/ai && npm run dev:tsc", - "check": "biome check --write --error-on-warnings . && tsgo --noEmit && npm run check:installer && npm run check:browser-smoke", - "check:installer": "node scripts/check-installer-render.mjs", - "check:browser-smoke": "node scripts/check-browser-smoke.mjs", - "profile:tui": "node scripts/profile-coding-agent-node.mjs --mode tui", - "profile:rpc": "node scripts/profile-coding-agent-node.mjs --mode rpc", + "clean": "bun scripts/remove-paths.ts packages/tui/dist packages/ai/dist packages/agent/dist packages/coding-agent/dist", + "build": "bun run --cwd packages/tui build && bun run --cwd packages/ai build && bun run --cwd packages/agent build && bun run --cwd packages/coding-agent build", + "dev": "bun scripts/dev.ts", + "dev:tsc": "bun run --cwd packages/ai dev:tsc", + "check": "bun --bun biome check --write --error-on-warnings . && bun run check:type && bun run check:installer && bun run check:browser-smoke", + "check:installer": "bun scripts/check-installer-render.mjs", + "check:browser-smoke": "bun scripts/check-browser-smoke.mjs", + "profile:tui": "bun scripts/profile-coding-agent-node.mjs --mode tui", + "profile:rpc": "bun scripts/profile-coding-agent-node.mjs --mode rpc", "test": "npm run test --workspaces --if-present", "version:patch": "npm version patch -ws --no-git-tag-version && node scripts/sync-versions.js && shx rm -rf node_modules packages/*/node_modules package-lock.json && npm install", "version:minor": "npm version minor -ws --no-git-tag-version && node scripts/sync-versions.js && shx rm -rf node_modules packages/*/node_modules package-lock.json && npm install", @@ -32,14 +32,16 @@ "release:patch": "node scripts/release.mjs patch", "release:minor": "node scripts/release.mjs minor", "release:major": "node scripts/release.mjs major", - "prepare": "husky" + "prepare": "bun --bun husky", + "build:binary": "bun run --cwd packages/coding-agent build:binary", + "dev:cli": "bun packages/coding-agent/src/bun/cli.ts", + "check:type": "bun --bun tsgo --noEmit" }, "devDependencies": { "@anthropic-ai/sandbox-runtime": "^0.0.55", "@biomejs/biome": "2.5.5", "@types/node": "^22.10.5", "@typescript/native-preview": "7.0.0-dev.20260120.1", - "concurrently": "^9.2.4", "husky": "^9.1.7", "jiti": "^2.7.0", "shx": "^0.4.0", @@ -47,7 +49,8 @@ "typescript": "^7.0.2" }, "engines": { - "node": ">=22.8.0" + "node": ">=22.8.0", + "bun": "1.3.14" }, "version": "0.9.1", "dependencies": { diff --git a/packages/agent/package.json b/packages/agent/package.json index e512344cc35..751c50bac9f 100644 --- a/packages/agent/package.json +++ b/packages/agent/package.json @@ -10,11 +10,11 @@ "README.md" ], "scripts": { - "clean": "shx rm -rf dist", - "build": "tsgo -p tsconfig.build.json", - "dev": "tsgo -p tsconfig.build.json --watch --preserveWatchOutput", + "clean": "bun ../../scripts/remove-paths.ts dist", + "build": "bun --bun tsgo -p tsconfig.build.json", + "dev": "bun --bun tsgo -p tsconfig.build.json --watch --preserveWatchOutput", "test": "vitest --run", - "prepublishOnly": "npm run clean && npm run build" + "prepublishOnly": "bun run clean && bun run build" }, "dependencies": { "@earendil-works/pi-ai": "^0.9.1", diff --git a/packages/ai/package.json b/packages/ai/package.json index 3e8400cb5ed..0e0748117de 100644 --- a/packages/ai/package.json +++ b/packages/ai/package.json @@ -63,19 +63,19 @@ "README.md" ], "scripts": { - "clean": "shx rm -rf dist", - "generate-models": "npx tsx scripts/generate-models.ts", - "build": "npm run generate-models && tsgo -p tsconfig.build.json", - "dev": "tsgo -p tsconfig.build.json --watch --preserveWatchOutput", - "dev:tsc": "tsgo -p tsconfig.build.json --watch --preserveWatchOutput", + "clean": "bun ../../scripts/remove-paths.ts dist", + "generate-models": "bun scripts/generate-models.ts", + "build": "bun run generate-models && bun --bun tsgo -p tsconfig.build.json", + "dev": "bun --bun tsgo -p tsconfig.build.json --watch --preserveWatchOutput", + "dev:tsc": "bun --bun tsgo -p tsconfig.build.json --watch --preserveWatchOutput", "test": "vitest --run", - "prepublishOnly": "npm run clean && npm run build" + "prepublishOnly": "bun run clean && bun run build" }, "dependencies": { "@anthropic-ai/sdk": "^0.91.1", "@aws-sdk/client-bedrock-runtime": "^3.1095.0", "@google/genai": "^1.40.0", - "@mistralai/mistralai": "^2.2.0", + "@mistralai/mistralai": "2.2.1", "typebox": "^1.3.9", "chalk": "^5.6.2", "openai": "6.47.0", diff --git a/packages/coding-agent/package.json b/packages/coding-agent/package.json index 994c27e8d17..6fa9370fe3f 100644 --- a/packages/coding-agent/package.json +++ b/packages/coding-agent/package.json @@ -31,19 +31,19 @@ "CHANGELOG.md" ], "scripts": { - "clean": "shx rm -rf dist", - "dev": "tsgo -p tsconfig.build.json --watch --preserveWatchOutput", - "build": "tsgo -p tsconfig.build.json && shx chmod +x dist/cli.js && npm run copy-assets && npm run bundle", - "build:binary": "npm --prefix ../tui run build && npm --prefix ../ai run build && npm --prefix ../agent run build && npm run build && bun build --compile ./dist/bun/cli.js --outfile dist/pi && npm run copy-binary-assets", - "copy-assets": "shx mkdir -p dist/modes/interactive/theme && shx cp src/modes/interactive/theme/*.json dist/modes/interactive/theme/ && shx mkdir -p dist/modes/interactive/assets && shx cp src/modes/interactive/assets/*.png dist/modes/interactive/assets/ && shx mkdir -p dist/core/export-html/vendor && shx cp src/core/export-html/template.html src/core/export-html/template.css src/core/export-html/template.js dist/core/export-html/ && shx cp src/core/export-html/vendor/*.js dist/core/export-html/vendor/ && shx rm -rf dist/prime-agent-runtime && shx cp -r ../../prime-agent-runtime dist/prime-agent-runtime && shx rm -rf dist/skills && shx cp -r skills dist/skills", - "copy-binary-assets": "shx cp package.json dist/ && shx cp README.md dist/ && shx cp CHANGELOG.md dist/ && shx mkdir -p dist/theme && shx cp src/modes/interactive/theme/*.json dist/theme/ && shx mkdir -p dist/assets && shx cp src/modes/interactive/assets/*.png dist/assets/ && shx mkdir -p dist/export-html/vendor && shx cp src/core/export-html/template.html dist/export-html/ && shx cp src/core/export-html/vendor/*.js dist/export-html/vendor/ && shx cp -r docs dist/ && shx cp -r examples dist/ && shx cp ../../node_modules/@silvia-odwyer/photon-node/photon_rs_bg.wasm dist/ && shx rm -rf dist/skills && shx cp -r skills dist/skills", + "clean": "bun ../../scripts/remove-paths.ts dist", + "dev": "bun --bun tsgo -p tsconfig.build.json --watch --preserveWatchOutput", + "build": "bun --bun tsgo -p tsconfig.build.json && bun scripts/copy-assets.ts package && bun run bundle", + "build:binary": "bun run --cwd ../tui build && bun run --cwd ../ai build && bun run --cwd ../agent build && bun run build && bun build --compile ./dist/bun/cli.js --outfile dist/pi && bun scripts/copy-assets.ts binary", + "copy-assets": "bun scripts/copy-assets.ts package", + "copy-binary-assets": "bun scripts/copy-assets.ts binary", "test": "vitest --run", "test:ci": "tsx src/core/kernel/bootstrap-cli.ts && vitest --run --exclude test/daemon-supervisor-process.test.ts", "test:process": "vitest --run test/daemon-supervisor-process.test.ts", "test:process-stress": "vitest --run --tagsFilter process-stress test/daemon-supervisor-process.test.ts", - "postinstall": "node postinstall.cjs", - "prepublishOnly": "npm run clean && npm run build", - "bundle": "node scripts/bundle.mjs", + "postinstall": "bun postinstall.cjs", + "prepublishOnly": "bun run clean && bun run build", + "bundle": "bun scripts/bundle.mjs", "test:kernel": "vitest --run --no-file-parallelism --tagsFilter kernel-heavy test/acp-kernel-features.test.ts test/acp-cold-cli.test.ts test/kernel-goal-skill.test.ts test/repl-kernel-state-roundtrip.test.ts test/repl-kernel-mcp-shutdown.test.ts" }, "dependencies": { diff --git a/packages/coding-agent/scripts/copy-assets.ts b/packages/coding-agent/scripts/copy-assets.ts new file mode 100644 index 00000000000..40b491bee06 --- /dev/null +++ b/packages/coding-agent/scripts/copy-assets.ts @@ -0,0 +1,63 @@ +import { chmod, cp, mkdir, readdir, rm } from "node:fs/promises"; +import { basename, dirname, join, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; + +const packageDir = resolve(dirname(fileURLToPath(import.meta.url)), ".."); +const repoDir = resolve(packageDir, "../.."); +const distDir = join(packageDir, "dist"); +const mode = process.argv[2]; + +async function copyFiles(sourceDir: string, targetDir: string, suffix: string): Promise { + await mkdir(targetDir, { recursive: true }); + for (const entry of await readdir(sourceDir, { withFileTypes: true })) { + if (entry.isFile() && entry.name.endsWith(suffix)) { + await cp(join(sourceDir, entry.name), join(targetDir, entry.name)); + } + } +} + +async function replaceDirectory(source: string, target: string): Promise { + await rm(target, { recursive: true, force: true }); + await cp(source, target, { recursive: true }); +} + +async function copyFile(source: string, targetDir: string): Promise { + await mkdir(targetDir, { recursive: true }); + await cp(source, join(targetDir, basename(source))); +} + +async function copyPackageAssets(): Promise { + await chmod(join(distDir, "cli.js"), 0o755); + await copyFiles(join(packageDir, "src/modes/interactive/theme"), join(distDir, "modes/interactive/theme"), ".json"); + await copyFiles(join(packageDir, "src/modes/interactive/assets"), join(distDir, "modes/interactive/assets"), ".png"); + const exportDir = join(distDir, "core/export-html"); + for (const name of ["template.html", "template.css", "template.js"]) { + await copyFile(join(packageDir, "src/core/export-html", name), exportDir); + } + await copyFiles(join(packageDir, "src/core/export-html/vendor"), join(exportDir, "vendor"), ".js"); + await replaceDirectory(join(repoDir, "prime-agent-runtime"), join(distDir, "prime-agent-runtime")); + await replaceDirectory(join(packageDir, "skills"), join(distDir, "skills")); +} + +async function copyBinaryAssets(): Promise { + for (const name of ["package.json", "README.md", "CHANGELOG.md"]) { + await copyFile(join(packageDir, name), distDir); + } + await copyFiles(join(packageDir, "src/modes/interactive/theme"), join(distDir, "theme"), ".json"); + await copyFiles(join(packageDir, "src/modes/interactive/assets"), join(distDir, "assets"), ".png"); + await copyFile(join(packageDir, "src/core/export-html/template.html"), join(distDir, "export-html")); + await copyFiles(join(packageDir, "src/core/export-html/vendor"), join(distDir, "export-html/vendor"), ".js"); + await replaceDirectory(join(packageDir, "docs"), join(distDir, "docs")); + await replaceDirectory(join(packageDir, "examples"), join(distDir, "examples")); + await copyFile(join(repoDir, "node_modules/@silvia-odwyer/photon-node/photon_rs_bg.wasm"), distDir); + await replaceDirectory(join(packageDir, "skills"), join(distDir, "skills")); +} + +if (mode === "package") { + await copyPackageAssets(); +} else if (mode === "binary") { + await copyBinaryAssets(); +} else { + console.error("Usage: bun scripts/copy-assets.ts "); + process.exit(2); +} diff --git a/packages/coding-agent/src/bun/cli.ts b/packages/coding-agent/src/bun/cli.ts index 0aacb95b086..fb9318f941a 100644 --- a/packages/coding-agent/src/bun/cli.ts +++ b/packages/coding-agent/src/bun/cli.ts @@ -1,4 +1,4 @@ -#!/usr/bin/env node +#!/usr/bin/env bun import { APP_NAME } from "../config.js"; process.title = APP_NAME; diff --git a/packages/tui/package.json b/packages/tui/package.json index ee67bf2d750..7e0ecbd82df 100644 --- a/packages/tui/package.json +++ b/packages/tui/package.json @@ -5,11 +5,11 @@ "type": "module", "main": "dist/index.js", "scripts": { - "clean": "shx rm -rf dist", - "build": "tsgo -p tsconfig.build.json", - "dev": "tsgo -p tsconfig.build.json --watch --preserveWatchOutput", + "clean": "bun ../../scripts/remove-paths.ts dist", + "build": "bun --bun tsgo -p tsconfig.build.json", + "dev": "bun --bun tsgo -p tsconfig.build.json --watch --preserveWatchOutput", "test": "node --test --import tsx test/*.test.ts", - "prepublishOnly": "npm run clean && npm run build" + "prepublishOnly": "bun run clean && bun run build" }, "files": [ "dist/**/*", diff --git a/prime-agent.sh b/prime-agent.sh index 8baf90081f8..53691f8e138 100755 --- a/prime-agent.sh +++ b/prime-agent.sh @@ -62,20 +62,27 @@ if [[ "$NO_ENV" == "true" ]]; then echo "Running Prime Agent without API keys..." fi -# --dist runs the bundled build (what users get; ~3x faster startup than tsx). +BUN_BIN="$(command -v bun || true)" +if [[ -z "$BUN_BIN" ]]; then + echo "Bun is required. Install version $(cat "$SCRIPT_DIR/.bun-version") and run bun install --frozen-lockfile." >&2 + exit 1 +fi + +EXPECTED_BUN_VERSION="$(cat "$SCRIPT_DIR/.bun-version")" +ACTUAL_BUN_VERSION="$($BUN_BIN --version)" +if [[ "$ACTUAL_BUN_VERSION" != "$EXPECTED_BUN_VERSION" ]]; then + echo "Prime Agent requires Bun $EXPECTED_BUN_VERSION; found $ACTUAL_BUN_VERSION." >&2 + exit 1 +fi + +# --dist runs the bundled build. Source mode runs TypeScript directly with Bun. if [[ "$USE_DIST" == "true" ]]; then BUNDLE="$SCRIPT_DIR/packages/coding-agent/dist/bundle/cli.js" if [[ ! -f "$BUNDLE" ]]; then - echo "Bundle not found at $BUNDLE. Run npm run build first." >&2 + echo "Bundle not found at $BUNDLE. Run bun run build first." >&2 exit 1 fi - exec node "$BUNDLE" ${ARGS[@]+"${ARGS[@]}"} -fi - -TSX_BIN="$SCRIPT_DIR/node_modules/.bin/tsx" -if [[ ! -x "$TSX_BIN" ]]; then - echo "tsx not found at $TSX_BIN. Run npm install from the repo root first." >&2 - exit 1 + exec "$BUN_BIN" "$BUNDLE" ${ARGS[@]+"${ARGS[@]}"} fi -"$TSX_BIN" "$SCRIPT_DIR/packages/coding-agent/src/cli.ts" ${ARGS[@]+"${ARGS[@]}"} +exec "$BUN_BIN" "$SCRIPT_DIR/packages/coding-agent/src/bun/cli.ts" ${ARGS[@]+"${ARGS[@]}"} diff --git a/scripts/dev.ts b/scripts/dev.ts new file mode 100644 index 00000000000..3027221be3a --- /dev/null +++ b/scripts/dev.ts @@ -0,0 +1,41 @@ +import { fileURLToPath } from "node:url"; + +const workspaceDirs = ["packages/ai", "packages/agent", "packages/coding-agent", "packages/tui"] as const; +const rootDir = fileURLToPath(new URL("..", import.meta.url)); +const children = workspaceDirs.map((cwd) => + Bun.spawn([process.execPath, "run", "--bun", "--cwd", cwd, "dev"], { + cwd: rootDir, + env: process.env, + stdin: "inherit", + stdout: "inherit", + stderr: "inherit", + }), +); + +let stopping = false; + +async function stop(signal: NodeJS.Signals, exitCode: number): Promise { + if (!stopping) { + stopping = true; + for (const child of children) { + child.kill(signal); + } + } + await Promise.allSettled(children.map((child) => child.exited)); + process.exit(exitCode); +} + +process.once("SIGINT", () => { + void stop("SIGINT", 130); +}); +process.once("SIGTERM", () => { + void stop("SIGTERM", 143); +}); + +const firstExit = await Promise.race( + children.map(async (child, index) => ({ index, exitCode: await child.exited })), +); +if (!stopping) { + console.error(`${workspaceDirs[firstExit.index]} watcher exited with code ${firstExit.exitCode}`); + await stop("SIGTERM", firstExit.exitCode || 1); +} diff --git a/scripts/remove-paths.ts b/scripts/remove-paths.ts new file mode 100644 index 00000000000..312d708fed5 --- /dev/null +++ b/scripts/remove-paths.ts @@ -0,0 +1,9 @@ +import { rm } from "node:fs/promises"; +import { resolve } from "node:path"; + +if (process.argv.length < 3) { + console.error("Usage: bun scripts/remove-paths.ts [...path]"); + process.exit(2); +} + +await Promise.all(process.argv.slice(2).map((path) => rm(resolve(path), { recursive: true, force: true }))); From ae5ed1edcfac882a87aae581d47270977ec995a6 Mon Sep 17 00:00:00 2001 From: Seth Date: Tue, 1 Sep 2026 17:38:08 -0400 Subject: [PATCH 03/45] fix: support Bun runtime process semantics --- packages/ai/src/cli.ts | 12 +++--- packages/coding-agent/src/cli.ts | 12 +++--- .../coding-agent/src/cli/daemon-launch.ts | 29 ++++++++++---- packages/coding-agent/src/cli/daemon-ps.ts | 4 +- .../src/cli/daemon-update-restart.ts | 7 ++-- .../src/cli/node-version-check.ts | 5 --- .../coding-agent/src/cli/subprocess-launch.ts | 27 ++----------- packages/coding-agent/src/config.ts | 40 ++++++++++++++----- .../src/core/extensions/loader.ts | 10 ++--- .../coding-agent/src/core/kernel/bootstrap.ts | 2 +- .../coding-agent/src/core/output-guard.ts | 17 ++++++++ .../src/core/prime-inference-auth.ts | 2 +- packages/coding-agent/src/core/prompts/rlm.ts | 2 +- packages/coding-agent/src/main.ts | 11 +++-- .../modes/daemon/daemon-catalog-process.ts | 9 +---- .../src/modes/daemon/daemon-client.ts | 5 ++- .../src/modes/daemon/daemon-mode.ts | 1 + .../src/modes/daemon/daemon-socket.ts | 5 ++- .../src/modes/daemon/daemon-supervisor.ts | 31 +++++++++++--- .../src/modes/interactive/interactive-mode.ts | 4 +- .../src/modes/interactive/theme/theme.ts | 6 ++- packages/coding-agent/src/utils/photon.ts | 2 +- .../coding-agent/src/utils/tools-manager.ts | 14 +++---- scripts/setup-kernel-venv.sh | 2 +- 24 files changed, 161 insertions(+), 98 deletions(-) diff --git a/packages/ai/src/cli.ts b/packages/ai/src/cli.ts index 7a4ff1fd334..a378141c64e 100644 --- a/packages/ai/src/cli.ts +++ b/packages/ai/src/cli.ts @@ -64,7 +64,7 @@ async function main(): Promise { if (!command || command === "help" || command === "--help" || command === "-h") { const providerList = PROVIDERS.map((p) => ` ${p.id.padEnd(20)} ${p.name}`).join("\n"); - console.log(`Usage: npx @earendil-works/pi-ai [provider] + console.log(`Usage: bunx @earendil-works/pi-ai [provider] Commands: login [provider] Login to an OAuth provider @@ -74,9 +74,9 @@ Providers: ${providerList} Examples: - npx @earendil-works/pi-ai login # interactive provider selection - npx @earendil-works/pi-ai login anthropic # login to specific provider - npx @earendil-works/pi-ai list # list providers + bunx @earendil-works/pi-ai login # interactive provider selection + bunx @earendil-works/pi-ai login anthropic # login to specific provider + bunx @earendil-works/pi-ai list # list providers `); return; } @@ -113,7 +113,7 @@ Examples: if (!PROVIDERS.some((p) => p.id === provider)) { console.error(`Unknown provider: ${provider}`); - console.error(`Use 'npx @earendil-works/pi-ai list' to see available providers`); + console.error(`Use 'bunx @earendil-works/pi-ai list' to see available providers`); process.exit(1); } @@ -123,7 +123,7 @@ Examples: } console.error(`Unknown command: ${command}`); - console.error(`Use 'npx @earendil-works/pi-ai --help' for usage`); + console.error(`Use 'bunx @earendil-works/pi-ai --help' for usage`); process.exit(1); } diff --git a/packages/coding-agent/src/cli.ts b/packages/coding-agent/src/cli.ts index 07d1b9b3d0d..38b8024c7ac 100644 --- a/packages/coding-agent/src/cli.ts +++ b/packages/coding-agent/src/cli.ts @@ -3,11 +3,13 @@ // behind the dynamic import, after the dependency-free guard runs. import { assertNodeVersion } from "./cli/node-version-check.js"; -const supported = assertNodeVersion({ - version: process.versions.node, - log: console.error, - exit: (code) => process.exit(code), -}); +const supported = process.versions.bun + ? true + : assertNodeVersion({ + version: process.versions.node, + log: console.error, + exit: (code) => process.exit(code), + }); if (supported) { const { runCli } = await import("./cli-main.js"); diff --git a/packages/coding-agent/src/cli/daemon-launch.ts b/packages/coding-agent/src/cli/daemon-launch.ts index 0d57425064f..37073959a71 100644 --- a/packages/coding-agent/src/cli/daemon-launch.ts +++ b/packages/coding-agent/src/cli/daemon-launch.ts @@ -346,11 +346,22 @@ async function shutdownStaleDaemonIfNotBusy(socketPath: string): Promise { +export interface DaemonLaunchTiming { + startupTimeoutMs?: number; + initialHelloTimeoutMs?: number; +} + +async function ensureDaemonRunning( + socketPath: string, + spawnCwd?: string, + timing: DaemonLaunchTiming = {}, +): Promise { + const startupTimeoutMs = timing.startupTimeoutMs ?? DAEMON_STARTUP_TIMEOUT_MS; + const initialHelloTimeoutMs = timing.initialHelloTimeoutMs ?? 2000; const probeStartedAt = Date.now(); - let probe = await probeDaemonVersion(socketPath); + let probe = await probeDaemonVersion(socketPath, initialHelloTimeoutMs); if (probe.status === "unresponsive") { - const remainingStartupMs = Math.max(1, DAEMON_STARTUP_TIMEOUT_MS - (Date.now() - probeStartedAt)); + const remainingStartupMs = Math.max(1, startupTimeoutMs - (Date.now() - probeStartedAt)); probe = await probeDaemonVersion(socketPath, remainingStartupMs); } if (probe.status === "current") { @@ -358,7 +369,7 @@ async function ensureDaemonRunning(socketPath: string, spawnCwd?: string): Promi } if (probe.status === "unresponsive") { throw new Error( - `Prime Agent daemon on ${socketPath} accepted connections but did not finish startup within ${DAEMON_STARTUP_TIMEOUT_MS / 1000} seconds. ` + + `Prime Agent daemon on ${socketPath} accepted connections but did not finish startup within ${startupTimeoutMs / 1000} seconds. ` + `It was left running to avoid interrupting active work. Run: @@ -436,7 +447,7 @@ Then retry the original command.`, // A child exit is not immediately fatal: it may have lost the socket to a // concurrent launcher whose daemon is still booting. Keep probing for a // short grace window before attributing the failure to the exit. - const deadline = Date.now() + DAEMON_STARTUP_TIMEOUT_MS; + const deadline = Date.now() + startupTimeoutMs; let exitDeadline: number | undefined; while (Date.now() < Math.min(deadline, exitDeadline ?? Number.POSITIVE_INFINITY)) { const started = await probeDaemonVersion(socketPath); @@ -489,10 +500,14 @@ const ensurePromises = new Map>(); * main.ts share one probe/spawn; failed attempts are forgotten so a later call * retries (and surfaces the real error at its await site). */ -export function ensureInteractiveDaemonRunning(socketPath: string, spawnCwd?: string): Promise { +export function ensureInteractiveDaemonRunning( + socketPath: string, + spawnCwd?: string, + timing?: DaemonLaunchTiming, +): Promise { let promise = ensurePromises.get(socketPath); if (!promise) { - promise = ensureDaemonRunning(socketPath, spawnCwd); + promise = ensureDaemonRunning(socketPath, spawnCwd, timing); ensurePromises.set(socketPath, promise); const clear = () => { if (ensurePromises.get(socketPath) === promise) { diff --git a/packages/coding-agent/src/cli/daemon-ps.ts b/packages/coding-agent/src/cli/daemon-ps.ts index 42c05f9a73f..17dbf005a7c 100644 --- a/packages/coding-agent/src/cli/daemon-ps.ts +++ b/packages/coding-agent/src/cli/daemon-ps.ts @@ -120,7 +120,9 @@ export function parseLsofListeners(stdout: string): DiscoveredDaemonProcess[] { if (field === "p") { pid = Number.parseInt(value, 10); } else if (field === "n" && pid !== undefined && value.startsWith("/")) { - const socketPath = normalizeSocketPath(value); + // Linux lsof appends metadata such as " type=STREAM" to Unix socket + // names, while macOS reports only the path. + const socketPath = normalizeSocketPath(value.replace(/\s+type=\w+(?:\s.*)?$/, "")); const key = `${pid}:${socketPath}`; if (!seen.has(key)) { seen.add(key); diff --git a/packages/coding-agent/src/cli/daemon-update-restart.ts b/packages/coding-agent/src/cli/daemon-update-restart.ts index f436f2f6d1f..3d6d251ee76 100644 --- a/packages/coding-agent/src/cli/daemon-update-restart.ts +++ b/packages/coding-agent/src/cli/daemon-update-restart.ts @@ -250,8 +250,9 @@ export class DaemonUpdateRestartStatusWriter { private readonly path: string, requestId: string, socketPath: string, + private readonly clock: () => Date = () => new Date(), ) { - const now = new Date().toISOString(); + const now = this.clock().toISOString(); const processStartId = getProcessStartId(process.pid); this.status = { version: 1, @@ -270,7 +271,7 @@ export class DaemonUpdateRestartStatusWriter { update( update: Partial>, ): void { - const now = new Date().toISOString(); + const now = this.clock().toISOString(); this.status = { ...this.status, ...update, @@ -293,7 +294,7 @@ export class DaemonUpdateRestartStatusWriter { } touch(): void { - this.status = { ...this.status, heartbeatAt: new Date().toISOString() }; + this.status = { ...this.status, heartbeatAt: this.clock().toISOString() }; this.persist(); } diff --git a/packages/coding-agent/src/cli/node-version-check.ts b/packages/coding-agent/src/cli/node-version-check.ts index 24eef4eb8d7..8e53590508f 100644 --- a/packages/coding-agent/src/cli/node-version-check.ts +++ b/packages/coding-agent/src/cli/node-version-check.ts @@ -38,11 +38,6 @@ function isSupportedNodeVersion(version: ParsedNodeVersion): boolean { } export function assertNodeVersion(io: NodeVersionGuardIO): boolean { - // Bun ships its own runtime; its node-compat version is unrelated to the user's Node. - if (process.versions.bun) { - return true; - } - const version = parseVersion(io.version); if (!version || isSupportedNodeVersion(version)) { return true; diff --git a/packages/coding-agent/src/cli/subprocess-launch.ts b/packages/coding-agent/src/cli/subprocess-launch.ts index f5843b48d17..d634bdb81a8 100644 --- a/packages/coding-agent/src/cli/subprocess-launch.ts +++ b/packages/coding-agent/src/cli/subprocess-launch.ts @@ -1,5 +1,4 @@ -import { existsSync } from "node:fs"; -import { dirname, isAbsolute, join, resolve } from "node:path"; +import { isAbsolute, resolve } from "node:path"; import { isBunBinary } from "../config.js"; export interface CliSubprocessLaunchSpec { @@ -7,28 +6,8 @@ export interface CliSubprocessLaunchSpec { args: string[]; } -export function createCliSubprocessEnv( - source: NodeJS.ProcessEnv = process.env, - entrypoint = process.argv[1], - execArgs: readonly string[] = process.execArgv, -): NodeJS.ProcessEnv { - const environment = { ...source }; - if (environment.TSX_TSCONFIG_PATH !== undefined || !entrypoint || !execArgs.some((arg) => arg.includes("tsx"))) { - return environment; - } - let directory = dirname(resolve(entrypoint)); - while (true) { - const tsconfigPath = join(directory, "tsconfig.json"); - if (existsSync(tsconfigPath) && existsSync(join(directory, "node_modules", "tsx", "package.json"))) { - environment.TSX_TSCONFIG_PATH = tsconfigPath; - return environment; - } - const parent = dirname(directory); - if (parent === directory) { - return environment; - } - directory = parent; - } +export function createCliSubprocessEnv(source: NodeJS.ProcessEnv = process.env): NodeJS.ProcessEnv { + return { ...source }; } function quoteCommandArgument(value: string): string { diff --git a/packages/coding-agent/src/config.ts b/packages/coding-agent/src/config.ts index 596d468d1ae..1d6fbbc8c7f 100644 --- a/packages/coding-agent/src/config.ts +++ b/packages/coding-agent/src/config.ts @@ -98,7 +98,7 @@ export function detectInstallMethod(): InstallMethod { if (resolvedPath.includes("/yarn/") || resolvedPath.includes("/.yarn/")) { return "yarn"; } - if (isBunRuntime || resolvedPath.includes("/install/global/node_modules/")) { + if (resolvedPath.includes("/install/global/node_modules/")) { return "bun"; } if (resolvedPath.includes("/npm/") || resolvedPath.includes("/node_modules/")) { @@ -159,7 +159,20 @@ function getSelfUpdateCommandForMethod( ): SelfUpdateCommand | undefined { const uninstallAfterInstall = isDirectPackageArtifactSpec(updateSpec); switch (method) { - case "bun-binary": + case "bun-binary": { + // Bun-compiled binary: use install.sh sidecar for self-update + const installScript = join(getPackageDir(), "install.sh"); + if (existsSync(installScript)) { + const updateCmd = makeSelfUpdateCommandStep(installScript, ["--update"]); + return { + ...updateCmd, + steps: [updateCmd], + }; + } + // install.sh not bundled; self-update not available via command. + // The caller shows a download link as fallback. + return undefined; + } case "homebrew": return undefined; case "pnpm": @@ -211,6 +224,7 @@ function readCommandOutput( encoding: "utf-8", stdio: ["ignore", "pipe", "pipe"], shell: shouldUseWindowsShell(command), + env: process.env, }); if (result.status === 0) return result.stdout.trim() || undefined; if (options.requireSuccess) { @@ -314,8 +328,12 @@ export function getSelfUpdateCommand( ): SelfUpdateCommand | undefined { const method = detectInstallMethod(); const command = getSelfUpdateCommandForMethod(method, packageName, updateSpec, npmCommand, updatePackageName); - if (!command || !isManagedByGlobalPackageManager(method, packageName, npmCommand) || !isSelfUpdatePathWritable()) { - return undefined; + if (!command) return undefined; + // Compiled binary installs are managed by their versioned installer directory. + if (method !== "bun-binary") { + if (!isManagedByGlobalPackageManager(method, packageName, npmCommand) || !isSelfUpdatePathWritable()) { + return undefined; + } } return command; } @@ -328,6 +346,10 @@ export function getSelfUpdateUnavailableInstruction( ): string { const method = detectInstallMethod(); if (method === "bun-binary") { + const installScript = join(getPackageDir(), "install.sh"); + if (existsSync(installScript)) { + return `Update with: ${installScript} --update`; + } return `Download from: https://github.com/PrimeIntellect-ai/prime-agent/releases/latest`; } if (method === "homebrew") { @@ -360,7 +382,7 @@ export function getUpdateInstruction(packageName: string): string { * Get the base directory for resolving package assets (themes, package.json, README.md, CHANGELOG.md). * - For Bun binary: returns the directory containing the executable * - For Node.js (dist/): returns __dirname (the dist/ directory) - * - For tsx (src/): returns parent directory (the package root) + * - For direct source execution (src/): returns parent directory (the package root) */ export function getPackageDir(): string { // Allow override via environment variable (useful for Nix/Guix where store paths tokenize poorly) @@ -391,7 +413,7 @@ export function getPackageDir(): string { * Get path to built-in themes directory (shipped with package) * - For Bun binary: theme/ next to executable * - For Node.js (dist/): dist/modes/interactive/theme/ - * - For tsx (src/): src/modes/interactive/theme/ + * - For direct source execution (src/): src/modes/interactive/theme/ */ export function getThemesDir(): string { if (isBunBinary) { @@ -407,7 +429,7 @@ export function getThemesDir(): string { * Get path to HTML export template directory (shipped with package) * - For Bun binary: export-html/ next to executable * - For Node.js (dist/): dist/core/export-html/ - * - For tsx (src/): src/core/export-html/ + * - For direct source execution (src/): src/core/export-html/ */ export function getExportTemplateDir(): string { if (isBunBinary) { @@ -437,7 +459,7 @@ export function getChangelogPath(): string { * Get path to built-in interactive assets directory. * - For Bun binary: assets/ next to executable * - For Node.js (dist/): dist/modes/interactive/assets/ - * - For tsx (src/): src/modes/interactive/assets/ + * - For direct source execution (src/): src/modes/interactive/assets/ */ export function getInteractiveAssetsDir(): string { if (isBunBinary) { @@ -457,7 +479,7 @@ export function getBundledInteractiveAssetPath(name: string): string { * Get the directory containing built-in skills shipped with the package. * - For Bun binary: skills/ next to executable * - For Node.js (dist/): dist/skills/ - * - For tsx (src/): skills/ at the package root + * - For direct source execution (src/): skills/ at the package root */ export function getBundledSkillsDir(): string { if (isBunBinary) { diff --git a/packages/coding-agent/src/core/extensions/loader.ts b/packages/coding-agent/src/core/extensions/loader.ts index 2c7e81ad6bf..ff978c8e8c7 100644 --- a/packages/coding-agent/src/core/extensions/loader.ts +++ b/packages/coding-agent/src/core/extensions/loader.ts @@ -320,8 +320,8 @@ function createExtensionAPI( return api; } -// Replaced with `true` by the esbuild CLI bundle (scripts/bundle.mjs); stays -// undefined in unbundled dist/ and under tsx. +// Replaced with `true` by the Bun CLI bundle (scripts/bundle.mjs); stays +// undefined in unbundled dist/ and direct source execution. declare const __PI_BUNDLED__: boolean | undefined; const isBundledCli = typeof __PI_BUNDLED__ !== "undefined" && __PI_BUNDLED__ === true; @@ -333,11 +333,11 @@ async function loadExtensionModule(extensionPath: string) { const { createJiti } = await import("jiti/static"); const jiti = createJiti(import.meta.url, { moduleCache: false, - // In the Bun binary and the esbuild CLI bundle: serve pi packages from + // In the Bun binary and the Bun CLI bundle: serve pi packages from // virtualModules so extensions share the bundle's module instances // (file-path aliases would load a second, divergent copy of each package). // Also disable tryNative so jiti handles ALL imports (not just the entry point) - // In Node.js/dev: use aliases to resolve to node_modules paths + // In direct source execution: use aliases to resolve to node_modules paths ...(isBunBinary || isBundledCli ? { virtualModules: (await import("./bundled-modules.js")).VIRTUAL_MODULES, tryNative: false } : { alias: getAliases() }), @@ -522,7 +522,7 @@ function discoverExtensionsInDir(dir: string): string[] { const discovered: string[] = []; try { - const entries = fs.readdirSync(dir, { withFileTypes: true }); + const entries = fs.readdirSync(dir, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name)); for (const entry of entries) { const entryPath = path.join(dir, entry.name); diff --git a/packages/coding-agent/src/core/kernel/bootstrap.ts b/packages/coding-agent/src/core/kernel/bootstrap.ts index 2390749308f..16f13fac7d9 100644 --- a/packages/coding-agent/src/core/kernel/bootstrap.ts +++ b/packages/coding-agent/src/core/kernel/bootstrap.ts @@ -651,7 +651,7 @@ function runtimeCandidateDirs(): string[] { const moduleDir = path.dirname(fileURLToPath(import.meta.url)); // dist/prime-agent-runtime is listed first deliberately: it is the only path stable // across every shipped layout (dist/, dist/bundle/, bun), where import.meta.url-relative - // resolution breaks. `npm run build` rebuilds it from live source (copy-assets does + // resolution breaks. `bun run build` rebuilds it from live source (copy-assets does // rm -rf + cp), so the staleness hash still refreshes on every build. The relative // paths below cover running from source (tsx) where dist/ hasn't been built. return [ diff --git a/packages/coding-agent/src/core/output-guard.ts b/packages/coding-agent/src/core/output-guard.ts index c078376046a..59c1465566e 100644 --- a/packages/coding-agent/src/core/output-guard.ts +++ b/packages/coding-agent/src/core/output-guard.ts @@ -2,6 +2,9 @@ interface StdoutTakeoverState { rawStdoutWrite: (chunk: string, callback?: (error?: Error | null) => void) => boolean; rawStderrWrite: (chunk: string, callback?: (error?: Error | null) => void) => boolean; originalStdoutWrite: typeof process.stdout.write; + originalConsoleLog: typeof console.log; + originalConsoleInfo: typeof console.info; + originalConsoleDebug: typeof console.debug; } let stdoutTakeoverState: StdoutTakeoverState | undefined; @@ -14,6 +17,9 @@ export function takeOverStdout(): void { const rawStdoutWrite = process.stdout.write.bind(process.stdout) as StdoutTakeoverState["rawStdoutWrite"]; const rawStderrWrite = process.stderr.write.bind(process.stderr) as StdoutTakeoverState["rawStderrWrite"]; const originalStdoutWrite = process.stdout.write; + const originalConsoleLog = console.log; + const originalConsoleInfo = console.info; + const originalConsoleDebug = console.debug; process.stdout.write = (( chunk: string | Uint8Array, @@ -25,11 +31,19 @@ export function takeOverStdout(): void { } return rawStderrWrite(String(chunk), callback); }) as typeof process.stdout.write; + // Bun's console methods write through native bindings instead of the mutable + // process.stdout.write property. Redirect them explicitly as well. + console.log = (...args: unknown[]) => console.error(...args); + console.info = (...args: unknown[]) => console.error(...args); + console.debug = (...args: unknown[]) => console.error(...args); stdoutTakeoverState = { rawStdoutWrite, rawStderrWrite, originalStdoutWrite, + originalConsoleLog, + originalConsoleInfo, + originalConsoleDebug, }; } @@ -39,6 +53,9 @@ export function restoreStdout(): void { } process.stdout.write = stdoutTakeoverState.originalStdoutWrite; + console.log = stdoutTakeoverState.originalConsoleLog; + console.info = stdoutTakeoverState.originalConsoleInfo; + console.debug = stdoutTakeoverState.originalConsoleDebug; stdoutTakeoverState = undefined; } diff --git a/packages/coding-agent/src/core/prime-inference-auth.ts b/packages/coding-agent/src/core/prime-inference-auth.ts index 3331ae36052..f761489ec62 100644 --- a/packages/coding-agent/src/core/prime-inference-auth.ts +++ b/packages/coding-agent/src/core/prime-inference-auth.ts @@ -87,7 +87,7 @@ export type PrimeTeam = { }; function defaultPrimeCliConfigPath(): string { - return join(homedir(), ".prime", "config.json"); + return join(process.env.HOME || homedir(), ".prime", "config.json"); } export function getPrimeCliConfigPath(configPath?: string): string { diff --git a/packages/coding-agent/src/core/prompts/rlm.ts b/packages/coding-agent/src/core/prompts/rlm.ts index ef610268e61..735f96a9f54 100644 --- a/packages/coding-agent/src/core/prompts/rlm.ts +++ b/packages/coding-agent/src/core/prompts/rlm.ts @@ -34,7 +34,7 @@ const REPL_CONTROL_PROMPT = [ "", "Do not assume the REPL is the native runtime of the external thing being investigated. A repository, package, service, dataset, paper, website, benchmark, or API may have its own environment and normal interface. Evaluate external systems through their own interface, then use the REPL to coordinate the process and analyze what comes back.", "", - "`bash(command)` starts a shell command in the background and returns a handle immediately: `h = bash('npm test')`. Use `h.pid` / `h.running` for liveness, `h.tail(n)` / `h.output()` for combined stdout+stderr so far, `h.poll()` for a non-blocking result, `h.kill()` to terminate (SIGTERM, escalating to SIGKILL; on Windows kill() uses taskkill /T and detached or reparented descendants may survive), and `await h` (or `await bash('cmd')`) for the completed result with exit_code, output, and duration. Prefer bash() for long-running commands so the turn keeps working. Run shell commands with `bash()`, not `subprocess`/`os.system`: subprocess calls block the kernel, show the user nothing while they run, and spawn processes the harness cannot see or stop.", + "`bash(command)` starts a shell command in the background and returns a handle immediately: `h = bash('bun test')`. Use `h.pid` / `h.running` for liveness, `h.tail(n)` / `h.output()` for combined stdout+stderr so far, `h.poll()` for a non-blocking result, `h.kill()` to terminate (SIGTERM, escalating to SIGKILL; on Windows kill() uses taskkill /T and detached or reparented descendants may survive), and `await h` (or `await bash('cmd')`) for the completed result with exit_code, output, and duration. Prefer bash() for long-running commands so the turn keeps working. Run shell commands with `bash()`, not `subprocess`/`os.system`: subprocess calls block the kernel, show the user nothing while they run, and spawn processes the harness cannot see or stop.", "", "Important: do not install dependencies into the kernel just to make an external project import or run there. If a project import, test, script, CLI, or dependency check is needed, run it through that project's own environment and normal command interface. For example, in a Python repo use its documented commands, `uv run ...`, `.venv/bin/python ...`, or the active project interpreter from the repo root. Treat failures from that native environment as the relevant result.", "", diff --git a/packages/coding-agent/src/main.ts b/packages/coding-agent/src/main.ts index e90b2e5dcc9..b4968d66c2e 100644 --- a/packages/coding-agent/src/main.ts +++ b/packages/coding-agent/src/main.ts @@ -59,7 +59,7 @@ import { KeybindingsManager } from "./core/keybindings.js"; import { installFileLogSink, setLogContext } from "./core/logging.js"; import type { ModelRegistry } from "./core/model-registry.js"; import { findInitialModel, resolveCliModel, resolveModelScope, type ScopedModel } from "./core/model-resolver.js"; -import { restoreStdout, takeOverStdout } from "./core/output-guard.js"; +import { restoreStdout, takeOverStdout, writeRawStdout } from "./core/output-guard.js"; import type { CreateAgentSessionOptions } from "./core/sdk.js"; import { formatMissingSessionCwdPrompt, @@ -1095,12 +1095,17 @@ export async function main(args: string[], options?: MainOptions) { takeOverStdout(); } + const writeCliMetadata = (text: string): void => { + const explicitMachineMode = parsed.print || parsed.mode !== undefined; + if (explicitMachineMode) console.error(text); + else writeRawStdout(`${text}\n`); + }; if (parsed.version) { - console.log(VERSION); + writeCliMetadata(VERSION); process.exit(0); } if (parsed.help) { - console.log(formatTopLevelHelp()); + writeCliMetadata(formatTopLevelHelp()); process.exit(0); } diff --git a/packages/coding-agent/src/modes/daemon/daemon-catalog-process.ts b/packages/coding-agent/src/modes/daemon/daemon-catalog-process.ts index 17453aaabbf..670cb2abb4c 100644 --- a/packages/coding-agent/src/modes/daemon/daemon-catalog-process.ts +++ b/packages/coding-agent/src/modes/daemon/daemon-catalog-process.ts @@ -1,7 +1,6 @@ import { type ChildProcess, spawn } from "node:child_process"; import { randomUUID } from "node:crypto"; import { existsSync } from "node:fs"; -import { createRequire } from "node:module"; import { join, sep } from "node:path"; import { fileURLToPath } from "node:url"; import { createCliSubprocessEnv, createCliSubprocessLaunchSpec } from "../../cli/subprocess-launch.js"; @@ -344,20 +343,16 @@ export class DaemonCatalogClient { private async spawnCatalog(): Promise { let command: string; let args: string[]; - let environment = createCliSubprocessEnv({ ...process.env, [DAEMON_CATALOG_ROLE_ENV]: "1" }); + const environment = createCliSubprocessEnv({ ...process.env, [DAEMON_CATALOG_ROLE_ENV]: "1" }); if (isBunBinary) { const launch = createCliSubprocessLaunchSpec(["--version"]); command = launch.command; args = launch.args; } else { const catalogEntry = resolveDaemonCatalogEntrypoint(); - const execArgs = catalogEntry.endsWith(".ts") - ? [...process.execArgv, "--import", createRequire(import.meta.url).resolve("tsx")] - : process.execArgv; - const launch = createCliSubprocessLaunchSpec([], undefined, execArgs, catalogEntry); + const launch = createCliSubprocessLaunchSpec([], undefined, process.execArgv, catalogEntry); command = launch.command; args = launch.args; - environment = createCliSubprocessEnv(environment, catalogEntry, execArgs); } const child = spawn(command, args, { cwd: process.cwd(), diff --git a/packages/coding-agent/src/modes/daemon/daemon-client.ts b/packages/coding-agent/src/modes/daemon/daemon-client.ts index b5ded67982a..6af47df0ace 100644 --- a/packages/coding-agent/src/modes/daemon/daemon-client.ts +++ b/packages/coding-agent/src/modes/daemon/daemon-client.ts @@ -1,5 +1,5 @@ import { randomUUID } from "node:crypto"; -import { createConnection, type Socket } from "node:net"; +import { Socket } from "node:net"; import { getDaemonLogPath } from "../../config.js"; import { attachJsonlLineReader, serializeJsonLine } from "../rpc/jsonl.js"; import { @@ -200,7 +200,7 @@ export class DaemonClient { } this.helloMessage = undefined; this.daemonClosingReason = undefined; - const socket = createConnection(this.socketPath); + const socket = new Socket(); this.socket = socket; this.detachReader = attachJsonlLineReader(socket, (line) => this.handleLine(line)); @@ -235,6 +235,7 @@ export class DaemonClient { }; socket.once("connect", onConnect); socket.once("error", onError); + socket.connect(this.socketPath); }); socket.on("error", (error) => diff --git a/packages/coding-agent/src/modes/daemon/daemon-mode.ts b/packages/coding-agent/src/modes/daemon/daemon-mode.ts index b99c047ccf5..bcecaba035a 100644 --- a/packages/coding-agent/src/modes/daemon/daemon-mode.ts +++ b/packages/coding-agent/src/modes/daemon/daemon-mode.ts @@ -7314,6 +7314,7 @@ export class AgentDaemon { for (const client of this.clients) { client.detachInput(); client.socket.end(); + client.socket.destroy(); } await new Promise((resolveClose) => { if (!this.server) { diff --git a/packages/coding-agent/src/modes/daemon/daemon-socket.ts b/packages/coding-agent/src/modes/daemon/daemon-socket.ts index 99a3a151058..dd6d5e2cf7e 100644 --- a/packages/coding-agent/src/modes/daemon/daemon-socket.ts +++ b/packages/coding-agent/src/modes/daemon/daemon-socket.ts @@ -278,7 +278,10 @@ function assertSocketLeaseHeld(socketPath: string, lease: DaemonSocketPathLease) export function defaultDaemonSocketDir(): string { const suffix = typeof process.getuid === "function" ? String(process.getuid()) : "user"; - return join(tmpdir(), `prime-agent-${suffix}`); + // Bun caches os.tmpdir() at startup. Read the standard overrides here so + // isolated processes and tests can select a runtime temporary directory. + const runtimeTmpDir = process.env.TMPDIR || process.env.TMP || process.env.TEMP || tmpdir(); + return join(runtimeTmpDir, `prime-agent-${suffix}`); } function ensureDefaultDaemonSocketDir(socketPath: string): void { diff --git a/packages/coding-agent/src/modes/daemon/daemon-supervisor.ts b/packages/coding-agent/src/modes/daemon/daemon-supervisor.ts index b5afde5da3e..bf92424d328 100644 --- a/packages/coding-agent/src/modes/daemon/daemon-supervisor.ts +++ b/packages/coding-agent/src/modes/daemon/daemon-supervisor.ts @@ -481,20 +481,29 @@ function unrefDelay(ms: number): Promise { function commitWorkerStartupGate(gate: Writable): Promise { return new Promise((resolveCommit, rejectCommit) => { let settled = false; + let onError: (error: Error) => void; + let onClose: () => void; const finish = (error?: Error | null) => { if (settled) { return; } settled = true; + gate.off("error", onError); + gate.off("close", onClose); if (error) { rejectCommit(error); } else { resolveCommit(); } }; - const onError = (error: Error) => finish(error); - gate.on("error", onError); - gate.once("close", () => gate.off("error", onError)); + onError = (error: Error) => finish(error); + onClose = () => finish(new Error("Daemon session worker startup gate closed before commit")); + gate.once("error", onError); + gate.once("close", onClose); + if (gate.destroyed || gate.closed || !gate.writable) { + onClose(); + return; + } gate.end(DAEMON_WORKER_STARTUP_GATE_COMMIT, (error?: Error | null) => finish(error)); }); } @@ -2881,7 +2890,10 @@ export class DaemonSupervisor { }) : () => {}; child.once("close", detachWorkerStderr); - const childClosed = new Promise((resolveClose) => child.once("close", () => resolveClose())); + const childClosed = + child.exitCode !== null || child.signalCode !== null + ? Promise.resolve() + : new Promise((resolveClose) => child.once("close", () => resolveClose())); let spawnFailure: Error | undefined; const spawnSettled = new Promise((resolveSpawn) => { child.once("spawn", () => resolveSpawn()); @@ -2889,6 +2901,9 @@ export class DaemonSupervisor { spawnFailure = error instanceof Error ? error : new Error(String(error)); resolveSpawn(); }); + // Bun can emit "spawn" while a test or instrumentation wrapper is still + // returning the ChildProcess. A pid already proves successful admission. + if (child.pid) resolveSpawn(); }); child.on("error", (error) => { this.log( @@ -2983,7 +2998,12 @@ export class DaemonSupervisor { try { try { - await commitWorkerStartupGate(startupGate); + await Promise.race([ + commitWorkerStartupGate(startupGate), + childClosed.then(() => { + throw new Error("Daemon session worker exited before startup gate commit"); + }), + ]); } catch (error) { startupGate.destroy(); await childClosed; @@ -6535,6 +6555,7 @@ export class DaemonSupervisor { for (const client of this.clients) { client.detachInput(); client.socket.end(); + client.socket.destroy(); } await new Promise((resolveClose) => this.server?.close(() => resolveClose()) ?? resolveClose()); await this.runCleanupStep("daemon socket", () => this.cleanupSocket()); diff --git a/packages/coding-agent/src/modes/interactive/interactive-mode.ts b/packages/coding-agent/src/modes/interactive/interactive-mode.ts index 53cc14670cd..9c81ae25e8b 100644 --- a/packages/coding-agent/src/modes/interactive/interactive-mode.ts +++ b/packages/coding-agent/src/modes/interactive/interactive-mode.ts @@ -3348,11 +3348,11 @@ export class InteractiveMode { } private clearFeatureHintPresentation(): void { - if (this.featureHintTimer) { + if (this.featureHintTimer !== undefined) { clearTimeout(this.featureHintTimer); this.featureHintTimer = undefined; } - if (this.featureHintAnimationTimer) { + if (this.featureHintAnimationTimer !== undefined) { clearInterval(this.featureHintAnimationTimer); this.featureHintAnimationTimer = undefined; } diff --git a/packages/coding-agent/src/modes/interactive/theme/theme.ts b/packages/coding-agent/src/modes/interactive/theme/theme.ts index dd4a48f7f71..40424a81aae 100644 --- a/packages/coding-agent/src/modes/interactive/theme/theme.ts +++ b/packages/coding-agent/src/modes/interactive/theme/theme.ts @@ -820,7 +820,7 @@ function getDefaultTheme(): string { // Global Theme Instance // ============================================================================ -// Use globalThis to share theme across module loaders (tsx + jiti in dev mode) +// Use globalThis to share theme across module loaders (direct Bun source execution + jiti in dev mode) const THEME_KEY = Symbol.for("@earendil-works/pi-coding-agent:theme"); // Export theme as a getter that reads from globalThis @@ -1026,6 +1026,10 @@ function startThemeWatcher(): void { ) ?? undefined; } +export function getActiveThemeWatcher(): fs.FSWatcher | undefined { + return themeWatcher; +} + export function stopThemeWatcher(): void { if (themeReloadTimer) { clearTimeout(themeReloadTimer); diff --git a/packages/coding-agent/src/utils/photon.ts b/packages/coding-agent/src/utils/photon.ts index 6c320705eb9..ed6d3e24c28 100644 --- a/packages/coding-agent/src/utils/photon.ts +++ b/packages/coding-agent/src/utils/photon.ts @@ -2,7 +2,7 @@ * Photon image processing wrapper. * * This module provides a unified interface to @silvia-odwyer/photon-node that works in: - * 1. Node.js (development, npm run build) + * 1. Bun (development and build) * 2. Bun compiled binaries (standalone distribution) * * The challenge: photon-node's CJS entry uses fs.readFileSync(__dirname + '/photon_rs_bg.wasm') diff --git a/packages/coding-agent/src/utils/tools-manager.ts b/packages/coding-agent/src/utils/tools-manager.ts index c3da7045ec0..414c5cb50c9 100644 --- a/packages/coding-agent/src/utils/tools-manager.ts +++ b/packages/coding-agent/src/utils/tools-manager.ts @@ -8,7 +8,6 @@ import { Readable } from "stream"; import { pipeline } from "stream/promises"; import { APP_NAME, getBinDir } from "../config.js"; -const TOOLS_DIR = getBinDir(); const NETWORK_TIMEOUT_MS = 10_000; const DOWNLOAD_TIMEOUT_MS = 120_000; const COMMAND_TIMEOUT_MS = 5_000; @@ -100,7 +99,7 @@ const TOOLS: Record = { // Check that a command both launches and reports a successful version. function commandWorks(cmd: string): boolean { try { - const result = spawnSync(cmd, ["--version"], { stdio: "pipe", timeout: COMMAND_TIMEOUT_MS }); + const result = spawnSync(cmd, ["--version"], { env: process.env, stdio: "pipe", timeout: COMMAND_TIMEOUT_MS }); return !result.error && result.status === 0; } catch { return false; @@ -113,7 +112,7 @@ export function getToolPath(tool: ManagedTool): string | null { if (!config) return null; // Check our tools directory first - const localPath = join(TOOLS_DIR, config.binaryName + (platform() === "win32" ? ".exe" : "")); + const localPath = join(getBinDir(), config.binaryName + (platform() === "win32" ? ".exe" : "")); if (existsSync(localPath) && commandWorks(localPath)) { return localPath; } @@ -190,6 +189,7 @@ class UnsupportedToolPlatformError extends Error {} async function downloadTool(tool: ManagedTool): Promise { const config = TOOLS[tool]; if (!config) throw new Error(`Unknown tool: ${tool}`); + const toolsDir = getBinDir(); const plat = platform(); const architecture = arch(); @@ -204,12 +204,12 @@ async function downloadTool(tool: ManagedTool): Promise { if (!assetName) throw new UnsupportedToolPlatformError(`Unsupported platform: ${plat}/${architecture}`); // Create tools directory - mkdirSync(TOOLS_DIR, { recursive: true }); + mkdirSync(toolsDir, { recursive: true }); const downloadUrl = `https://github.com/${config.repo}/releases/download/${config.tagPrefix}${version}/${assetName}`; - const archivePath = join(TOOLS_DIR, assetName); + const archivePath = join(toolsDir, assetName); const binaryExt = plat === "win32" ? ".exe" : ""; - const binaryPath = join(TOOLS_DIR, config.binaryName + binaryExt); + const binaryPath = join(toolsDir, config.binaryName + binaryExt); // Download await downloadFile(downloadUrl, archivePath); @@ -217,7 +217,7 @@ async function downloadTool(tool: ManagedTool): Promise { // Extract into a unique temp directory. fd and rg downloads can run concurrently // during startup, so sharing a fixed directory causes races. const extractDir = join( - TOOLS_DIR, + toolsDir, `extract_tmp_${config.binaryName}_${process.pid}_${Date.now()}_${Math.random().toString(36).slice(2, 10)}`, ); mkdirSync(extractDir, { recursive: true }); diff --git a/scripts/setup-kernel-venv.sh b/scripts/setup-kernel-venv.sh index 4a2df7314a2..cd02b0a8594 100755 --- a/scripts/setup-kernel-venv.sh +++ b/scripts/setup-kernel-venv.sh @@ -1,4 +1,4 @@ #!/usr/bin/env bash set -euo pipefail cd "$(dirname "$0")/.." -npx tsx packages/coding-agent/src/core/kernel/bootstrap-cli.ts +bun packages/coding-agent/src/core/kernel/bootstrap-cli.ts From 7ae24933778d71c8ed04614b2396e29391484501 Mon Sep 17 00:00:00 2001 From: Seth Date: Tue, 1 Sep 2026 17:38:30 -0400 Subject: [PATCH 04/45] test: run suites with Bun isolation --- bun.lock | 305 +------ bunfig.toml | 5 + package.json | 37 +- packages/agent/package.json | 8 +- packages/agent/vitest.config.ts | 21 - packages/ai/package.json | 8 +- packages/ai/test/anthropic-oauth.test.ts | 20 +- .../ai/test/azure-openai-base-url.test.ts | 10 +- .../test/bedrock-endpoint-resolution.test.ts | 10 +- .../ai/test/codex-websocket-cached-probe.ts | 4 +- .../ai/test/github-copilot-anthropic.test.ts | 15 +- packages/ai/test/github-copilot-oauth.test.ts | 118 ++- .../google-vertex-api-key-resolution.test.ts | 10 +- .../google-vertex-thinking-budget.test.ts | 25 +- packages/ai/test/lazy-module-load.test.ts | 84 +- packages/ai/test/mcp-oauth.test.ts | 197 ++-- packages/ai/test/openai-codex-oauth.test.ts | 34 +- packages/ai/test/openai-codex-stream.test.ts | 5 +- ...i-completions-cache-control-format.test.ts | 6 +- .../openai-completions-empty-tools.test.ts | 8 +- .../openai-completions-prompt-cache.test.ts | 11 +- .../openai-completions-response-model.test.ts | 6 +- .../openai-completions-tool-choice.test.ts | 29 +- packages/ai/vitest.config.ts | 9 - packages/coding-agent/bunfig.toml | 3 + packages/coding-agent/package.json | 18 +- packages/coding-agent/scripts/run-tests.ts | 61 ++ .../coding-agent/test/acp-cold-cli.test.ts | 10 +- .../test/acp-kernel-features.test.ts | 300 +++--- .../test/agent-connection-daemon.test.ts | 6 +- .../test/agent-session-concurrent.test.ts | 2 +- .../test/agent-session-services.test.ts | 4 +- .../test/agents-view-mode.test.ts | 25 +- .../test/assistant-message-streaming-bench.ts | 2 +- .../coding-agent/test/builtin-skills.test.ts | 19 +- packages/coding-agent/test/bun-bundle.test.ts | 122 +++ .../test/bun-installer-e2e.test.ts | 104 +++ .../coding-agent/test/bun-installer.test.ts | 214 +++++ .../test/bun-release-archives.test.ts | 150 +++ .../coding-agent/test/bun-test-preload.ts | 142 +++ .../test/bun-test-runtime-isolation.test.ts | 33 + .../clipboard-image-bmp-conversion.test.ts | 6 +- .../coding-agent/test/clipboard-image.test.ts | 1 - .../test/compaction-summary-reasoning.test.ts | 13 +- .../test/compiled-artifact-smoke.test.ts | 61 ++ .../coding-agent/test/daemon-client.test.ts | 40 +- .../coding-agent/test/daemon-command.test.ts | 11 +- .../coding-agent/test/daemon-launch.test.ts | 43 +- .../coding-agent/test/daemon-mode.test.ts | 15 +- .../test/daemon-multiclient-bench.ts | 8 +- packages/coding-agent/test/daemon-ps.test.ts | 6 +- .../daemon-supervisor-lazy-subagents.test.ts | 144 ++- .../test/daemon-supervisor-monitor.test.ts | 176 ++-- .../test/daemon-supervisor-process.test.ts | 862 +++++++++--------- packages/coding-agent/test/file-lines.test.ts | 13 +- .../fixtures/eng-4600-supervisor-fixture.ts | 56 +- .../test/fixtures/eng-4606-update-launcher.ts | 3 +- .../coding-agent/test/git-context.test.ts | 2 +- packages/coding-agent/test/git-update.test.ts | 1 + .../test/interactive-mode-status.test.ts | 67 +- .../test/interactive-update-relaunch.test.ts | 14 +- .../test/kernel-bash-shell.test.ts | 15 +- .../test/kernel-goal-skill.test.ts | 5 +- .../coding-agent/test/mcp-command.test.ts | 10 +- .../test/owned-session-worker-process.test.ts | 4 +- .../test/package-command-paths.test.ts | 16 +- .../test/package-self-update-daemon.test.ts | 20 +- packages/coding-agent/test/print-mode.test.ts | 2 +- .../coding-agent/test/public-command.test.ts | 4 +- packages/coding-agent/test/refinement.test.ts | 26 +- .../test/repl-kernel-abort.test.ts | 7 +- .../test/repl-kernel-mcp-shutdown.test.ts | 5 +- .../test/repl-kernel-parent-watchdog.test.ts | 20 +- .../test/repl-kernel-startup.test.ts | 10 +- .../test/repl-kernel-state-roundtrip.test.ts | 5 +- packages/coding-agent/test/rlm-ledger.test.ts | 4 +- packages/coding-agent/test/rpc-example.ts | 2 +- .../test/sdk-codex-cache-probe-tool-loop.ts | 4 +- .../test/session-manager-flush.test.ts | 4 +- .../test/stdout-cleanliness.test.ts | 4 +- .../test/streaming-render-debug.ts | 2 +- .../test/suite/acp-features.test.ts | 2 +- .../coding-agent/test/suite/acp-mode.test.ts | 2 +- .../suite/agent-session-action-races.test.ts | 4 +- ...nt-session-compaction-continuation.test.ts | 3 +- .../suite/agent-session-compaction.test.ts | 32 +- .../test/suite/agent-session-prompt.test.ts | 12 +- .../test/suite/agent-session-queue.test.ts | 31 +- .../daemon-serialized-refine-process.test.ts | 5 +- .../2791-fswatch-error-crash.test.ts | 12 +- .../4600-supervisor-singleton.test.ts | 8 +- ...4602-snapshot-transfer-idempotency.test.ts | 4 +- .../regressions/4603-worker-recovery.test.ts | 55 +- .../4606-update-restart-coordinator.test.ts | 23 +- .../4685-daemon-client-modes.test.ts | 14 +- ...17-subagent-terminal-agent-message.test.ts | 2 +- .../regressions/623-acp-canonical-cwd.test.ts | 2 +- .../coding-agent/test/suite/scheduling.ts | 16 + ...rialized-refine-config-integration.test.ts | 2 +- packages/coding-agent/test/test-tags.ts | 6 + .../coding-agent/test/test-theme-colors.ts | 6 +- packages/coding-agent/vitest.config.ts | 50 - packages/tui/package.json | 5 +- packages/tui/test/image-test.ts | 2 +- packages/tui/test/key-tester.ts | 2 +- packages/tui/vitest.config.ts | 7 - scripts/publish-workspaces.ts | 18 + scripts/run-with-clean-env.ts | 34 + scripts/set-version.ts | 88 ++ scripts/sync-versions.js | 90 -- test.sh | 2 +- tsconfig.json | 4 +- types/vitest.d.ts | 45 + 113 files changed, 2583 insertions(+), 1916 deletions(-) delete mode 100644 packages/agent/vitest.config.ts delete mode 100644 packages/ai/vitest.config.ts create mode 100644 packages/coding-agent/bunfig.toml create mode 100644 packages/coding-agent/scripts/run-tests.ts create mode 100644 packages/coding-agent/test/bun-bundle.test.ts create mode 100644 packages/coding-agent/test/bun-installer-e2e.test.ts create mode 100644 packages/coding-agent/test/bun-installer.test.ts create mode 100644 packages/coding-agent/test/bun-release-archives.test.ts create mode 100644 packages/coding-agent/test/bun-test-preload.ts create mode 100644 packages/coding-agent/test/bun-test-runtime-isolation.test.ts create mode 100644 packages/coding-agent/test/compiled-artifact-smoke.test.ts create mode 100644 packages/coding-agent/test/test-tags.ts delete mode 100644 packages/coding-agent/vitest.config.ts delete mode 100644 packages/tui/vitest.config.ts create mode 100644 scripts/publish-workspaces.ts create mode 100644 scripts/run-with-clean-env.ts create mode 100644 scripts/set-version.ts delete mode 100644 scripts/sync-versions.js create mode 100644 types/vitest.d.ts diff --git a/bun.lock b/bun.lock index 8fafded7e3b..dcae26ccae8 100644 --- a/bun.lock +++ b/bun.lock @@ -5,7 +5,7 @@ "": { "name": "prime-agent", "dependencies": { - "@earendil-works/pi-coding-agent": "^0.8.1", + "@earendil-works/pi-coding-agent": "^0.9.1", "get-east-asian-width": "^1.6.0", }, "devDependencies": { @@ -13,29 +13,30 @@ "@biomejs/biome": "2.5.5", "@types/node": "^22.10.5", "@typescript/native-preview": "7.0.0-dev.20260120.1", + "@vitest/expect": "4.1.10", + "@vitest/runner": "4.1.10", + "@vitest/spy": "4.1.10", + "bun-types": "1.3.14", "husky": "^9.1.7", "jiti": "^2.7.0", - "shx": "^0.4.0", - "tsx": "^4.23.1", "typescript": "^7.0.2", }, }, "packages/agent": { "name": "@earendil-works/pi-agent-core", - "version": "0.8.1", + "version": "0.9.1", "dependencies": { - "@earendil-works/pi-ai": "^0.8.1", + "@earendil-works/pi-ai": "^0.9.1", "typebox": "^1.3.9", }, "devDependencies": { "@types/node": "^24.3.0", "typescript": "^7.0.2", - "vitest": "^4.1.10", }, }, "packages/ai": { "name": "@earendil-works/pi-ai", - "version": "0.8.1", + "version": "0.9.1", "bin": { "pi-ai": "./dist/cli.js", }, @@ -55,20 +56,19 @@ "devDependencies": { "@types/node": "^24.3.0", "canvas": "^3.2.0", - "vitest": "^4.1.10", }, }, "packages/coding-agent": { "name": "@earendil-works/pi-coding-agent", - "version": "0.8.1", + "version": "0.9.1", "bin": { "pi": "dist/bundle/cli.js", }, "dependencies": { "@agentclientprotocol/sdk": "^1.3.0", - "@earendil-works/pi-agent-core": "^0.8.1", - "@earendil-works/pi-ai": "^0.8.1", - "@earendil-works/pi-tui": "^0.8.1", + "@earendil-works/pi-agent-core": "^0.9.1", + "@earendil-works/pi-ai": "^0.9.1", + "@earendil-works/pi-tui": "^0.9.1", "@silvia-odwyer/photon-node": "^0.3.4", "chalk": "^5.5.0", "cli-highlight": "^2.1.11", @@ -95,10 +95,7 @@ "@types/ms": "^2.1.0", "@types/node": "^24.3.0", "@types/proper-lockfile": "^4.1.4", - "esbuild": "^0.28.1", - "shx": "^0.4.0", "typescript": "^7.0.2", - "vitest": "^4.1.10", }, "optionalDependencies": { "@mariozechner/clipboard": "^0.3.9", @@ -106,25 +103,25 @@ }, "packages/coding-agent/examples/extensions/custom-provider-anthropic": { "name": "pi-extension-custom-provider-anthropic", - "version": "0.0.1", + "version": "0.1.1", "dependencies": { "@anthropic-ai/sdk": "^0.52.0", }, }, "packages/coding-agent/examples/extensions/custom-provider-gitlab-duo": { "name": "pi-extension-custom-provider-gitlab-duo", - "version": "0.0.1", + "version": "0.1.1", }, "packages/coding-agent/examples/extensions/sandbox": { "name": "pi-extension-sandbox", - "version": "1.4.0", + "version": "1.5.1", "dependencies": { "@anthropic-ai/sandbox-runtime": "^0.0.55", }, }, "packages/coding-agent/examples/extensions/with-deps": { "name": "pi-extension-with-deps", - "version": "0.0.1", + "version": "0.1.1", "dependencies": { "ms": "^2.1.3", }, @@ -134,7 +131,7 @@ }, "packages/tui": { "name": "@earendil-works/pi-tui", - "version": "0.8.1", + "version": "0.9.1", "dependencies": { "@types/mime-types": "^3.0.1", "chalk": "^5.5.0", @@ -230,62 +227,8 @@ "@earendil-works/pi-tui": ["@earendil-works/pi-tui@workspace:packages/tui"], - "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.28.2", "", { "os": "aix", "cpu": "ppc64" }, "sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ=="], - - "@esbuild/android-arm": ["@esbuild/android-arm@0.28.2", "", { "os": "android", "cpu": "arm" }, "sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg=="], - - "@esbuild/android-arm64": ["@esbuild/android-arm64@0.28.2", "", { "os": "android", "cpu": "arm64" }, "sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A=="], - - "@esbuild/android-x64": ["@esbuild/android-x64@0.28.2", "", { "os": "android", "cpu": "x64" }, "sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q=="], - - "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.28.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw=="], - - "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.28.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw=="], - - "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.28.2", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw=="], - - "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.28.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg=="], - - "@esbuild/linux-arm": ["@esbuild/linux-arm@0.28.2", "", { "os": "linux", "cpu": "arm" }, "sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w=="], - - "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.28.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug=="], - - "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.28.2", "", { "os": "linux", "cpu": "ia32" }, "sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ=="], - - "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.28.2", "", { "os": "linux", "cpu": "none" }, "sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ=="], - - "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.28.2", "", { "os": "linux", "cpu": "none" }, "sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA=="], - - "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.28.2", "", { "os": "linux", "cpu": "ppc64" }, "sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ=="], - - "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.28.2", "", { "os": "linux", "cpu": "none" }, "sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA=="], - - "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.28.2", "", { "os": "linux", "cpu": "s390x" }, "sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg=="], - - "@esbuild/linux-x64": ["@esbuild/linux-x64@0.28.2", "", { "os": "linux", "cpu": "x64" }, "sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ=="], - - "@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.28.2", "", { "os": "none", "cpu": "arm64" }, "sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw=="], - - "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.28.2", "", { "os": "none", "cpu": "x64" }, "sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw=="], - - "@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.28.2", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ=="], - - "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.28.2", "", { "os": "openbsd", "cpu": "x64" }, "sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw=="], - - "@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.28.2", "", { "os": "none", "cpu": "arm64" }, "sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q=="], - - "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.28.2", "", { "os": "sunos", "cpu": "x64" }, "sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g=="], - - "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.28.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ=="], - - "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.28.2", "", { "os": "win32", "cpu": "ia32" }, "sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA=="], - - "@esbuild/win32-x64": ["@esbuild/win32-x64@0.28.2", "", { "os": "win32", "cpu": "x64" }, "sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g=="], - "@google/genai": ["@google/genai@1.52.0", "", { "dependencies": { "google-auth-library": "^10.3.0", "p-retry": "^4.6.2", "protobufjs": "^7.5.4", "ws": "^8.18.0" }, "peerDependencies": { "@modelcontextprotocol/sdk": "^1.25.2" }, "optionalPeers": ["@modelcontextprotocol/sdk"] }, "sha512-gwSvbpiN/17O9TbsqSsE/OzZcpv5Fo4RQjdngGgogtuB9RsyJ8ZHhX5KjHj1bp5N9snN2eK8LDGXSaWW2hof8Q=="], - "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="], - "@mariozechner/clipboard": ["@mariozechner/clipboard@0.3.9", "", { "optionalDependencies": { "@mariozechner/clipboard-darwin-arm64": "0.3.9", "@mariozechner/clipboard-darwin-universal": "0.3.9", "@mariozechner/clipboard-darwin-x64": "0.3.9", "@mariozechner/clipboard-linux-arm64-gnu": "0.3.9", "@mariozechner/clipboard-linux-arm64-musl": "0.3.9", "@mariozechner/clipboard-linux-riscv64-gnu": "0.3.9", "@mariozechner/clipboard-linux-x64-gnu": "0.3.9", "@mariozechner/clipboard-linux-x64-musl": "0.3.9", "@mariozechner/clipboard-win32-arm64-msvc": "0.3.9", "@mariozechner/clipboard-win32-x64-msvc": "0.3.9" } }, "sha512-ABnA53mdfkGZwOFUdZNv2S0CWGO/EIuPj8Vv9xmBFmSYg/qFc7ihO6q5FcQjvoE67kZpWkEc4AhD6B/os04yuA=="], "@mariozechner/clipboard-darwin-arm64": ["@mariozechner/clipboard-darwin-arm64@0.3.9", "", { "os": "darwin", "cpu": "arm64" }, "sha512-BfgV7vCEWZwJwZJw03r6bP5+tf0iI/ANuQYCxi9RNn7FrWB3yzGuMKCrNLRl6V761vXRdL8+OqZ0wd4TqlsNOQ=="], @@ -310,14 +253,6 @@ "@mistralai/mistralai": ["@mistralai/mistralai@2.2.1", "", { "dependencies": { "ws": "^8.18.0", "zod": "^3.25.0 || ^4.0.0", "zod-to-json-schema": "^3.25.0" } }, "sha512-uKU8CZmL2RzYKmplsU01hii4p3pe4HqJefpWNRWXm1Tcm0Sm4xXfwSLIy4k7ZCPlbETCGcp69E7hZs+WOJ5itQ=="], - "@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="], - - "@nodelib/fs.stat": ["@nodelib/fs.stat@2.0.5", "", {}, "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A=="], - - "@nodelib/fs.walk": ["@nodelib/fs.walk@1.2.8", "", { "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" } }, "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg=="], - - "@oxc-project/types": ["@oxc-project/types@0.146.0", "", {}, "sha512-XC0QsnnhVe7sLIWmYmdPw7x5P0h4W8vUU3Nv1ySgWXtvCz8NizoAEpGXA0sOYoJQV2Rl13LgURAHQ5cI5ILCSA=="], - "@pondwader/socks5-server": ["@pondwader/socks5-server@1.0.10", "", {}, "sha512-bQY06wzzR8D2+vVCUoBsr5QS2U6UgPUQRmErNwtsuI6vLcyRKkafjkr3KxbtGFf9aBBIV2mcvlsKD1UYaIV+sg=="], "@protobufjs/aspromise": ["@protobufjs/aspromise@1.1.2", "", {}, "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ=="], @@ -338,38 +273,6 @@ "@protobufjs/utf8": ["@protobufjs/utf8@1.1.2", "", {}, "sha512-b1UQwcEZ4yCnMCD8DAL1VlbvBJE9/IX4FTIp7BG1xYpf29SLazLSrqUkj4w7Y5y7cCVP6E5tcqqcI0xemPkHug=="], - "@rolldown/binding-android-arm-eabi": ["@rolldown/binding-android-arm-eabi@1.2.5", "", { "os": "android", "cpu": "arm" }, "sha512-DLe/i+l8ynIBY7XEQ191TeZvCoowIGa18R+dIV30GW7DiOtp74i/xX8hs8GUjW5ARV7VZuie3d6AumSmCwbeRA=="], - - "@rolldown/binding-android-arm64": ["@rolldown/binding-android-arm64@1.2.5", "", { "os": "android", "cpu": "arm64" }, "sha512-zXcwKlQApYAOELHd8PwKDFkagYF9Wy4e0RJ+0qnzl9Pjnpj75TEG8ufv40p2J7kCEfwZAsNiuzRIyNNMWT38ig=="], - - "@rolldown/binding-darwin-arm64": ["@rolldown/binding-darwin-arm64@1.2.5", "", { "os": "darwin", "cpu": "arm64" }, "sha512-dK4QakI42nzWgJT5sm4y4y/O//D4OxM75/cH28RLV+nzIN9AY+YsbuUVrUTjlLjXR6vpyxFbSsbmNuJ6BP9sww=="], - - "@rolldown/binding-darwin-x64": ["@rolldown/binding-darwin-x64@1.2.5", "", { "os": "darwin", "cpu": "x64" }, "sha512-fqSALaUu1Wjd1nK2uW2kJDWdLCc8lx1IcY+MTY26Aurfdx19anlzhqXOgCFbBFQnlFDTn4TC1/7Nz4Bl2mLP3A=="], - - "@rolldown/binding-freebsd-x64": ["@rolldown/binding-freebsd-x64@1.2.5", "", { "os": "freebsd", "cpu": "x64" }, "sha512-/vCnNxlkxs9tKxNDcyWUePpJ/PgTzxIaVhoM5SmG8UV+GR/IcPam4VYxi7GIMo7PSDuNqlJqvprqii9NqqVCMw=="], - - "@rolldown/binding-linux-arm-gnueabihf": ["@rolldown/binding-linux-arm-gnueabihf@1.2.5", "", { "os": "linux", "cpu": "arm" }, "sha512-abk0NLA519LxRCszmbE0jYKuQ9YPocOXTiOXOo6Yr+YAT95VH+PtqYAjOJvGKt3viEd/x4qzabAlwd5bHOOARg=="], - - "@rolldown/binding-linux-arm64-gnu": ["@rolldown/binding-linux-arm64-gnu@1.2.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-Y7eALiJ8lr0M2HH103Js+g7V34wf6snlpZLAsHI90uLhr3PVlNsbFVAXJC9d/V6BnPyKtpSwI+NcB/RLxsQxuA=="], - - "@rolldown/binding-linux-arm64-musl": ["@rolldown/binding-linux-arm64-musl@1.2.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-xMvZgnbZg4YVnR/AX2b3oOPDTFYJvUVaJg5FedA/LuvexAtXibZQej4cnTkw3rjsJ/ggUROB64TdtETiim+FYA=="], - - "@rolldown/binding-linux-ppc64-gnu": ["@rolldown/binding-linux-ppc64-gnu@1.2.5", "", { "os": "linux", "cpu": "ppc64" }, "sha512-GRjeqTUDHTo5GwntsLaAMcBahG3nlpjftXWZLN73HiYQlhwEowvarFgQnRnQZtIp4keXX7quXFbG38uPZBa2EA=="], - - "@rolldown/binding-linux-s390x-gnu": ["@rolldown/binding-linux-s390x-gnu@1.2.5", "", { "os": "linux", "cpu": "s390x" }, "sha512-vLNTR45F2Uwc8AufkNXPmB4VliaXs+FvcheEogIzOXzO4l+LzieXF5A/TWxLy5HtqpsRCHUfd0lPVrrdgXdLHQ=="], - - "@rolldown/binding-linux-x64-gnu": ["@rolldown/binding-linux-x64-gnu@1.2.5", "", { "os": "linux", "cpu": "x64" }, "sha512-Mgj59/HTuYeK9Gz2MA+mBWKnHsAgkBSec15ZMb1st3oIfFbX7gCjOae7GydHhzcyQi9Z/7M1QuN9bR3oFqF0jQ=="], - - "@rolldown/binding-linux-x64-musl": ["@rolldown/binding-linux-x64-musl@1.2.5", "", { "os": "linux", "cpu": "x64" }, "sha512-mY8AP0/ichsbhAxGnLa3d3+MwV0EfgrPND2bplI3Ym8T6R2pJ0N87bvrKVwNXmdy3jnr6eQBecdqx/HMknBmpA=="], - - "@rolldown/binding-openharmony-arm64": ["@rolldown/binding-openharmony-arm64@1.2.5", "", { "os": "none", "cpu": "arm64" }, "sha512-8SLssA2oweAxyRgDp789ACfRb/3P+zNRJpzZxSizxF9m8NUDQ4+3xjo8ttjhVGGw6Qxb70oZiEtIjaKikCO7Yw=="], - - "@rolldown/binding-win32-arm64-msvc": ["@rolldown/binding-win32-arm64-msvc@1.2.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-vGbruD5zquhoc8D9SViXgN2FBJtNdTyQ4DtG+SWiEGlJiAzoKcZ2xp+xuXCffhubVdt0NJlTZqkeRuERy7g8Cw=="], - - "@rolldown/binding-win32-x64-msvc": ["@rolldown/binding-win32-x64-msvc@1.2.5", "", { "os": "win32", "cpu": "x64" }, "sha512-e/SXpgISz+IoqVcSSI0rx/d/he8zqLex+/rCWpnHpmVfmPIUjag9H6P7zotf0gJHwPUhQxZ/mF8tr6acebT9yw=="], - - "@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.1", "", {}, "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw=="], - "@silvia-odwyer/photon-node": ["@silvia-odwyer/photon-node@0.3.4", "", {}, "sha512-bnly4BKB3KDTFxrUIcgCLbaeVVS8lrAkri1pEzskpmxu9MdfGQTy8b8EgcD83ywD3RPMsIulY8xJH5Awa+t9fA=="], "@smithy/core": ["@smithy/core@3.33.3", "", { "dependencies": { "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-CsOeKq/9kA3y6VJHt+/+VTCtBaxJ4OTFpgrjIUhPpDIKxBci1k2bJaQASF2h/ELWrulGp+t97DZ0mevfAD8idg=="], @@ -398,8 +301,6 @@ "@types/diff": ["@types/diff@8.0.0", "", { "dependencies": { "diff": "*" } }, "sha512-o7jqJM04gfaYrdCecCVMbZhNdG6T1MHg/oQoRFdERLV+4d+V7FijhiEAbFu0Usww84Yijk9yH58U4Jk4HbtzZw=="], - "@types/estree": ["@types/estree@1.0.9", "", {}, "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg=="], - "@types/hosted-git-info": ["@types/hosted-git-info@3.0.5", "", {}, "sha512-Dmngh7U003cOHPhKGyA7LWqrnvcTyILNgNPmNCxlx7j8MIi54iBliiT8XqVLIQ3GchoOjVAyBzNJVyuaJjqokg=="], "@types/mime-types": ["@types/mime-types@3.0.1", "", {}, "sha512-xRMsfuQbnRq1Ef+C+RKaENOxXX87Ygl38W1vDfPHRku02TgQr+Qd8iivLtAMcR0KF5/29xlnFihkTlbqFrGOVQ=="], @@ -470,19 +371,15 @@ "@typescript/typescript-win32-x64": ["@typescript/typescript-win32-x64@7.0.2", "", { "os": "win32", "cpu": "x64" }, "sha512-0BQ3HkAHHlKLSp1qRvf3SUhGpGsDuhB/jgFw75guyqbxJqEaS0Cw/VFO8i2nHglJUzQCRtMMR/IBAKE3ETMC4g=="], - "@vitest/expect": ["@vitest/expect@4.1.11", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "@types/chai": "^5.2.2", "@vitest/spy": "4.1.11", "@vitest/utils": "4.1.11", "chai": "^6.2.2", "tinyrainbow": "^3.1.0" } }, "sha512-VX2x5vNJXET47KAFzwERI+KRMtTTCSWTfSMKsW7JsUsXV4psq++e3DvZpuTDOpHcxytiDs6p2nhVb2tVDiiUYw=="], - - "@vitest/mocker": ["@vitest/mocker@4.1.11", "", { "dependencies": { "@vitest/spy": "4.1.11", "estree-walker": "^3.0.3", "magic-string": "^0.30.21" }, "peerDependencies": { "msw": "^2.4.9", "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" }, "optionalPeers": ["msw", "vite"] }, "sha512-2XJVD55d1o5AZous5CCGKS74g/riOj9odEt2bQpCVZeblHyHdnMeFl4jl0XjU21stf4mbjUkew2eXQZt65g5CQ=="], + "@vitest/expect": ["@vitest/expect@4.1.10", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "@types/chai": "^5.2.2", "@vitest/spy": "4.1.10", "@vitest/utils": "4.1.10", "chai": "^6.2.2", "tinyrainbow": "^3.1.0" } }, "sha512-YsCn+qAk1GWjQOWFEsEcL2gNQ0zmVmQu3T03qP6UyjhtmdtwtbuI+DASn/7iQB3HGTXkdBwGddzxPlmiql5vlA=="], - "@vitest/pretty-format": ["@vitest/pretty-format@4.1.11", "", { "dependencies": { "tinyrainbow": "^3.1.0" } }, "sha512-yiZzPbGTS9Sr/JpFl8zHrcIkAofNbFV6k21vIgQN/cY/oxZeXhJv5sc/MBJ5jFKWmWs+oJHw0UXLZjmf931+Vw=="], + "@vitest/pretty-format": ["@vitest/pretty-format@4.1.10", "", { "dependencies": { "tinyrainbow": "^3.1.0" } }, "sha512-W1HsjSH4MXQ9YfmmhLAoIYf1HRfekQCGngeIgcei6MP5QQGWUe0gkopdZQaVCFO+JDJMrAJGwa5pRpNpvy4P8Q=="], - "@vitest/runner": ["@vitest/runner@4.1.11", "", { "dependencies": { "@vitest/utils": "4.1.11", "pathe": "^2.0.3" } }, "sha512-LztvUgdwMNJMIkj3hQnnxiC2Xy1zNxq928W/xhjCLaNCzqTZOudjwbQf6v9IntZGPw132i2Lq2rgTRZHD3JHNw=="], + "@vitest/runner": ["@vitest/runner@4.1.10", "", { "dependencies": { "@vitest/utils": "4.1.10", "pathe": "^2.0.3" } }, "sha512-IKI6kpIH+LmpROplyLwBBaCfMgOZOMsygVa6BARD6ahA04VRuJSa6OaVG7kRvSEMD870Vd91rSSw0eegtWyLGg=="], - "@vitest/snapshot": ["@vitest/snapshot@4.1.11", "", { "dependencies": { "@vitest/pretty-format": "4.1.11", "@vitest/utils": "4.1.11", "magic-string": "^0.30.21", "pathe": "^2.0.3" } }, "sha512-pN7ikn1ON7h8ee4gIAp4AzyK+zBtJPzVbqOgu5LCEh4VaJVbPQcgYQYJIMGQPXVeJJq1fnfazis7a5pFNPahog=="], + "@vitest/spy": ["@vitest/spy@4.1.10", "", {}, "sha512-PLf/Ugvoq5wO/b4rwYCR1h2PSIdXz7wnkQFMiUpLdtM7l6pqVFcQIBEHyT1+l+cj7mNwAfZHzqXqDyjvOuwbDw=="], - "@vitest/spy": ["@vitest/spy@4.1.11", "", {}, "sha512-apNa/prQy2qCeywhnixOHPRCgGNhvg7T4Dapfl1GahLp/R+uhBm5cPyFoNVyqsNd2h1nJxL6BqqdIjiABL60YA=="], - - "@vitest/utils": ["@vitest/utils@4.1.11", "", { "dependencies": { "@vitest/pretty-format": "4.1.11", "convert-source-map": "^2.0.0", "tinyrainbow": "^3.1.0" } }, "sha512-zTCVGpyFsGWBhllOyKlTw/vnr6D9qxsfSDyfbyZmTyjHw5N/VuvzHpHoQjm2ZJzn4RJgx5w4r7V0er69CmLgPQ=="], + "@vitest/utils": ["@vitest/utils@4.1.10", "", { "dependencies": { "@vitest/pretty-format": "4.1.10", "convert-source-map": "^2.0.0", "tinyrainbow": "^3.1.0" } }, "sha512-fy9am/HWxbaGt/Sawrp90vt6Y6jQwf1RX77cz3uwoJwJVMli/e1IEwRPnMNJ7vKfPTwo0diXifkpPvwH9v7nGA=="], "@xterm/headless": ["@xterm/headless@6.0.0", "", {}, "sha512-5Yj1QINYCyzrZtf8OFIHi47iQtI+0qYFPHmouEfG8dHNxbZ9Tb9YGSuLcsEwj9Z+OL75GJqPyJbyoFer80a2Hw=="], @@ -514,14 +411,14 @@ "brace-expansion": ["brace-expansion@5.0.9", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg=="], - "braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="], - "buffer": ["buffer@5.7.1", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.1.13" } }, "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ=="], "buffer-crc32": ["buffer-crc32@0.2.13", "", {}, "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ=="], "buffer-equal-constant-time": ["buffer-equal-constant-time@1.0.1", "", {}, "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA=="], + "bun-types": ["bun-types@1.3.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-4N0ig0fEomHt5R0KCFWjovxow98rIoRwKolrYdCcknNwMekCXRnWEUvgu5soYV8QXtVsrUD8B95MBOZGPvr6KQ=="], + "canvas": ["canvas@3.2.3", "", { "dependencies": { "node-addon-api": "^7.0.0", "prebuild-install": "^7.1.3" } }, "sha512-PzE5nJZPz72YUAfo8oTp0u3fqqY7IzlTubneAihqDYAUcBk7ryeCmBbdJBEdaH0bptSOe2VT2Zwcb3UaFyaSWw=="], "chai": ["chai@6.2.2", "", {}, "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg=="], @@ -542,8 +439,6 @@ "convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="], - "cross-spawn": ["cross-spawn@6.0.6", "", { "dependencies": { "nice-try": "^1.0.4", "path-key": "^2.0.1", "semver": "^5.5.0", "shebang-command": "^1.2.0", "which": "^1.2.9" } }, "sha512-VqCUuhcd1iB+dsv8gxPttb5iZh/D0iubSP21g36KXdEuf6I5JiioesUVjpCdHV9MZRUfVFlvwtIUyPfxo5trtw=="], - "data-uri-to-buffer": ["data-uri-to-buffer@6.0.2", "", {}, "sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw=="], "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" }, "peerDependencies": { "supports-color": "*" }, "optionalPeers": ["supports-color"] }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], @@ -564,12 +459,6 @@ "end-of-stream": ["end-of-stream@1.4.5", "", { "dependencies": { "once": "^1.4.0" } }, "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg=="], - "es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="], - - "es-module-lexer": ["es-module-lexer@2.3.2", "", {}, "sha512-poHGpORABojJJucnV9KbOavETW8lBVnphkW77ER5/BQ5Fz7oXSoCNek7IH3vR5nRjdsEz926ibFYX8KtLQmdyw=="], - - "esbuild": ["esbuild@0.28.2", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.28.2", "@esbuild/android-arm": "0.28.2", "@esbuild/android-arm64": "0.28.2", "@esbuild/android-x64": "0.28.2", "@esbuild/darwin-arm64": "0.28.2", "@esbuild/darwin-x64": "0.28.2", "@esbuild/freebsd-arm64": "0.28.2", "@esbuild/freebsd-x64": "0.28.2", "@esbuild/linux-arm": "0.28.2", "@esbuild/linux-arm64": "0.28.2", "@esbuild/linux-ia32": "0.28.2", "@esbuild/linux-loong64": "0.28.2", "@esbuild/linux-mips64el": "0.28.2", "@esbuild/linux-ppc64": "0.28.2", "@esbuild/linux-riscv64": "0.28.2", "@esbuild/linux-s390x": "0.28.2", "@esbuild/linux-x64": "0.28.2", "@esbuild/netbsd-arm64": "0.28.2", "@esbuild/netbsd-x64": "0.28.2", "@esbuild/openbsd-arm64": "0.28.2", "@esbuild/openbsd-x64": "0.28.2", "@esbuild/openharmony-arm64": "0.28.2", "@esbuild/sunos-x64": "0.28.2", "@esbuild/win32-arm64": "0.28.2", "@esbuild/win32-ia32": "0.28.2", "@esbuild/win32-x64": "0.28.2" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA=="], - "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="], "escodegen": ["escodegen@2.1.0", "", { "dependencies": { "esprima": "^4.0.1", "estraverse": "^5.2.0", "esutils": "^2.0.2" }, "optionalDependencies": { "source-map": "~0.6.1" }, "bin": { "esgenerate": "bin/esgenerate.js", "escodegen": "bin/escodegen.js" } }, "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w=="], @@ -578,42 +467,24 @@ "estraverse": ["estraverse@5.3.0", "", {}, "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA=="], - "estree-walker": ["estree-walker@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g=="], - "esutils": ["esutils@2.0.3", "", {}, "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="], - "execa": ["execa@1.0.0", "", { "dependencies": { "cross-spawn": "^6.0.0", "get-stream": "^4.0.0", "is-stream": "^1.1.0", "npm-run-path": "^2.0.0", "p-finally": "^1.0.0", "signal-exit": "^3.0.0", "strip-eof": "^1.0.0" } }, "sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA=="], - "expand-template": ["expand-template@2.0.3", "", {}, "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg=="], - "expect-type": ["expect-type@1.4.0", "", {}, "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA=="], - "extend": ["extend@3.0.2", "", {}, "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="], "extract-zip": ["extract-zip@2.0.1", "", { "dependencies": { "debug": "^4.1.1", "get-stream": "^5.1.0", "yauzl": "^2.10.0" }, "optionalDependencies": { "@types/yauzl": "^2.9.1" }, "bin": { "extract-zip": "cli.js" } }, "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg=="], - "fast-glob": ["fast-glob@3.3.3", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.8" } }, "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg=="], - - "fastq": ["fastq@1.20.1", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw=="], - "fd-slicer": ["fd-slicer@1.1.0", "", { "dependencies": { "pend": "~1.2.0" } }, "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g=="], - "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], - "fetch-blob": ["fetch-blob@3.2.0", "", { "dependencies": { "node-domexception": "^1.0.0", "web-streams-polyfill": "^3.0.3" } }, "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ=="], "file-type": ["file-type@21.3.4", "", { "dependencies": { "@tokenizer/inflate": "^0.4.1", "strtok3": "^10.3.4", "token-types": "^6.1.1", "uint8array-extras": "^1.4.0" } }, "sha512-Ievi/yy8DS3ygGvT47PjSfdFoX+2isQueoYP1cntFW1JLYAuS4GD7NUPGg4zv2iZfV52uDyk5w5Z0TdpRS6Q1g=="], - "fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="], - "formdata-polyfill": ["formdata-polyfill@4.0.10", "", { "dependencies": { "fetch-blob": "^3.1.2" } }, "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g=="], "fs-constants": ["fs-constants@1.0.0", "", {}, "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow=="], - "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], - - "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.2", "", { "dependencies": { "gaxios": "^7.0.0", "google-logging-utils": "^1.0.0", "json-bigint": "^1.0.0" } }, "sha512-zV/5HKTfCeKWnxG0Dmrw51hEWFGfcF2xiXqcA3+J90WDuP0SvoiSO5ORvcBsifmx/FoIjgQN3oNOGaQ5PhLFkg=="], @@ -630,8 +501,6 @@ "glob": ["glob@13.0.6", "", { "dependencies": { "minimatch": "^10.2.2", "minipass": "^7.1.3", "path-scurry": "^2.0.2" } }, "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw=="], - "glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], - "google-auth-library": ["google-auth-library@10.9.1", "", { "dependencies": { "base64-js": "^1.3.0", "ecdsa-sig-formatter": "^1.0.11", "gaxios": "^7.1.4", "gcp-metadata": "8.1.2", "google-logging-utils": "1.1.3", "jws": "^4.0.0" } }, "sha512-i1ydyHrqcIxXkWh/uBmVkzCvIuq5yiK2ATndIe5XxKholrG/MTYP9xGYka4sQhrbIAgGjL2B6NOE7rFaiF3fXw=="], "google-logging-utils": ["google-logging-utils@1.1.3", "", {}, "sha512-eAmLkjDjAFCVXg7A1unxHsLf961m6y17QFqXqAXGj/gVkKFrEICfStRfwUlGNfeCEjNRa32JEWOUTlYXPyyKvA=="], @@ -642,8 +511,6 @@ "has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], - "hasown": ["hasown@2.0.4", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A=="], - "highlight.js": ["highlight.js@10.7.3", "", {}, "sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A=="], "hosted-git-info": ["hosted-git-info@9.0.3", "", { "dependencies": { "lru-cache": "^11.1.0" } }, "sha512-Hc+ghLoSt6QaYZUv0WBiIvmMDZuZZ7oaDvdH8MbfOO4lOsxdXLEvuC6ePoGs9H1X9oCLyq6+NVN0MKqD+ydxyg=="], @@ -662,24 +529,10 @@ "ini": ["ini@1.3.8", "", {}, "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew=="], - "interpret": ["interpret@1.4.0", "", {}, "sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA=="], - "ip-address": ["ip-address@10.5.0", "", {}, "sha512-R5SnVLJmgYYvf2F2ZgwSBnelz5G4q5AxIC277GDfUaNbrZKNANcBC7RHqYYePlszf4kBolVkJauG0ZjHHFh55g=="], - "is-core-module": ["is-core-module@2.16.2", "", { "dependencies": { "hasown": "^2.0.3" } }, "sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA=="], - - "is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="], - "is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="], - "is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="], - - "is-number": ["is-number@7.0.0", "", {}, "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="], - - "is-stream": ["is-stream@1.1.0", "", {}, "sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ=="], - - "isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], - "jiti": ["jiti@2.7.0", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ=="], "json-bigint": ["json-bigint@1.0.0", "", { "dependencies": { "bignumber.js": "^9.0.0" } }, "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ=="], @@ -692,42 +545,12 @@ "koffi": ["koffi@2.16.3", "", {}, "sha512-E9y1AsgYGlaxMhcZzHr8y96QF2U5XzA12GGVAfbWqIubTwPNMXQarfBzePNXHe0xtIEtNd6ifAv3GAKYGUeBAQ=="], - "lightningcss": ["lightningcss@1.33.0", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.33.0", "lightningcss-darwin-arm64": "1.33.0", "lightningcss-darwin-x64": "1.33.0", "lightningcss-freebsd-x64": "1.33.0", "lightningcss-linux-arm-gnueabihf": "1.33.0", "lightningcss-linux-arm64-gnu": "1.33.0", "lightningcss-linux-arm64-musl": "1.33.0", "lightningcss-linux-x64-gnu": "1.33.0", "lightningcss-linux-x64-musl": "1.33.0", "lightningcss-win32-arm64-msvc": "1.33.0", "lightningcss-win32-x64-msvc": "1.33.0" } }, "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA=="], - - "lightningcss-android-arm64": ["lightningcss-android-arm64@1.33.0", "", { "os": "android", "cpu": "arm64" }, "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg=="], - - "lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.33.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg=="], - - "lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.33.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ=="], - - "lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.33.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg=="], - - "lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.33.0", "", { "os": "linux", "cpu": "arm" }, "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ=="], - - "lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.33.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg=="], - - "lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.33.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ=="], - - "lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.33.0", "", { "os": "linux", "cpu": "x64" }, "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg=="], - - "lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.33.0", "", { "os": "linux", "cpu": "x64" }, "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw=="], - - "lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.33.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA=="], - - "lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.33.0", "", { "os": "win32", "cpu": "x64" }, "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA=="], - "long": ["long@5.3.2", "", {}, "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA=="], "lru-cache": ["lru-cache@7.18.3", "", {}, "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA=="], - "magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="], - "marked": ["marked@18.0.11", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-HnslJfsZkRPBDJRHvVtAaWlZHEpSu7u8LgQuJCELjRKuWR+hpq4A7sLq3p8HaI9ypVoXDXxV34CsQJEe1+J5Aw=="], - "merge2": ["merge2@1.4.1", "", {}, "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="], - - "micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="], - "mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="], "mime-types": ["mime-types@3.0.2", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A=="], @@ -746,14 +569,10 @@ "mz": ["mz@2.7.0", "", { "dependencies": { "any-promise": "^1.0.0", "object-assign": "^4.0.1", "thenify-all": "^1.0.0" } }, "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q=="], - "nanoid": ["nanoid@3.3.18", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w=="], - "napi-build-utils": ["napi-build-utils@2.0.0", "", {}, "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA=="], "netmask": ["netmask@2.1.1", "", {}, "sha512-eonl3sLUha+S1GzTPxychyhnUzKyeQkZ7jLjKrBagJgPla13F+uQ71HgpFefyHgqrjEbCPkDArxYsjY8/+gLKA=="], - "nice-try": ["nice-try@1.0.5", "", {}, "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ=="], - "node-abi": ["node-abi@3.95.0", "", { "dependencies": { "semver": "^7.3.5" } }, "sha512-T9iGctuocf0qIWFFOTxPzjT5q0SILqaBYXt272tlBHvTKC5+3JnkMirLxNJNkXHtFyBjU2Jx+NL4Zipr0B/c6Q=="], "node-addon-api": ["node-addon-api@7.1.1", "", {}, "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ=="], @@ -764,18 +583,12 @@ "node-forge": ["node-forge@1.4.0", "", {}, "sha512-LarFH0+6VfriEhqMMcLX2F7SwSXeWwnEAJEsYm5QKWchiVYVvJyV9v7UDvUv+w5HO23ZpQTXDv/GxdDdMyOuoQ=="], - "npm-run-path": ["npm-run-path@2.0.2", "", { "dependencies": { "path-key": "^2.0.0" } }, "sha512-lJxZYlT4DW/bRUtFh1MQIWqmLwQfAxnqWG4HhEdjMlkrJYnJn0Jrr2u3mgxqaWsdiBc76TYkTG/mhrnYTuzfHw=="], - "object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="], - "obug": ["obug@2.1.4", "", {}, "sha512-4a+OsYv9UktOJKE+l1A4OufDgdRF9PifWj+tJnHURo/P+WOxpG4GzUFL9qCalmWauao6ogiG+QvnCovwPoyAWA=="], - "once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="], "openai": ["openai@6.47.0", "", { "peerDependencies": { "@aws-sdk/credential-provider-node": ">=3.972.0 <4", "@smithy/hash-node": ">=4.3.0 <5", "@smithy/signature-v4": ">=5.4.0 <6", "ws": "^8.18.0", "zod": "^3.25 || ^4.0" }, "optionalPeers": ["@aws-sdk/credential-provider-node", "@smithy/hash-node", "@smithy/signature-v4", "ws", "zod"] }, "sha512-xYr+R9woSzWxVxeiqkkNbHhv89tZDEI6eBMbrdPnv3poh+mijHvbhS35a+3o6xHa411/ns8j5ENY3So9DCXWYw=="], - "p-finally": ["p-finally@1.0.0", "", {}, "sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow=="], - "p-retry": ["p-retry@4.6.2", "", { "dependencies": { "@types/retry": "0.12.0", "retry": "^0.13.1" } }, "sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ=="], "pac-proxy-agent": ["pac-proxy-agent@7.2.0", "", { "dependencies": { "@tootallnate/quickjs-emscripten": "^0.23.0", "agent-base": "^7.1.2", "debug": "^4.3.4", "get-uri": "^6.0.1", "http-proxy-agent": "^7.0.0", "https-proxy-agent": "^7.0.6", "pac-resolver": "^7.0.1", "socks-proxy-agent": "^8.0.5" } }, "sha512-TEB8ESquiLMc0lV8vcd5Ql/JAKAoyzHFXaStwjkzpOpC5Yv+pIzLfHvjTSdf3vpa2bMiUQrg9i6276yn8666aA=="], @@ -788,10 +601,6 @@ "partial-json": ["partial-json@0.1.7", "", {}, "sha512-Njv/59hHaokb/hRUjce3Hdv12wd60MtM9Z5Olmn+nehe0QDAsRtRbJPvJ0Z91TusF0SuZRIvnM+S4l6EIP8leA=="], - "path-key": ["path-key@2.0.1", "", {}, "sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw=="], - - "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=="], "pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], @@ -806,12 +615,6 @@ "pi-extension-with-deps": ["pi-extension-with-deps@workspace:packages/coding-agent/examples/extensions/with-deps"], - "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], - - "picomatch": ["picomatch@4.0.7", "", {}, "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA=="], - - "postcss": ["postcss@8.5.26", "", { "dependencies": { "nanoid": "^3.3.17", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ=="], - "prebuild-install": ["prebuild-install@7.1.3", "", { "dependencies": { "detect-libc": "^2.0.0", "expand-template": "^2.0.3", "github-from-package": "0.0.0", "minimist": "^1.2.3", "mkdirp-classic": "^0.5.3", "napi-build-utils": "^2.0.0", "node-abi": "^3.3.0", "pump": "^3.0.0", "rc": "^1.2.7", "simple-get": "^4.0.0", "tar-fs": "^2.0.0", "tunnel-agent": "^0.6.0" }, "bin": { "prebuild-install": "bin.js" } }, "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug=="], "proper-lockfile": ["proper-lockfile@4.1.2", "", { "dependencies": { "graceful-fs": "^4.2.4", "retry": "^0.12.0", "signal-exit": "^3.0.2" } }, "sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA=="], @@ -824,42 +627,20 @@ "pump": ["pump@3.0.4", "", { "dependencies": { "end-of-stream": "^1.1.0", "once": "^1.3.1" } }, "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA=="], - "queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="], - "rc": ["rc@1.2.8", "", { "dependencies": { "deep-extend": "^0.6.0", "ini": "~1.3.0", "minimist": "^1.2.0", "strip-json-comments": "~2.0.1" }, "bin": { "rc": "./cli.js" } }, "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw=="], "readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="], - "rechoir": ["rechoir@0.6.2", "", { "dependencies": { "resolve": "^1.1.6" } }, "sha512-HFM8rkZ+i3zrV+4LQjwQ0W+ez98pApMGM3HUrN04j3CqzPOzl9nmP15Y8YXNm8QHGv/eacOVEjqhmWpkRV0NAw=="], - "require-directory": ["require-directory@2.1.1", "", {}, "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q=="], - "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=="], - "retry": ["retry@0.12.0", "", {}, "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow=="], - "reusify": ["reusify@1.1.0", "", {}, "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw=="], - - "rolldown": ["rolldown@1.2.5", "", { "dependencies": { "@oxc-project/types": "=0.146.0", "@rolldown/pluginutils": "^1.0.0" }, "optionalDependencies": { "@rolldown/binding-android-arm-eabi": "1.2.5", "@rolldown/binding-android-arm64": "1.2.5", "@rolldown/binding-darwin-arm64": "1.2.5", "@rolldown/binding-darwin-x64": "1.2.5", "@rolldown/binding-freebsd-x64": "1.2.5", "@rolldown/binding-linux-arm-gnueabihf": "1.2.5", "@rolldown/binding-linux-arm64-gnu": "1.2.5", "@rolldown/binding-linux-arm64-musl": "1.2.5", "@rolldown/binding-linux-ppc64-gnu": "1.2.5", "@rolldown/binding-linux-s390x-gnu": "1.2.5", "@rolldown/binding-linux-x64-gnu": "1.2.5", "@rolldown/binding-linux-x64-musl": "1.2.5", "@rolldown/binding-openharmony-arm64": "1.2.5", "@rolldown/binding-win32-arm64-msvc": "1.2.5", "@rolldown/binding-win32-x64-msvc": "1.2.5" }, "bin": { "rolldown": "./bin/cli.mjs" } }, "sha512-VD2IE5PUG4Oj8zz2VGykiYd5wbnjdIiSsNQb8Qu5B+noEp+A78mu2iVvpp27g8es14Tk9rofNs5Tku9iQCS4fA=="], - - "run-parallel": ["run-parallel@1.2.0", "", { "dependencies": { "queue-microtask": "^1.2.2" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="], - "safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="], "semver": ["semver@7.8.5", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA=="], - "shebang-command": ["shebang-command@1.2.0", "", { "dependencies": { "shebang-regex": "^1.0.0" } }, "sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg=="], - - "shebang-regex": ["shebang-regex@1.0.0", "", {}, "sha512-wpoSFAxys6b2a2wHZ1XpDSgD7N9iVjg29Ph9uV/uaP9Ex/KXlkTZTeddxDPSYQpgvzKLGJke2UU0AzoGCjNIvQ=="], - "shell-quote": ["shell-quote@1.10.0", "", {}, "sha512-w1aiOKwKuRgtwAReIIj89puqg+I7GvX4IbLrvmhXbzQsj1+Zwi4VO3+fa6ZF91TWSjIxoEkKnMeHcLEODK5ZXA=="], - "shelljs": ["shelljs@0.9.2", "", { "dependencies": { "execa": "^1.0.0", "fast-glob": "^3.3.2", "interpret": "^1.0.0", "rechoir": "^0.6.2" }, "bin": { "shjs": "bin/shjs" } }, "sha512-S3I64fEiKgTZzKCC46zT/Ib9meqofLrQVbpSswtjFfAVDW+AZ54WTnAM/3/yENoxz/V1Cy6u3kiiEbQ4DNphvw=="], - - "shx": ["shx@0.4.0", "", { "dependencies": { "minimist": "^1.2.8", "shelljs": "^0.9.2" }, "bin": { "shx": "lib/cli.js" } }, "sha512-Z0KixSIlGPpijKgcH6oCMCbltPImvaKy0sGH8AkLRXw1KyzpKtaCTizP2xen+hNDqVF4xxgvA0KXSb9o4Q6hnA=="], - - "siginfo": ["siginfo@2.0.0", "", {}, "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g=="], - "signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="], "simple-concat": ["simple-concat@1.0.1", "", {}, "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q=="], @@ -874,28 +655,18 @@ "source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], - "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], - - "stackback": ["stackback@0.0.2", "", {}, "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw=="], - - "std-env": ["std-env@4.2.0", "", {}, "sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw=="], - "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=="], "string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="], "strip-ansi": ["strip-ansi@7.2.0", "", { "dependencies": { "ansi-regex": "^6.2.2" } }, "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w=="], - "strip-eof": ["strip-eof@1.0.0", "", {}, "sha512-7FCwGGmx8mD5xQd3RPUvnSpUXHM3BWuzjtpD4TXsfcZ9EL4azvVVUscFYwD9nx8Kh+uCBC00XBtAykoMHwTh8Q=="], - "strip-json-comments": ["strip-json-comments@2.0.1", "", {}, "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ=="], "strtok3": ["strtok3@10.3.5", "", { "dependencies": { "@tokenizer/token": "^0.3.0" } }, "sha512-ki4hZQfh5rX0QDLLkOCj+h+CVNkqmp/CMf8v8kZpkNVK6jGQooMytqzLZYUVYIZcFZ6yDB70EfD8POcFXiF5oA=="], "supports-color": ["supports-color@8.1.1", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q=="], - "supports-preserve-symlinks-flag": ["supports-preserve-symlinks-flag@1.0.0", "", {}, "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="], - "tar-fs": ["tar-fs@2.1.5", "", { "dependencies": { "chownr": "^1.1.1", "mkdirp-classic": "^0.5.2", "pump": "^3.0.0", "tar-stream": "^2.1.4" } }, "sha512-OboTd8mmMhZDNPV+UjQcK9yKAatXu2aJ+r1w4im1Otd4M4fl2hwvdoXUxIYHFTHWK/3y3FarBP70v3vwmGlOxw=="], "tar-stream": ["tar-stream@2.2.0", "", { "dependencies": { "bl": "^4.0.3", "end-of-stream": "^1.4.1", "fs-constants": "^1.0.0", "inherits": "^2.0.3", "readable-stream": "^3.1.1" } }, "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ=="], @@ -904,24 +675,14 @@ "thenify-all": ["thenify-all@1.6.0", "", { "dependencies": { "thenify": ">= 3.1.0 < 4" } }, "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA=="], - "tinybench": ["tinybench@2.9.0", "", {}, "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg=="], - - "tinyexec": ["tinyexec@1.3.0", "", {}, "sha512-QKAl9m8gWWGHV8jZcPeym6j+XULi6tOf1mT83WYJ4Lk2ytW/uwAWkrP0uFsdoYMdueVJ0qs26wZ+23xeB4ibNQ=="], - - "tinyglobby": ["tinyglobby@0.2.17", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g=="], - "tinyrainbow": ["tinyrainbow@3.1.1", "", {}, "sha512-yau8yJdTt989Mm0Bd/236QnzEiPf2xLLTqUZRUJOo/3CB078LSwzei343DgtJVmfJKJE3TMINY1u42SQsP6mXw=="], - "to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="], - "token-types": ["token-types@6.1.2", "", { "dependencies": { "@borewit/text-codec": "^0.2.1", "@tokenizer/token": "^0.3.0", "ieee754": "^1.2.1" } }, "sha512-dRXchy+C0IgK8WPC6xvCHFRIWYUbqqdEIKPaKo/AcTUNzwLTK6AH7RjdLWsEZcAN/TBdtfUw3PYEgPr5VPr6ww=="], "ts-algebra": ["ts-algebra@2.0.0", "", {}, "sha512-FPAhNPFMrkwz76P7cdjdmiShwMynZYN6SgOujD1urY4oNm80Ou9oMdmbR45LotcKOXoy7wSmHkRFE6Mxbrhefw=="], "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], - "tsx": ["tsx@4.23.12", "", { "dependencies": { "esbuild": "~0.28.0" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "bin": { "tsx": "dist/cli.mjs" } }, "sha512-FDf4L4sYzKtzWYhU/Xm0AQFdTjdIxNo9ElTf2mxXM6k8YMHXzYUe4yODVaXP4V9uMFbVg8c0qyBccK2OOxb45Q=="], - "tunnel-agent": ["tunnel-agent@0.6.0", "", { "dependencies": { "safe-buffer": "^5.0.1" } }, "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w=="], "typebox": ["typebox@1.3.18", "", {}, "sha512-/wYPoDqxWZSxV/XD8Eskzr3YluXC9CaWJOuUYMkj+lLVLkyeEIQKzHvMuS/IRc3OLTIBC32LAtHgXo/WFEOMHQ=="], @@ -938,16 +699,8 @@ "uuid": ["uuid@14.0.2", "", { "bin": { "uuid": "dist-node/bin/uuid" } }, "sha512-xZe/16rV4aa+HGSOCiY2YeLT1OybRLrrkL/Rqaq7p7GMVXjFh+6wN4oMYgjFmnSnhY8t6Xpdl2l9qmnHYuMHwQ=="], - "vite": ["vite@8.2.2", "", { "dependencies": { "lightningcss": "^1.33.0", "picomatch": "^4.0.5", "postcss": "^8.5.26", "rolldown": "~1.2.4", "tinyglobby": "^0.2.17" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "@vitejs/devtools": "^0.4.0 || ^0.5.0", "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "@vitejs/devtools", "esbuild", "jiti", "less", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-cFKLV/PRgAUlIRm5WjMjJ86jrftzpqcgH+Us+DS8mI3CDNiH30Whrz8uHL3+MOLPAgqbMBAqWdAHAphOAM+z/Q=="], - - "vitest": ["vitest@4.1.11", "", { "dependencies": { "@vitest/expect": "4.1.11", "@vitest/mocker": "4.1.11", "@vitest/pretty-format": "4.1.11", "@vitest/runner": "4.1.11", "@vitest/snapshot": "4.1.11", "@vitest/spy": "4.1.11", "@vitest/utils": "4.1.11", "es-module-lexer": "^2.0.0", "expect-type": "^1.3.0", "magic-string": "^0.30.21", "obug": "^2.1.1", "pathe": "^2.0.3", "picomatch": "^4.0.3", "std-env": "^4.0.0-rc.1", "tinybench": "^2.9.0", "tinyexec": "^1.0.2", "tinyglobby": "^0.2.15", "tinyrainbow": "^3.1.0", "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", "why-is-node-running": "^2.3.0" }, "peerDependencies": { "@edge-runtime/vm": "*", "@opentelemetry/api": "^1.9.0", "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", "@vitest/browser-playwright": "4.1.11", "@vitest/browser-preview": "4.1.11", "@vitest/browser-webdriverio": "4.1.11", "@vitest/coverage-istanbul": "4.1.11", "@vitest/coverage-v8": "4.1.11", "@vitest/ui": "4.1.11", "happy-dom": "*", "jsdom": "*" }, "optionalPeers": ["@edge-runtime/vm", "@opentelemetry/api", "@types/node", "@vitest/browser-playwright", "@vitest/browser-preview", "@vitest/browser-webdriverio", "@vitest/coverage-istanbul", "@vitest/coverage-v8", "@vitest/ui", "happy-dom", "jsdom"], "bin": { "vitest": "./vitest.mjs" } }, "sha512-fhACrNXUidIbGSBr5FlbuBkO7VWC1ZyLl0DO4CU2DrQoAPxX84Ysxs+HeGQpii5lZWV1Q4gBZTTu49mF+A6Edw=="], - "web-streams-polyfill": ["web-streams-polyfill@3.3.3", "", {}, "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw=="], - "which": ["which@1.3.1", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "which": "./bin/which" } }, "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ=="], - - "why-is-node-running": ["why-is-node-running@2.3.0", "", { "dependencies": { "siginfo": "^2.0.0", "stackback": "0.0.2" }, "bin": { "why-is-node-running": "cli.js" } }, "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w=="], - "wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="], "wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="], @@ -980,18 +733,14 @@ "@types/yauzl/@types/node": ["@types/node@24.13.3", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-Dh8vAsV36ig5wa9OX4pXvMc9D3Veibfw2wix0CUwYODLD8nkj9UsLjASr49nPg+2eKzxhBV+v7L8pXvT4e639Q=="], + "bun-types/@types/node": ["@types/node@24.13.3", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-Dh8vAsV36ig5wa9OX4pXvMc9D3Veibfw2wix0CUwYODLD8nkj9UsLjASr49nPg+2eKzxhBV+v7L8pXvT4e639Q=="], + "cli-highlight/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], "cliui/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], - "cross-spawn/semver": ["semver@5.7.2", "", { "bin": { "semver": "bin/semver" } }, "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g=="], - - "execa/get-stream": ["get-stream@4.1.0", "", { "dependencies": { "pump": "^3.0.0" } }, "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w=="], - "hosted-git-info/lru-cache": ["lru-cache@11.5.2", "", {}, "sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g=="], - "micromatch/picomatch": ["picomatch@2.3.2", "", {}, "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA=="], - "node-fetch/data-uri-to-buffer": ["data-uri-to-buffer@4.0.1", "", {}, "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A=="], "p-retry/@types/retry": ["@types/retry@0.12.0", "", {}, "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA=="], @@ -1018,6 +767,8 @@ "@types/yauzl/@types/node/undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="], + "bun-types/@types/node/undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="], + "cli-highlight/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], "cliui/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], diff --git a/bunfig.toml b/bunfig.toml index 35daac48646..09021efcc11 100644 --- a/bunfig.toml +++ b/bunfig.toml @@ -1,3 +1,8 @@ [install] linker = "hoisted" minimumReleaseAge = 604800 + +[test] +preload = ["./packages/coding-agent/test/bun-test-preload.ts"] +timeout = 30000 +pathIgnorePatterns = ["**/daemon-supervisor-process.test.ts", "**/compiled-artifact-smoke.test.ts", "**/daemon-supervisor-monitor.test.ts"] diff --git a/package.json b/package.json index 6d936c8d7eb..5009177fa15 100644 --- a/package.json +++ b/package.json @@ -20,33 +20,38 @@ "check:browser-smoke": "bun scripts/check-browser-smoke.mjs", "profile:tui": "bun scripts/profile-coding-agent-node.mjs --mode tui", "profile:rpc": "bun scripts/profile-coding-agent-node.mjs --mode rpc", - "test": "npm run test --workspaces --if-present", - "version:patch": "npm version patch -ws --no-git-tag-version && node scripts/sync-versions.js && shx rm -rf node_modules packages/*/node_modules package-lock.json && npm install", - "version:minor": "npm version minor -ws --no-git-tag-version && node scripts/sync-versions.js && shx rm -rf node_modules packages/*/node_modules package-lock.json && npm install", - "version:major": "npm version major -ws --no-git-tag-version && node scripts/sync-versions.js && shx rm -rf node_modules packages/*/node_modules package-lock.json && npm install", - "version:set": "npm version -ws", - "prepublishOnly": "npm run clean && npm run build && npm run check", - "publish": "npm run prepublishOnly && npm publish -ws --access public", - "publish:dry": "npm run prepublishOnly && npm publish -ws --access public --dry-run", - "release:pack": "node scripts/pack-prime-agent-release.mjs", - "release:patch": "node scripts/release.mjs patch", - "release:minor": "node scripts/release.mjs minor", - "release:major": "node scripts/release.mjs major", + "test": "bun run --cwd packages/tui test && bun run --cwd packages/ai test && bun run --cwd packages/agent test && bun run --cwd packages/coding-agent test", + "version:patch": "bun scripts/set-version.ts patch", + "version:minor": "bun scripts/set-version.ts minor", + "version:major": "bun scripts/set-version.ts major", + "version:set": "bun scripts/set-version.ts", + "prepublishOnly": "bun run clean && bun run build && bun run check", + "publish": "bun run prepublishOnly && bun scripts/publish-workspaces.ts", + "publish:dry": "bun run prepublishOnly && bun scripts/publish-workspaces.ts --dry-run", + "release:pack": "bun scripts/pack-prime-agent-release.mjs", + "release:patch": "bun scripts/release.mjs patch", + "release:minor": "bun scripts/release.mjs minor", + "release:major": "bun scripts/release.mjs major", "prepare": "bun --bun husky", "build:binary": "bun run --cwd packages/coding-agent build:binary", "dev:cli": "bun packages/coding-agent/src/bun/cli.ts", - "check:type": "bun --bun tsgo --noEmit" + "check:type": "bun --bun tsgo --noEmit", + "test:process": "bun run --cwd packages/coding-agent test:process", + "test:process-stress": "bun run --cwd packages/coding-agent test:process-stress", + "test:kernel": "bun run --cwd packages/coding-agent test:kernel" }, "devDependencies": { "@anthropic-ai/sandbox-runtime": "^0.0.55", "@biomejs/biome": "2.5.5", "@types/node": "^22.10.5", "@typescript/native-preview": "7.0.0-dev.20260120.1", + "bun-types": "1.3.14", "husky": "^9.1.7", "jiti": "^2.7.0", - "shx": "^0.4.0", - "tsx": "^4.23.1", - "typescript": "^7.0.2" + "typescript": "^7.0.2", + "@vitest/expect": "4.1.10", + "@vitest/runner": "4.1.10", + "@vitest/spy": "4.1.10" }, "engines": { "node": ">=22.8.0", diff --git a/packages/agent/package.json b/packages/agent/package.json index 751c50bac9f..a056fb36094 100644 --- a/packages/agent/package.json +++ b/packages/agent/package.json @@ -13,7 +13,7 @@ "clean": "bun ../../scripts/remove-paths.ts dist", "build": "bun --bun tsgo -p tsconfig.build.json", "dev": "bun --bun tsgo -p tsconfig.build.json --watch --preserveWatchOutput", - "test": "vitest --run", + "test": "bun ../../scripts/run-with-clean-env.ts bun test --isolate --timeout 30000", "prepublishOnly": "bun run clean && bun run build" }, "dependencies": { @@ -35,11 +35,11 @@ "directory": "packages/agent" }, "engines": { - "node": ">=20.0.0" + "node": ">=20.0.0", + "bun": "1.3.14" }, "devDependencies": { "@types/node": "^24.3.0", - "typescript": "^7.0.2", - "vitest": "^4.1.10" + "typescript": "^7.0.2" } } diff --git a/packages/agent/vitest.config.ts b/packages/agent/vitest.config.ts deleted file mode 100644 index f80e2c04978..00000000000 --- a/packages/agent/vitest.config.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { fileURLToPath } from "node:url"; -import { defineConfig } from "vitest/config"; - -const aiSrcIndex = fileURLToPath(new URL("../ai/src/index.ts", import.meta.url)); -const aiSrcOAuth = fileURLToPath(new URL("../ai/src/oauth.ts", import.meta.url)); - -export default defineConfig({ - test: { - globals: true, - environment: "node", - testTimeout: 30000, // 30 seconds for API calls - }, - resolve: { - alias: [ - { find: /^@earendil-works\/pi-ai$/, replacement: aiSrcIndex }, - { find: /^@earendil-works\/pi-ai\/oauth$/, replacement: aiSrcOAuth }, - { find: /^@mariozechner\/pi-ai$/, replacement: aiSrcIndex }, - { find: /^@mariozechner\/pi-ai\/oauth$/, replacement: aiSrcOAuth }, - ], - }, -}); diff --git a/packages/ai/package.json b/packages/ai/package.json index 0e0748117de..73ada05d790 100644 --- a/packages/ai/package.json +++ b/packages/ai/package.json @@ -68,7 +68,7 @@ "build": "bun run generate-models && bun --bun tsgo -p tsconfig.build.json", "dev": "bun --bun tsgo -p tsconfig.build.json --watch --preserveWatchOutput", "dev:tsc": "bun --bun tsgo -p tsconfig.build.json --watch --preserveWatchOutput", - "test": "vitest --run", + "test": "bun ../../scripts/run-with-clean-env.ts bun test --isolate --timeout 30000", "prepublishOnly": "bun run clean && bun run build" }, "dependencies": { @@ -102,11 +102,11 @@ "directory": "packages/ai" }, "engines": { - "node": ">=20.0.0" + "node": ">=20.0.0", + "bun": "1.3.14" }, "devDependencies": { "@types/node": "^24.3.0", - "canvas": "^3.2.0", - "vitest": "^4.1.10" + "canvas": "^3.2.0" } } diff --git a/packages/ai/test/anthropic-oauth.test.ts b/packages/ai/test/anthropic-oauth.test.ts index f8e066c669e..49b8d790aa6 100644 --- a/packages/ai/test/anthropic-oauth.test.ts +++ b/packages/ai/test/anthropic-oauth.test.ts @@ -1,4 +1,4 @@ -import { afterEach, describe, expect, it, vi } from "vitest"; +import { afterEach, describe, expect, it, vi } from "bun:test"; import { loginAnthropic, refreshAnthropicToken } from "../src/utils/oauth/anthropic.js"; function jsonResponse(body: unknown, status: number = 200): Response { @@ -30,12 +30,17 @@ function getJsonBody(init?: RequestInit): Record { return JSON.parse(init.body) as Record; } -describe.sequential("Anthropic OAuth", () => { +describe("Anthropic OAuth", () => { + let origFetch: typeof globalThis.fetch | undefined; afterEach(() => { - vi.unstubAllGlobals(); + if (origFetch !== undefined) { + globalThis.fetch = origFetch; + origFetch = undefined; + } }); it("keeps the localhost redirect_uri for manual callback login", async () => { + origFetch = globalThis.fetch; let authUrl = ""; const fetchMock = vi.fn(async (input: unknown, init?: RequestInit): Promise => { expect(getUrl(input)).toBe("https://platform.claude.com/v1/oauth/token"); @@ -50,7 +55,7 @@ describe.sequential("Anthropic OAuth", () => { expires_in: 3600, }); }); - vi.stubGlobal("fetch", fetchMock); + globalThis.fetch = fetchMock; const credentials = await loginAnthropic({ onAuth: (info) => { @@ -70,10 +75,11 @@ describe.sequential("Anthropic OAuth", () => { expect(credentials.access).toBe("access-token"); expect(credentials.refresh).toBe("refresh-token"); - expect(fetchMock).toHaveBeenCalledOnce(); + expect(fetchMock).toHaveBeenCalledTimes(1); }); it("omits scope from refresh token requests", async () => { + origFetch = globalThis.fetch; const fetchMock = vi.fn(async (input: unknown, init?: RequestInit): Promise => { expect(getUrl(input)).toBe("https://platform.claude.com/v1/oauth/token"); expect(init?.method).toBe("POST"); @@ -88,12 +94,12 @@ describe.sequential("Anthropic OAuth", () => { expires_in: 3600, }); }); - vi.stubGlobal("fetch", fetchMock); + globalThis.fetch = fetchMock; const credentials = await refreshAnthropicToken("refresh-token"); expect(credentials.access).toBe("new-access-token"); expect(credentials.refresh).toBe("new-refresh-token"); - expect(fetchMock).toHaveBeenCalledOnce(); + expect(fetchMock).toHaveBeenCalledTimes(1); }); }); diff --git a/packages/ai/test/azure-openai-base-url.test.ts b/packages/ai/test/azure-openai-base-url.test.ts index 3a8e9c19160..4271eb1c528 100644 --- a/packages/ai/test/azure-openai-base-url.test.ts +++ b/packages/ai/test/azure-openai-base-url.test.ts @@ -1,4 +1,4 @@ -import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { afterEach, beforeEach, describe, expect, it, mock } from "bun:test"; import { getModel } from "../src/models.js"; import { streamAzureOpenAIResponses } from "../src/providers/azure-openai-responses.js"; import type { Context } from "../src/types.js"; @@ -11,11 +11,11 @@ interface CapturedAzureClientOptions { baseURL: string; } -const azureMock = vi.hoisted(() => ({ - constructorCalls: [] as CapturedAzureClientOptions[], -})); +const azureMock: { constructorCalls: CapturedAzureClientOptions[] } = { + constructorCalls: [], +}; -vi.mock("openai", () => { +mock.module("openai", () => { class AzureOpenAI { responses = { create: () => { diff --git a/packages/ai/test/bedrock-endpoint-resolution.test.ts b/packages/ai/test/bedrock-endpoint-resolution.test.ts index 374599a473f..d2f8d69dddb 100644 --- a/packages/ai/test/bedrock-endpoint-resolution.test.ts +++ b/packages/ai/test/bedrock-endpoint-resolution.test.ts @@ -1,10 +1,10 @@ -import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { afterEach, beforeEach, describe, expect, it, mock } from "bun:test"; -const bedrockMock = vi.hoisted(() => ({ - constructorCalls: [] as Array>, -})); +const bedrockMock: { constructorCalls: Array> } = { + constructorCalls: [], +}; -vi.mock("@aws-sdk/client-bedrock-runtime", () => { +mock.module("@aws-sdk/client-bedrock-runtime", () => { class BedrockRuntimeServiceException extends Error {} class BedrockRuntimeClient { diff --git a/packages/ai/test/codex-websocket-cached-probe.ts b/packages/ai/test/codex-websocket-cached-probe.ts index 9bab1466875..93e72f9a364 100644 --- a/packages/ai/test/codex-websocket-cached-probe.ts +++ b/packages/ai/test/codex-websocket-cached-probe.ts @@ -1,4 +1,4 @@ -#!/usr/bin/env tsx +#!/usr/bin/env bun import { tmpdir } from "node:os"; import { join, resolve } from "node:path"; @@ -79,7 +79,7 @@ function required(value: string | undefined, flag: string): string { } function printHelp(): void { - console.log(`Usage: npx tsx test/codex-websocket-cached-probe.ts [options] + console.log(`Usage: bun test/codex-websocket-cached-probe.ts [options] Options: --turns Number of user turns. Default: ${DEFAULT_TURNS} diff --git a/packages/ai/test/github-copilot-anthropic.test.ts b/packages/ai/test/github-copilot-anthropic.test.ts index 92bff083bec..5362c4171e4 100644 --- a/packages/ai/test/github-copilot-anthropic.test.ts +++ b/packages/ai/test/github-copilot-anthropic.test.ts @@ -1,13 +1,16 @@ -import { describe, expect, it, vi } from "vitest"; +import { describe, expect, it, mock } from "bun:test"; import { getModel } from "../src/models.js"; import type { Context } from "../src/types.js"; -const mockState = vi.hoisted(() => ({ - constructorOpts: undefined as Record | undefined, - createParams: undefined as Record | undefined, -})); +const mockState: { + constructorOpts: Record | undefined; + createParams: Record | undefined; +} = { + constructorOpts: undefined, + createParams: undefined, +}; -vi.mock("@anthropic-ai/sdk", () => { +mock.module("@anthropic-ai/sdk", () => { function createSseResponse(): Response { const body = [ `event: message_start\ndata: ${JSON.stringify({ diff --git a/packages/ai/test/github-copilot-oauth.test.ts b/packages/ai/test/github-copilot-oauth.test.ts index 0367892cb85..9808b166244 100644 --- a/packages/ai/test/github-copilot-oauth.test.ts +++ b/packages/ai/test/github-copilot-oauth.test.ts @@ -1,6 +1,13 @@ -import { afterEach, describe, expect, it, vi } from "vitest"; +import { afterEach, describe, expect, it, vi } from "bun:test"; import { loginGitHubCopilot } from "../src/utils/oauth/github-copilot.js"; +/** Flush microtask queue so async continuations run */ +async function flush(times = 10): Promise { + for (let i = 0; i < times; i++) { + await new Promise((resolve) => queueMicrotask(() => resolve())); + } +} + function jsonResponse(body: unknown, status: number = 200): Response { return new Response(JSON.stringify(body), { status, @@ -20,19 +27,22 @@ function getUrl(input: unknown): string { if (input instanceof Request) { return input.url; } - throw new Error(`Unsupported fetch input: ${String(input)}`); + throw new Error(`Unsupported fetch input: \${String(input)}`); } describe("GitHub Copilot OAuth device flow", () => { + let origFetchGH: typeof globalThis.fetch | undefined; afterEach(() => { - vi.unstubAllGlobals(); + if (origFetchGH !== undefined) { + globalThis.fetch = origFetchGH; + origFetchGH = undefined; + } vi.useRealTimers(); }); it("waits before the first poll and increases the safety margin after slow_down", async () => { vi.useFakeTimers(); - const startTime = new Date("2026-03-09T00:00:00Z"); - vi.setSystemTime(startTime); + const baseTime = Date.now(); const accessTokenPollTimes: number[] = []; const accessTokenResponses = [ @@ -89,10 +99,11 @@ describe("GitHub Copilot OAuth device flow", () => { return new Response("", { status: 200 }); } - throw new Error(`Unexpected fetch URL: ${url}`); + throw new Error(`Unexpected fetch URL: \${url}`); }); - vi.stubGlobal("fetch", fetchMock); + origFetchGH = globalThis.fetch; + globalThis.fetch = fetchMock; const loginPromise = loginGitHubCopilot({ onAuth: () => {}, @@ -100,38 +111,41 @@ describe("GitHub Copilot OAuth device flow", () => { onProgress: () => {}, }); - await vi.advanceTimersByTimeAsync(0); + // Let loginGitHubCopilot initialize and enter the poll loop + await flush(30); + expect(accessTokenPollTimes).toHaveLength(0); - await vi.advanceTimersByTimeAsync(5999); + vi.advanceTimersByTime(5999); + await flush(); expect(accessTokenPollTimes).toHaveLength(0); - await vi.advanceTimersByTimeAsync(1); + vi.advanceTimersByTime(1); + await flush(); expect(accessTokenPollTimes).toHaveLength(1); - await vi.advanceTimersByTimeAsync(5999); + vi.advanceTimersByTime(5999); + await flush(); expect(accessTokenPollTimes).toHaveLength(1); - await vi.advanceTimersByTimeAsync(1); + vi.advanceTimersByTime(1); + await flush(); expect(accessTokenPollTimes).toHaveLength(2); - await vi.advanceTimersByTimeAsync(13999); + vi.advanceTimersByTime(13999); + await flush(); expect(accessTokenPollTimes).toHaveLength(2); - await vi.advanceTimersByTimeAsync(1); + vi.advanceTimersByTime(1); + await flush(); await loginPromise; - expect(accessTokenPollTimes).toEqual([ - startTime.getTime() + 6000, - startTime.getTime() + 12000, - startTime.getTime() + 26000, - ]); + expect(accessTokenPollTimes).toEqual([baseTime + 6000, baseTime + 12000, baseTime + 26000]); }); it("uses the remaining lifetime for a final poll before timing out after repeated slow_down responses", async () => { vi.useFakeTimers(); - const startTime = new Date("2026-03-09T00:00:00Z"); - vi.setSystemTime(startTime); + const baseTime = Date.now(); const accessTokenPollTimes: number[] = []; const accessTokenResponses = [ @@ -142,7 +156,6 @@ describe("GitHub Copilot OAuth device flow", () => { const fetchMock = vi.fn(async (input: unknown): Promise => { const url = getUrl(input); - if (url.endsWith("/login/device/code")) { return jsonResponse({ device_code: "device-code", @@ -152,45 +165,50 @@ describe("GitHub Copilot OAuth device flow", () => { expires_in: 25, }); } - if (url.endsWith("/login/oauth/access_token")) { accessTokenPollTimes.push(Date.now()); const response = accessTokenResponses.shift(); - if (!response) { - throw new Error("Unexpected extra access token poll"); - } + if (!response) throw new Error("Unexpected extra access token poll"); return response; } - - throw new Error(`Unexpected fetch URL: ${url}`); + throw new Error(`Unexpected fetch URL: \${url}`); }); - vi.stubGlobal("fetch", fetchMock); + origFetchGH = globalThis.fetch; + globalThis.fetch = fetchMock; const loginPromise = loginGitHubCopilot({ onAuth: () => {}, onPrompt: async () => "", }); - const rejection = expect(loginPromise).rejects.toThrow( - /Device flow timed out after one or more slow_down responses/, - ); - - await vi.advanceTimersByTimeAsync(6000); - expect(accessTokenPollTimes).toEqual([startTime.getTime() + 6000]); - - await vi.advanceTimersByTimeAsync(14000); - expect(accessTokenPollTimes).toEqual([startTime.getTime() + 6000, startTime.getTime() + 20000]); - - await vi.advanceTimersByTimeAsync(4999); - expect(accessTokenPollTimes).toEqual([startTime.getTime() + 6000, startTime.getTime() + 20000]); - - await vi.advanceTimersByTimeAsync(1); - await rejection; - - expect(accessTokenPollTimes).toEqual([ - startTime.getTime() + 6000, - startTime.getTime() + 20000, - startTime.getTime() + 25000, - ]); + // Let loginGitHubCopilot initialize and enter the poll loop + await flush(30); + + // First wait: ceil(5000 * 1.2) = 6000ms + vi.advanceTimersByTime(6000); + await flush(); + expect(accessTokenPollTimes).toEqual([baseTime + 6000]); + + // After slow_down: ceil(10000 * 1.4) = 14000ms + vi.advanceTimersByTime(14000); + await flush(); + expect(accessTokenPollTimes).toEqual([baseTime + 6000, baseTime + 20000]); + + // After second slow_down: min(ceil(15000*1.4), remaining=5000) = 5000ms + vi.advanceTimersByTime(4999); + await flush(); + expect(accessTokenPollTimes).toEqual([baseTime + 6000, baseTime + 20000]); + + // Deadline crossed, loop exits, slowDownResponses>0 -> throws + vi.advanceTimersByTime(1); + await flush(); + try { + await loginPromise; + expect.unreachable("should have thrown"); + } catch (e) { + expect((e as Error).message).toMatch(/Device flow timed out after one or more slow_down responses/); + } + + expect(accessTokenPollTimes).toEqual([baseTime + 6000, baseTime + 20000, baseTime + 25000]); }); }); diff --git a/packages/ai/test/google-vertex-api-key-resolution.test.ts b/packages/ai/test/google-vertex-api-key-resolution.test.ts index d05702c3af2..be1d69945a6 100644 --- a/packages/ai/test/google-vertex-api-key-resolution.test.ts +++ b/packages/ai/test/google-vertex-api-key-resolution.test.ts @@ -1,10 +1,10 @@ -import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { afterEach, beforeEach, describe, expect, it, mock } from "bun:test"; -const googleGenAiMock = vi.hoisted(() => ({ - constructorCalls: [] as Array>, -})); +const googleGenAiMock: { constructorCalls: Array> } = { + constructorCalls: [], +}; -vi.mock("@google/genai", () => { +mock.module("@google/genai", () => { class GoogleGenAI { models = { generateContentStream: async function* () { diff --git a/packages/ai/test/google-vertex-thinking-budget.test.ts b/packages/ai/test/google-vertex-thinking-budget.test.ts index 88513882156..9406b137c82 100644 --- a/packages/ai/test/google-vertex-thinking-budget.test.ts +++ b/packages/ai/test/google-vertex-thinking-budget.test.ts @@ -1,9 +1,10 @@ -import type * as GoogleGenAi from "@google/genai"; +import { describe, expect, it, mock } from "bun:test"; import type { GenerateContentParameters } from "@google/genai"; -import { describe, expect, it, vi } from "vitest"; -vi.mock("@google/genai", async (importOriginal) => { - const actual = await importOriginal(); +// Import the module normally before mocking so we can spread its exports +import * as googleGenAiActual from "@google/genai"; + +mock.module("@google/genai", () => { class GoogleGenAI { models = { generateContentStream: async function* () { @@ -16,7 +17,7 @@ vi.mock("@google/genai", async (importOriginal) => { } return { - ...actual, + ...googleGenAiActual, GoogleGenAI, ResourceScope: { COLLECTION: "COLLECTION" }, ThinkingLevel: { @@ -62,12 +63,14 @@ async function captureMinimalReasoningPayload( } describe("Google Vertex thinking budget payload", () => { - it.each(flashLiteModels)("uses the supported minimal budget for $id", async (model) => { - const payload = await captureMinimalReasoningPayload(model); + for (const model of flashLiteModels) { + it(`uses the supported minimal budget for ${model.id}`, async () => { + const payload = await captureMinimalReasoningPayload(model); - expect(payload.config?.thinkingConfig).toEqual({ - includeThoughts: true, - thinkingBudget: 512, + expect(payload.config?.thinkingConfig).toEqual({ + includeThoughts: true, + thinkingBudget: 512, + }); }); - }); + } }); diff --git a/packages/ai/test/lazy-module-load.test.ts b/packages/ai/test/lazy-module-load.test.ts index c94669abfb4..7c707c86841 100644 --- a/packages/ai/test/lazy-module-load.test.ts +++ b/packages/ai/test/lazy-module-load.test.ts @@ -1,12 +1,11 @@ import { spawnSync } from "node:child_process"; -import { createRequire } from "node:module"; import { dirname, resolve } from "node:path"; import { fileURLToPath } from "node:url"; import { describe, expect, it } from "vitest"; -const require = createRequire(import.meta.url); -const tsxLoader = require.resolve("tsx/esm"); -const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), ".."); +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); +const packageRoot = resolve(__dirname, ".."); const aiEntryUrl = new URL("../src/index.ts", import.meta.url).href; const SDK_SPECIFIERS = [ @@ -22,27 +21,17 @@ type ProbeResult = { }; function runProbe(action: string): ProbeResult { - const script = ` - import { registerHooks } from "node:module"; - - const targets = new Set(${JSON.stringify(SDK_SPECIFIERS)}); - const loaded = []; - - registerHooks({ - resolve(specifier, context, nextResolve) { - if (targets.has(specifier)) { - loaded.push(specifier); - } - return nextResolve(specifier, context); - }, - }); - - const mod = await import(${JSON.stringify(aiEntryUrl)}); - ${action} - console.log(JSON.stringify({ loadedSpecifiers: [...new Set(loaded)] })); - `; - - const result = spawnSync(process.execPath, ["--import", tsxLoader, "--input-type=module", "--eval", script], { + const script = [ + `const targets = ${JSON.stringify([...SDK_SPECIFIERS])};`, + `const mod = await import("${aiEntryUrl}");`, + action, + `const Module = require("module");`, + `const cacheKeys = Object.keys(Module._cache);`, + `const loaded = targets.filter((spec) => cacheKeys.some((k) => k.includes(spec)));`, + `console.log(JSON.stringify({ loadedSpecifiers: loaded }));`, + ].join("\n"); + + const result = spawnSync(process.execPath, ["--eval", script], { cwd: packageRoot, encoding: "utf8", }); @@ -70,32 +59,35 @@ describe("lazy provider module loading", () => { }); it("loads only the Anthropic SDK when calling the root lazy wrapper", () => { - const result = runProbe(` - const model = { - id: "claude-sonnet-4-6", - name: "Claude Sonnet 4", - api: "anthropic-messages", - provider: "anthropic", - baseUrl: "https://api.anthropic.com", - reasoning: true, - input: ["text"], - cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, - contextWindow: 200000, - maxTokens: 8192, - }; - const context = { messages: [{ role: "user", content: "hi" }] }; - await mod.streamSimpleAnthropic(model, context).result(); - `); + const result = runProbe( + [ + `const model = {`, + ` id: "claude-sonnet-4-6",`, + ` api: "anthropic-messages",`, + ` provider: "anthropic",`, + ` baseUrl: "http://127.0.0.1:9",`, + ` reasoning: true,`, + ` input: ["text"],`, + ` cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },`, + ` contextWindow: 200000,`, + ` maxTokens: 8192,`, + `};`, + `const context = { messages: [{ role: "user", content: "hi", timestamp: Date.now() }] };`, + `try { await mod.streamSimpleAnthropic(model, context).result(); } catch (_e) {}`, + ].join("\n"), + ); expect(result.loadedSpecifiers).toEqual(["@anthropic-ai/sdk"]); }); it("loads only the Anthropic SDK when dispatching through streamSimple", () => { - const result = runProbe(` - const model = mod.getModel("anthropic", "claude-sonnet-4-6"); - const context = { messages: [{ role: "user", content: "hi" }] }; - await mod.streamSimple(model, context).result(); - `); + const result = runProbe( + [ + `const model = mod.getModel("anthropic", "claude-sonnet-4-6");`, + `const context = { messages: [{ role: "user", content: "hi", timestamp: Date.now() }] };`, + `try { await mod.streamSimple(model, context).result(); } catch (_e) {}`, + ].join("\n"), + ); expect(result.loadedSpecifiers).toEqual(["@anthropic-ai/sdk"]); }); diff --git a/packages/ai/test/mcp-oauth.test.ts b/packages/ai/test/mcp-oauth.test.ts index e35f2a009d5..7d222c7a379 100644 --- a/packages/ai/test/mcp-oauth.test.ts +++ b/packages/ai/test/mcp-oauth.test.ts @@ -1,5 +1,5 @@ +import { afterEach, describe, expect, it, vi } from "bun:test"; import { createServer } from "node:http"; -import { afterEach, describe, expect, it, vi } from "vitest"; import { createMcpOAuthProvider } from "../src/mcp/oauth.js"; function jsonResponse(body: unknown, status = 200, headers?: Record): Response { @@ -58,9 +58,13 @@ async function loginWithManualCode( return { creds, authUrl }; } -describe.sequential("MCP OAuth provider", () => { +describe("MCP OAuth provider", () => { + let origFetchMCP: typeof globalThis.fetch | undefined; afterEach(() => { - vi.unstubAllGlobals(); + if (origFetchMCP !== undefined) { + globalThis.fetch = origFetchMCP; + origFetchMCP = undefined; + } }); it("has a namespaced id and label", () => { @@ -92,7 +96,8 @@ describe.sequential("MCP OAuth provider", () => { } throw new Error(`unexpected fetch: ${url}`); }); - vi.stubGlobal("fetch", fetchMock); + origFetchMCP = globalThis.fetch; + globalThis.fetch = fetchMock; const { creds, authUrl } = await loginWithManualCode(createMcpOAuthProvider({ server: "plane", url: RESOURCE })); expect(creds).toMatchObject({ @@ -119,45 +124,40 @@ describe.sequential("MCP OAuth provider", () => { token_endpoint: "https://login.example/tenant/token", registration_endpoint: "https://login.example/tenant/register", }; - vi.stubGlobal( - "fetch", - vi.fn(async (input: unknown): Promise => { - const url = urlOf(input); - if (url === RESOURCE) return new Response("", { status: 404 }); - if (url === PLANE_PRM_URL) return jsonResponse({ resource: RESOURCE, authorization_servers: [issuer] }); - if (url === "https://login.example/.well-known/oauth-authorization-server/tenant") - return new Response("not metadata", { - status: 200, - headers: { "Content-Type": "text/html" }, - }); - if (url === oidcMeta) return jsonResponse(metadata); - if (url === metadata.registration_endpoint) return jsonResponse({ client_id: "c" }); - if (url === metadata.token_endpoint) return jsonResponse({ access_token: "a", expires_in: 60 }); - throw new Error(`unexpected fetch: ${url}`); - }), - ); + origFetchMCP = globalThis.fetch; + globalThis.fetch = vi.fn(async (input: unknown): Promise => { + const url = urlOf(input); + if (url === RESOURCE) return new Response("", { status: 404 }); + if (url === PLANE_PRM_URL) return jsonResponse({ resource: RESOURCE, authorization_servers: [issuer] }); + if (url === "https://login.example/.well-known/oauth-authorization-server/tenant") + return new Response("not metadata", { + status: 200, + headers: { "Content-Type": "text/html" }, + }); + if (url === oidcMeta) return jsonResponse(metadata); + if (url === metadata.registration_endpoint) return jsonResponse({ client_id: "c" }); + if (url === metadata.token_endpoint) return jsonResponse({ access_token: "a", expires_in: 60 }); + throw new Error(`unexpected fetch: ${url}`); + }); const { creds } = await loginWithManualCode(createMcpOAuthProvider({ server: "plane", url: RESOURCE })); expect(creds).toMatchObject({ resource: RESOURCE, issuer }); }); it("fails closed after protected-resource metadata selects an issuer", async () => { - vi.stubGlobal( - "fetch", - vi.fn(async (input: unknown): Promise => { - const url = urlOf(input); - if (url === RESOURCE) - return new Response("", { - status: 401, - headers: { "WWW-Authenticate": `Bearer resource_metadata="${PLANE_PRM_URL}"` }, - }); - if (url === PLANE_PRM_URL) - return jsonResponse({ resource: RESOURCE, authorization_servers: [PLANE_ISSUER] }); - if (url === PLANE_META_URL) return jsonResponse({ ...PLANE_META, issuer: "https://wrong.example" }); - if (url === "https://mcp.plane.so/http/.well-known/openid-configuration") - return new Response("", { status: 404 }); - throw new Error(`unexpected fetch: ${url}`); - }), - ); + origFetchMCP = globalThis.fetch; + globalThis.fetch = vi.fn(async (input: unknown): Promise => { + const url = urlOf(input); + if (url === RESOURCE) + return new Response("", { + status: 401, + headers: { "WWW-Authenticate": `Bearer resource_metadata="${PLANE_PRM_URL}"` }, + }); + if (url === PLANE_PRM_URL) return jsonResponse({ resource: RESOURCE, authorization_servers: [PLANE_ISSUER] }); + if (url === PLANE_META_URL) return jsonResponse({ ...PLANE_META, issuer: "https://wrong.example" }); + if (url === "https://mcp.plane.so/http/.well-known/openid-configuration") + return new Response("", { status: 404 }); + throw new Error(`unexpected fetch: ${url}`); + }); await expect( createMcpOAuthProvider({ server: "plane", url: RESOURCE }).login({ onAuth: () => {}, @@ -180,7 +180,8 @@ describe.sequential("MCP OAuth provider", () => { } throw new Error(`unexpected fetch: ${url}`); }); - vi.stubGlobal("fetch", fetchMock); + origFetchMCP = globalThis.fetch; + globalThis.fetch = fetchMock; const { creds, authUrl } = await loginWithManualCode( createMcpOAuthProvider({ server: "origin", url: ORIGIN_URL }), ); @@ -213,7 +214,8 @@ describe.sequential("MCP OAuth provider", () => { } throw new Error(`unexpected fetch: ${url}`); }); - vi.stubGlobal("fetch", fetchMock); + origFetchMCP = globalThis.fetch; + globalThis.fetch = fetchMock; const provider = createMcpOAuthProvider({ server: "plane", url: RESOURCE }); const refreshed = await provider.refreshToken({ access: "access-1", @@ -260,17 +262,15 @@ describe.sequential("MCP OAuth provider", () => { authorization_endpoint: "https://root.example/authorize", token_endpoint: "https://root.example/token", }; - vi.stubGlobal( - "fetch", - vi.fn(async (input: unknown): Promise => { - const url = urlOf(input); - if (url === "https://root.example/") return new Response("", { status: 401 }); - if (url === prm) return jsonResponse({ resource, authorization_servers: [issuer] }); - if (url === asMetadata) return jsonResponse(metadata); - if (url === metadata.token_endpoint) return jsonResponse({ access_token: "root-access" }); - throw new Error(`unexpected fetch: ${url}`); - }), - ); + origFetchMCP = globalThis.fetch; + globalThis.fetch = vi.fn(async (input: unknown): Promise => { + const url = urlOf(input); + if (url === "https://root.example/") return new Response("", { status: 401 }); + if (url === prm) return jsonResponse({ resource, authorization_servers: [issuer] }); + if (url === asMetadata) return jsonResponse(metadata); + if (url === metadata.token_endpoint) return jsonResponse({ access_token: "root-access" }); + throw new Error(`unexpected fetch: ${url}`); + }); const { creds, authUrl } = await loginWithManualCode( createMcpOAuthProvider({ server: "root", url: resource, clientId: "root-client" }), ); @@ -296,7 +296,8 @@ describe.sequential("MCP OAuth provider", () => { if (url === metadata.token_endpoint) return jsonResponse({ access_token: "query-access" }); throw new Error(`unexpected fetch: ${url}`); }); - vi.stubGlobal("fetch", fetchMock); + origFetchMCP = globalThis.fetch; + globalThis.fetch = fetchMock; const { creds } = await loginWithManualCode( createMcpOAuthProvider({ server: "query", url: resource, clientId: "query-client" }), ); @@ -314,7 +315,8 @@ describe.sequential("MCP OAuth provider", () => { if (url === PLANE_META_URL) return jsonResponse(PLANE_META); throw new Error(`unexpected fetch: ${url}`); }); - vi.stubGlobal("fetch", fetchMock); + origFetchMCP = globalThis.fetch; + globalThis.fetch = fetchMock; await expect( createMcpOAuthProvider({ server: "plane", url: RESOURCE }).refreshToken({ access: "origin-access", @@ -329,20 +331,18 @@ describe.sequential("MCP OAuth provider", () => { }); it("rejects a redirected token POST", async () => { - vi.stubGlobal( - "fetch", - vi.fn(async (input: unknown, init?: RequestInit): Promise => { - const missing = absentPrm(input); - if (missing) return missing; - const url = urlOf(input); - if (url === "https://srv.test/.well-known/oauth-authorization-server") return jsonResponse(ORIGIN_META); - if (url === ORIGIN_META.token_endpoint) { - expect(init?.redirect).toBe("error"); - return new Response("redirect", { status: 302, headers: { Location: "https://evil.test/token" } }); - } - throw new Error(`unexpected fetch: ${url}`); - }), - ); + origFetchMCP = globalThis.fetch; + globalThis.fetch = vi.fn(async (input: unknown, init?: RequestInit): Promise => { + const missing = absentPrm(input); + if (missing) return missing; + const url = urlOf(input); + if (url === "https://srv.test/.well-known/oauth-authorization-server") return jsonResponse(ORIGIN_META); + if (url === ORIGIN_META.token_endpoint) { + expect(init?.redirect).toBe("error"); + return new Response("redirect", { status: 302, headers: { Location: "https://evil.test/token" } }); + } + throw new Error(`unexpected fetch: ${url}`); + }); const provider = createMcpOAuthProvider({ server: "origin", url: ORIGIN_URL, clientId: "c" }); await expect( provider.refreshToken({ @@ -370,7 +370,8 @@ describe.sequential("MCP OAuth provider", () => { if (url === PLANE_META.token_endpoint) return jsonResponse({ access_token: "pointer-access" }); throw new Error(`unexpected fetch: ${url}`); }); - vi.stubGlobal("fetch", fetchMock); + origFetchMCP = globalThis.fetch; + globalThis.fetch = fetchMock; const { creds } = await loginWithManualCode(createMcpOAuthProvider({ server: "plane", url: RESOURCE })); expect(creds).toMatchObject({ access: "pointer-access", resource: RESOURCE, issuer: PLANE_ISSUER }); @@ -378,16 +379,14 @@ describe.sequential("MCP OAuth provider", () => { }); it("rejects protected-resource metadata for a different resource", async () => { - vi.stubGlobal( - "fetch", - vi.fn(async (input: unknown): Promise => { - const url = urlOf(input); - if (url === RESOURCE) return new Response("", { status: 401 }); - if (url === PLANE_PRM_URL) - return jsonResponse({ resource: "https://attacker.example/mcp", authorization_servers: [PLANE_ISSUER] }); - throw new Error(`unexpected fetch: ${url}`); - }), - ); + origFetchMCP = globalThis.fetch; + globalThis.fetch = vi.fn(async (input: unknown): Promise => { + const url = urlOf(input); + if (url === RESOURCE) return new Response("", { status: 401 }); + if (url === PLANE_PRM_URL) + return jsonResponse({ resource: "https://attacker.example/mcp", authorization_servers: [PLANE_ISSUER] }); + throw new Error(`unexpected fetch: ${url}`); + }); await expect( createMcpOAuthProvider({ server: "plane", url: RESOURCE }).login({ @@ -404,18 +403,16 @@ describe.sequential("MCP OAuth provider", () => { blocker.listen(53700, "127.0.0.1", () => resolve(true)); }); try { - vi.stubGlobal( - "fetch", - vi.fn(async (input: unknown): Promise => { - const missing = absentPrm(input); - if (missing) return missing; - const url = urlOf(input); - if (url === "https://srv.test/.well-known/oauth-authorization-server") return jsonResponse(ORIGIN_META); - if (url === ORIGIN_META.registration_endpoint) return jsonResponse({ client_id: "c" }); - if (url === ORIGIN_META.token_endpoint) return jsonResponse({ access_token: "a", expires_in: 60 }); - throw new Error(`unexpected fetch: ${url}`); - }), - ); + origFetchMCP = globalThis.fetch; + globalThis.fetch = vi.fn(async (input: unknown): Promise => { + const missing = absentPrm(input); + if (missing) return missing; + const url = urlOf(input); + if (url === "https://srv.test/.well-known/oauth-authorization-server") return jsonResponse(ORIGIN_META); + if (url === ORIGIN_META.registration_endpoint) return jsonResponse({ client_id: "c" }); + if (url === ORIGIN_META.token_endpoint) return jsonResponse({ access_token: "a", expires_in: 60 }); + throw new Error(`unexpected fetch: ${url}`); + }); const { authUrl } = await loginWithManualCode(createMcpOAuthProvider({ server: "demo", url: ORIGIN_URL })); const redirect = new URL(authUrl).searchParams.get("redirect_uri") ?? ""; expect(redirect).not.toContain(":53700/"); @@ -426,17 +423,15 @@ describe.sequential("MCP OAuth provider", () => { }); it("fails clearly when dynamic client registration is unavailable", async () => { - vi.stubGlobal( - "fetch", - vi.fn(async (input: unknown): Promise => { - const missing = absentPrm(input); - if (missing) return missing; - const url = urlOf(input); - if (url === "https://srv.test/.well-known/oauth-authorization-server") - return jsonResponse({ ...ORIGIN_META, registration_endpoint: undefined }); - throw new Error(`unexpected fetch: ${url}`); - }), - ); + origFetchMCP = globalThis.fetch; + globalThis.fetch = vi.fn(async (input: unknown): Promise => { + const missing = absentPrm(input); + if (missing) return missing; + const url = urlOf(input); + if (url === "https://srv.test/.well-known/oauth-authorization-server") + return jsonResponse({ ...ORIGIN_META, registration_endpoint: undefined }); + throw new Error(`unexpected fetch: ${url}`); + }); await expect( createMcpOAuthProvider({ server: "slackish", url: ORIGIN_URL }).login({ onAuth: () => {}, diff --git a/packages/ai/test/openai-codex-oauth.test.ts b/packages/ai/test/openai-codex-oauth.test.ts index 276abe1bfe5..6e86f5e9820 100644 --- a/packages/ai/test/openai-codex-oauth.test.ts +++ b/packages/ai/test/openai-codex-oauth.test.ts @@ -1,28 +1,30 @@ -import { afterEach, describe, expect, it, vi } from "vitest"; +import { afterEach, describe, expect, it, vi } from "bun:test"; import { refreshOpenAICodexToken } from "../src/utils/oauth/openai-codex.js"; describe("OpenAI Codex OAuth", () => { + let origFetchCodex: typeof globalThis.fetch | undefined; afterEach(() => { vi.restoreAllMocks(); - vi.unstubAllGlobals(); + if (origFetchCodex !== undefined) { + globalThis.fetch = origFetchCodex; + origFetchCodex = undefined; + } }); it("does not write token refresh failures to stderr", async () => { const consoleError = vi.spyOn(console, "error").mockImplementation(() => {}); - vi.stubGlobal( - "fetch", - vi.fn(async (): Promise => { - return new Response( - JSON.stringify({ - error: { - message: "Could not validate your token. Please try signing in again.", - type: "invalid_request_error", - }, - }), - { status: 401, statusText: "Unauthorized", headers: { "Content-Type": "application/json" } }, - ); - }), - ); + origFetchCodex = globalThis.fetch; + globalThis.fetch = vi.fn(async (): Promise => { + return new Response( + JSON.stringify({ + error: { + message: "Could not validate your token. Please try signing in again.", + type: "invalid_request_error", + }, + }), + { status: 401, statusText: "Unauthorized", headers: { "Content-Type": "application/json" } }, + ); + }); await expect(refreshOpenAICodexToken("invalid-refresh-token")).rejects.toThrow( /OpenAI Codex token refresh failed \(401\).*Could not validate your token/, diff --git a/packages/ai/test/openai-codex-stream.test.ts b/packages/ai/test/openai-codex-stream.test.ts index e05b9d98317..4a6c84b583b 100644 --- a/packages/ai/test/openai-codex-stream.test.ts +++ b/packages/ai/test/openai-codex-stream.test.ts @@ -543,6 +543,7 @@ describe("openai-codex streaming", () => { provider: "openai-codex", baseUrl: "https://chatgpt.com/backend-api", reasoning: true, + thinkingLevelMap: { minimal: "low" }, input: ["text"], cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, contextWindow: 400000, @@ -554,9 +555,9 @@ describe("openai-codex streaming", () => { messages: [{ role: "user", content: "Say hello", timestamp: Date.now() }], }; - const streamResult = streamOpenAICodexResponses(model, context, { + const streamResult = streamSimpleOpenAICodexResponses(model, context, { apiKey: token, - reasoningEffort: "minimal", + reasoning: "minimal", }); await streamResult.result(); }); diff --git a/packages/ai/test/openai-completions-cache-control-format.test.ts b/packages/ai/test/openai-completions-cache-control-format.test.ts index 9b6f66c46a9..66ac4ed9b8d 100644 --- a/packages/ai/test/openai-completions-cache-control-format.test.ts +++ b/packages/ai/test/openai-completions-cache-control-format.test.ts @@ -28,9 +28,9 @@ interface CapturedParams { tools?: ToolWithCacheControl[]; } -const mockState = vi.hoisted(() => ({ - lastParams: undefined as CapturedParams | undefined, -})); +const mockState: { lastParams: CapturedParams | undefined } = { + lastParams: undefined, +}; const emptyUsage: Usage = { input: 0, diff --git a/packages/ai/test/openai-completions-empty-tools.test.ts b/packages/ai/test/openai-completions-empty-tools.test.ts index 61a65c1df4d..9dbb7339058 100644 --- a/packages/ai/test/openai-completions-empty-tools.test.ts +++ b/packages/ai/test/openai-completions-empty-tools.test.ts @@ -4,10 +4,10 @@ import { CLOUDFLARE_AI_GATEWAY_COMPAT_BASE_URL } from "../src/providers/cloudfla import { streamSimple } from "../src/stream.js"; import type { Model } from "../src/types.js"; -const mockState = vi.hoisted(() => ({ - lastParams: undefined as unknown, - lastClientOptions: undefined as unknown, -})); +const mockState: { lastParams: unknown; lastClientOptions: unknown } = { + lastParams: undefined, + lastClientOptions: undefined, +}; vi.mock("openai", () => { class FakeOpenAI { diff --git a/packages/ai/test/openai-completions-prompt-cache.test.ts b/packages/ai/test/openai-completions-prompt-cache.test.ts index 147097efc44..d2c54f2fff4 100644 --- a/packages/ai/test/openai-completions-prompt-cache.test.ts +++ b/packages/ai/test/openai-completions-prompt-cache.test.ts @@ -15,10 +15,13 @@ interface CapturedCompletionsPayload { prompt_cache_retention?: "24h" | "in-memory" | null; } -const mockState = vi.hoisted(() => ({ - lastParams: undefined as CapturedCompletionsPayload | undefined, - lastClientOptions: undefined as FakeOpenAIClientOptions | undefined, -})); +const mockState: { + lastParams: CapturedCompletionsPayload | undefined; + lastClientOptions: FakeOpenAIClientOptions | undefined; +} = { + lastParams: undefined, + lastClientOptions: undefined, +}; vi.mock("openai", () => { class FakeOpenAI { diff --git a/packages/ai/test/openai-completions-response-model.test.ts b/packages/ai/test/openai-completions-response-model.test.ts index a7592b0eb0d..a85f14fec73 100644 --- a/packages/ai/test/openai-completions-response-model.test.ts +++ b/packages/ai/test/openai-completions-response-model.test.ts @@ -2,9 +2,9 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; import { complete } from "../src/stream.js"; import type { Model } from "../src/types.js"; -const mockState = vi.hoisted(() => ({ - chunks: [] as unknown[], -})); +const mockState: { chunks: unknown[] } = { + chunks: [], +}; vi.mock("openai", () => { class FakeOpenAI { diff --git a/packages/ai/test/openai-completions-tool-choice.test.ts b/packages/ai/test/openai-completions-tool-choice.test.ts index 64dd360b759..4f305bacf87 100644 --- a/packages/ai/test/openai-completions-tool-choice.test.ts +++ b/packages/ai/test/openai-completions-tool-choice.test.ts @@ -5,21 +5,20 @@ import { streamSimple } from "../src/stream.js"; import type { Tool } from "../src/types.js"; import { getZaiTestModel } from "./zai-test-model.js"; -const mockState = vi.hoisted(() => ({ - lastParams: undefined as unknown, - chunks: undefined as - | Array; finish_reason: string | null; usage?: unknown }>; - usage?: { - prompt_tokens: number; - completion_tokens: number; - prompt_tokens_details: { cached_tokens: number; cache_write_tokens?: number }; - completion_tokens_details: { reasoning_tokens: number }; - }; - }> - | undefined, -})); +interface ChunkItem { + id?: string; + choices?: Array<{ delta: Record; finish_reason: string | null; usage?: unknown }>; + usage?: { + prompt_tokens: number; + completion_tokens: number; + prompt_tokens_details: { cached_tokens: number; cache_write_tokens?: number }; + completion_tokens_details: { reasoning_tokens: number }; + }; +} +const mockState: { lastParams: unknown; chunks: Array | undefined } = { + lastParams: undefined, + chunks: undefined, +}; vi.mock("openai", () => { class FakeOpenAI { diff --git a/packages/ai/vitest.config.ts b/packages/ai/vitest.config.ts deleted file mode 100644 index 1b07c96f24a..00000000000 --- a/packages/ai/vitest.config.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { defineConfig } from 'vitest/config'; - -export default defineConfig({ - test: { - globals: true, - environment: 'node', - testTimeout: 30000, // 30 seconds for API calls - } -}); \ No newline at end of file diff --git a/packages/coding-agent/bunfig.toml b/packages/coding-agent/bunfig.toml new file mode 100644 index 00000000000..03378bb026c --- /dev/null +++ b/packages/coding-agent/bunfig.toml @@ -0,0 +1,3 @@ +[test] +preload = ["./test/bun-test-preload.ts"] +timeout = 30000 diff --git a/packages/coding-agent/package.json b/packages/coding-agent/package.json index 6fa9370fe3f..7d2896f277a 100644 --- a/packages/coding-agent/package.json +++ b/packages/coding-agent/package.json @@ -37,14 +37,14 @@ "build:binary": "bun run --cwd ../tui build && bun run --cwd ../ai build && bun run --cwd ../agent build && bun run build && bun build --compile ./dist/bun/cli.js --outfile dist/pi && bun scripts/copy-assets.ts binary", "copy-assets": "bun scripts/copy-assets.ts package", "copy-binary-assets": "bun scripts/copy-assets.ts binary", - "test": "vitest --run", - "test:ci": "tsx src/core/kernel/bootstrap-cli.ts && vitest --run --exclude test/daemon-supervisor-process.test.ts", - "test:process": "vitest --run test/daemon-supervisor-process.test.ts", - "test:process-stress": "vitest --run --tagsFilter process-stress test/daemon-supervisor-process.test.ts", + "test": "bun ../../scripts/run-with-clean-env.ts bun scripts/run-tests.ts", + "test:ci": "bun ../../scripts/run-with-clean-env.ts bun src/core/kernel/bootstrap-cli.ts && bun ../../scripts/run-with-clean-env.ts bun scripts/run-tests.ts", + "test:process": "bun ../../scripts/run-with-clean-env.ts bun test --isolate --timeout 30000 test/daemon-supervisor-process.test.ts", + "test:process-stress": "PRIME_AGENT_TEST_TAGS=process-stress bun ../../scripts/run-with-clean-env.ts bun test --isolate --timeout 30000 test/daemon-supervisor-process.test.ts", "postinstall": "bun postinstall.cjs", "prepublishOnly": "bun run clean && bun run build", "bundle": "bun scripts/bundle.mjs", - "test:kernel": "vitest --run --no-file-parallelism --tagsFilter kernel-heavy test/acp-kernel-features.test.ts test/acp-cold-cli.test.ts test/kernel-goal-skill.test.ts test/repl-kernel-state-roundtrip.test.ts test/repl-kernel-mcp-shutdown.test.ts" + "test:kernel": "PRIME_AGENT_TEST_TAGS=kernel-heavy bun ../../scripts/run-with-clean-env.ts bun test --isolate --timeout 30000 test/acp-kernel-features.test.ts test/acp-cold-cli.test.ts test/kernel-goal-skill.test.ts test/repl-kernel-state-roundtrip.test.ts test/repl-kernel-mcp-shutdown.test.ts test/repl-kernel-parent-watchdog.test.ts" }, "dependencies": { "@agentclientprotocol/sdk": "^1.3.0", @@ -83,10 +83,7 @@ "@types/ms": "^2.1.0", "@types/node": "^24.3.0", "@types/proper-lockfile": "^4.1.4", - "esbuild": "^0.28.1", - "shx": "^0.4.0", - "typescript": "^7.0.2", - "vitest": "^4.1.10" + "typescript": "^7.0.2" }, "keywords": [ "coding-agent", @@ -104,6 +101,7 @@ "directory": "packages/coding-agent" }, "engines": { - "node": ">=22.8.0" + "node": ">=22.8.0", + "bun": "1.3.14" } } diff --git a/packages/coding-agent/scripts/run-tests.ts b/packages/coding-agent/scripts/run-tests.ts new file mode 100644 index 00000000000..caaf9d42acd --- /dev/null +++ b/packages/coding-agent/scripts/run-tests.ts @@ -0,0 +1,61 @@ +#!/usr/bin/env bun + +interface Shard { + index: number; + total: number; +} + +function parseShard(args: string[]): { shard?: Shard; forwarded: string[] } { + let shard: Shard | undefined; + const forwarded: string[] = []; + for (const arg of args) { + if (!arg.startsWith("--shard=")) { + forwarded.push(arg); + continue; + } + const match = arg.slice("--shard=".length).match(/^(\d+)\/(\d+)$/); + if (!match) throw new Error(`Invalid shard: ${arg}`); + const index = Number(match[1]); + const total = Number(match[2]); + if (!Number.isInteger(index) || !Number.isInteger(total) || index < 1 || index > total) { + throw new Error(`Invalid shard: ${arg}`); + } + shard = { index, total }; + } + return { shard, forwarded }; +} + +const excluded = new Set(["test/compiled-artifact-smoke.test.ts", "test/daemon-supervisor-process.test.ts"]); +const { shard, forwarded } = parseShard(process.argv.slice(2)); +const discovered: string[] = []; +for await (const file of new Bun.Glob("test/**/*.test.ts").scan({ cwd: process.cwd(), onlyFiles: true })) { + if (!excluded.has(file)) discovered.push(file); +} +discovered.sort(); +const selected = shard + ? discovered.filter((_file, position) => position % shard.total === shard.index - 1) + : discovered; + +console.log( + `Running ${selected.length}/${discovered.length} coding-agent test files${shard ? ` (shard ${shard.index}/${shard.total})` : ""}`, +); + +const failures: Array<{ file: string; exitCode: number }> = []; +for (const file of selected) { + const child = Bun.spawn([process.execPath, "test", "--isolate", "--timeout", "30000", ...forwarded, file], { + cwd: process.cwd(), + env: process.env, + stdin: "ignore", + stdout: "inherit", + stderr: "inherit", + }); + const exitCode = await child.exited; + if (exitCode !== 0) failures.push({ file, exitCode }); +} + +if (failures.length > 0) { + console.error(`Failed coding-agent test files (${failures.length}/${selected.length}):`); + for (const failure of failures) console.error(`- ${failure.file} (exit ${failure.exitCode})`); + process.exit(1); +} +console.log(`Passed ${selected.length} coding-agent test files.`); diff --git a/packages/coding-agent/test/acp-cold-cli.test.ts b/packages/coding-agent/test/acp-cold-cli.test.ts index 38a3760e1a4..a91ca9039b5 100644 --- a/packages/coding-agent/test/acp-cold-cli.test.ts +++ b/packages/coding-agent/test/acp-cold-cli.test.ts @@ -5,6 +5,7 @@ import { tmpdir } from "node:os"; import { join, resolve } from "node:path"; import { afterEach, describe, expect, it } from "vitest"; import { ENV_AGENT_DIR } from "../src/config.js"; +import { isTestTagEnabled } from "./test-tags.js"; /** * Cold real-CLI ACP coverage. @@ -17,7 +18,6 @@ import { ENV_AGENT_DIR } from "../src/config.js"; */ const cliPath = resolve(__dirname, "../src/cli.ts"); -const tsxPath = resolve(__dirname, "../../../node_modules/tsx/dist/cli.mjs"); const tempDirs: string[] = []; const servers: Server[] = []; @@ -74,7 +74,6 @@ async function driveAcpTurn(baseUrl: string): Promise { const child = spawn( process.execPath, [ - tsxPath, cliPath, "--mode", "acp", @@ -93,7 +92,6 @@ async function driveAcpTurn(baseUrl: string): Promise { ...process.env, [ENV_AGENT_DIR]: agentDir, HOME: agentDir, - TSX_TSCONFIG_PATH: resolve(__dirname, "../../../tsconfig.json"), }, stdio: ["pipe", "pipe", "pipe"], }, @@ -186,10 +184,8 @@ async function driveAcpTurn(baseUrl: string): Promise { } describe("ACP mode over a cold real CLI process", () => { - it("reports a provider failure instead of a silent end_turn", { - tags: ["kernel-heavy"], - timeout: 180_000, - }, async () => { + const kernelHeavyIt = isTestTagEnabled("kernel-heavy") ? it : it.skip; + kernelHeavyIt("reports a provider failure instead of a silent end_turn", { timeout: 180_000 }, async () => { const baseUrl = await startRejectingProvider(); const { responses, updates } = await driveAcpTurn(baseUrl); diff --git a/packages/coding-agent/test/acp-kernel-features.test.ts b/packages/coding-agent/test/acp-kernel-features.test.ts index e333d382e60..78303ff1994 100644 --- a/packages/coding-agent/test/acp-kernel-features.test.ts +++ b/packages/coding-agent/test/acp-kernel-features.test.ts @@ -9,6 +9,7 @@ import { IpythonKernelProvisioner } from "../src/core/tools/ipython.js"; import { acpUpdatesForSessionEvent } from "../src/modes/acp/acp-events.js"; import { PRIME_AGENT_META_NAMESPACE } from "../src/modes/acp/acp-meta.js"; import type { AgentConnectionSessionEvent } from "../src/modes/agent-connection/types.js"; +import { isTestTagEnabled } from "./test-tags.js"; /** * Real-kernel verification for ACP mode. @@ -43,6 +44,7 @@ function toolEndEvent(toolCallId: string, output: string, isError = false): Agen } describe("ACP mode over a real Python kernel", () => { + const kernelHeavyIt = isTestTagEnabled("kernel-heavy") ? it : it.skip; let tempDir: string; let provisioner: IpythonKernelProvisioner | undefined; @@ -57,43 +59,44 @@ describe("ACP mode over a real Python kernel", () => { rmSync(tempDir, { recursive: true, force: true }); }); - it("keeps Python state across cells and represents each cell as an ACP execute call", { - tags: ["kernel-heavy"], - timeout: 180_000, - }, async () => { - // Every provisioner in this file requests the same skill set: the kernel venv - // is shared, and a skill-less kernel here can leave a later skill-dependent - // test with an unsynced venv when files run concurrently. - provisioner = new IpythonKernelProvisioner(tempDir, { pythonSkills: [AGENT_MESSAGE_SKILL] }); - const manager: KernelClient = await provisioner.ensure(); + kernelHeavyIt( + "keeps Python state across cells and represents each cell as an ACP execute call", + { timeout: 180_000 }, + async () => { + // Every provisioner in this file requests the same skill set: the kernel venv + // is shared, and a skill-less kernel here can leave a later skill-dependent + // test with an unsynced venv when files run concurrently. + provisioner = new IpythonKernelProvisioner(tempDir, { pythonSkills: [AGENT_MESSAGE_SKILL] }); + const manager: KernelClient = await provisioner.ensure(); - const first = await manager.execute("acp_state = 41\nprint('set')"); - expect(first.status).toBe("ok"); - // Persistence is the whole point of the kernel: a second cell must see it. - const second = await manager.execute("print(acp_state + 1)"); - expect(second.status).toBe("ok"); - expect(second.stdout.trim()).toBe("42"); + const first = await manager.execute("acp_state = 41\nprint('set')"); + expect(first.status).toBe("ok"); + // Persistence is the whole point of the kernel: a second cell must see it. + const second = await manager.execute("print(acp_state + 1)"); + expect(second.status).toBe("ok"); + expect(second.stdout.trim()).toBe("42"); - const updates = acpUpdatesForSessionEvent(toolEndEvent("cell-2", second.stdout)); - expect(updates[0]).toMatchObject({ - sessionUpdate: "tool_call_update", - toolCallId: "cell-2", - status: "completed", - }); - expect(JSON.stringify(updates[0]?.content)).toContain("42"); - }); + const updates = acpUpdatesForSessionEvent(toolEndEvent("cell-2", second.stdout)); + expect(updates[0]).toMatchObject({ + sessionUpdate: "tool_call_update", + toolCallId: "cell-2", + status: "completed", + }); + expect(JSON.stringify(updates[0]?.content)).toContain("42"); + }, + ); - it("runs continual-harness CRUD in the kernel and can represent the result over ACP", { - tags: ["kernel-heavy"], - timeout: 180_000, - }, async () => { - provisioner = new IpythonKernelProvisioner(tempDir, { - pythonSkills: [AGENT_MESSAGE_SKILL], - env: { RLM_GLOBAL_HARNESS_STATE_DIR: join(tempDir, "harness") }, - }); - const manager = await provisioner.ensure(); + kernelHeavyIt( + "runs continual-harness CRUD in the kernel and can represent the result over ACP", + { timeout: 180_000 }, + async () => { + provisioner = new IpythonKernelProvisioner(tempDir, { + pythonSkills: [AGENT_MESSAGE_SKILL], + env: { RLM_GLOBAL_HARNESS_STATE_DIR: join(tempDir, "harness") }, + }); + const manager = await provisioner.ensure(); - const allKinds = await manager.execute(` + const allKinds = await manager.execute(` import json mem = rlm.harness.create_memory(title="m", content="memory content", global_=True) note = rlm.harness.create_prompt_note(title="n", content="prompt note content", global_=True) @@ -110,15 +113,15 @@ print(json.dumps({ "skill_ref": skill.reference.get("import"), }, sort_keys=True)) `); - expect(allKinds.status, why(allKinds)).toBe("ok"); - // Every editable harness kind, not just memory: skills additionally require - // a python reference and an argument contract. - expect(JSON.parse(allKinds.stdout.trim())).toMatchObject({ - kinds: ["memory", "prompt", "skill", "subagent"], - skill_ref: "pkg.mod", - }); + expect(allKinds.status, why(allKinds)).toBe("ok"); + // Every editable harness kind, not just memory: skills additionally require + // a python reference and an argument contract. + expect(JSON.parse(allKinds.stdout.trim())).toMatchObject({ + kinds: ["memory", "prompt", "skill", "subagent"], + skill_ref: "pkg.mod", + }); - const result = await manager.execute(` + const result = await manager.execute(` import json entry = rlm.harness.create_memory( title="ACP verification memory", @@ -137,61 +140,62 @@ print(json.dumps({ "after": after.title if after else None, }, sort_keys=True)) `); - expect(result.status, why(result)).toBe("ok"); - const payload = JSON.parse(result.stdout.trim()); - expect(payload.found).toBe("ACP verification memory"); - expect(payload.listed).toContain(payload.created); - expect(payload.deleted).toBe(true); - expect(payload.after).toBeNull(); + expect(result.status, why(result)).toBe("ok"); + const payload = JSON.parse(result.stdout.trim()); + expect(payload.found).toBe("ACP verification memory"); + expect(payload.listed).toContain(payload.created); + expect(payload.deleted).toBe(true); + expect(payload.after).toBeNull(); - // A refinement outcome for that CRUD is expressible as namespaced metadata. - const refined = acpUpdatesForSessionEvent({ - type: "refine_complete", - result: { - summary: "persisted ACP verification memory", - appliedEdits: [{ applied: true, action: "create", kind: "memory", id: payload.created }], - }, - } as AgentConnectionSessionEvent); - expect(refined[0]?._meta).toMatchObject({ - [PRIME_AGENT_META_NAMESPACE]: { refinement: { status: "complete" } }, - }); - }); + // A refinement outcome for that CRUD is expressible as namespaced metadata. + const refined = acpUpdatesForSessionEvent({ + type: "refine_complete", + result: { + summary: "persisted ACP verification memory", + appliedEdits: [{ applied: true, action: "create", kind: "memory", id: payload.created }], + }, + } as AgentConnectionSessionEvent); + expect(refined[0]?._meta).toMatchObject({ + [PRIME_AGENT_META_NAMESPACE]: { refinement: { status: "complete" } }, + }); + }, + ); - it("exposes rlm depth and subagent APIs to the kernel behind the ACP front end", { - tags: ["kernel-heavy"], - timeout: 180_000, - }, async () => { - provisioner = new IpythonKernelProvisioner(tempDir, { - pythonSkills: [AGENT_MESSAGE_SKILL], - env: { RLM_DEPTH: "0", RLM_MAX_DEPTH: "1" }, - hostHandlers: { - "rlm.list_subagents": async () => ({ - subagents: [ - { - rlm_child_id: "child-1", - active_session_id: "active-1", + kernelHeavyIt( + "exposes rlm depth and subagent APIs to the kernel behind the ACP front end", + { timeout: 180_000 }, + async () => { + provisioner = new IpythonKernelProvisioner(tempDir, { + pythonSkills: [AGENT_MESSAGE_SKILL], + env: { RLM_DEPTH: "0", RLM_MAX_DEPTH: "1" }, + hostHandlers: { + "rlm.list_subagents": async () => ({ + subagents: [ + { + rlm_child_id: "child-1", + active_session_id: "active-1", + session_id: "session-1", + session_name: "reviewer", + session_dir: tempDir, + status: "completed", + }, + ], + }), + "rlm.delete_subagent": async (payload) => ({ + subagent: { + rlm_child_id: String(payload.target), + active_session_id: null, session_id: "session-1", session_name: "reviewer", session_dir: tempDir, status: "completed", }, - ], - }), - "rlm.delete_subagent": async (payload) => ({ - subagent: { - rlm_child_id: String(payload.target), - active_session_id: null, - session_id: "session-1", - session_name: "reviewer", - session_dir: tempDir, - status: "completed", - }, - }), - }, - }); - const manager = await provisioner.ensure(); + }), + }, + }); + const manager = await provisioner.ensure(); - const result = await manager.execute(` + const result = await manager.execute(` import json, os children = await rlm.list_subagents() removed = await rlm.delete_subagent(children[0]) @@ -202,50 +206,53 @@ print(json.dumps({ "removed": removed.session_name, }, sort_keys=True)) `); - expect(result.status, why(result)).toBe("ok"); - const payload = JSON.parse(result.stdout.trim()); - expect(payload.names).toEqual(["reviewer"]); - expect(payload.removed).toBe("reviewer"); - expect(payload.depth).toBe("0"); - expect(payload.max_depth).toBe("1"); - }); + expect(result.status, why(result)).toBe("ok"); + const payload = JSON.parse(result.stdout.trim()); + expect(payload.names).toEqual(["reviewer"]); + expect(payload.removed).toBe("reviewer"); + expect(payload.depth).toBe("0"); + expect(payload.max_depth).toBe("1"); + }, + ); - it("sends an agent-to-agent message from the kernel and surfaces it over ACP", { - tags: ["kernel-heavy"], - timeout: 180_000, - }, async () => { - provisioner = new IpythonKernelProvisioner(tempDir, { - pythonSkills: [AGENT_MESSAGE_SKILL], - hostHandlers: { - // The family roster: parent, siblings, and children of this agent. - "agent_message.list_agents": async () => ({ - current: { name: "root", id: "session-alpha", depth: 0 }, - entries: [{ relationship: "child", name: "reviewer", id: "session-beta", depth: 1, status: "idle" }], - }), - "agent_message.send": async (payload) => ({ - id: "agentmsg-acp", - source: "agent_message", - target: { activeSessionId: "beta", sessionId: "session-beta", sessionName: "reviewer" }, - message: payload.message, - deliveryStatus: "queued", - queuedAt: "2026-08-04T00:00:00.000Z", - deliveryMode: payload.mode ?? "auto", - }), - }, - }); - const manager = await provisioner.ensure(); + kernelHeavyIt( + "sends an agent-to-agent message from the kernel and surfaces it over ACP", + { timeout: 180_000 }, + async () => { + provisioner = new IpythonKernelProvisioner(tempDir, { + pythonSkills: [AGENT_MESSAGE_SKILL], + hostHandlers: { + // The family roster: parent, siblings, and children of this agent. + "agent_message.list_agents": async () => ({ + current: { name: "root", id: "session-alpha", depth: 0 }, + entries: [{ relationship: "child", name: "reviewer", id: "session-beta", depth: 1, status: "idle" }], + }), + "agent_message.send": async (payload) => ({ + id: "agentmsg-acp", + source: "agent_message", + target: { activeSessionId: "beta", sessionId: "session-beta", sessionName: "reviewer" }, + message: payload.message, + deliveryStatus: "queued", + queuedAt: "2026-08-04T00:00:00.000Z", + deliveryMode: payload.mode ?? "auto", + }), + }, + }); + const manager = await provisioner.ensure(); - // The kernel venv is shared across test files. If a concurrently running - // file rebuilt it without this skill, say so plainly rather than failing - // later with an opaque AttributeError on the stub object. - const available = await manager.execute("import json; print(json.dumps({'kind': type(agent_message).__name__}))"); - expect(available.status, why(available)).toBe("ok"); - expect( - JSON.parse(available.stdout.trim()).kind, - "agent_message skill was not installed into the shared kernel venv", - ).not.toBe("_PrimeAgentUnavailableSkill"); + // The kernel venv is shared across test files. If a concurrently running + // file rebuilt it without this skill, say so plainly rather than failing + // later with an opaque AttributeError on the stub object. + const available = await manager.execute( + "import json; print(json.dumps({'kind': type(agent_message).__name__}))", + ); + expect(available.status, why(available)).toBe("ok"); + expect( + JSON.parse(available.stdout.trim()).kind, + "agent_message skill was not installed into the shared kernel venv", + ).not.toBe("_PrimeAgentUnavailableSkill"); - const result = await manager.execute(` + const result = await manager.execute(` import json roster = await agent_message.list_agents() receipt = await agent_message.send("status update", receiver_role="child", receiver_name="reviewer") @@ -254,22 +261,23 @@ print(json.dumps({ "status": receipt["deliveryStatus"], })) `); - expect(result.status, why(result)).toBe("ok"); - const payload = JSON.parse(result.stdout.trim()); - expect(payload.roster).toEqual(["reviewer"]); - expect(payload.status).toBe("queued"); + expect(result.status, why(result)).toBe("ok"); + const payload = JSON.parse(result.stdout.trim()); + expect(payload.roster).toEqual(["reviewer"]); + expect(payload.status).toBe("queued"); - // The kernel reports the send; ACP carries it as namespaced metadata. - const sentMessages = result.sentAgentMessages ?? []; - expect(sentMessages.length).toBeGreaterThan(0); - const sent = sentMessages[0]; - const updates = acpUpdatesForSessionEvent({ - type: "ipython_sent_agent_message", - toolCallId: "cell-msg", - message: sent, - } as AgentConnectionSessionEvent); - expect(updates[0]?._meta).toMatchObject({ - [PRIME_AGENT_META_NAMESPACE]: { agentMessage: { toolCallId: "cell-msg", deliveryStatus: "queued" } }, - }); - }); + // The kernel reports the send; ACP carries it as namespaced metadata. + const sentMessages = result.sentAgentMessages ?? []; + expect(sentMessages.length).toBeGreaterThan(0); + const sent = sentMessages[0]; + const updates = acpUpdatesForSessionEvent({ + type: "ipython_sent_agent_message", + toolCallId: "cell-msg", + message: sent, + } as AgentConnectionSessionEvent); + expect(updates[0]?._meta).toMatchObject({ + [PRIME_AGENT_META_NAMESPACE]: { agentMessage: { toolCallId: "cell-msg", deliveryStatus: "queued" } }, + }); + }, + ); }); diff --git a/packages/coding-agent/test/agent-connection-daemon.test.ts b/packages/coding-agent/test/agent-connection-daemon.test.ts index a0ebd665570..5c2ec6e5003 100644 --- a/packages/coding-agent/test/agent-connection-daemon.test.ts +++ b/packages/coding-agent/test/agent-connection-daemon.test.ts @@ -1958,11 +1958,9 @@ describe("DaemonAgentConnection", () => { expect(fakeClient.reconnectCount).toBe(0); expect(closedEvents).toHaveLength(1); - expect(closedEvents[0]).toMatchObject({ - type: "closed", - error: expect.stringContaining("The Prime Agent daemon shut down while this window was attached."), - }); + expect(closedEvents[0]?.type).toBe("closed"); const closedError = closedEvents[0]?.type === "closed" ? closedEvents[0].error : undefined; + expect(closedError).toContain("The Prime Agent daemon shut down while this window was attached."); expect(closedError).toContain("Session ID: session-current."); expect(closedError).toContain("Session file: /tmp/session-current.jsonl."); expect(closedError).toContain("Diagnostic log:"); diff --git a/packages/coding-agent/test/agent-session-concurrent.test.ts b/packages/coding-agent/test/agent-session-concurrent.test.ts index 1c8a6f950f8..2e8db829a61 100644 --- a/packages/coding-agent/test/agent-session-concurrent.test.ts +++ b/packages/coding-agent/test/agent-session-concurrent.test.ts @@ -817,7 +817,7 @@ describe("AgentSession concurrent prompt guard", () => { await session.prompt("First message"); expect(session.isStreaming).toBe(false); - await expect(session.prompt("Second message")).resolves.not.toThrow(); + await expect(session.prompt("Second message")).resolves.toBeUndefined(); }); it("should wait for queued agent events before emitting tool_call", async () => { diff --git a/packages/coding-agent/test/agent-session-services.test.ts b/packages/coding-agent/test/agent-session-services.test.ts index 7c1eec6c7c4..244b4866c47 100644 --- a/packages/coding-agent/test/agent-session-services.test.ts +++ b/packages/coding-agent/test/agent-session-services.test.ts @@ -298,7 +298,7 @@ describe("createAgentSessionFromServices", () => { _createKernelHostHandlers(): Record; } )._createKernelHostHandlers(), - ).not.toHaveProperty("agent_message.send"); + ).not.toHaveProperty(["agent_message.send"]); } finally { session.dispose(); } @@ -403,7 +403,7 @@ describe("createAgentSessionFromServices", () => { }); try { expect(visibleSkillNames(withMessageController)).toContain(AGENT_MESSAGE_SKILL_NAME); - expect(kernelHostHandlers(withMessageController)).toHaveProperty("agent_message.send"); + expect(kernelHostHandlers(withMessageController)).toHaveProperty(["agent_message.send"]); } finally { withMessageController.dispose(); } diff --git a/packages/coding-agent/test/agents-view-mode.test.ts b/packages/coding-agent/test/agents-view-mode.test.ts index d4712697236..1e5ef8a03e6 100644 --- a/packages/coding-agent/test/agents-view-mode.test.ts +++ b/packages/coding-agent/test/agents-view-mode.test.ts @@ -1,3 +1,4 @@ +import { createRequire } from "node:module"; import { setKeybindings } from "@earendil-works/pi-tui"; import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; import { KeybindingsManager } from "../src/core/keybindings.js"; @@ -29,10 +30,8 @@ const modeMocks = vi.hoisted(() => ({ clientRequest: vi.fn<() => Promise>(), })); -vi.mock("../src/config.js", async (importOriginal) => { - const actual = await importOriginal(); - return { ...actual, appendRotatingLog: vi.fn() }; -}); +const __configJs = createRequire(import.meta.url)("../src/config.js"); +vi.mock("../src/config.js", () => ({ ...__configJs, appendRotatingLog: vi.fn() })); vi.mock("../src/modes/daemon/daemon-client.js", () => ({ DaemonClient: class { @@ -49,16 +48,14 @@ vi.mock("../src/modes/agent-connection/daemon-agent-connection.js", () => ({ }), })); -vi.mock("../src/modes/interactive/interactive-mode.js", async (importOriginal) => { - const actual = await importOriginal(); - return { - ...actual, - InteractiveMode: class { - run = modeMocks.interactiveRun; - teardownSessionUi = modeMocks.teardownSessionUi; - }, - }; -}); +const __interactiveMode = createRequire(import.meta.url)("../src/modes/interactive/interactive-mode.js"); +vi.mock("../src/modes/interactive/interactive-mode.js", () => ({ + ...__interactiveMode, + InteractiveMode: class { + run = modeMocks.interactiveRun; + teardownSessionUi = modeMocks.teardownSessionUi; + }, +})); function summary(overrides: Partial = {}): SessionSummary { return { diff --git a/packages/coding-agent/test/assistant-message-streaming-bench.ts b/packages/coding-agent/test/assistant-message-streaming-bench.ts index 1597ca6dec2..b22d70b3517 100644 --- a/packages/coding-agent/test/assistant-message-streaming-bench.ts +++ b/packages/coding-agent/test/assistant-message-streaming-bench.ts @@ -4,7 +4,7 @@ * Mimics the interactive-mode `message_update` handler: for each token chunk, * call updateContent() with the grown message and render(). Run with: * - * npx tsx test/assistant-message-streaming-bench.ts + * bun test/assistant-message-streaming-bench.ts */ import { performance } from "node:perf_hooks"; import type { AssistantMessage } from "@earendil-works/pi-ai"; diff --git a/packages/coding-agent/test/builtin-skills.test.ts b/packages/coding-agent/test/builtin-skills.test.ts index 8272ff0ca14..a94f81b1467 100644 --- a/packages/coding-agent/test/builtin-skills.test.ts +++ b/packages/coding-agent/test/builtin-skills.test.ts @@ -2,6 +2,7 @@ import { mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { dirname, join } from "node:path"; import { afterEach, beforeEach, describe, expect, it } from "vitest"; +import { REQUIRED_BINARY_SIDECARS } from "../../../scripts/compiled-artifact-smoke.js"; import { getBundledSkillsDir } from "../src/config.js"; import { DefaultPackageManager } from "../src/core/package-manager.js"; import { DefaultResourceLoader } from "../src/core/resource-loader.js"; @@ -297,26 +298,16 @@ describe("builtin skills", () => { // Verify every shipping path includes bundled skills; source-only success would hide a release packaging regression. describe("packaging ships bundled skills", () => { const packageRoot = join(__dirname, ".."); - const repoRoot = join(packageRoot, "..", ".."); - it("npm build (copy-assets) copies skills into dist", () => { + it("package and binary builds require bundled skills", () => { const pkg = JSON.parse(readFileSync(join(packageRoot, "package.json"), "utf-8")) as { files?: string[]; scripts?: Record; }; - expect(pkg.scripts?.["copy-assets"]).toContain("skills dist/skills"); - // npm publish ships the source skills/ dir via the files allowlist too. + expect(pkg.scripts?.["copy-assets"]).toBe("bun scripts/copy-assets.ts package"); + expect(pkg.scripts?.["copy-binary-assets"]).toBe("bun scripts/copy-assets.ts binary"); expect(pkg.files).toContain("skills"); - }); - - it("binary release script copies skills next to the executable", () => { - const script = readFileSync(join(repoRoot, "scripts", "build-binaries.sh"), "utf-8"); - expect(script).toMatch(/cp -r skills binaries\/\$platform\//); - }); - - it("release packer includes skills in the packed package", () => { - const script = readFileSync(join(repoRoot, "scripts", "pack-prime-agent-release.mjs"), "utf-8"); - expect(script).toContain('"skills"'); + expect(REQUIRED_BINARY_SIDECARS).toContain("skills"); }); }); }); diff --git a/packages/coding-agent/test/bun-bundle.test.ts b/packages/coding-agent/test/bun-bundle.test.ts new file mode 100644 index 00000000000..24aaa3689ba --- /dev/null +++ b/packages/coding-agent/test/bun-bundle.test.ts @@ -0,0 +1,122 @@ +import { execFileSync } from "node:child_process"; +import { existsSync, readdirSync, readFileSync, rmSync, statSync } from "node:fs"; +import { join } from "node:path"; +import { beforeAll, describe, expect, it } from "vitest"; + +const bundleDir = join(process.cwd(), "dist", "bundle"); +const bundleEntry = join(bundleDir, "cli.js"); +const distEntry = join(process.cwd(), "dist", "cli.js"); +const bundleScript = join(process.cwd(), "scripts", "bundle.mjs"); + +beforeAll(() => { + if (!existsSync(distEntry)) { + for (const packageDir of ["../tui", "../ai", "../agent", "."]) { + execFileSync("bun", ["--bun", "tsgo", "-p", "tsconfig.build.json"], { + cwd: join(process.cwd(), packageDir), + }); + } + } + if (existsSync(bundleDir)) { + rmSync(bundleDir, { recursive: true, force: true }); + } + execFileSync("bun", [bundleScript], { + cwd: process.cwd(), + encoding: "utf8", + }); +}); + +function readEntry(): string { + return readFileSync(bundleEntry, "utf8"); +} + +function listDir(): string[] { + return readdirSync(bundleDir); +} + +describe("bun-bundle build output", () => { + it("creates dist/bundle/ directory", () => { + expect(existsSync(bundleDir)).toBe(true); + }); + + it("creates entry point cli.js", () => { + expect(existsSync(bundleEntry)).toBe(true); + }); + + it("sets cli.js executable", () => { + const mode = statSync(bundleEntry).mode; + expect(mode & 0o100).toBeTruthy(); + }); +}); + +describe("bun-bundle entry content", () => { + it("preserves shebang", () => { + expect(readEntry().startsWith("#!/usr/bin/env node")).toBe(true); + }); + + it("injects require polyfill banner (createRequire)", () => { + const text = readEntry(); + expect(text).toContain("createRequire as __piBundleCreateRequire"); + expect(text).toContain("node:module"); + }); + + it("inlines __PI_BUNDLED__ define", () => { + expect(readEntry()).not.toContain("__PI_BUNDLED__"); + }); + + it("inlines __PI_BUILD_ID__ define", () => { + expect(readEntry()).not.toContain("__PI_BUILD_ID__"); + }); +}); + +describe("bun-bundle externals", () => { + const packages = ["koffi", "undici", "@silvia-odwyer/photon-node", "@mariozechner/clipboard"]; + for (const pkg of packages) { + it(`keeps ${pkg} external (not inlined)`, () => { + expect(readEntry()).not.toContain(pkg); + }); + } +}); + +describe("bun-bundle structure", () => { + it("produces at least 20 chunk files", () => { + const chunks = listDir().filter((f) => f !== "cli.js"); + expect(chunks.length).toBeGreaterThanOrEqual(20); + }); + + it("produces named provider chunks", () => { + const named = listDir().filter( + (f) => + f.startsWith("anthropic-") || + f.startsWith("google-") || + f.startsWith("azure-") || + f.startsWith("code-highlighter-"), + ); + expect(named.length).toBeGreaterThanOrEqual(3); + }); + + it("entry imports relative chunks", () => { + const relativeImports = readEntry().match(/from\s+["']\.\//g) || []; + expect(relativeImports.length).toBeGreaterThanOrEqual(1); + }); + + it("writes linked source maps for the entry and chunks", () => { + const files = listDir(); + const scripts = files.filter((file) => file.endsWith(".js")); + const maps = files.filter((file) => file.endsWith(".js.map")); + expect(maps.length).toBe(scripts.length); + expect(readEntry()).toContain("sourceMappingURL=cli.js.map"); + }); +}); + +describe("bun-bundle banner in chunks", () => { + it("injects require polyfill into chunk files too", () => { + const chunks = listDir() + .filter((file) => file.endsWith(".js") && file !== "cli.js") + .slice(0, 3); + for (const chunk of chunks) { + const content = readFileSync(join(bundleDir, chunk), "utf8"); + expect(content).toContain("createRequire"); + expect(content).toContain("node:module"); + } + }); +}); diff --git a/packages/coding-agent/test/bun-installer-e2e.test.ts b/packages/coding-agent/test/bun-installer-e2e.test.ts new file mode 100644 index 00000000000..298278cb2b7 --- /dev/null +++ b/packages/coding-agent/test/bun-installer-e2e.test.ts @@ -0,0 +1,104 @@ +import { afterEach, describe, expect, test } from "bun:test"; +import { spawnSync } from "node:child_process"; +import { createHash } from "node:crypto"; +import { chmodSync, mkdirSync, mkdtempSync, readFileSync, readlinkSync, rmSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { dirname, join } from "node:path"; +import { fileURLToPath } from "node:url"; + +const installer = join(dirname(fileURLToPath(import.meta.url)), "../../../install.sh"); +const temporaryRoots: string[] = []; + +afterEach(() => { + for (const root of temporaryRoots.splice(0)) rmSync(root, { recursive: true, force: true }); +}); + +function makeRelease(root: string, version: string, executable: string, checksumIsValid = true): void { + const platform = `${process.platform === "darwin" ? "darwin" : "linux"}-${process.arch === "arm64" ? "arm64" : "x64"}`; + const releaseDir = join(root, "server", "releases", `v${version}`); + const stage = join(root, `stage-${version}`); + mkdirSync(releaseDir, { recursive: true }); + mkdirSync(stage, { recursive: true }); + writeFileSync(join(stage, "prime-agent"), executable); + chmodSync(join(stage, "prime-agent"), 0o755); + writeFileSync(join(stage, "package.json"), JSON.stringify({ name: "prime-agent", version })); + writeFileSync(join(stage, "install.sh"), "#!/bin/sh\n"); + const archiveName = `prime-agent-${version}-${platform}.tar.gz`; + const archive = join(releaseDir, archiveName); + const packed = spawnSync("tar", ["-czf", archive, "-C", stage, "."], { encoding: "utf8" }); + if (packed.status !== 0) throw new Error(packed.stderr || "tar failed"); + const checksum = checksumIsValid ? createHash("sha256").update(readFileSync(archive)).digest("hex") : "0".repeat(64); + writeFileSync(join(releaseDir, "SHA256SUMS"), `${checksum} ${archiveName}\n`); +} + +function runInstaller(root: string, args: string[]): { exitCode: number; stdout: string; stderr: string } { + const home = join(root, "home"); + const versions = join(root, "apps", "versions"); + const bin = join(root, "bin"); + mkdirSync(join(home, ".prime"), { recursive: true }); + writeFileSync(join(home, ".prime", "sentinel"), "user data"); + const result = spawnSync("sh", [installer, ...args], { + env: { + ...process.env, + HOME: home, + PRIME_AGENT_DOWNLOAD_BASE_URL: `file://${join(root, "server")}`, + PRIME_AGENT_VERSIONS_DIR: versions, + PRIME_AGENT_BIN_DIR: bin, + TERM: "dumb", + }, + encoding: "utf8", + }); + return { + exitCode: result.status ?? 1, + stdout: result.stdout ?? "", + stderr: result.stderr ?? result.error?.message ?? "", + }; +} + +function goodExecutable(version: string): string { + return `#!/bin/sh\nif [ "${"$"}1" = "--version" ]; then echo "prime-agent ${version}"; exit 0; fi\nexit 0\n`; +} + +describe("compiled binary installer", () => { + test("installs a flat archive into a versioned app directory and preserves user data", () => { + const root = mkdtempSync(join(tmpdir(), "prime-agent-installer-")); + temporaryRoots.push(root); + makeRelease(root, "1.2.3", goodExecutable("1.2.3")); + + const result = runInstaller(root, ["1.2.3"]); + expect(result.exitCode, result.stderr).toBe(0); + const target = join(root, "apps", "versions", "v1.2.3", "prime-agent"); + expect(readlinkSync(join(root, "bin", "prime-agent"))).toBe(target); + expect(readFileSync(join(root, "apps", "versions", "v1.2.3", "package.json"), "utf8")).toContain('"1.2.3"'); + expect(readFileSync(join(root, "home", ".prime", "sentinel"), "utf8")).toBe("user data"); + + const secondInstall = runInstaller(root, ["1.2.3"]); + expect(secondInstall.exitCode, secondInstall.stderr).toBe(0); + expect(readlinkSync(join(root, "bin", "prime-agent"))).toBe(target); + }); + + test("rejects a bad checksum before changing the active version", () => { + const root = mkdtempSync(join(tmpdir(), "prime-agent-installer-")); + temporaryRoots.push(root); + makeRelease(root, "1.2.3", goodExecutable("1.2.3"), false); + + const result = runInstaller(root, ["1.2.3"]); + expect(result.exitCode).not.toBe(0); + expect(() => readlinkSync(join(root, "bin", "prime-agent"))).toThrow(); + }); + + test("keeps the previous symlink when an update fails its smoke test", () => { + const root = mkdtempSync(join(tmpdir(), "prime-agent-installer-")); + temporaryRoots.push(root); + makeRelease(root, "1.2.3", goodExecutable("1.2.3")); + expect(runInstaller(root, ["1.2.3"]).exitCode).toBe(0); + const link = join(root, "bin", "prime-agent"); + const oldTarget = readlinkSync(link); + + makeRelease(root, "2.0.0", "#!/bin/sh\nexit 1\n"); + const result = runInstaller(root, ["--update", "2.0.0"]); + expect(result.exitCode).not.toBe(0); + expect(readlinkSync(link)).toBe(oldTarget); + expect(readFileSync(join(root, "home", ".prime", "sentinel"), "utf8")).toBe("user data"); + }); +}); diff --git a/packages/coding-agent/test/bun-installer.test.ts b/packages/coding-agent/test/bun-installer.test.ts new file mode 100644 index 00000000000..12ba085d729 --- /dev/null +++ b/packages/coding-agent/test/bun-installer.test.ts @@ -0,0 +1,214 @@ +import { afterEach, describe, expect, it } from "bun:test"; +import { execFileSync } from "node:child_process"; +import { mkdtempSync, readFileSync, rmSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { dirname, join } from "node:path"; +import { fileURLToPath } from "node:url"; + +const repoRoot = join(dirname(fileURLToPath(import.meta.url)), "..", "..", ".."); +const installer = join(repoRoot, "install.sh"); +const installerText = readFileSync(installer, "utf-8"); + +let tempDirs: string[] = []; +afterEach(() => { + for (const d of tempDirs) { + try { + rmSync(d, { recursive: true, force: true }); + } catch {} + } + tempDirs = []; +}); +function mkTemp(): string { + const d = mkdtempSync(join(tmpdir(), "pi-installer-")); + tempDirs.push(d); + return d; +} + +function sourceVar(v: string): string { + // Source installer with main call disabled via sed, capture variable + const result = execFileSync( + "sh", + ["-c", `eval "$(sed 's/^main "$@"/# &/' "$1")" 2>/dev/null; printf '%s' "$${v}"`, "--", installer], + { encoding: "utf-8", env: process.env as any }, + ); + return result.trim(); +} + +// ============================================================================ +describe("install.sh shell syntax", () => { + it("passes POSIX shell syntax check", () => { + expect(() => execFileSync("sh", ["-n", installer], { stdio: "pipe" })).not.toThrow(); + }); + + it("contains the npm install function", () => { + expect(installerText).toContain("prime_agent_npm_install"); + }); + + it("supports --method and --update flags", () => { + expect(installerText).toContain("--method=*"); + expect(installerText).toContain("binary|npm|auto"); + expect(installerText).toContain("--update"); + }); +}); + +// ============================================================================ +describe("default install method (auto) routes to binary", () => { + it("routes auto and binary to binary_fresh_install, never npm", () => { + expect(installerText).toMatch(/\$prime_agent_install_method" != npm/); + }); + + it("uses npm only with explicit --method=npm", () => { + const matches = installerText.match(/\$prime_agent_install_method"\s*!=\s*npm/g); + expect(matches).not.toBeNull(); + expect(matches!.length).toBeGreaterThanOrEqual(1); + }); +}); + +// ============================================================================ +describe("versioned-dir variables", () => { + it("uses XDG_DATA_HOME/prime-agent/versions for versions dir", () => { + const dir = sourceVar("prime_agent_binary_versions_dir"); + expect(dir).toMatch(/prime-agent.versions$/); + expect(dir).not.toContain(".prime"); + }); + + it("sets symlink to XDG_BIN_HOME/prime-agent", () => { + const link = sourceVar("prime_agent_binary_symlink"); + expect(link).toMatch(/prime-agent$/); + expect(link).not.toContain(".prime"); + }); + + it("respects PRIME_AGENT_VERSIONS_DIR env var", () => { + const result = execFileSync( + "sh", + [ + "-c", + "PRIME_AGENT_VERSIONS_DIR=/custom/versions; " + + 'eval "$(sed \'s/^main "$@"$/# &/\' "$1")" 2>/dev/null; ' + + "printf '%s' $prime_agent_binary_versions_dir", + "--", + installer, + ], + { encoding: "utf-8", env: process.env as any }, + ).trim(); + expect(result).toBe("/custom/versions"); + }); + + it("respects PRIME_AGENT_BIN_DIR env var for symlink path", () => { + const result = execFileSync( + "sh", + [ + "-c", + "PRIME_AGENT_BIN_DIR=/custom/bin; " + + 'eval "$(sed \'s/^main "$@"$/# &/\' "$1")" 2>/dev/null; ' + + "printf '%s' $prime_agent_binary_symlink", + "--", + installer, + ], + { encoding: "utf-8", env: process.env as any }, + ).trim(); + expect(result).toBe("/custom/bin/prime-agent"); + }); +}); + +// ============================================================================ +describe("platform detection", () => { + it("detects current platform as a valid binary platform", () => { + const platform = execFileSync( + "sh", + [ + "-c", + 'eval "$(sed \'s/^main "$@"$/# &/\' "$1")" 2>/dev/null; prime_agent_detect_binary_platform', + "--", + installer, + ], + { encoding: "utf-8", env: process.env as any }, + ).trim(); + expect(["darwin-arm64", "darwin-x64", "linux-x64", "linux-arm64"]).toContain(platform); + }); +}); + +// ============================================================================ +describe("atomic symlink function", () => { + it("creates and replaces symlink atomically", () => { + const tmp = mkTemp(); + const v1 = `${tmp}/versions/v1`; + const v2 = `${tmp}/versions/v2`; + const link = `${tmp}/bin/prime-agent`; + const cmd = + 'eval "$(sed \'s/^main "$@"$/# &/\' "$1")" 2>/dev/null' + + "; mkdir -p " + + v1 + + " " + + v2 + + "; touch " + + v1 + + "/pi " + + v2 + + "/pi" + + "; chmod +x " + + v1 + + "/pi " + + v2 + + "/pi" + + "; mkdir -p " + + tmp + + "/bin" + + "; prime_agent_binary_symlink=" + + link + + "; prime_agent_binary_atomic_symlink " + + v1 + + "/pi " + + link + + "; prime_agent_binary_atomic_symlink " + + v2 + + "/pi " + + link + + '; [ "$(readlink ' + + link + + ')" = "' + + v2 + + "/pi\" ] && printf 'OK'"; + const result = execFileSync("sh", ["-c", cmd, "--", installer], { + encoding: "utf-8", + env: process.env as any, + }).trim(); + expect(result).toBe("OK"); + }); +}); + +// ============================================================================ +describe("config dir isolation", () => { + it("does not use ~/.prime for binary install paths", () => { + const versionsLine = installerText.match(/^prime_agent_binary_versions_dir=.*$/m); + const symlinkLine = installerText.match(/^prime_agent_binary_symlink=.*$/m); + if (versionsLine) expect(versionsLine[0]).not.toContain(".prime"); + if (symlinkLine) expect(symlinkLine[0]).not.toContain(".prime"); + }); + + it("uses XDG data dir for versions", () => { + expect(installerText).toContain("XDG_DATA_HOME"); + }); + + it("uses separate bin dir for symlink", () => { + expect(installerText).toContain("prime_agent_binary_symlink"); + }); + + it("does not copy sidecars into ~/.prime", () => { + const freshSection = installerText.match(/prime_agent_binary_fresh_install[^}]*}/s)?.[0] || ""; + expect(freshSection).not.toContain(".prime"); + }); +}); + +// ============================================================================ +describe("install.sh sidecar", () => { + it("ensures install.sh is made executable in versioned dir", () => { + expect(installerText).toContain('chmod +x "$_version_dir/install.sh"'); + }); + + it("appears in both fresh_install and update paths", () => { + const matches = installerText.match(/chmod \+x "\$_version_dir\/install\.sh"/g); + expect(matches).not.toBeNull(); + expect(matches!.length).toBe(2); + }); +}); diff --git a/packages/coding-agent/test/bun-release-archives.test.ts b/packages/coding-agent/test/bun-release-archives.test.ts new file mode 100644 index 00000000000..d92b6f9af18 --- /dev/null +++ b/packages/coding-agent/test/bun-release-archives.test.ts @@ -0,0 +1,150 @@ +import { afterEach, describe, expect, test } from "bun:test"; +import { spawnSync } from "node:child_process"; +import { + chmodSync, + cpSync, + existsSync, + mkdirSync, + mkdtempSync, + readdirSync, + readFileSync, + rmSync, + writeFileSync, +} from "node:fs"; +import { tmpdir } from "node:os"; +import { dirname, join } from "node:path"; +import { fileURLToPath } from "node:url"; + +const packageDir = join(dirname(fileURLToPath(import.meta.url)), ".."); +const repoRoot = join(packageDir, "..", ".."); +const packScript = join(repoRoot, "scripts", "pack-prime-agent-release.mjs"); +const releaseRoot = join(packageDir, "release"); +const temporaryRoots: string[] = []; +const outputDirs: string[] = []; +const platforms = ["darwin-arm64", "darwin-x64", "linux-arm64", "linux-x64"]; + +function fixture(): { root: string; binaries: string; sidecars: string; output: string } { + const root = mkdtempSync(join(tmpdir(), "prime-agent-release-")); + temporaryRoots.push(root); + const binaries = join(root, "binaries"); + const sidecars = join(root, "sidecars"); + for (const platform of platforms) { + const dir = join(binaries, platform); + mkdirSync(dir, { recursive: true }); + cpSync("/bin/echo", join(dir, "pi")); + chmodSync(join(dir, "pi"), 0o755); + } + mkdirSync(sidecars, { recursive: true }); + for (const name of ["prime-agent-runtime", "skills", "theme", "assets", "export-html", "docs", "examples"]) { + mkdirSync(join(sidecars, name)); + writeFileSync(join(sidecars, name, ".keep"), "fixture"); + } + writeFileSync(join(sidecars, "package.json"), JSON.stringify({ name: "prime-agent", version: "1.2.3" })); + writeFileSync(join(sidecars, "README.md"), "readme"); + writeFileSync(join(sidecars, "CHANGELOG.md"), "changelog"); + writeFileSync( + join(sidecars, "install.sh"), + '#!/bin/sh\nbase="__PRIME_AGENT_DOWNLOAD_BASE_URL__"\nchannel="__PRIME_AGENT_DEFAULT_RELEASE_CHANNEL__"\n', + ); + writeFileSync(join(sidecars, "photon_rs_bg.wasm"), "wasm"); + const output = join(releaseRoot, `test-${process.pid}-${Math.random().toString(36).slice(2)}`); + outputDirs.push(output); + return { root, binaries, sidecars, output }; +} + +function pack(f: ReturnType, extra: string[] = []) { + return spawnSync( + process.execPath, + [ + packScript, + "--base-url", + "https://downloads.example.test", + "--channel", + "stable", + "--version", + "1.2.3", + "--binary-base-dir", + f.binaries, + "--sidecar-dir", + f.sidecars, + "--out-dir", + f.output, + ...extra, + ], + { encoding: "utf8" }, + ); +} + +afterEach(() => { + for (const root of temporaryRoots.splice(0)) rmSync(root, { recursive: true, force: true }); + for (const output of outputDirs.splice(0)) rmSync(output, { recursive: true, force: true }); +}); + +describe("compiled release archives", () => { + test("creates all platform archives with flat required sidecars and rendered installer", () => { + const f = fixture(); + const result = pack(f); + expect(result.status, result.stderr).toBe(0); + const artifacts = join(f.output, "artifacts"); + const archives = readdirSync(artifacts).filter((name) => name.endsWith(".tar.gz")); + expect(archives.sort()).toEqual(platforms.map((platform) => `prime-agent-1.2.3-${platform}.tar.gz`).sort()); + expect(readFileSync(join(artifacts, "stable"), "utf8")).toBe("v1.2.3\n"); + expect(readFileSync(join(artifacts, "SHA256SUMS"), "utf8").trim().split("\n")).toHaveLength(4); + + const extracted = join(f.root, "extracted"); + mkdirSync(extracted); + const archive = join(artifacts, "prime-agent-1.2.3-darwin-arm64.tar.gz"); + expect(spawnSync("tar", ["-xzf", archive, "-C", extracted]).status).toBe(0); + expect(readFileSync(join(extracted, "install.sh"), "utf8")).toContain('base="https://downloads.example.test"'); + expect(readFileSync(join(extracted, "install.sh"), "utf8")).toContain('channel="stable"'); + const manifest = JSON.parse(readFileSync(join(extracted, "package.json"), "utf8")); + expect(manifest).toMatchObject({ + name: "prime-agent", + version: "1.2.3", + bin: { "prime-agent": "./prime-agent" }, + packageManager: "bun@1.3.14", + }); + for (const name of [ + "prime-agent", + "package.json", + "README.md", + "CHANGELOG.md", + "install.sh", + "prime-agent-runtime", + "skills", + "theme", + "assets", + "export-html", + "docs", + "examples", + "photon_rs_bg.wasm", + ]) { + expect(existsSync(join(extracted, name))).toBe(true); + } + }); + + test("supports a single-platform local archive", () => { + const f = fixture(); + const result = pack(f, ["--platform", "linux-x64"]); + expect(result.status, result.stderr).toBe(0); + expect(readdirSync(join(f.output, "artifacts")).filter((name) => name.endsWith(".tar.gz"))).toEqual([ + "prime-agent-1.2.3-linux-x64.tar.gz", + ]); + }); + + test("fails closed when a required sidecar is missing", () => { + const f = fixture(); + rmSync(join(f.sidecars, "theme"), { recursive: true }); + const result = pack(f); + expect(result.status).not.toBe(0); + expect(result.stderr).toContain("Missing required sidecars"); + }); + + test("rejects dependency directories in release sidecars", () => { + const f = fixture(); + mkdirSync(join(f.sidecars, "examples", "node_modules")); + const result = pack(f); + expect(result.status).not.toBe(0); + expect(result.stderr).toContain("forbidden dependency or cache directory"); + }); +}); diff --git a/packages/coding-agent/test/bun-test-preload.ts b/packages/coding-agent/test/bun-test-preload.ts new file mode 100644 index 00000000000..aa313ca9537 --- /dev/null +++ b/packages/coding-agent/test/bun-test-preload.ts @@ -0,0 +1,142 @@ +import { jest } from "bun:test"; +import { readFile } from "node:fs/promises"; +import { describe, expect, vi } from "vitest"; + +type AnyFunction = (...args: never[]) => unknown; +type ViCompat = Record; + +const compat = vi as unknown as ViCompat; +const nativeSetTimeout = globalThis.setTimeout; +process.env.DO_NOT_TRACK ??= "1"; +// Do not let the parent Prime Agent daemon make CLI tests operate on live sessions. +for (const name of Object.keys(process.env)) { + if (name.startsWith("PRIME_AGENT_INTERNAL_") || name.startsWith("RLM_")) { + delete process.env[name]; + } +} +delete process.env.PRIME_AGENT_CODING_AGENT_DIR; +delete process.env.PRIME_AGENT_KERNEL_OWNER_PID; +const gitConfigCount = Number.parseInt(process.env.GIT_CONFIG_COUNT ?? "0", 10) || 0; +process.env[`GIT_CONFIG_KEY_${gitConfigCount}`] = "commit.gpgsign"; +process.env[`GIT_CONFIG_VALUE_${gitConfigCount}`] = "false"; +process.env.GIT_CONFIG_COUNT = String(gitConfigCount + 1); +const originalEnv = new Map(); +const originalGlobals = new Map(); + +compat.hoisted ??= (factory: AnyFunction) => factory(); +compat.mocked ??= (value: unknown) => value; +compat.setSystemTime ??= (value: Date | number) => jest.setSystemTime(value); +compat.advanceTimersByTime = (milliseconds: number) => { + const targetTime = Date.now() + milliseconds; + jest.advanceTimersByTime(milliseconds); + jest.setSystemTime(targetTime); + return vi; +}; +const flushMicrotasks = async (): Promise => { + // Timer callbacks in these tests often cross several awaited boundaries before + // scheduling the next timer. Keep fake time still while those jobs settle. + for (let index = 0; index < 8; index++) await Promise.resolve(); +}; + +compat.advanceTimersByTimeAsync ??= async (milliseconds: number) => { + await flushMicrotasks(); + let remaining = milliseconds; + while (remaining > 0) { + const step = Math.min(remaining, 10); + const targetTime = Date.now() + step; + jest.advanceTimersByTime(step); + jest.setSystemTime(targetTime); + remaining -= step; + await flushMicrotasks(); + } + if (milliseconds === 0) { + jest.advanceTimersByTime(0); + await flushMicrotasks(); + } +}; +compat.runAllTimersAsync ??= async () => { + await flushMicrotasks(); + for (let pass = 0; pass < 10_000 && jest.getTimerCount() > 0; pass++) { + jest.runAllTimers(); + await flushMicrotasks(); + } +}; +compat.advanceTimersToNextTimerAsync ??= async () => { + await flushMicrotasks(); + jest.advanceTimersToNextTimer(); + await flushMicrotasks(); +}; +compat.runOnlyPendingTimersAsync ??= async () => { + await flushMicrotasks(); + jest.runOnlyPendingTimers(); + await flushMicrotasks(); +}; +compat.waitFor ??= async (assertion: AnyFunction, options: { timeout?: number; interval?: number } = {}) => { + const timeout = options.timeout ?? 1_000; + const interval = options.interval ?? 20; + const deadline = Date.now() + timeout; + let lastError: unknown; + while (Date.now() <= deadline) { + try { + return await assertion(); + } catch (error) { + lastError = error; + } + if (jest.isFakeTimers()) { + const targetTime = Date.now() + interval; + jest.advanceTimersByTime(interval); + jest.setSystemTime(targetTime); + await flushMicrotasks(); + // Promise jobs alone do not let spawned-process and socket events run. + await readFile("/dev/null"); + } else { + await new Promise((resolve) => nativeSetTimeout(resolve, interval)); + } + } + throw lastError ?? new Error(`waitFor timed out after ${timeout} ms`); +}; + +const expectCompat = expect as typeof expect & { + poll?: (actual: () => unknown | Promise, options?: { timeout?: number; interval?: number }) => unknown; +}; +expectCompat.poll ??= (actual: () => unknown | Promise, options?: { timeout?: number; interval?: number }) => + new Proxy( + {}, + { + get: + (_target, matcher: string) => + async (...args: unknown[]) => + vi.waitFor(async () => { + const received = await actual(); + return (expect(received) as unknown as Record unknown>)[matcher]?.( + ...args, + ); + }, options), + }, + ); + +compat.stubEnv ??= (name: string, value: string | undefined) => { + if (!originalEnv.has(name)) originalEnv.set(name, process.env[name]); + if (value === undefined) delete process.env[name]; + else process.env[name] = value; +}; +compat.unstubAllEnvs ??= () => { + for (const [name, value] of originalEnv) { + if (value === undefined) delete process.env[name]; + else process.env[name] = value; + } + originalEnv.clear(); +}; +compat.stubGlobal ??= (name: PropertyKey, value: unknown) => { + if (!originalGlobals.has(name)) originalGlobals.set(name, Object.getOwnPropertyDescriptor(globalThis, name)); + Object.defineProperty(globalThis, name, { configurable: true, writable: true, value }); +}; +compat.unstubAllGlobals ??= () => { + for (const [name, descriptor] of originalGlobals) { + if (descriptor) Object.defineProperty(globalThis, name, descriptor); + else Reflect.deleteProperty(globalThis, name); + } + originalGlobals.clear(); +}; + +(describe as typeof describe & { sequential?: typeof describe }).sequential ??= describe; diff --git a/packages/coding-agent/test/bun-test-runtime-isolation.test.ts b/packages/coding-agent/test/bun-test-runtime-isolation.test.ts new file mode 100644 index 00000000000..07d9fdd1eca --- /dev/null +++ b/packages/coding-agent/test/bun-test-runtime-isolation.test.ts @@ -0,0 +1,33 @@ +import { spawnSync } from "node:child_process"; +import { resolve } from "node:path"; +import { describe, expect, it } from "vitest"; + +const repoRoot = resolve(import.meta.dirname, "../../.."); +const launcher = resolve(repoRoot, "scripts/run-with-clean-env.ts"); + +describe("Bun test runtime isolation", () => { + it("removes live Prime Agent orchestration state before Bun and its descendants start", () => { + const probe = `console.log(JSON.stringify({ + internal: process.env.PRIME_AGENT_INTERNAL_ORPHAN_PROCESS_JOURNAL, + rlm: process.env.RLM_SESSION_DIR, + agentDir: process.env.PRIME_AGENT_CODING_AGENT_DIR, + owner: process.env.PRIME_AGENT_KERNEL_OWNER_PID, + tags: process.env.PRIME_AGENT_TEST_TAGS, + }))`; + const result = spawnSync(process.execPath, [launcher, "bun", "-e", probe], { + cwd: repoRoot, + encoding: "utf8", + env: { + ...process.env, + PRIME_AGENT_INTERNAL_ORPHAN_PROCESS_JOURNAL: "/live/orphans.jsonl", + RLM_SESSION_DIR: "/live/session", + PRIME_AGENT_CODING_AGENT_DIR: "/live/agent", + PRIME_AGENT_KERNEL_OWNER_PID: "1234", + PRIME_AGENT_TEST_TAGS: "kernel-heavy", + }, + }); + + expect(result.status, result.stderr).toBe(0); + expect(JSON.parse(result.stdout)).toEqual({ tags: "kernel-heavy" }); + }); +}); diff --git a/packages/coding-agent/test/clipboard-image-bmp-conversion.test.ts b/packages/coding-agent/test/clipboard-image-bmp-conversion.test.ts index 95107506b5c..87a7e0e1664 100644 --- a/packages/coding-agent/test/clipboard-image-bmp-conversion.test.ts +++ b/packages/coding-agent/test/clipboard-image-bmp-conversion.test.ts @@ -5,6 +5,7 @@ * This tests the fix for WSL2/WSLg where clipboard often provides image/bmp * instead of image/png. */ +import * as childProcess from "node:child_process"; import { describe, expect, test, vi } from "vitest"; function createTinyBmp1x1Red24bpp(): Uint8Array { @@ -42,10 +43,9 @@ function createTinyBmp1x1Red24bpp(): Uint8Array { } // Mock wl-paste to return BMP -vi.mock("child_process", async () => { - const actual = await vi.importActual("child_process"); +vi.mock("child_process", () => { return { - ...actual, + ...childProcess, spawnSync: vi.fn((command: string, args: string[]) => { if (command === "wl-paste" && args.includes("--list-types")) { return { status: 0, stdout: Buffer.from("image/bmp\n"), error: null }; diff --git a/packages/coding-agent/test/clipboard-image.test.ts b/packages/coding-agent/test/clipboard-image.test.ts index eda61a29e5f..c99a03387ce 100644 --- a/packages/coding-agent/test/clipboard-image.test.ts +++ b/packages/coding-agent/test/clipboard-image.test.ts @@ -49,7 +49,6 @@ function spawnError(error: Error): SpawnSyncReturns { describe("readClipboardImage", () => { beforeEach(() => { - vi.resetModules(); mocks.spawnSync.mockReset(); mocks.clipboard.hasImage.mockReset(); mocks.clipboard.getImageBinary.mockReset(); diff --git a/packages/coding-agent/test/compaction-summary-reasoning.test.ts b/packages/coding-agent/test/compaction-summary-reasoning.test.ts index c674ef1a7df..886760bc4f2 100644 --- a/packages/coding-agent/test/compaction-summary-reasoning.test.ts +++ b/packages/coding-agent/test/compaction-summary-reasoning.test.ts @@ -1,3 +1,4 @@ +import { createRequire } from "node:module"; import type { AgentMessage } from "@earendil-works/pi-agent-core"; import type { AssistantMessage, Model } from "@earendil-works/pi-ai"; import { beforeEach, describe, expect, it, vi } from "vitest"; @@ -7,13 +8,11 @@ const { completeSimpleMock } = vi.hoisted(() => ({ completeSimpleMock: vi.fn(), })); -vi.mock("@earendil-works/pi-ai", async (importOriginal) => { - const actual = await importOriginal(); - return { - ...actual, - completeSimple: completeSimpleMock, - }; -}); +const __piAi = createRequire(import.meta.url)("@earendil-works/pi-ai"); +vi.mock("@earendil-works/pi-ai", () => ({ + ...__piAi, + completeSimple: completeSimpleMock, +})); function createModel(reasoning: boolean): Model<"anthropic-messages"> { return { diff --git a/packages/coding-agent/test/compiled-artifact-smoke.test.ts b/packages/coding-agent/test/compiled-artifact-smoke.test.ts new file mode 100644 index 00000000000..4215ce290f5 --- /dev/null +++ b/packages/coding-agent/test/compiled-artifact-smoke.test.ts @@ -0,0 +1,61 @@ +import { describe, expect, it } from "bun:test"; +import { existsSync, readdirSync, statSync } from "node:fs"; +import { dirname, join, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; +import { + inspectNativeExecutable, + REQUIRED_BINARY_SIDECARS, + runEmptyDiagnostic, + runPackagedSmoke, +} from "../../../scripts/compiled-artifact-smoke.js"; + +const packageDir = resolve(dirname(fileURLToPath(import.meta.url)), ".."); +const distDir = join(packageDir, "dist"); +const binary = join(distDir, "pi"); + +function dependencyDirectories(root: string): string[] { + const found: string[] = []; + for (const entry of readdirSync(root, { withFileTypes: true })) { + if (!entry.isDirectory()) continue; + const path = join(root, entry.name); + if (["node_modules", ".venv", "__pycache__", ".pytest_cache"].includes(entry.name)) found.push(path); + else found.push(...dependencyDirectories(path)); + } + return found; +} + +describe("compiled Bun artifact", () => { + it("is a native executable", () => { + expect(existsSync(binary)).toBe(true); + expect(statSync(binary).size).toBeGreaterThan(10 * 1024 * 1024); + expect(["elf", "mach-o"]).toContain(inspectNativeExecutable(binary)); + }); + + it("documents the unsupported binary-only layout", () => { + const result = runEmptyDiagnostic(binary); + expect(result.detectedMissingPackageJson).toBe(true); + expect(result.run.exitCode).not.toBe(0); + }); + + it("contains the complete supported sidecar set", () => { + for (const name of REQUIRED_BINARY_SIDECARS) { + expect(existsSync(join(distDir, name)), name).toBe(true); + } + }); + + it("contains no installed dependency or cache directories", () => { + expect(dependencyDirectories(distDir)).toEqual([]); + }); + + it("runs --version and --help with no Node or npm on PATH", () => { + const result = runPackagedSmoke(binary, distDir); + expect(result.nodeAvailable).toBe(false); + expect(result.npmAvailable).toBe(false); + expect(result.passed).toBe(true); + for (const run of result.runs) { + expect(run.exitCode).toBe(0); + expect(run.stdout.trim().length).toBeGreaterThan(0); + expect(run.stderr).not.toContain("ENOENT"); + } + }); +}); diff --git a/packages/coding-agent/test/daemon-client.test.ts b/packages/coding-agent/test/daemon-client.test.ts index f7614733fa2..42d75a3cafe 100644 --- a/packages/coding-agent/test/daemon-client.test.ts +++ b/packages/coding-agent/test/daemon-client.test.ts @@ -15,7 +15,17 @@ const netMock = vi.hoisted(() => { readonly writes: string[] = []; destroyed = false; - constructor(readonly path: string) {} + path = ""; + + constructor(path = "") { + this.path = path; + } + + connect(path: string): this { + this.path = path; + sockets.push(this); + return this; + } on(event: string, listener: Listener): this { const listeners = this.listeners.get(event) ?? new Set(); @@ -83,11 +93,12 @@ const netMock = vi.hoisted(() => { return socket; }); - return { createConnection, sockets }; + return { createConnection, MockSocket, sockets }; }); vi.mock("node:net", () => ({ createConnection: netMock.createConnection, + Socket: netMock.MockSocket, })); function emitHello( @@ -155,11 +166,9 @@ describe("DaemonClient", () => { expect(netMock.sockets).toHaveLength(1); const firstSocket = netMock.sockets[0]!; - const timeoutRejection = expect(firstAttempt).resolves.toMatchObject({ - message: expect.stringContaining("Timed out after 5ms connecting to the Prime Agent daemon."), - }); await vi.advanceTimersByTimeAsync(5); - await timeoutRejection; + const timeoutError = await firstAttempt; + expect(timeoutError.message).toContain("Timed out after 5ms connecting to the Prime Agent daemon."); expect(firstSocket.destroyed).toBe(true); expect(firstSocket.listenerCount("data")).toBe(0); @@ -419,17 +428,12 @@ describe("DaemonClient", () => { const request = client.request({ type: "list", all: true }); - await expect(request).rejects.toMatchObject({ - message: expect.stringContaining( - 'Cannot send daemon command "list" because the Prime Agent daemon is not connected.', - ), - }); - await expect(request).rejects.toMatchObject({ - message: expect.stringContaining("Socket: /tmp/prime-agent.sock."), - }); - await expect(request).rejects.toMatchObject({ - message: expect.stringContaining("Daemon log:"), - }); + const requestError = await captureRejection(request); + expect(requestError.message).toContain( + 'Cannot send daemon command "list" because the Prime Agent daemon is not connected.', + ); + expect(requestError.message).toContain("Socket: /tmp/prime-agent.sock."); + expect(requestError.message).toContain("Daemon log:"); }); it("keeps durable command envelopes on the session-action protocol", async () => { @@ -931,7 +935,7 @@ describe("DaemonClient", () => { }); }); -async function captureRejection(promise: Promise): Promise { +async function captureRejection(promise: Promise): Promise { try { await promise; } catch (error) { diff --git a/packages/coding-agent/test/daemon-command.test.ts b/packages/coding-agent/test/daemon-command.test.ts index 65866521c9b..43e0e0ef4ea 100644 --- a/packages/coding-agent/test/daemon-command.test.ts +++ b/packages/coding-agent/test/daemon-command.test.ts @@ -1,3 +1,4 @@ +import { createRequire } from "node:module"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; const daemonClientMock = vi.hoisted(() => { @@ -121,10 +122,8 @@ const spawnMock = vi.hoisted(() => { }; }); -vi.mock("node:child_process", async (importOriginal) => { - const original = (await importOriginal()) as Record; - return { ...original, spawn: spawnMock.mockSpawn as never }; -}); +const __childProcess = createRequire(import.meta.url)("node:child_process"); +vi.mock("node:child_process", () => ({ ...__childProcess, spawn: spawnMock.mockSpawn as never })); import { handleDaemonCommand } from "../src/cli/daemon-command.js"; @@ -132,7 +131,7 @@ describe("daemon command", () => { let consoleErrorMessages: unknown[]; beforeEach(() => { - process.exitCode = undefined; + process.exitCode = 0; daemonClientMock.instances.length = 0; daemonClientMock.behavior.promptSucceeds = false; daemonClientMock.behavior.emitStaleAgentEndOnAttach = false; @@ -149,7 +148,7 @@ describe("daemon command", () => { }); afterEach(() => { - process.exitCode = undefined; + process.exitCode = 0; vi.restoreAllMocks(); }); diff --git a/packages/coding-agent/test/daemon-launch.test.ts b/packages/coding-agent/test/daemon-launch.test.ts index 26cc049ef33..3a12012b31c 100644 --- a/packages/coding-agent/test/daemon-launch.test.ts +++ b/packages/coding-agent/test/daemon-launch.test.ts @@ -3,7 +3,7 @@ import { existsSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node: import { createServer, type Server, type Socket } from "node:net"; import { tmpdir } from "node:os"; import { dirname, join } from "node:path"; -import { afterEach, describe, expect, it, vi } from "vitest"; +import { afterEach, describe, expect, it } from "vitest"; import { ensureInteractiveDaemonRunning, probeDaemonVersion, @@ -292,7 +292,6 @@ describe("ensureInteractiveDaemonRunning", () => { }); it("reuses a daemon that becomes current inside the startup window", async () => { - vi.useFakeTimers(); let resolveFirstConnection = () => {}; let resolveSecondConnection = () => {}; const firstConnection = new Promise((resolve) => { @@ -311,19 +310,16 @@ describe("ensureInteractiveDaemonRunning", () => { }); cleanups.push(daemon.close); - try { - const ensuring = ensureInteractiveDaemonRunning(daemon.socketPath); - await firstConnection; - await vi.advanceTimersByTimeAsync(2000); - await secondConnection; - await expect(ensuring).resolves.toBeUndefined(); - } finally { - vi.useRealTimers(); - } + const ensuring = ensureInteractiveDaemonRunning(daemon.socketPath, undefined, { + initialHelloTimeoutMs: 10, + startupTimeoutMs: 100, + }); + await firstConnection; + await secondConnection; + await expect(ensuring).resolves.toBeUndefined(); }); it("leaves a connected unresponsive daemon running after the startup window", async () => { - vi.useFakeTimers(); const connections: Array<() => void> = []; const connected = [0, 1].map( (index) => @@ -339,19 +335,16 @@ describe("ensureInteractiveDaemonRunning", () => { }); cleanups.push(daemon.close); - try { - const ensuring = ensureInteractiveDaemonRunning(daemon.socketPath); - const rejected = expect(ensuring).rejects.toThrow(/accepted connections but did not finish startup/); - await connected[0]; - await vi.advanceTimersByTimeAsync(2000); - await connected[1]; - await vi.advanceTimersByTimeAsync(30_000); - await rejected; - expect(commands).not.toContain("list"); - expect(commands).not.toContain("shutdown"); - } finally { - vi.useRealTimers(); - } + const ensuring = ensureInteractiveDaemonRunning(daemon.socketPath, undefined, { + initialHelloTimeoutMs: 10, + startupTimeoutMs: 100, + }); + const rejected = expect(ensuring).rejects.toThrow(/accepted connections but did not finish startup/); + await connected[0]; + await connected[1]; + await rejected; + expect(commands).not.toContain("list"); + expect(commands).not.toContain("shutdown"); }); it("cancels replacement when a stale-looking daemon becomes current", async () => { diff --git a/packages/coding-agent/test/daemon-mode.test.ts b/packages/coding-agent/test/daemon-mode.test.ts index 2fe4077d067..e2705dbf53f 100644 --- a/packages/coding-agent/test/daemon-mode.test.ts +++ b/packages/coding-agent/test/daemon-mode.test.ts @@ -1837,8 +1837,11 @@ describe("daemon mode helpers", () => { const tempDir = mkdtempSync(join(tmpdir(), "pa-msg-")); const socketPath = join(tempDir, "d.sock"); let connectionCount = 0; + const sockets = new Set(); const server: Server = createServer((socket) => { connectionCount++; + sockets.add(socket); + socket.once("close", () => sockets.delete(socket)); socket.on("error", () => undefined); socket.write( `${JSON.stringify({ @@ -1893,9 +1896,14 @@ describe("daemon mode helpers", () => { } ).sendRemoteAgentSessionMessage.bind(daemon); - await expect(sendRemoteAgentSessionMessage(makeState("source"), "remote", "continue")).rejects.toThrow( - "Target session has too many pending messages", - ); + let rejection: unknown; + try { + await sendRemoteAgentSessionMessage(makeState("source"), "remote", "continue"); + } catch (error) { + rejection = error; + } + expect(rejection).toBeInstanceOf(Error); + expect((rejection as Error).message).toContain("Target session has too many pending messages"); expect(connectionCount).toBe(1); } finally { if (previousSupervisorSocket === undefined) { @@ -1903,6 +1911,7 @@ describe("daemon mode helpers", () => { } else { process.env[DAEMON_WORKER_SUPERVISOR_SOCKET_ENV] = previousSupervisorSocket; } + for (const socket of sockets) socket.destroy(); await new Promise((resolve) => server.close(() => resolve())); rmSync(tempDir, { recursive: true, force: true }); } diff --git a/packages/coding-agent/test/daemon-multiclient-bench.ts b/packages/coding-agent/test/daemon-multiclient-bench.ts index 998e4b48c1f..91a5bf1822f 100644 --- a/packages/coding-agent/test/daemon-multiclient-bench.ts +++ b/packages/coding-agent/test/daemon-multiclient-bench.ts @@ -7,10 +7,10 @@ * * Run from packages/coding-agent: * - * npx tsx test/daemon-multiclient-bench.ts - * npx tsx test/daemon-multiclient-bench.ts --session-file /path/to/session.jsonl - * npx tsx test/daemon-multiclient-bench.ts --generated-session-mib 100 - * npx tsx test/daemon-multiclient-bench.ts --generated-session-mib 500 + * bun test/daemon-multiclient-bench.ts + * bun test/daemon-multiclient-bench.ts --session-file /path/to/session.jsonl + * bun test/daemon-multiclient-bench.ts --generated-session-mib 100 + * bun test/daemon-multiclient-bench.ts --generated-session-mib 500 */ import { randomUUID } from "node:crypto"; import { createWriteStream } from "node:fs"; diff --git a/packages/coding-agent/test/daemon-ps.test.ts b/packages/coding-agent/test/daemon-ps.test.ts index 6233a9353e6..bf2dce7d09e 100644 --- a/packages/coding-agent/test/daemon-ps.test.ts +++ b/packages/coding-agent/test/daemon-ps.test.ts @@ -19,7 +19,7 @@ import { getProcessStartId } from "../src/core/session-lease.js"; import { defaultDaemonSocketDir } from "../src/modes/daemon/daemon-socket.js"; describe("worker socket classification", () => { - it.runIf(process.platform !== "win32")("recognizes only worker sockets in the default service directory", () => { + it.skipIf(process.platform === "win32")("recognizes only worker sockets in the default service directory", () => { expect(isWorkerSocketPath(join(defaultDaemonSocketDir(), "worker-abc.sock"))).toBe(true); expect(isWorkerSocketPath(join(defaultDaemonSocketDir(), "daemon.sock"))).toBe(false); expect(isWorkerSocketPath("/tmp/worker-abc.sock")).toBe(false); @@ -57,7 +57,9 @@ describe("parseSsListeners", () => { describe("parseLsofListeners", () => { it("pairs each pid with its listening unix socket paths", () => { - const stdout = ["p1234", "fu", "n/tmp/a.sock", "p5678", "n/tmp/b.sock", "n0x0 (not a path)", ""].join("\n"); + const stdout = ["p1234", "fu", "n/tmp/a.sock type=STREAM", "p5678", "n/tmp/b.sock", "n0x0 (not a path)", ""].join( + "\n", + ); expect(parseLsofListeners(stdout)).toEqual([ { pid: 1234, socketPath: "/tmp/a.sock" }, { pid: 5678, socketPath: "/tmp/b.sock" }, diff --git a/packages/coding-agent/test/daemon-supervisor-lazy-subagents.test.ts b/packages/coding-agent/test/daemon-supervisor-lazy-subagents.test.ts index becf9a98f38..7d6a0306344 100644 --- a/packages/coding-agent/test/daemon-supervisor-lazy-subagents.test.ts +++ b/packages/coding-agent/test/daemon-supervisor-lazy-subagents.test.ts @@ -19,6 +19,9 @@ interface SupervisorInternals { workers: Map; start(): Promise; cleanupSupervisorResources(): Promise; + clearIdleEvictionTimer(): void; + clearRosterWatchdogTimer(): void; + catalog: { stop?: () => Promise }; refreshWorkerSummaries(worker: WorkerFixture): Promise; findSummaryInWorker(worker: WorkerFixture, selector: string): SessionSummary | undefined; createOrReuseWorker( @@ -55,11 +58,33 @@ interface WorkerFixture { } const tempDirs: string[] = []; - -afterEach(() => { +const supervisors: SupervisorInternals[] = []; + +afterEach(async () => { + for (const supervisor of supervisors.splice(0)) { + supervisor.clearIdleEvictionTimer(); + supervisor.clearRosterWatchdogTimer(); + await supervisor.catalog.stop?.(); + } for (const directory of tempDirs.splice(0)) rmSync(directory, { recursive: true, force: true }); }); +function createSupervisor(directory: string): SupervisorInternals { + const supervisor = new DaemonSupervisor(join(directory, "daemon.sock"), { + defaultSessionConfig: { agentDir: directory, cwd: directory }, + descriptorDir: join(directory, "workers"), + }) as unknown as SupervisorInternals; + supervisors.push(supervisor); + return supervisor; +} + +function capturePromise(promise: Promise): Promise<{ value?: T; error?: unknown }> { + return promise.then( + (value) => ({ value }), + (error: unknown) => ({ error }), + ); +} + function summary(overrides: Partial & Pick): SessionSummary { return { lifecycle: "live", @@ -98,10 +123,7 @@ describe("daemon supervisor passive subagent topology", () => { it("finds a child summary by its displayed session ID suffix", () => { const directory = mkdtempSync(join(tmpdir(), "prime-supervisor-child-suffix-")); tempDirs.push(directory); - const supervisor = new DaemonSupervisor(join(directory, "daemon.sock"), { - defaultSessionConfig: { agentDir: directory, cwd: directory }, - descriptorDir: join(directory, "workers"), - }) as unknown as SupervisorInternals; + const supervisor = createSupervisor(directory); const child = summary({ id: "bbbb6666777788889999cccc", activeSessionId: "bbbb6666777788889999cccc", @@ -116,10 +138,7 @@ describe("daemon supervisor passive subagent topology", () => { it("rejects an explicit root name that collides with a saved root", async () => { const directory = mkdtempSync(join(tmpdir(), "prime-supervisor-root-name-")); tempDirs.push(directory); - const supervisor = new DaemonSupervisor(join(directory, "daemon.sock"), { - defaultSessionConfig: { agentDir: directory, cwd: directory }, - descriptorDir: join(directory, "workers"), - }) as unknown as SupervisorInternals; + const supervisor = createSupervisor(directory); const launchWorker = vi.fn(); Object.assign(supervisor, { catalog: { @@ -160,10 +179,7 @@ describe("daemon supervisor passive subagent topology", () => { if (!forkedPath) throw new Error("Missing forked session path"); const forkedInfo = await readSessionInfo(forkedPath); if (!forkedInfo) throw new Error("Missing forked session info"); - const supervisor = new DaemonSupervisor(join(directory, "daemon.sock"), { - defaultSessionConfig: { agentDir: directory, cwd: directory }, - descriptorDir: join(directory, "workers"), - }) as unknown as SupervisorInternals; + const supervisor = createSupervisor(directory); Object.assign(supervisor, { catalog: { siblings: vi.fn(async () => [forkedInfo]), @@ -193,10 +209,7 @@ describe("daemon supervisor passive subagent topology", () => { it("normalizes explicit root names before supervisor validation and launch", async () => { const directory = mkdtempSync(join(tmpdir(), "prime-supervisor-normalized-root-name-")); tempDirs.push(directory); - const supervisor = new DaemonSupervisor(join(directory, "daemon.sock"), { - defaultSessionConfig: { agentDir: directory, cwd: directory }, - descriptorDir: join(directory, "workers"), - }) as unknown as SupervisorInternals; + const supervisor = createSupervisor(directory); const launchWorker = vi.fn(); Object.assign(supervisor, { catalog: { @@ -243,10 +256,7 @@ describe("daemon supervisor passive subagent topology", () => { allMessagesText: "", }; const duplicate = { ...target, id: "duplicate", path: duplicatePath, name: "taken" }; - const supervisor = new DaemonSupervisor(join(directory, "daemon.sock"), { - defaultSessionConfig: { agentDir: directory, cwd: directory }, - descriptorDir: join(directory, "workers"), - }) as unknown as SupervisorInternals; + const supervisor = createSupervisor(directory); Object.assign(supervisor, { rlmLedgerSiblings: vi.fn(async () => [target]), catalog: { @@ -263,10 +273,7 @@ describe("daemon supervisor passive subagent topology", () => { it("retains a legacy child's parent edge when its depth is unknown", () => { const directory = mkdtempSync(join(tmpdir(), "prime-supervisor-legacy-family-")); tempDirs.push(directory); - const supervisor = new DaemonSupervisor(join(directory, "daemon.sock"), { - defaultSessionConfig: { agentDir: directory, cwd: directory }, - descriptorDir: join(directory, "workers"), - }) as unknown as SupervisorInternals; + const supervisor = createSupervisor(directory); const parentPath = join(directory, "parent.jsonl"); const child = supervisor.familyCatalogEntry( summary({ @@ -312,10 +319,7 @@ describe("daemon supervisor passive subagent topology", () => { }; const target = { ...base, id: "target", path: join(directory, "target.jsonl"), parentSessionPath, rlmDepth: 1 }; const legacy = { ...base, id: "legacy", path: join(directory, "legacy.jsonl"), parentSessionPath, name: "taken" }; - const supervisor = new DaemonSupervisor(join(directory, "daemon.sock"), { - defaultSessionConfig: { agentDir: directory, cwd: directory }, - descriptorDir: join(directory, "workers"), - }) as unknown as SupervisorInternals; + const supervisor = createSupervisor(directory); expect(() => supervisor.assertSavedSiblingNameAvailable([target, legacy], target, "taken")).toThrow( "an agent of that name already exists at depth 1 under this parent", @@ -330,10 +334,7 @@ describe("daemon supervisor passive subagent topology", () => { const siblingGate = new Promise((resolve) => { releaseSiblings = resolve; }); - const supervisor = new DaemonSupervisor(join(directory, "daemon.sock"), { - defaultSessionConfig: { agentDir: directory, cwd: directory }, - descriptorDir: join(directory, "workers"), - }) as unknown as SupervisorInternals; + const supervisor = createSupervisor(directory); const resident = worker("opened"); const launchWorker = vi.fn(async () => resident); Object.assign(supervisor, { @@ -367,10 +368,7 @@ describe("daemon supervisor passive subagent topology", () => { const launchGate = new Promise((resolve) => { releaseLaunch = resolve; }); - const supervisor = new DaemonSupervisor(join(directory, "daemon.sock"), { - defaultSessionConfig: { agentDir: directory, cwd: directory }, - descriptorDir: join(directory, "workers"), - }) as unknown as SupervisorInternals; + const supervisor = createSupervisor(directory); const resident = worker("opened"); resident.descriptor.ownerClientId = "owner"; const launchWorker = vi.fn(async () => { @@ -383,13 +381,13 @@ describe("daemon supervisor passive subagent topology", () => { const first = supervisor.createOrReuseWorker("owner", create); const sameOwner = supervisor.createOrReuseWorker("owner", create); const otherClient = supervisor.createOrReuseWorker("intruder", create); - const expectations = Promise.all([ - expect(first).resolves.toBe(resident), - expect(sameOwner).resolves.toBe(resident), - expect(otherClient).rejects.toMatchObject({ code: "session_already_active" }), - ]); + const firstResult = capturePromise(first); + const sameOwnerResult = capturePromise(sameOwner); + const otherClientResult = capturePromise(otherClient); releaseLaunch(); - await expectations; + expect(await firstResult).toEqual({ value: resident }); + expect(await sameOwnerResult).toEqual({ value: resident }); + expect((await otherClientResult).error).toHaveProperty("code", "session_already_active"); expect(launchWorker).toHaveBeenCalledOnce(); }); @@ -401,10 +399,7 @@ describe("daemon supervisor passive subagent topology", () => { const reclaimGate = new Promise((resolve) => { releaseReclaim = resolve; }); - const supervisor = new DaemonSupervisor(join(directory, "daemon.sock"), { - defaultSessionConfig: { agentDir: directory, cwd: directory }, - descriptorDir: join(directory, "workers"), - }) as unknown as SupervisorInternals; + const supervisor = createSupervisor(directory); const stale = worker("stale"); stale.descriptor.createCommand = { config: { cwd: directory }, sessionPath }; supervisor.workers.set(stale.descriptor.workerId, stale); @@ -422,13 +417,13 @@ describe("daemon supervisor passive subagent topology", () => { const first = supervisor.createOrReuseWorker("owner", create); const second = supervisor.createOrReuseWorker("owner", create); const intruder = supervisor.createOrReuseWorker("intruder", create); - const expectations = Promise.all([ - expect(first).resolves.toBe(resident), - expect(second).resolves.toBe(resident), - expect(intruder).rejects.toMatchObject({ code: "session_already_active" }), - ]); + const firstResult = capturePromise(first); + const secondResult = capturePromise(second); + const intruderResult = capturePromise(intruder); releaseReclaim(); - await expectations; + expect(await firstResult).toEqual({ value: resident }); + expect(await secondResult).toEqual({ value: resident }); + expect((await intruderResult).error).toHaveProperty("code", "session_already_active"); expect(launchWorker).toHaveBeenCalledOnce(); }); @@ -440,10 +435,7 @@ describe("daemon supervisor passive subagent topology", () => { const launchGate = new Promise((resolve) => { releaseLaunch = resolve; }); - const supervisor = new DaemonSupervisor(join(directory, "daemon.sock"), { - defaultSessionConfig: { agentDir: directory, cwd: directory }, - descriptorDir: join(directory, "workers"), - }) as unknown as SupervisorInternals; + const supervisor = createSupervisor(directory); const launchWorker = vi.fn(async () => { await launchGate; throw new Error("launch exploded"); @@ -453,12 +445,13 @@ describe("daemon supervisor passive subagent topology", () => { const create = { type: "create" as const, sessionPath, lifecycle: "client_owned" as const }; const first = supervisor.createOrReuseWorker("owner", create); const joiner = supervisor.createOrReuseWorker("intruder", create); - const expectations = Promise.all([ - expect(first).rejects.toThrow("launch exploded"), - expect(joiner).rejects.toThrow("launch exploded"), - ]); + const firstResult = capturePromise(first); + const joinerResult = capturePromise(joiner); releaseLaunch(); - await expectations; + expect((await firstResult).error).toBeInstanceOf(Error); + expect((await firstResult).error).toHaveProperty("message", "launch exploded"); + expect((await joinerResult).error).toBeInstanceOf(Error); + expect((await joinerResult).error).toHaveProperty("message", "launch exploded"); }); it("uses injective structural session name reservation keys", () => { @@ -497,10 +490,7 @@ describe("daemon supervisor passive subagent topology", () => { }); const secondWorker = worker("second", [secondSummary]); secondWorker.client.request.mockResolvedValue(success(undefined, "rename", secondSummary)); - const supervisor = new DaemonSupervisor(join(directory, "daemon.sock"), { - defaultSessionConfig: { agentDir: directory, cwd: directory }, - descriptorDir: join(directory, "workers"), - }) as unknown as SupervisorInternals; + const supervisor = createSupervisor(directory); supervisor.workers.set("first", firstWorker); supervisor.workers.set("second", secondWorker); seedSupervisorRoster(supervisor, firstWorker, secondWorker); @@ -542,10 +532,7 @@ describe("daemon supervisor passive subagent topology", () => { const ownedWorker = worker("owned", [ownedSummary]); ownedWorker.descriptor.ownerClientId = "interactive-client"; ownedWorker.client.request.mockResolvedValue(success(undefined, "set_session_name")); - const supervisor = new DaemonSupervisor(join(directory, "daemon.sock"), { - defaultSessionConfig: { agentDir: directory, cwd: directory }, - descriptorDir: join(directory, "workers"), - }) as unknown as SupervisorInternals; + const supervisor = createSupervisor(directory); supervisor.workers.set("owned", ownedWorker); seedSupervisorRoster(supervisor, ownedWorker); Object.assign(supervisor, { catalog: { list: vi.fn(async () => []) } }); @@ -614,10 +601,7 @@ describe("daemon supervisor passive subagent topology", () => { }); const secondWorker = worker("second", [secondSummary]); secondWorker.client.request.mockResolvedValue(success(undefined, "rename_saved_session", secondSummary)); - const supervisor = new DaemonSupervisor(join(directory, "daemon.sock"), { - defaultSessionConfig: { agentDir: directory, cwd: directory }, - descriptorDir: join(directory, "workers"), - }) as unknown as SupervisorInternals; + const supervisor = createSupervisor(directory); supervisor.workers.set("first", firstWorker); supervisor.workers.set("second", secondWorker); seedSupervisorRoster(supervisor, firstWorker, secondWorker); @@ -677,10 +661,7 @@ describe("daemon supervisor passive subagent topology", () => { releaseRename = resolve; }); const rename = vi.fn(async () => renameGate); - const supervisor = new DaemonSupervisor(join(directory, "daemon.sock"), { - defaultSessionConfig: { agentDir: directory, cwd: directory }, - descriptorDir: join(directory, "workers"), - }) as unknown as SupervisorInternals; + const supervisor = createSupervisor(directory); Object.assign(supervisor, { rlmLedgerSiblings: vi.fn(async () => saved), rlmSpawnLedger: vi.fn(() => ({ appendRenameByChildPath: vi.fn(async () => {}) })), @@ -734,10 +715,7 @@ describe("daemon supervisor passive subagent topology", () => { await launchGate; return launched; }); - const supervisor = new DaemonSupervisor(join(directory, "daemon.sock"), { - defaultSessionConfig: { agentDir: directory, cwd: directory }, - descriptorDir: join(directory, "workers"), - }) as unknown as SupervisorInternals; + const supervisor = createSupervisor(directory); Object.assign(supervisor, { rlmLedgerSiblings: vi.fn(async (path: string) => [path === firstChild.path ? firstChild : secondChild]), catalog: { diff --git a/packages/coding-agent/test/daemon-supervisor-monitor.test.ts b/packages/coding-agent/test/daemon-supervisor-monitor.test.ts index 8d6954ffdee..e017e6f1950 100644 --- a/packages/coding-agent/test/daemon-supervisor-monitor.test.ts +++ b/packages/coding-agent/test/daemon-supervisor-monitor.test.ts @@ -1,6 +1,7 @@ import type { ChildProcess, SpawnOptions } from "node:child_process"; import { EventEmitter } from "node:events"; import { existsSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, rmSync, writeFileSync } from "node:fs"; +import { createRequire } from "node:module"; import type { Socket } from "node:net"; import { tmpdir } from "node:os"; import { join } from "node:path"; @@ -50,96 +51,92 @@ const workerLaunchTestState = vi.hoisted(() => ({ spawned: [] as Array<{ child: ChildProcess; args: readonly string[] }>, })); -vi.mock("node:child_process", async (importOriginal) => { - const actual = (await importOriginal()) as Record & { - spawn(command: string, args: readonly string[], options: SpawnOptions): ChildProcess; - }; - return { - ...actual, - spawn(command: string, args: readonly string[], options: SpawnOptions): ChildProcess { - const failureCode = workerLaunchTestState.spawnFailureCode; - if (failureCode) { - // Node's failed-spawn shape: no pid, stdio undefined, "error" then "close". - const failing = Object.assign(new EventEmitter(), { - pid: undefined, - stdio: undefined, - stderr: undefined, - unref: () => {}, - }) as unknown as ChildProcess; - process.nextTick(() => { - failing.emit( - "error", - Object.assign(new Error(`spawn ${command} ${failureCode}`), { code: failureCode }), - ); - failing.emit("close", null, null); - }); - return failing; - } - const child = actual.spawn(command, args, options); - if (workerLaunchTestState.capture) { - workerLaunchTestState.spawned.push({ child, args }); - } - return child; - }, - }; -}); +const __childProcess = createRequire(import.meta.url)("node:child_process") as Record & { + spawn(command: string, args: readonly string[], options: SpawnOptions): ChildProcess; +}; +const spawnChildProcess = __childProcess.spawn; +vi.mock("node:child_process", () => ({ + ...__childProcess, + spawn(command: string, args: readonly string[], options: SpawnOptions): ChildProcess { + const failureCode = workerLaunchTestState.spawnFailureCode; + if (failureCode) { + // Node's failed-spawn shape: no pid, stdio undefined, "error" then "close". + const failing = Object.assign(new EventEmitter(), { + pid: undefined, + stdio: undefined, + stderr: undefined, + unref: () => {}, + }) as unknown as ChildProcess; + queueMicrotask(() => { + failing.emit("error", Object.assign(new Error(`spawn ${command} ${failureCode}`), { code: failureCode })); + failing.emit("close", null, null); + }); + return failing; + } + const child = spawnChildProcess(command, args, options); + if (workerLaunchTestState.capture) { + workerLaunchTestState.spawned.push({ child, args }); + } + return child; + }, +})); -vi.mock("../src/cli/subprocess-launch.js", async (importOriginal) => { - const actual = (await importOriginal()) as Record; - return { - ...actual, - createCliSubprocessLaunchSpec(args: readonly string[]) { - if (!workerLaunchTestState.capture) { - return (actual.createCliSubprocessLaunchSpec as (args: readonly string[]) => unknown)(args); - } - if (workerLaunchTestState.fixtureMode === "rollback-gate") { - const markerPath = JSON.stringify(workerLaunchTestState.gateMarkerPath); - const commitMarker = JSON.stringify(DAEMON_WORKER_STARTUP_GATE_COMMIT); - return { - command: process.execPath, - args: [ - "--eval", - `const fs = require("node:fs"); const marker = fs.readFileSync(3, "utf8"); if (marker === ${commitMarker}) { fs.writeFileSync(${markerPath}, marker); setInterval(() => {}, 1000); }`, - "--", - ...args, - ], - }; - } - if (workerLaunchTestState.fixtureMode === "close-gate") { - return { - command: process.execPath, - args: ["--eval", 'require("node:fs").closeSync(3)'], - }; - } - if (workerLaunchTestState.fixtureMode === "successful-gate") { - const markerPath = JSON.stringify(workerLaunchTestState.gateMarkerPath); - return { - command: process.execPath, - args: [ - "--eval", - `const fs = require("node:fs"); const marker = fs.readFileSync(3, "utf8"); fs.writeFileSync(${markerPath}, marker); setInterval(() => {}, 1000);`, - ], - }; - } +const __subprocessLaunch = createRequire(import.meta.url)("../src/cli/subprocess-launch.js") as Record; +const createSubprocessLaunchSpec = __subprocessLaunch.createCliSubprocessLaunchSpec as ( + args: readonly string[], +) => unknown; +vi.mock("../src/cli/subprocess-launch.js", () => ({ + ...__subprocessLaunch, + createCliSubprocessLaunchSpec(args: readonly string[]) { + if (!workerLaunchTestState.capture) { + return createSubprocessLaunchSpec(args); + } + if (workerLaunchTestState.fixtureMode === "rollback-gate") { + const markerPath = JSON.stringify(workerLaunchTestState.gateMarkerPath); + const commitMarker = JSON.stringify(DAEMON_WORKER_STARTUP_GATE_COMMIT); return { command: process.execPath, - args: [workerLaunchTestState.tsxCliPath, workerLaunchTestState.cliEntrypoint, ...args], + args: [ + "--eval", + `const fs = require("node:fs"); const marker = fs.readFileSync(3, "utf8"); if (marker === ${commitMarker}) { fs.writeFileSync(${markerPath}, marker); setInterval(() => {}, 1000); }`, + "--", + ...args, + ], }; - }, - }; -}); + } + if (workerLaunchTestState.fixtureMode === "close-gate") { + return { + command: process.execPath, + args: ["--eval", 'require("node:fs").closeSync(3); process.reallyExit(0)'], + }; + } + if (workerLaunchTestState.fixtureMode === "successful-gate") { + const markerPath = JSON.stringify(workerLaunchTestState.gateMarkerPath); + return { + command: process.execPath, + args: [ + "--eval", + `const fs = require("node:fs"); const marker = fs.readFileSync(3, "utf8"); fs.writeFileSync(${markerPath}, marker); setInterval(() => {}, 1000);`, + ], + }; + } + return { + command: process.execPath, + args: [workerLaunchTestState.tsxCliPath, workerLaunchTestState.cliEntrypoint, ...args], + }; + }, +})); -vi.mock("../src/core/session-lease.js", async (importOriginal) => { - const actual = (await importOriginal()) as Record & { - getProcessStartId(pid: number): string | undefined; - }; - return { - ...actual, - getProcessStartId(pid: number): string | undefined { - return workerLaunchTestState.forceMissingProcessStartId ? undefined : actual.getProcessStartId(pid); - }, - }; -}); +const __sessionLease = createRequire(import.meta.url)("../src/core/session-lease.js") as Record & { + getProcessStartId(pid: number): string | undefined; +}; +const readProcessStartId = __sessionLease.getProcessStartId; +vi.mock("../src/core/session-lease.js", () => ({ + ...__sessionLease, + getProcessStartId(pid: number): string | undefined { + return workerLaunchTestState.forceMissingProcessStartId ? undefined : readProcessStartId(pid); + }, +})); const supervisorRegistryDirEnv = "PRIME_AGENT_INTERNAL_DAEMON_SUPERVISOR_REGISTRY_DIR"; const previousSupervisorRegistryDir = process.env[supervisorRegistryDirEnv]; @@ -699,9 +696,12 @@ describe("daemon worker supervisor monitoring", () => { launchWorker(command: { type: "create"; config: { cwd: string; agentDir: string } }): Promise; }; - await expect(supervisor.launchWorker({ type: "create", config: { cwd: root, agentDir: root } })).rejects.toThrow( - /EMFILE.*resident session workers.*ulimit -n/s, - ); + const emfileFailure = await supervisor + .launchWorker({ type: "create", config: { cwd: root, agentDir: root } }) + .then(() => undefined) + .catch((error: Error) => error); + expect(emfileFailure).toBeInstanceOf(Error); + expect(emfileFailure?.message).toMatch(/EMFILE.*resident session workers.*ulimit -n/s); expect(workers.size).toBe(0); workerLaunchTestState.spawnFailureCode = "ENOENT"; diff --git a/packages/coding-agent/test/daemon-supervisor-process.test.ts b/packages/coding-agent/test/daemon-supervisor-process.test.ts index 765a9edc26e..7fb14bc803a 100644 --- a/packages/coding-agent/test/daemon-supervisor-process.test.ts +++ b/packages/coding-agent/test/daemon-supervisor-process.test.ts @@ -23,9 +23,9 @@ import { isDaemonWorkerFrameHeader, } from "../src/modes/daemon/daemon-worker-protocol.js"; import { encodePrivateFrame, PrivateFrameDecoder } from "../src/modes/session-worker/private-framing.js"; +import { isTestTagEnabled } from "./test-tags.js"; const cliPath = resolve(__dirname, "../src/cli.ts"); -const tsxPath = resolve(__dirname, "../../../node_modules/tsx/dist/cli.mjs"); const blockingProcessPath = resolve(__dirname, "fixtures/blocking-process.mjs"); const tempDirs: string[] = []; const children = new Set(); @@ -33,6 +33,7 @@ const workerPids = new Set(); const daemonSockets = new Set(); const childDiagnostics = new WeakMap(); const PROCESS_STRESS_WORKERS = Number.parseInt(process.env.PRIME_AGENT_STRESS_WORKERS ?? "10", 10); +const processStressIt = isTestTagEnabled("process-stress") ? it : it.skip; afterEach(async () => { for (const socketPath of daemonSockets) { @@ -94,7 +95,7 @@ function spawnSupervisor( daemonSockets.add(socketPath); const child = spawn( process.execPath, - [tsxPath, cliPath, "--mode", "daemon", "--daemon-socket", socketPath, "--offline", ...extraArgs], + [cliPath, "--mode", "daemon", "--daemon-socket", socketPath, "--offline", ...extraArgs], { cwd, env: { @@ -102,7 +103,6 @@ function spawnSupervisor( ...extraEnv, [ENV_AGENT_DIR]: agentDir, PI_OFFLINE: "1", - TSX_TSCONFIG_PATH: resolve(__dirname, "../../../tsconfig.json"), }, stdio: ["ignore", "pipe", "pipe"], }, @@ -697,7 +697,7 @@ describe("daemon supervisor resident workers", () => { type: "create", lifecycle: "client_owned", noSession: true, - launchEnv: { TSX_TSCONFIG_PATH: resolve(__dirname, "../../../tsconfig.json") }, + launchEnv: {}, config: { cwd: projectDir, agentDir, noTools: true, noExtensions: true }, }); if (!created.success) { @@ -782,7 +782,7 @@ describe("daemon supervisor resident workers", () => { await waitForSocketGone(socketPath); }, 30_000); - it("cancels an archived session heartbeat without spawning a worker", { tags: ["process-stress"] }, async () => { + processStressIt("cancels an archived session heartbeat without spawning a worker", async () => { const root = tempDir(); const agentDir = join(root, "agent"); const projectDir = join(root, "project"); @@ -835,9 +835,7 @@ describe("daemon supervisor resident workers", () => { await waitForSocketGone(socketPath); }); - it("cancels an orphan heartbeat instead of recreating a descriptorless active session", { - tags: ["process-stress"], - }, async () => { + processStressIt("cancels an orphan heartbeat instead of recreating a descriptorless active session", async () => { const root = tempDir(); const agentDir = join(root, "agent"); const projectDir = join(root, "project"); @@ -912,7 +910,7 @@ describe("daemon supervisor resident workers", () => { await waitForSocketGone(socketPath); }); - it("survives a worker process spawn error", { tags: ["process-stress"] }, async () => { + processStressIt("survives a worker process spawn error", async () => { const root = tempDir(); const agentDir = join(root, "agent"); const projectDir = join(root, "project"); @@ -1008,233 +1006,244 @@ describe("daemon supervisor resident workers", () => { await waitForSocketGone(socketPath); }, 30_000); - it("finalizes a timed-out worker stop by force-stopping the process and removing its registration", { - tags: ["process-stress"], - timeout: 45_000, - }, async () => { - const root = tempDir(); - const agentDir = join(root, "agent"); - const projectDir = join(root, "project"); - const sessionDir = join(agentDir, "sessions"); - const socketPath = join( - tmpdir(), - `prime-supervisor-stop-finalize-${process.pid}-${randomUUID().slice(0, 8)}.sock`, - ); - mkdirSync(projectDir, { recursive: true }); - const sessionManager = SessionManager.create(projectDir, sessionDir); - sessionManager.appendMessage({ role: "user", content: "finalize me", timestamp: 1 }); - const sessionFile = sessionManager.getSessionFile(); - if (!sessionFile) { - throw new Error("Fixture session did not persist"); - } - - const supervisor = spawnSupervisor(agentDir, socketPath, projectDir); - const client = await connectEventually(socketPath, supervisor); - const created = await client.request({ - type: "create", - sessionPath: sessionFile, - lifecycle: "client_owned", - config: { cwd: projectDir, agentDir, sessionDir, noTools: true, noExtensions: true }, - }); - if (!created.success) { - throw new Error(created.error); - } - const summary = requireSummary(created.data); - if (!summary.workerPid) { - throw new Error("Resident worker did not expose its pid"); - } - workerPids.add(summary.workerPid); - const activeSessionId = summary.activeSessionId ?? summary.id; - - // A suspended worker cannot exit within the stop deadline, so the stop - // times out and used to leave a tombstoned registration behind forever. - process.kill(summary.workerPid, "SIGSTOP"); - const stopResult = await client.request({ type: "complete_owned_session", activeSessionId }, 30_000); - expect(stopResult).toMatchObject({ - success: false, - error: expect.stringContaining("did not stop"), - }); - const tombstone = readWorkerDescriptor(agentDir); - expect(tombstone.stopRequestedAt).toEqual(expect.any(String)); - - // The supervisor finishes the interrupted stop on its own: it escalates - // to SIGKILL, waits for the process to die, and removes the registration. - await waitForProcessGone(summary.workerPid); - workerPids.delete(summary.workerPid); - await waitForCondition( - () => countWorkerDescriptors(agentDir) === 0, - "Timed-out worker stop was not finalized", - 20_000, - ); - - await client.request({ type: "shutdown" }); - client.close(); - await waitForSocketGone(socketPath); - }); - - it("resumes a saved session immediately after a worker stop fails and the process dies", { - tags: ["process-stress"], - timeout: 45_000, - }, async () => { - const root = tempDir(); - const agentDir = join(root, "agent"); - const projectDir = join(root, "project"); - const sessionDir = join(agentDir, "sessions"); - const socketPath = join(tmpdir(), `prime-supervisor-resume-heal-${process.pid}-${randomUUID().slice(0, 8)}.sock`); - mkdirSync(projectDir, { recursive: true }); - const sessionManager = SessionManager.create(projectDir, sessionDir); - sessionManager.appendMessage({ role: "user", content: "resume me", timestamp: 1 }); - const sessionFile = sessionManager.getSessionFile(); - if (!sessionFile) { - throw new Error("Fixture session did not persist"); - } - - const supervisor = spawnSupervisor(agentDir, socketPath, projectDir); - const client = await connectEventually(socketPath, supervisor); - const created = await client.request({ - type: "create", - sessionPath: sessionFile, - lifecycle: "client_owned", - config: { cwd: projectDir, agentDir, sessionDir, noTools: true, noExtensions: true }, - }); - if (!created.success) { - throw new Error(created.error); - } - const summary = requireSummary(created.data); - if (!summary.workerPid) { - throw new Error("Resident worker did not expose its pid"); - } - workerPids.add(summary.workerPid); - const activeSessionId = summary.activeSessionId ?? summary.id; + processStressIt( + "finalizes a timed-out worker stop by force-stopping the process and removing its registration", + { timeout: 45_000 }, + async () => { + const root = tempDir(); + const agentDir = join(root, "agent"); + const projectDir = join(root, "project"); + const sessionDir = join(agentDir, "sessions"); + const socketPath = join( + tmpdir(), + `prime-supervisor-stop-finalize-${process.pid}-${randomUUID().slice(0, 8)}.sock`, + ); + mkdirSync(projectDir, { recursive: true }); + const sessionManager = SessionManager.create(projectDir, sessionDir); + sessionManager.appendMessage({ role: "user", content: "finalize me", timestamp: 1 }); + const sessionFile = sessionManager.getSessionFile(); + if (!sessionFile) { + throw new Error("Fixture session did not persist"); + } - // A suspended worker forces the stop past its deadline, leaving a - // tombstoned registration for a process that dies moments later. - process.kill(summary.workerPid, "SIGSTOP"); - const stopResult = await client.request({ type: "complete_owned_session", activeSessionId }, 30_000); - expect(stopResult).toMatchObject({ - success: false, - error: expect.stringContaining("did not stop"), - }); - process.kill(summary.workerPid, "SIGKILL"); - await waitForProcessGone(summary.workerPid); - workerPids.delete(summary.workerPid); + const supervisor = spawnSupervisor(agentDir, socketPath, projectDir); + const client = await connectEventually(socketPath, supervisor); + const created = await client.request({ + type: "create", + sessionPath: sessionFile, + lifecycle: "client_owned", + config: { cwd: projectDir, agentDir, sessionDir, noTools: true, noExtensions: true }, + }); + if (!created.success) { + throw new Error(created.error); + } + const summary = requireSummary(created.data); + if (!summary.workerPid) { + throw new Error("Resident worker did not expose its pid"); + } + workerPids.add(summary.workerPid); + const activeSessionId = summary.activeSessionId ?? summary.id; + + // A suspended worker cannot exit within the stop deadline, so the stop + // times out and used to leave a tombstoned registration behind forever. + process.kill(summary.workerPid, "SIGSTOP"); + const stopResult = await client.request({ type: "complete_owned_session", activeSessionId }, 30_000); + expect(stopResult).toMatchObject({ + success: false, + error: expect.stringContaining("did not stop"), + }); + const tombstone = readWorkerDescriptor(agentDir); + expect(tombstone.stopRequestedAt).toEqual(expect.any(String)); + + // The supervisor finishes the interrupted stop on its own: it escalates + // to SIGKILL, waits for the process to die, and removes the registration. + await waitForProcessGone(summary.workerPid); + workerPids.delete(summary.workerPid); + await waitForCondition( + () => countWorkerDescriptors(agentDir) === 0, + "Timed-out worker stop was not finalized", + 20_000, + ); - // Resuming the saved transcript must not be blocked by the stale - // registration. Whichever cleanup wins the race — the background stop - // finalizer or the resume-time reclaim (each covered deterministically - // by unit tests) — the user-visible guarantee is the same: the resume - // below must succeed with a fresh worker. - const resumed = await client.request({ - type: "create", - sessionPath: sessionFile, - config: { cwd: projectDir, agentDir, sessionDir, noTools: true, noExtensions: true }, - }); - expect(resumed.success).toBe(true); - const resumedSummary = requireSummary(resumed.success ? resumed.data : undefined); - expect(resumedSummary.sessionId).toBe(summary.sessionId); - expect(resumedSummary.workerPid).not.toBe(summary.workerPid); - expect(resumedSummary.workerState).toBe("ready"); - if (resumedSummary.workerPid) { - workerPids.add(resumedSummary.workerPid); - } + await client.request({ type: "shutdown" }); + client.close(); + await waitForSocketGone(socketPath); + }, + ); - const attached = await client.request({ - type: "attach", - activeSessionId: resumedSummary.activeSessionId ?? resumedSummary.id, - }); - expect(attached.success).toBe(true); + processStressIt( + "resumes a saved session immediately after a worker stop fails and the process dies", + { timeout: 45_000 }, + async () => { + const root = tempDir(); + const agentDir = join(root, "agent"); + const projectDir = join(root, "project"); + const sessionDir = join(agentDir, "sessions"); + const socketPath = join( + tmpdir(), + `prime-supervisor-resume-heal-${process.pid}-${randomUUID().slice(0, 8)}.sock`, + ); + mkdirSync(projectDir, { recursive: true }); + const sessionManager = SessionManager.create(projectDir, sessionDir); + sessionManager.appendMessage({ role: "user", content: "resume me", timestamp: 1 }); + const sessionFile = sessionManager.getSessionFile(); + if (!sessionFile) { + throw new Error("Fixture session did not persist"); + } - await client.request({ type: "shutdown" }); - client.close(); - await waitForSocketGone(socketPath); - if (resumedSummary.workerPid) { - await waitForProcessGone(resumedSummary.workerPid); - workerPids.delete(resumedSummary.workerPid); - } - }); + const supervisor = spawnSupervisor(agentDir, socketPath, projectDir); + const client = await connectEventually(socketPath, supervisor); + const created = await client.request({ + type: "create", + sessionPath: sessionFile, + lifecycle: "client_owned", + config: { cwd: projectDir, agentDir, sessionDir, noTools: true, noExtensions: true }, + }); + if (!created.success) { + throw new Error(created.error); + } + const summary = requireSummary(created.data); + if (!summary.workerPid) { + throw new Error("Resident worker did not expose its pid"); + } + workerPids.add(summary.workerPid); + const activeSessionId = summary.activeSessionId ?? summary.id; + + // A suspended worker forces the stop past its deadline, leaving a + // tombstoned registration for a process that dies moments later. + process.kill(summary.workerPid, "SIGSTOP"); + const stopResult = await client.request({ type: "complete_owned_session", activeSessionId }, 30_000); + expect(stopResult).toMatchObject({ + success: false, + error: expect.stringContaining("did not stop"), + }); + process.kill(summary.workerPid, "SIGKILL"); + await waitForProcessGone(summary.workerPid); + workerPids.delete(summary.workerPid); + + // Resuming the saved transcript must not be blocked by the stale + // registration. Whichever cleanup wins the race — the background stop + // finalizer or the resume-time reclaim (each covered deterministically + // by unit tests) — the user-visible guarantee is the same: the resume + // below must succeed with a fresh worker. + const resumed = await client.request({ + type: "create", + sessionPath: sessionFile, + config: { cwd: projectDir, agentDir, sessionDir, noTools: true, noExtensions: true }, + }); + expect(resumed.success).toBe(true); + const resumedSummary = requireSummary(resumed.success ? resumed.data : undefined); + expect(resumedSummary.sessionId).toBe(summary.sessionId); + expect(resumedSummary.workerPid).not.toBe(summary.workerPid); + expect(resumedSummary.workerState).toBe("ready"); + if (resumedSummary.workerPid) { + workerPids.add(resumedSummary.workerPid); + } - it("does not resurrect an intentionally stopped root when the supervisor dies during kill", { - tags: ["process-stress"], - timeout: 30_000, - }, async () => { - const root = tempDir(); - const agentDir = join(root, "agent"); - const projectDir = join(root, "project"); - const sessionDir = join(agentDir, "sessions"); - const socketPath = join(tmpdir(), `prime-supervisor-stop-race-${process.pid}-${randomUUID().slice(0, 8)}.sock`); - mkdirSync(projectDir, { recursive: true }); - const sessionManager = SessionManager.create(projectDir, sessionDir); - sessionManager.appendMessage({ role: "user", content: "stop me", timestamp: 1 }); - sessionManager.appendSessionState({ status: "active" }); - const sessionFile = sessionManager.getSessionFile(); - if (!sessionFile) { - throw new Error("Fixture session did not persist"); - } + const attached = await client.request({ + type: "attach", + activeSessionId: resumedSummary.activeSessionId ?? resumedSummary.id, + }); + expect(attached.success).toBe(true); - const firstSupervisor = spawnSupervisor(agentDir, socketPath, projectDir); - const client = await connectEventually(socketPath, firstSupervisor); - const firstSupervisorPid = client.hello?.supervisorPid; - if (!firstSupervisorPid) { - throw new Error("Daemon hello did not expose its supervisor pid"); - } - const created = await client.request({ - type: "create", - sessionPath: sessionFile, - config: { cwd: projectDir, agentDir, sessionDir, noTools: true, noExtensions: true }, - }); - if (!created.success) { - throw new Error(created.error); - } - const summary = requireSummary(created.data); - if (!summary.workerPid) { - throw new Error("Resident worker did not expose its pid"); - } - workerPids.add(summary.workerPid); - const activeSessionId = summary.activeSessionId ?? summary.id; - const heartbeatResponse = await client.request({ - type: "heartbeat_set", - activeSessionId, - schedule: "every 1h", - prompt: "continue old work", - }); - if (!heartbeatResponse.success || !heartbeatResponse.data || typeof heartbeatResponse.data !== "object") { - throw new Error(heartbeatResponse.success ? "Heartbeat response was missing data" : heartbeatResponse.error); - } - const heartbeat = (heartbeatResponse.data as { heartbeat: { id: string } }).heartbeat; - const cronStore = AgentCronJobStore.forSessionArtifacts(); - cronStore.registerSessionArtifact(summary.sessionId, sessionManager.getSessionArtifactDir()!); - process.kill(summary.workerPid, "SIGSTOP"); - const killResult = client.request({ type: "kill", activeSessionId }).catch((error: unknown) => error); - const tombstone = await waitForWorkerStopTombstone(agentDir); - expect(tombstone.stopRequestedAt).toEqual(expect.any(String)); - expect(tombstone.archiveOnStop).toBe(true); + await client.request({ type: "shutdown" }); + client.close(); + await waitForSocketGone(socketPath); + if (resumedSummary.workerPid) { + await waitForProcessGone(resumedSummary.workerPid); + workerPids.delete(resumedSummary.workerPid); + } + }, + ); - process.kill(firstSupervisorPid, "SIGKILL"); - await waitForExit(firstSupervisor); - children.delete(firstSupervisor); - client.close(); - await expect(killResult).resolves.toBeInstanceOf(Error); + processStressIt( + "does not resurrect an intentionally stopped root when the supervisor dies during kill", + { timeout: 30_000 }, + async () => { + const root = tempDir(); + const agentDir = join(root, "agent"); + const projectDir = join(root, "project"); + const sessionDir = join(agentDir, "sessions"); + const socketPath = join( + tmpdir(), + `prime-supervisor-stop-race-${process.pid}-${randomUUID().slice(0, 8)}.sock`, + ); + mkdirSync(projectDir, { recursive: true }); + const sessionManager = SessionManager.create(projectDir, sessionDir); + sessionManager.appendMessage({ role: "user", content: "stop me", timestamp: 1 }); + sessionManager.appendSessionState({ status: "active" }); + const sessionFile = sessionManager.getSessionFile(); + if (!sessionFile) { + throw new Error("Fixture session did not persist"); + } - const replacementSupervisor = spawnSupervisor(agentDir, socketPath, projectDir); - const replacementClient = await connectEventually(socketPath, replacementSupervisor); - const listed = await replacementClient.request({ type: "list" }); - expect(listed.success).toBe(true); - const sessions = requireSessionList(listed.success ? listed.data : undefined); - expect(sessions.filter((session) => session.activeSessionId || session.workerPid)).toEqual([]); - await waitForProcessGone(summary.workerPid); - workerPids.delete(summary.workerPid); - await waitForCondition( - () => countWorkerDescriptors(agentDir) === 0, - "Intentional worker stop descriptor was not removed", - ); - expect(countWorkerDescriptors(agentDir)).toBe(0); - expect((await readSessionInfo(sessionFile))?.state).toEqual({ status: "archived" }); - expect(cronStore.list().find((job) => job.id === heartbeat.id)).toMatchObject({ status: "cancelled" }); + const firstSupervisor = spawnSupervisor(agentDir, socketPath, projectDir); + const client = await connectEventually(socketPath, firstSupervisor); + const firstSupervisorPid = client.hello?.supervisorPid; + if (!firstSupervisorPid) { + throw new Error("Daemon hello did not expose its supervisor pid"); + } + const created = await client.request({ + type: "create", + sessionPath: sessionFile, + config: { cwd: projectDir, agentDir, sessionDir, noTools: true, noExtensions: true }, + }); + if (!created.success) { + throw new Error(created.error); + } + const summary = requireSummary(created.data); + if (!summary.workerPid) { + throw new Error("Resident worker did not expose its pid"); + } + workerPids.add(summary.workerPid); + const activeSessionId = summary.activeSessionId ?? summary.id; + const heartbeatResponse = await client.request({ + type: "heartbeat_set", + activeSessionId, + schedule: "every 1h", + prompt: "continue old work", + }); + if (!heartbeatResponse.success || !heartbeatResponse.data || typeof heartbeatResponse.data !== "object") { + throw new Error( + heartbeatResponse.success ? "Heartbeat response was missing data" : heartbeatResponse.error, + ); + } + const heartbeat = (heartbeatResponse.data as { heartbeat: { id: string } }).heartbeat; + const cronStore = AgentCronJobStore.forSessionArtifacts(); + cronStore.registerSessionArtifact(summary.sessionId, sessionManager.getSessionArtifactDir()!); + process.kill(summary.workerPid, "SIGSTOP"); + const killResult = client.request({ type: "kill", activeSessionId }).catch((error: unknown) => error); + const tombstone = await waitForWorkerStopTombstone(agentDir); + expect(tombstone.stopRequestedAt).toEqual(expect.any(String)); + expect(tombstone.archiveOnStop).toBe(true); + + process.kill(firstSupervisorPid, "SIGKILL"); + await waitForExit(firstSupervisor); + children.delete(firstSupervisor); + client.close(); + await expect(killResult).resolves.toBeInstanceOf(Error); + + const replacementSupervisor = spawnSupervisor(agentDir, socketPath, projectDir); + const replacementClient = await connectEventually(socketPath, replacementSupervisor); + const listed = await replacementClient.request({ type: "list" }); + expect(listed.success).toBe(true); + const sessions = requireSessionList(listed.success ? listed.data : undefined); + expect(sessions.filter((session) => session.activeSessionId || session.workerPid)).toEqual([]); + await waitForProcessGone(summary.workerPid); + workerPids.delete(summary.workerPid); + await waitForCondition( + () => countWorkerDescriptors(agentDir) === 0, + "Intentional worker stop descriptor was not removed", + ); + expect(countWorkerDescriptors(agentDir)).toBe(0); + expect((await readSessionInfo(sessionFile))?.state).toEqual({ status: "archived" }); + expect(cronStore.list().find((job) => job.id === heartbeat.id)).toMatchObject({ status: "cancelled" }); - await replacementClient.request({ type: "shutdown" }); - replacementClient.close(); - await waitForSocketGone(socketPath); - }); + await replacementClient.request({ type: "shutdown" }); + replacementClient.close(); + await waitForSocketGone(socketPath); + }, + ); it("hosts and adopts isolated worker processes", async () => { const root = tempDir(); @@ -1307,142 +1316,146 @@ describe("daemon supervisor resident workers", () => { ); }, 60_000); - it("hosts resident roots in isolated worker processes without a session cap", { - tags: ["process-stress"], - timeout: 180_000, - }, async () => { - const root = tempDir(); - const agentDir = join(root, "agent"); - const projectDir = join(root, "project"); - const sessionDir = join(agentDir, "sessions"); - const socketPath = join(tmpdir(), `prime-supervisor-many-roots-${process.pid}-${randomUUID().slice(0, 8)}.sock`); - mkdirSync(projectDir, { recursive: true }); - const sessionFiles = Array.from({ length: PROCESS_STRESS_WORKERS }, (_, index) => { - const manager = SessionManager.create(projectDir, sessionDir); - manager.appendMessage({ role: "user", content: `root ${index}`, timestamp: index + 1 }); - const sessionFile = manager.getSessionFile(); - if (!sessionFile) { - throw new Error("Fixture session did not persist"); - } - return sessionFile; - }); + processStressIt( + "hosts resident roots in isolated worker processes without a session cap", + { timeout: 180_000 }, + async () => { + const root = tempDir(); + const agentDir = join(root, "agent"); + const projectDir = join(root, "project"); + const sessionDir = join(agentDir, "sessions"); + const socketPath = join( + tmpdir(), + `prime-supervisor-many-roots-${process.pid}-${randomUUID().slice(0, 8)}.sock`, + ); + mkdirSync(projectDir, { recursive: true }); + const sessionFiles = Array.from({ length: PROCESS_STRESS_WORKERS }, (_, index) => { + const manager = SessionManager.create(projectDir, sessionDir); + manager.appendMessage({ role: "user", content: `root ${index}`, timestamp: index + 1 }); + const sessionFile = manager.getSessionFile(); + if (!sessionFile) { + throw new Error("Fixture session did not persist"); + } + return sessionFile; + }); - const supervisor = spawnSupervisor(agentDir, socketPath, projectDir); - const client = await connectEventually(socketPath, supervisor); - const externalLease = acquireSessionLease(sessionFiles[0], agentDir, { - [SESSION_LEASES_ENABLED_ENV]: "1", - [SESSION_LEASE_OWNER_ID_ENV]: "external-owner", - }); - const conflict = await client.request({ - type: "create", - sessionPath: sessionFiles[0], - config: { cwd: projectDir, agentDir, sessionDir, noTools: true, noExtensions: true }, - }); - expect(conflict).toMatchObject({ - success: false, - errorInfo: { code: "session_already_active", activeSessionId: "external-owner" }, - }); - const emptyAfterConflict = await client.request({ type: "list" }); - expect(requireSessionList(emptyAfterConflict.success ? emptyAfterConflict.data : undefined)).toHaveLength(0); - externalLease?.release(); - const created = await Promise.all( - sessionFiles.map((sessionPath) => - client.request({ - type: "create", - sessionPath, - config: { cwd: projectDir, agentDir, sessionDir, noTools: true, noExtensions: true }, - }), - ), - ); - const summaries = created.map((response) => { - if (!response.success) { - throw new Error(response.error); + const supervisor = spawnSupervisor(agentDir, socketPath, projectDir); + const client = await connectEventually(socketPath, supervisor); + const externalLease = acquireSessionLease(sessionFiles[0], agentDir, { + [SESSION_LEASES_ENABLED_ENV]: "1", + [SESSION_LEASE_OWNER_ID_ENV]: "external-owner", + }); + const conflict = await client.request({ + type: "create", + sessionPath: sessionFiles[0], + config: { cwd: projectDir, agentDir, sessionDir, noTools: true, noExtensions: true }, + }); + expect(conflict).toMatchObject({ + success: false, + errorInfo: { code: "session_already_active", activeSessionId: "external-owner" }, + }); + const emptyAfterConflict = await client.request({ type: "list" }); + expect(requireSessionList(emptyAfterConflict.success ? emptyAfterConflict.data : undefined)).toHaveLength(0); + externalLease?.release(); + const created = await Promise.all( + sessionFiles.map((sessionPath) => + client.request({ + type: "create", + sessionPath, + config: { cwd: projectDir, agentDir, sessionDir, noTools: true, noExtensions: true }, + }), + ), + ); + const summaries = created.map((response) => { + if (!response.success) { + throw new Error(response.error); + } + return requireSummary(response.data); + }); + const pids = summaries.map((summary) => summary.workerPid); + expect(new Set(pids).size).toBe(PROCESS_STRESS_WORKERS); + expect(pids).not.toContain(supervisor.pid); + for (const pid of pids) { + if (!pid) { + throw new Error("Resident root did not expose a worker pid"); + } + workerPids.add(pid); } - return requireSummary(response.data); - }); - const pids = summaries.map((summary) => summary.workerPid); - expect(new Set(pids).size).toBe(PROCESS_STRESS_WORKERS); - expect(pids).not.toContain(supervisor.pid); - for (const pid of pids) { - if (!pid) { - throw new Error("Resident root did not expose a worker pid"); + const firstActiveSessionId = summaries[0]!.activeSessionId ?? summaries[0]!.id; + const addedCron = await client.request({ + type: "cron_add", + activeSessionId: firstActiveSessionId, + schedule: "every 1h", + prompt: "check status", + }); + expect(addedCron.success).toBe(true); + const cronJob = (addedCron.success ? addedCron.data : undefined) as { job?: { id?: string } } | undefined; + if (!cronJob?.job?.id) { + throw new Error("Supervisor did not persist the cron job"); } - workerPids.add(pid); - } - const firstActiveSessionId = summaries[0]!.activeSessionId ?? summaries[0]!.id; - const addedCron = await client.request({ - type: "cron_add", - activeSessionId: firstActiveSessionId, - schedule: "every 1h", - prompt: "check status", - }); - expect(addedCron.success).toBe(true); - const cronJob = (addedCron.success ? addedCron.data : undefined) as { job?: { id?: string } } | undefined; - if (!cronJob?.job?.id) { - throw new Error("Supervisor did not persist the cron job"); - } - const listedCron = await client.request({ type: "cron_list", activeSessionId: firstActiveSessionId }); - expect(listedCron).toMatchObject({ success: true, data: { jobs: [{ id: cronJob.job.id }] } }); - const cancelledCron = await client.request({ type: "cron_cancel", jobId: cronJob.job.id }); - expect(cancelledCron.success).toBe(true); - const activeSessionIds = summaries.map((summary) => summary.activeSessionId ?? summary.id); - await Promise.all( - activeSessionIds.map((activeSessionId, index) => - startBlockingBash(client, activeSessionId, join(root, `stress-blocker-${index}.ready`)), - ), - ); - const heartbeats = await Promise.all( - activeSessionIds.map((activeSessionId, index) => - client.request({ - type: "heartbeat_set", - activeSessionId, - schedule: "every 10s", - prompt: `heartbeat ${index}`, - }), - ), - ); - expect(heartbeats.every((response) => response.success)).toBe(true); - await waitForCondition( - () => { - const stores = summaries.map((summary, index) => { - const store = AgentCronJobStore.forSessionArtifacts(); - store.registerSessionArtifact( - summary.sessionId, - join(dirname(dirname(sessionFiles[index]!)), "session-artifacts", summary.sessionId), - ); - return store; - }); - return stores.every((store) => store.list().some((job) => job.lastSkippedAt !== undefined)); - }, - "Session workers did not advance their heartbeats independently", - 15_000, - ); + const listedCron = await client.request({ type: "cron_list", activeSessionId: firstActiveSessionId }); + expect(listedCron).toMatchObject({ success: true, data: { jobs: [{ id: cronJob.job.id }] } }); + const cancelledCron = await client.request({ type: "cron_cancel", jobId: cronJob.job.id }); + expect(cancelledCron.success).toBe(true); + const activeSessionIds = summaries.map((summary) => summary.activeSessionId ?? summary.id); + await Promise.all( + activeSessionIds.map((activeSessionId, index) => + startBlockingBash(client, activeSessionId, join(root, `stress-blocker-${index}.ready`)), + ), + ); + const heartbeats = await Promise.all( + activeSessionIds.map((activeSessionId, index) => + client.request({ + type: "heartbeat_set", + activeSessionId, + schedule: "every 10s", + prompt: `heartbeat ${index}`, + }), + ), + ); + expect(heartbeats.every((response) => response.success)).toBe(true); + await waitForCondition( + () => { + const stores = summaries.map((summary, index) => { + const store = AgentCronJobStore.forSessionArtifacts(); + store.registerSessionArtifact( + summary.sessionId, + join(dirname(dirname(sessionFiles[index]!)), "session-artifacts", summary.sessionId), + ); + return store; + }); + return stores.every((store) => store.list().some((job) => job.lastSkippedAt !== undefined)); + }, + "Session workers did not advance their heartbeats independently", + 15_000, + ); - const listed = await client.request({ type: "list" }); - expect(listed.success).toBe(true); - expect(requireSessionList(listed.success ? listed.data : undefined)).toHaveLength(PROCESS_STRESS_WORKERS); - supervisor.kill("SIGTERM"); - await waitForExit(supervisor); - children.delete(supervisor); - client.close(); - const replacementClient = await connectEventually(socketPath); - const adopted = await replacementClient.request({ type: "list" }); - expect(adopted.success).toBe(true); - expect( - new Set(requireSessionList(adopted.success ? adopted.data : undefined).map((summary) => summary.workerPid)), - ).toEqual(new Set(pids)); - await replacementClient.request({ type: "shutdown" }); - replacementClient.close(); - await waitForSocketGone(socketPath); - await Promise.all( - pids.map(async (pid) => { - if (pid) { - await waitForProcessGone(pid); - workerPids.delete(pid); - } - }), - ); - }); + const listed = await client.request({ type: "list" }); + expect(listed.success).toBe(true); + expect(requireSessionList(listed.success ? listed.data : undefined)).toHaveLength(PROCESS_STRESS_WORKERS); + supervisor.kill("SIGTERM"); + await waitForExit(supervisor); + children.delete(supervisor); + client.close(); + const replacementClient = await connectEventually(socketPath); + const adopted = await replacementClient.request({ type: "list" }); + expect(adopted.success).toBe(true); + expect( + new Set(requireSessionList(adopted.success ? adopted.data : undefined).map((summary) => summary.workerPid)), + ).toEqual(new Set(pids)); + await replacementClient.request({ type: "shutdown" }); + replacementClient.close(); + await waitForSocketGone(socketPath); + await Promise.all( + pids.map(async (pid) => { + if (pid) { + await waitForProcessGone(pid); + workerPids.delete(pid); + } + }), + ); + }, + ); it("isolates a root, streams a chunked snapshot, and adopts the same worker after restart", async () => { const root = tempDir(); @@ -1643,71 +1656,72 @@ describe("daemon supervisor resident workers", () => { workerPids.delete(recovered.workerPid); }); - it("runs a session-artifact cron job while the supervisor is being replaced", { - tags: ["process-stress"], - timeout: 30_000, - }, async () => { - const root = tempDir(); - const agentDir = join(root, "agent"); - const projectDir = join(root, "project"); - const sessionDir = join(agentDir, "sessions"); - const socketPath = join(tmpdir(), `prime-worker-cron-${process.pid}-${randomUUID().slice(0, 8)}.sock`); - mkdirSync(projectDir, { recursive: true }); - const sessionManager = SessionManager.create(projectDir, sessionDir); - sessionManager.appendMessage({ role: "user", content: "scheduled work", timestamp: 1 }); - sessionManager.appendSessionState({ status: "active" }); - const sessionFile = sessionManager.getSessionFile(); - const artifactDir = sessionManager.getSessionArtifactDir(); - if (!sessionFile || !artifactDir) { - throw new Error("Fixture session did not persist"); - } - - const supervisor = spawnSupervisor(agentDir, socketPath, projectDir); - const client = await connectEventually(socketPath, supervisor); - const created = await client.request({ - type: "create", - sessionPath: sessionFile, - config: { cwd: projectDir, agentDir, sessionDir, noTools: true, noExtensions: true }, - }); - if (!created.success) { - throw new Error(created.error); - } - const summary = requireSummary(created.data); - if (!summary.workerPid) { - throw new Error("Resident worker did not expose its pid"); - } - workerPids.add(summary.workerPid); - await startBlockingBash(client, summary.activeSessionId ?? summary.id, join(root, "heartbeat-blocker.ready")); - const scheduled = await client.request({ - type: "heartbeat_set", - activeSessionId: summary.activeSessionId ?? summary.id, - schedule: "every 10s", - prompt: "continue without the supervisor", - }); - if (!scheduled.success || !scheduled.data || typeof scheduled.data !== "object") { - throw new Error(scheduled.success ? "Heartbeat response was missing its job" : scheduled.error); - } - const job = (scheduled.data as { heartbeat: { id: string } }).heartbeat; - - client.close(); - supervisor.kill("SIGKILL"); - await waitForExit(supervisor); - children.delete(supervisor); + processStressIt( + "runs a session-artifact cron job while the supervisor is being replaced", + { timeout: 30_000 }, + async () => { + const root = tempDir(); + const agentDir = join(root, "agent"); + const projectDir = join(root, "project"); + const sessionDir = join(agentDir, "sessions"); + const socketPath = join(tmpdir(), `prime-worker-cron-${process.pid}-${randomUUID().slice(0, 8)}.sock`); + mkdirSync(projectDir, { recursive: true }); + const sessionManager = SessionManager.create(projectDir, sessionDir); + sessionManager.appendMessage({ role: "user", content: "scheduled work", timestamp: 1 }); + sessionManager.appendSessionState({ status: "active" }); + const sessionFile = sessionManager.getSessionFile(); + const artifactDir = sessionManager.getSessionArtifactDir(); + if (!sessionFile || !artifactDir) { + throw new Error("Fixture session did not persist"); + } - const store = AgentCronJobStore.forSessionArtifacts(); - store.registerSessionArtifact(sessionManager.getSessionId(), artifactDir); - await waitForCondition( - () => store.list().find((candidate) => candidate.id === job.id)?.lastSkippedAt !== undefined, - "Resident worker did not advance its heartbeat without the supervisor", - 15_000, - ); - expect(store.list().find((candidate) => candidate.id === job.id)).toBeDefined(); + const supervisor = spawnSupervisor(agentDir, socketPath, projectDir); + const client = await connectEventually(socketPath, supervisor); + const created = await client.request({ + type: "create", + sessionPath: sessionFile, + config: { cwd: projectDir, agentDir, sessionDir, noTools: true, noExtensions: true }, + }); + if (!created.success) { + throw new Error(created.error); + } + const summary = requireSummary(created.data); + if (!summary.workerPid) { + throw new Error("Resident worker did not expose its pid"); + } + workerPids.add(summary.workerPid); + await startBlockingBash(client, summary.activeSessionId ?? summary.id, join(root, "heartbeat-blocker.ready")); + const scheduled = await client.request({ + type: "heartbeat_set", + activeSessionId: summary.activeSessionId ?? summary.id, + schedule: "every 10s", + prompt: "continue without the supervisor", + }); + if (!scheduled.success || !scheduled.data || typeof scheduled.data !== "object") { + throw new Error(scheduled.success ? "Heartbeat response was missing its job" : scheduled.error); + } + const job = (scheduled.data as { heartbeat: { id: string } }).heartbeat; - const replacement = await connectEventually(socketPath); - await replacement.request({ type: "shutdown" }); - replacement.close(); - await waitForSocketGone(socketPath); - await waitForProcessGone(summary.workerPid); - workerPids.delete(summary.workerPid); - }); + client.close(); + supervisor.kill("SIGKILL"); + await waitForExit(supervisor); + children.delete(supervisor); + + const store = AgentCronJobStore.forSessionArtifacts(); + store.registerSessionArtifact(sessionManager.getSessionId(), artifactDir); + await waitForCondition( + () => store.list().find((candidate) => candidate.id === job.id)?.lastSkippedAt !== undefined, + "Resident worker did not advance its heartbeat without the supervisor", + 15_000, + ); + expect(store.list().find((candidate) => candidate.id === job.id)).toBeDefined(); + + const replacement = await connectEventually(socketPath); + await replacement.request({ type: "shutdown" }); + replacement.close(); + await waitForSocketGone(socketPath); + await waitForProcessGone(summary.workerPid); + workerPids.delete(summary.workerPid); + }, + ); }); diff --git a/packages/coding-agent/test/file-lines.test.ts b/packages/coding-agent/test/file-lines.test.ts index 92421e9f99f..d78fdad4636 100644 --- a/packages/coding-agent/test/file-lines.test.ts +++ b/packages/coding-agent/test/file-lines.test.ts @@ -1,3 +1,4 @@ +import { createRequire } from "node:module"; import { Readable } from "node:stream"; import { afterEach, describe, expect, it, vi } from "vitest"; @@ -5,13 +6,11 @@ const fsMocks = vi.hoisted(() => ({ createReadStream: vi.fn(), })); -vi.mock("node:fs", async (importOriginal) => { - const actual = await importOriginal(); - return { - ...actual, - createReadStream: fsMocks.createReadStream, - }; -}); +const __fs = createRequire(import.meta.url)("node:fs"); +vi.mock("node:fs", () => ({ + ...__fs, + createReadStream: fsMocks.createReadStream, +})); const { readLinesAsBuffers } = await import("../src/utils/file-lines.js"); diff --git a/packages/coding-agent/test/fixtures/eng-4600-supervisor-fixture.ts b/packages/coding-agent/test/fixtures/eng-4600-supervisor-fixture.ts index 11adf1bc855..f44431ad4ab 100644 --- a/packages/coding-agent/test/fixtures/eng-4600-supervisor-fixture.ts +++ b/packages/coding-agent/test/fixtures/eng-4600-supervisor-fixture.ts @@ -20,16 +20,43 @@ function send(message: Record): void { process.send?.(message); } +function exitFixture(code: number): never { + process.exitCode = code; + if (process.connected) process.disconnect(); + const reallyExit = (process as NodeJS.Process & { reallyExit?: (exitCode?: number) => never }).reallyExit; + if (reallyExit) return reallyExit.call(process, code); + process.exit(code); +} + +function sendAndExit(message: Record, code: number): void { + send(message); + setTimeout(() => exitFixture(code), 10); +} + +const pendingControls = new Map void>>(); +const controlBacklog: ControlMessage["type"][] = []; + +process.on("message", (message: unknown) => { + if (!message || typeof message !== "object" || typeof (message as Partial).type !== "string") { + return; + } + const type = (message as ControlMessage).type; + const waiters = pendingControls.get(type); + const waiter = waiters?.shift(); + if (waiter) waiter(); + else controlBacklog.push(type); +}); + function waitForControl(type: ControlMessage["type"]): Promise { + const queuedIndex = controlBacklog.indexOf(type); + if (queuedIndex !== -1) { + controlBacklog.splice(queuedIndex, 1); + return Promise.resolve(); + } return new Promise((resolve) => { - const onMessage = (message: unknown) => { - if (!message || typeof message !== "object" || (message as Partial).type !== type) { - return; - } - process.off("message", onMessage); - resolve(); - }; - process.on("message", onMessage); + const waiters = pendingControls.get(type) ?? []; + waiters.push(resolve); + pendingControls.set(type, waiters); }); } @@ -47,7 +74,7 @@ async function runOwnershipHolder(): Promise { await ownership.release(); send({ type: "owner_released" }); await waitForControl("shutdown"); - process.exit(0); + exitFixture(0); } async function runSupervisor(): Promise { @@ -82,8 +109,8 @@ async function runSupervisor(): Promise { }); return await new Promise(() => {}); } catch (error) { - send({ type: "failed", error: error instanceof Error ? error.message : String(error) }); - process.exit(0); + sendAndExit({ type: "failed", error: error instanceof Error ? error.message : String(error) }, 0); + return await new Promise(() => {}); } } @@ -129,8 +156,8 @@ async function runLegacyCleanup(): Promise { } } } - send({ type: "cleanup_complete", skipped }); - process.exit(0); + sendAndExit({ type: "cleanup_complete", skipped }, 0); + return await new Promise(() => {}); } async function main(): Promise { @@ -158,6 +185,5 @@ async function main(): Promise { } void main().catch((error) => { - send({ type: "failed", error: error instanceof Error ? error.message : String(error) }); - process.exit(1); + sendAndExit({ type: "failed", error: error instanceof Error ? error.message : String(error) }, 1); }); diff --git a/packages/coding-agent/test/fixtures/eng-4606-update-launcher.ts b/packages/coding-agent/test/fixtures/eng-4606-update-launcher.ts index 1e7b693882a..8cacf3a74ba 100644 --- a/packages/coding-agent/test/fixtures/eng-4606-update-launcher.ts +++ b/packages/coding-agent/test/fixtures/eng-4606-update-launcher.ts @@ -14,11 +14,10 @@ const cliPath = requireEnvironment("ENG_4606_CLI_PATH"); const completionPath = requireEnvironment("ENG_4606_COMPLETION_PATH"); const pidPath = requireEnvironment("ENG_4606_PID_PATH"); const socketPath = requireEnvironment("ENG_4606_SOCKET_PATH"); -const tsxPath = requireEnvironment("ENG_4606_TSX_PATH"); const originActiveSessionId = requireEnvironment("PRIME_AGENT_INTERNAL_DAEMON_WORKER_ACTIVE_SESSION_ID"); process.argv[1] = cliPath; -process.execArgv.splice(0, process.execArgv.length, tsxPath); +process.execArgv.splice(0, process.execArgv.length); writeFileSync(pidPath, `${process.pid}\n`); await launchDaemonUpdateRestartCoordinator({ diff --git a/packages/coding-agent/test/git-context.test.ts b/packages/coding-agent/test/git-context.test.ts index 07b61ddc05a..ea61ff707a1 100644 --- a/packages/coding-agent/test/git-context.test.ts +++ b/packages/coding-agent/test/git-context.test.ts @@ -6,7 +6,7 @@ import { afterEach, beforeEach, describe, expect, it } from "vitest"; import { captureGitContext, gitContextsEqual } from "../src/utils/git.js"; function git(cwd: string, ...args: string[]): string { - return execFileSync("git", args, { cwd, encoding: "utf8" }).trim(); + return execFileSync("git", args, { cwd, encoding: "utf8", env: process.env }).trim(); } function initRepo(dir: string): void { diff --git a/packages/coding-agent/test/git-update.test.ts b/packages/coding-agent/test/git-update.test.ts index eb50d6003cf..e7df4163561 100644 --- a/packages/coding-agent/test/git-update.test.ts +++ b/packages/coding-agent/test/git-update.test.ts @@ -11,6 +11,7 @@ function git(args: string[], cwd: string): string { const result = spawnSync("git", args, { cwd, encoding: "utf-8", + env: process.env, }); if (result.status !== 0) { throw new Error(`Command failed: git ${args.join(" ")}\n${result.stderr}`); diff --git a/packages/coding-agent/test/interactive-mode-status.test.ts b/packages/coding-agent/test/interactive-mode-status.test.ts index 2ffdd91f7ce..dfdefce78aa 100644 --- a/packages/coding-agent/test/interactive-mode-status.test.ts +++ b/packages/coding-agent/test/interactive-mode-status.test.ts @@ -5026,9 +5026,9 @@ describe("InteractiveMode.showLoadedResources", () => { force: true, }); - expect(normalizeRenderedOutput(fakeThis.chatContainer)).toMatchInlineSnapshot(` -"[Extensions] - @scope/pi-scoped, answer.ts, cli-extension.ts, HazAT/pi-interactive-subagents, HazAT/pi-interactive-subagents:subagents, local-index, pi-markdown-preview, user-index"`); + expect(normalizeRenderedOutput(fakeThis.chatContainer)).toBe( + "[Extensions]\n @scope/pi-scoped, answer.ts, cli-extension.ts, HazAT/pi-interactive-subagents, HazAT/pi-interactive-subagents:subagents, local-index, pi-markdown-preview, user-index", + ); }); test("adds more parent folders until local extension labels are unique", () => { @@ -5072,9 +5072,7 @@ describe("InteractiveMode.showLoadedResources", () => { force: true, }); - expect(normalizeRenderedOutput(fakeThis.chatContainer)).toMatchInlineSnapshot(` -"[Extensions] - alpha/one, beta/one, gamma/one"`); + expect(normalizeRenderedOutput(fakeThis.chatContainer)).toBe("[Extensions]\n alpha/one, beta/one, gamma/one"); }); test("strips index.ts from local extension label, showing parent dir", () => { @@ -5100,9 +5098,7 @@ describe("InteractiveMode.showLoadedResources", () => { force: true, }); - expect(normalizeRenderedOutput(fakeThis.chatContainer)).toMatchInlineSnapshot(` -"[Extensions] - plan-mode"`); + expect(normalizeRenderedOutput(fakeThis.chatContainer)).toBe("[Extensions]\n plan-mode"); }); test("strips index.js from local extension label, showing parent dir", () => { @@ -5128,9 +5124,7 @@ describe("InteractiveMode.showLoadedResources", () => { force: true, }); - expect(normalizeRenderedOutput(fakeThis.chatContainer)).toMatchInlineSnapshot(` -"[Extensions] - plan-mode"`); + expect(normalizeRenderedOutput(fakeThis.chatContainer)).toBe("[Extensions]\n plan-mode"); }); test("mixed single-file and subdirectory index.ts extensions strip index.ts", () => { @@ -5165,9 +5159,7 @@ describe("InteractiveMode.showLoadedResources", () => { force: true, }); - expect(normalizeRenderedOutput(fakeThis.chatContainer)).toMatchInlineSnapshot(` -"[Extensions] - plan-mode, webfetch.ts"`); + expect(normalizeRenderedOutput(fakeThis.chatContainer)).toBe("[Extensions]\n plan-mode, webfetch.ts"); }); test("multiple index.ts with unique parent dirs need no disambiguation", () => { @@ -5202,9 +5194,7 @@ describe("InteractiveMode.showLoadedResources", () => { force: true, }); - expect(normalizeRenderedOutput(fakeThis.chatContainer)).toMatchInlineSnapshot(` -"[Extensions] - bar, foo"`); + expect(normalizeRenderedOutput(fakeThis.chatContainer)).toBe("[Extensions]\n bar, foo"); }); test("multiple index.ts with same parent dir name disambiguated with grandparent", () => { @@ -5239,9 +5229,7 @@ describe("InteractiveMode.showLoadedResources", () => { force: true, }); - expect(normalizeRenderedOutput(fakeThis.chatContainer)).toMatchInlineSnapshot(` -"[Extensions] - alpha/tools, beta/tools"`); + expect(normalizeRenderedOutput(fakeThis.chatContainer)).toBe("[Extensions]\n alpha/tools, beta/tools"); }); test("non-index file in subdirectory stays as filename", () => { @@ -5267,9 +5255,7 @@ describe("InteractiveMode.showLoadedResources", () => { force: true, }); - expect(normalizeRenderedOutput(fakeThis.chatContainer)).toMatchInlineSnapshot(` -"[Extensions] - main.ts"`); + expect(normalizeRenderedOutput(fakeThis.chatContainer)).toBe("[Extensions]\n main.ts"); }); test("package extensions still strip index.ts correctly (regression guard)", () => { @@ -5295,9 +5281,7 @@ describe("InteractiveMode.showLoadedResources", () => { force: true, }); - expect(normalizeRenderedOutput(fakeThis.chatContainer)).toMatchInlineSnapshot(` -"[Extensions] - pi-markdown-preview"`); + expect(normalizeRenderedOutput(fakeThis.chatContainer)).toBe("[Extensions]\n pi-markdown-preview"); }); test("captures mixed extension layouts in expanded output", () => { const fakeThis = createShowLoadedResourcesThis({ @@ -5311,22 +5295,9 @@ describe("InteractiveMode.showLoadedResources", () => { force: true, }); - expect(normalizeRenderedOutput(fakeThis.chatContainer)).toMatchInlineSnapshot(` -"[Extensions] - project - /tmp/project/.pi/extensions/answer.ts - /tmp/project/.pi/extensions/local-index - git:github.com/HazAT/pi-interactive-subagents - extensions - extensions/subagents - npm:@scope/pi-scoped - extensions - npm:pi-markdown-preview - extensions - user - /tmp/agent/extensions/user-index - path - /tmp/temp/cli-extension.ts"`); + expect(normalizeRenderedOutput(fakeThis.chatContainer)).toBe( + "[Extensions]\n project\n /tmp/project/.pi/extensions/answer.ts\n /tmp/project/.pi/extensions/local-index\n git:github.com/HazAT/pi-interactive-subagents\n extensions\n extensions/subagents\n npm:@scope/pi-scoped\n extensions\n npm:pi-markdown-preview\n extensions\n user\n /tmp/agent/extensions/user-index\n path\n /tmp/temp/cli-extension.ts", + ); }); test("shows context paths relative to cwd while preserving full external paths", () => { @@ -5419,13 +5390,9 @@ describe("InteractiveMode.showLoadedResources", () => { }); const output = normalizeRenderedOutput(fakeThis.chatContainer, 100); - expect(output).toMatchInlineSnapshot(` -"[Skill warning] - first line of the warning. - second line with guidance. - - indented detail" -`); + expect(output).toBe( + "[Skill warning]\n first line of the warning.\n second line with guidance.\n\n indented detail", + ); expect(output).not.toContain("[Skill conflicts]"); }); }); diff --git a/packages/coding-agent/test/interactive-update-relaunch.test.ts b/packages/coding-agent/test/interactive-update-relaunch.test.ts index b13e6b216d3..8a215bbe073 100644 --- a/packages/coding-agent/test/interactive-update-relaunch.test.ts +++ b/packages/coding-agent/test/interactive-update-relaunch.test.ts @@ -1,19 +1,21 @@ -import type * as ChildProcessModule from "child_process"; +import { createRequire } from "node:module"; import { describe, expect, it, vi } from "vitest"; -import type * as DaemonUpdateRestartModule from "../src/cli/daemon-update-restart.js"; const updateMocks = vi.hoisted(() => ({ spawnSync: vi.fn(), launchCoordinator: vi.fn(), })); -vi.mock("child_process", async (importOriginal) => ({ - ...(await importOriginal()), +const __childProcess = createRequire(import.meta.url)("child_process"); +const __daemonUpdateRestart = createRequire(import.meta.url)("../src/cli/daemon-update-restart.js"); + +vi.mock("child_process", () => ({ + ...__childProcess, spawnSync: updateMocks.spawnSync, })); -vi.mock("../src/cli/daemon-update-restart.js", async (importOriginal) => ({ - ...(await importOriginal()), +vi.mock("../src/cli/daemon-update-restart.js", () => ({ + ...__daemonUpdateRestart, launchDaemonUpdateRestartCoordinator: updateMocks.launchCoordinator, })); diff --git a/packages/coding-agent/test/kernel-bash-shell.test.ts b/packages/coding-agent/test/kernel-bash-shell.test.ts index bced79ae99a..80302b7b86f 100644 --- a/packages/coding-agent/test/kernel-bash-shell.test.ts +++ b/packages/coding-agent/test/kernel-bash-shell.test.ts @@ -1,3 +1,4 @@ +import { createRequire } from "node:module"; import { afterEach, describe, expect, it, vi } from "vitest"; const mocks = vi.hoisted(() => ({ @@ -5,17 +6,15 @@ const mocks = vi.hoisted(() => ({ spawnSync: vi.fn(), })); -vi.mock("node:fs", async (importOriginal) => { - const actual = await importOriginal(); +const __fs = createRequire(import.meta.url)("node:fs") as typeof import("node:fs"); +vi.mock("node:fs", () => { // Module-load reads (config.ts) must see the real fs; tests override per case. - mocks.existsSync.mockImplementation(actual.existsSync); - return { ...actual, existsSync: mocks.existsSync }; + mocks.existsSync.mockImplementation(__fs.existsSync); + return { ...__fs, existsSync: mocks.existsSync }; }); -vi.mock("child_process", async (importOriginal) => { - const actual = await importOriginal(); - return { ...actual, spawnSync: mocks.spawnSync }; -}); +const __childProcess = createRequire(import.meta.url)("child_process") as typeof import("child_process"); +vi.mock("child_process", () => ({ ...__childProcess, spawnSync: mocks.spawnSync })); import { resolveKernelBashShell } from "../src/utils/shell.js"; diff --git a/packages/coding-agent/test/kernel-goal-skill.test.ts b/packages/coding-agent/test/kernel-goal-skill.test.ts index 71a544b640c..76fded711ba 100644 --- a/packages/coding-agent/test/kernel-goal-skill.test.ts +++ b/packages/coding-agent/test/kernel-goal-skill.test.ts @@ -5,6 +5,7 @@ import { afterEach, beforeEach, describe, expect, it } from "vitest"; import { getBundledSkillsDir } from "../src/config.js"; import type { PythonSkillRuntimeInfo } from "../src/core/skills.js"; import { IpythonKernelProvisioner } from "../src/core/tools/ipython.js"; +import { isTestTagEnabled } from "./test-tags.js"; function bundledGoalSkill(): PythonSkillRuntimeInfo { const packagePath = join(getBundledSkillsDir(), "goal"); @@ -16,7 +17,9 @@ function bundledGoalSkill(): PythonSkillRuntimeInfo { }; } -describe("goal skill over the kernel host bridge", { tags: ["kernel-heavy"] }, () => { +const kernelHeavyDescribe = isTestTagEnabled("kernel-heavy") ? describe : describe.skip; + +kernelHeavyDescribe("goal skill over the kernel host bridge", () => { let tempDir: string; let provisioner: IpythonKernelProvisioner | undefined; diff --git a/packages/coding-agent/test/mcp-command.test.ts b/packages/coding-agent/test/mcp-command.test.ts index 81f19581b58..21ce7792960 100644 --- a/packages/coding-agent/test/mcp-command.test.ts +++ b/packages/coding-agent/test/mcp-command.test.ts @@ -43,10 +43,12 @@ describe("MCP management commands", () => { "--", "node", ]); - expect(config).toMatchObject({ - type: "stdio", - env: { __proto__: { env: "PROTO_SOURCE" }, constructor: { env: "CONSTRUCTOR_SOURCE" } }, - }); + expect(config.type).toBe("stdio"); + if (config.type !== "stdio") throw new Error("Expected stdio MCP config"); + expect(Object.hasOwn(config.env ?? {}, "__proto__")).toBe(true); + expect(Reflect.get(config.env ?? {}, "__proto__")).toEqual({ env: "PROTO_SOURCE" }); + expect(Object.hasOwn(config.env ?? {}, "constructor")).toBe(true); + expect(config.env?.constructor).toEqual({ env: "CONSTRUCTOR_SOURCE" }); }); it("validates transport, URL, names, auth, and stdio environment syntax", () => { diff --git a/packages/coding-agent/test/owned-session-worker-process.test.ts b/packages/coding-agent/test/owned-session-worker-process.test.ts index 0b661ff6318..d19cc8f01ca 100644 --- a/packages/coding-agent/test/owned-session-worker-process.test.ts +++ b/packages/coding-agent/test/owned-session-worker-process.test.ts @@ -5,7 +5,6 @@ import { join, resolve } from "node:path"; import { afterEach, describe, expect, it } from "vitest"; const fixturePath = resolve(__dirname, "fixtures/owned-session-worker-fixture.ts"); -const tsxPath = resolve(__dirname, "../../../node_modules/tsx/dist/cli.mjs"); const tempDirs: string[] = []; const children = new Set(); const workerPids = new Set(); @@ -45,14 +44,13 @@ function spawnFrontend( keepAlive = false, environment: NodeJS.ProcessEnv = {}, ): ChildProcess { - const child = spawn(process.execPath, [tsxPath, fixturePath, ...args], { + const child = spawn(process.execPath, [fixturePath, ...args], { env: { ...process.env, ...environment, PRIME_AGENT_INTERNAL_LEGACY_OWNED_WORKER_FRONTEND: "1", PRIME_AGENT_TEST_OWNED_PID_PATH: pidPath, ...(keepAlive ? { PRIME_AGENT_TEST_KEEP_ALIVE: "1" } : {}), - TSX_TSCONFIG_PATH: resolve(__dirname, "../../../tsconfig.json"), }, stdio: ["pipe", "pipe", "pipe"], }); diff --git a/packages/coding-agent/test/package-command-paths.test.ts b/packages/coding-agent/test/package-command-paths.test.ts index 3ddb7055a46..980da26b1d1 100644 --- a/packages/coding-agent/test/package-command-paths.test.ts +++ b/packages/coding-agent/test/package-command-paths.test.ts @@ -57,7 +57,7 @@ describe("package commands", () => { originalTmpDir = process.env.TMPDIR; originalExitCode = process.exitCode; originalExecPath = process.execPath; - process.exitCode = undefined; + process.exitCode = 0; process.env[ENV_AGENT_DIR] = agentDir; process.env.TMPDIR = tempDir; process.chdir(projectDir); @@ -66,7 +66,7 @@ describe("package commands", () => { afterEach(() => { vi.unstubAllGlobals(); process.chdir(originalCwd); - process.exitCode = originalExitCode; + process.exitCode = originalExitCode ?? 0; restoreEnv(ENV_AGENT_DIR, originalAgentDir); restoreEnv("PI_PACKAGE_DIR", originalPiPackageDir); restoreEnv("PRIME_AGENT_DOWNLOAD_BASE_URL", originalPrimeAgentDownloadBaseUrl); @@ -113,7 +113,7 @@ describe("package commands", () => { expect(stdout).toContain("Usage:"); expect(stdout).toContain(`${APP_NAME} package install [--local]`); expect(errorSpy).not.toHaveBeenCalled(); - expect(process.exitCode).toBeUndefined(); + expect(process.exitCode ?? 0).toBe(0); } finally { logSpy.mockRestore(); errorSpy.mockRestore(); @@ -219,7 +219,7 @@ else fs.writeFileSync(${JSON.stringify(recordPath)},JSON.stringify(args)); try { await expect(runSelfUpdateInstallChild(["update", "--self", "--force"])).resolves.toBeUndefined(); - expect(process.exitCode).toBeUndefined(); + expect(process.exitCode ?? 0).toBe(0); expect(errorSpy).not.toHaveBeenCalled(); expect(fetchMock).toHaveBeenCalledOnce(); const recordedArgs = JSON.parse(readFileSync(recordPath, "utf-8")) as string[]; @@ -263,7 +263,7 @@ else fs.writeFileSync(${JSON.stringify(recordPath)},JSON.stringify(args)); try { await expect(runSelfUpdateInstallChild(["update", "--self"])).resolves.toBeUndefined(); - expect(process.exitCode).toBeUndefined(); + expect(process.exitCode ?? 0).toBe(0); expect(errorSpy).not.toHaveBeenCalled(); expect(fetchMock).toHaveBeenCalledOnce(); const recordedArgs = JSON.parse(readFileSync(recordPath, "utf-8")) as string[]; @@ -312,7 +312,7 @@ else { try { await expect(runSelfUpdateInstallChild(["update", "--self"])).resolves.toBeUndefined(); - expect(process.exitCode).toBeUndefined(); + expect(process.exitCode ?? 0).toBe(0); expect(errorSpy).not.toHaveBeenCalled(); const recordedCalls = JSON.parse(readFileSync(recordPath, "utf-8")) as string[][]; expect(recordedCalls).toEqual([ @@ -365,7 +365,7 @@ else { try { await expect(runSelfUpdateInstallChild(["update", "--self"])).resolves.toBeUndefined(); - expect(process.exitCode).toBeUndefined(); + expect(process.exitCode ?? 0).toBe(0); expect(errorSpy).not.toHaveBeenCalled(); const recordedCalls = JSON.parse(readFileSync(recordPath, "utf-8")) as string[][]; expect(recordedCalls).toEqual([ @@ -419,7 +419,7 @@ else fs.writeFileSync(${JSON.stringify(recordPath)},JSON.stringify(args)); try { await expect(main(["update"])).resolves.toBeUndefined(); - expect(process.exitCode).toBeUndefined(); + expect(process.exitCode ?? 0).toBe(0); expect(errorSpy).not.toHaveBeenCalled(); expect(logSpy.mock.calls.map(([message]) => String(message)).join("\n")).toContain("is already up to date"); expect(existsSync(recordPath)).toBe(false); diff --git a/packages/coding-agent/test/package-self-update-daemon.test.ts b/packages/coding-agent/test/package-self-update-daemon.test.ts index af8ebf000db..0245271506a 100644 --- a/packages/coding-agent/test/package-self-update-daemon.test.ts +++ b/packages/coding-agent/test/package-self-update-daemon.test.ts @@ -2,7 +2,6 @@ import { existsSync, mkdirSync, rmSync, statSync, writeFileSync } from "node:fs" import { tmpdir } from "node:os"; import { join } from "node:path"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; -import type * as DaemonUpdateRestartModule from "../src/cli/daemon-update-restart.js"; import { acquireDaemonUpdateRestartCoordinator, type DaemonUpdateRestartStatus, @@ -20,7 +19,6 @@ import { } from "../src/config.js"; import type { AgentSessionRuntimeMetadata } from "../src/core/agent-session-runtime.js"; import { DAEMON_PROTOCOL_VERSION, DAEMON_SCHEMA_ID } from "../src/modes/daemon/daemon-protocol.js"; -import type * as DaemonSocketModule from "../src/modes/daemon/daemon-socket.js"; import { handlePackageCommand, prepareDaemonUpdateRestart, @@ -198,8 +196,8 @@ vi.mock("child_process", () => ({ })), })); -vi.mock("../src/cli/daemon-update-restart.js", async (importOriginal) => { - const original = await importOriginal(); +vi.mock("../src/cli/daemon-update-restart.js", () => { + const original = require("../src/cli/daemon-update-restart.js"); return { ...original, launchDaemonUpdateRestartCoordinator: vi.fn(async (options: { socketPath: string }) => { @@ -218,10 +216,10 @@ vi.mock("../src/cli/daemon-update-restart.js", async (importOriginal) => { }; }); -vi.mock("../src/modes/daemon/daemon-socket.js", async (importOriginal) => ({ - ...(await importOriginal()), - defaultDaemonSocketPath: () => mockState.socketPath, -})); +vi.mock("../src/modes/daemon/daemon-socket.js", () => { + const actual = require("../src/modes/daemon/daemon-socket.js"); + return { ...actual, defaultDaemonSocketPath: () => mockState.socketPath }; +}); vi.mock("../src/modes/daemon/daemon-supervisor-ownership.js", () => ({ acquireDaemonShutdownAdmission: vi.fn(async () => { @@ -511,7 +509,7 @@ describe("self-update daemon restart", () => { originalCwd = process.cwd(); originalExecPath = process.execPath; originalExitCode = process.exitCode; - process.exitCode = undefined; + process.exitCode = 0; process.env[ENV_AGENT_DIR] = agentDir; process.env.PI_PACKAGE_DIR = packageDir; process.chdir(projectDir); @@ -529,7 +527,7 @@ describe("self-update daemon restart", () => { afterEach(() => { vi.unstubAllGlobals(); process.chdir(originalCwd); - process.exitCode = originalExitCode; + process.exitCode = originalExitCode ?? 0; if (originalAgentDir === undefined) { delete process.env[ENV_AGENT_DIR]; } else { @@ -884,7 +882,7 @@ describe("self-update daemon restart", () => { try { await expect(performUpdateAndRunCoordinator()).resolves.toBeUndefined(); - expect(process.exitCode).toBeUndefined(); + expect(process.exitCode ?? 0).toBe(0); const spawnIndex = mockState.calls.findIndex((call) => call.startsWith("spawn:npm ")); const launchIndex = mockState.calls.indexOf(`launch-coordinator:${mockState.socketPath}`); const fenceIndex = mockState.calls.indexOf("persist-daemon-startup-fence"); diff --git a/packages/coding-agent/test/print-mode.test.ts b/packages/coding-agent/test/print-mode.test.ts index 065d5d04ae3..e75b48e3ec0 100644 --- a/packages/coding-agent/test/print-mode.test.ts +++ b/packages/coding-agent/test/print-mode.test.ts @@ -754,7 +754,7 @@ describe("runPrintMode", () => { }); expect(exitCode).toBe(0); - expect(session.waitForIdle).toHaveBeenCalledBefore(session.prompt); + expect(session.waitForIdle.mock.invocationCallOrder[0]).toBeLessThan(session.prompt.mock.invocationCallOrder[0]!); expect(session.prompt).toHaveBeenCalledTimes(2); expect(session.prompt.mock.calls[0][0]).toContain("Autonomous quality gate failed (attempt 1/3)"); expect(session.prompt.mock.calls[0][0]).toContain("0/9"); diff --git a/packages/coding-agent/test/public-command.test.ts b/packages/coding-agent/test/public-command.test.ts index 5fc4ae676e2..d0bca2b58bf 100644 --- a/packages/coding-agent/test/public-command.test.ts +++ b/packages/coding-agent/test/public-command.test.ts @@ -63,13 +63,13 @@ describe("public command routing", () => { mocks.reapCalls.length = 0; mocks.shutdownCalls.length = 0; mocks.mcpCommands.length = 0; - process.exitCode = undefined; + process.exitCode = 0; vi.spyOn(console, "log").mockImplementation(() => {}); vi.spyOn(console, "error").mockImplementation(() => {}); }); afterEach(() => { - process.exitCode = undefined; + process.exitCode = 0; vi.restoreAllMocks(); }); diff --git a/packages/coding-agent/test/refinement.test.ts b/packages/coding-agent/test/refinement.test.ts index 19a7313975f..bc7dbbe86df 100644 --- a/packages/coding-agent/test/refinement.test.ts +++ b/packages/coding-agent/test/refinement.test.ts @@ -2,7 +2,6 @@ import { appendFileSync, chmodSync, mkdtempSync, readdirSync, rmSync, statSync, import { tmpdir } from "node:os"; import { join } from "node:path"; import type { AgentMessage } from "@earendil-works/pi-agent-core"; -import type * as PiAi from "@earendil-works/pi-ai"; import type { AssistantMessage, Model } from "@earendil-works/pi-ai"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { @@ -34,8 +33,8 @@ const { completeSimpleMock } = vi.hoisted(() => ({ completeSimpleMock: vi.fn(), })); -vi.mock("@earendil-works/pi-ai", async (importOriginal) => { - const actual = await importOriginal(); +vi.mock("@earendil-works/pi-ai", () => { + const actual = require("@earendil-works/pi-ai"); return { ...actual, completeSimple: completeSimpleMock, @@ -1015,20 +1014,13 @@ describe("harness refinement", () => { ); expect(completeSimpleMock).toHaveBeenCalledTimes(1); - expect(completeSimpleMock.mock.calls[0][1]).toMatchObject({ - systemPrompt: expect.stringContaining("The default editable continual harness store is local"), - }); - expect(completeSimpleMock.mock.calls[0][1]).toMatchObject({ - systemPrompt: expect.stringContaining("A caller may explicitly request global refinement"), - }); - expect(completeSimpleMock.mock.calls[0][1]).toMatchObject({ - systemPrompt: expect.stringContaining("Always use the bare id (no prefix) in edits"), - }); - expect(completeSimpleMock.mock.calls[0][1]).toMatchObject({ - systemPrompt: expect.stringContaining( - "During a local refinement, global entries are read-only context: never propose update or delete edits for them", - ), - }); + const refinementSystemPrompt = completeSimpleMock.mock.calls[0][1]?.systemPrompt; + expect(refinementSystemPrompt).toContain("The default editable continual harness store is local"); + expect(refinementSystemPrompt).toContain("A caller may explicitly request global refinement"); + expect(refinementSystemPrompt).toContain("Always use the bare id (no prefix) in edits"); + expect(refinementSystemPrompt).toContain( + "During a local refinement, global entries are read-only context: never propose update or delete edits for them", + ); // Budget is derived from the model (8192) rather than a fixed literal. expect(completeSimpleMock.mock.calls[0][2]).toMatchObject({ maxTokens: 8192, diff --git a/packages/coding-agent/test/repl-kernel-abort.test.ts b/packages/coding-agent/test/repl-kernel-abort.test.ts index b3283a4191e..dbfdfd54a8f 100644 --- a/packages/coding-agent/test/repl-kernel-abort.test.ts +++ b/packages/coding-agent/test/repl-kernel-abort.test.ts @@ -204,13 +204,12 @@ describe("ReplKernelManager abort handling", () => { await expect(executePromise).resolves.toMatchObject({ status: "aborted" }); const secondExecutePromise = manager.execute("x = 1"); - const secondExecuteExpectation = expect(secondExecutePromise).rejects.toThrow( - "The Python kernel is still running the previously interrupted cell", - ); await Promise.resolve(); await vi.advanceTimersByTimeAsync(5000); - await secondExecuteExpectation; + await expect(secondExecutePromise).rejects.toThrow( + "The Python kernel is still running the previously interrupted cell", + ); expect(writeLine.mock.calls.filter((call) => (call[0] as { type?: string }).type === "execute")).toHaveLength(1); expect(writeLine.mock.calls.some((call) => (call[0] as { type?: string }).type === "interrupt")).toBe(true); manager.disposeSync(); diff --git a/packages/coding-agent/test/repl-kernel-mcp-shutdown.test.ts b/packages/coding-agent/test/repl-kernel-mcp-shutdown.test.ts index ca5545c7b2c..dca42a4e2bd 100644 --- a/packages/coding-agent/test/repl-kernel-mcp-shutdown.test.ts +++ b/packages/coding-agent/test/repl-kernel-mcp-shutdown.test.ts @@ -4,6 +4,7 @@ import { homedir, tmpdir } from "node:os"; import { join, resolve } from "node:path"; import { afterAll, beforeAll, describe, expect, it } from "vitest"; import { ReplKernelManager } from "../src/core/kernel/index.js"; +import { isTestTagEnabled } from "./test-tags.js"; const runtimePython = resolve("../../prime-agent-runtime/.venv/bin/python"); const fallbackPython = join(homedir(), ".prime", "agent", "kernel-venv", "bin", "python"); @@ -18,7 +19,7 @@ function resolveKernelPython(): string | null { } const python = resolveKernelPython(); -const describeIfKernel = python ? describe : describe.skip; +const describeIfKernel = python && isTestTagEnabled("kernel-heavy") ? describe : describe.skip; const MCP_SERVER = `import asyncio, json, os, sys from pathlib import Path @@ -56,7 +57,7 @@ async function waitForExit(pid: number, timeoutMs: number): Promise { return !pidExists(pid); } -describeIfKernel("real REPL kernel MCP shutdown", { tags: ["kernel-heavy"] }, () => { +describeIfKernel("real REPL kernel MCP shutdown", () => { let dir = ""; let fixture = ""; let pidFile = ""; diff --git a/packages/coding-agent/test/repl-kernel-parent-watchdog.test.ts b/packages/coding-agent/test/repl-kernel-parent-watchdog.test.ts index 5924881fcee..e3a6e897f51 100644 --- a/packages/coding-agent/test/repl-kernel-parent-watchdog.test.ts +++ b/packages/coding-agent/test/repl-kernel-parent-watchdog.test.ts @@ -4,14 +4,14 @@ import { chmodSync, existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync import { homedir, tmpdir } from "node:os"; import { join, resolve } from "node:path"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; -import type * as kernelBootstrap from "../src/core/kernel/bootstrap.js"; import { ReplKernelManager } from "../src/core/kernel/index.js"; import { ORPHAN_PROCESS_JOURNAL_ENV } from "../src/core/orphan-process-journal.js"; +import { isTestTagEnabled } from "./test-tags.js"; const ensureKernelPythonMock = vi.hoisted(() => vi.fn()); -vi.mock("../src/core/kernel/bootstrap.js", async (importOriginal) => { - const original = await importOriginal(); +vi.mock("../src/core/kernel/bootstrap.js", () => { + const original = require("../src/core/kernel/bootstrap.js"); return { ...original, ensureKernelPython: ensureKernelPythonMock }; }); @@ -70,12 +70,17 @@ describe("repl kernel parent watchdog", () => { expect(readFileSync(envDump, "utf8")).toMatch(new RegExp(`^PRIME_AGENT_KERNEL_OWNER_PID=${process.pid}$`, "m")); - // Self-exited child: the kill signals nothing, so the record must stay active. + // The append-only journal starts with an active ownership record. Bun can + // observe the child exit soon enough to append its inactive tombstone too. await vi.waitFor(() => { const records = readJournalRecords(journalPath); - expect(records).toHaveLength(1); + expect(records.length).toBeGreaterThanOrEqual(1); expect(records[0]?.ownerPid).toBe(process.pid); expect(records[0]?.active).toBe(true); + expect(records.every((record) => record.pid === records[0]?.pid && record.ownerPid === process.pid)).toBe( + true, + ); + if (records.length > 1) expect(records.at(-1)?.active).toBe(false); }); }); @@ -287,9 +292,10 @@ function resolveReplPython(): string | null { } const replPython = resolveReplPython(); -const describeIf = replPython && process.platform !== "win32" ? describe : describe.skip; +const describeIf = + replPython && process.platform !== "win32" && isTestTagEnabled("kernel-heavy") ? describe : describe.skip; -describeIf("repl runtime outlives-owner watchdog (real runtime)", { tags: ["kernel-heavy"] }, () => { +describeIf("repl runtime outlives-owner watchdog (real runtime)", () => { it("runtime exits after its owner is SIGKILLed (stdin EOF watchdog)", async () => { const dir = mkdtempSync(join(tmpdir(), "prime-agent-repl-watchdog-int-")); const pidFile = join(dir, "runtime.pid"); diff --git a/packages/coding-agent/test/repl-kernel-startup.test.ts b/packages/coding-agent/test/repl-kernel-startup.test.ts index 219453f729e..e52605d8cf3 100644 --- a/packages/coding-agent/test/repl-kernel-startup.test.ts +++ b/packages/coding-agent/test/repl-kernel-startup.test.ts @@ -78,12 +78,16 @@ describe("ReplKernelManager startup", () => { const manager = new ReplKernelManager({ python, cwd: tempDir }); try { - const startPromise = manager.start(); - const expectation = expect(startPromise).rejects.toThrow(/did not become ready within 30000ms/); + const startResult = manager + .start() + .then(() => undefined) + .catch((error: Error) => error); await vi.advanceTimersByTimeAsync(30_000); // The failure path runs a graceful shutdown bounded by its own deadline. await vi.advanceTimersByTimeAsync(5_000); - await expectation; + const startError = await startResult; + expect(startError).toBeInstanceOf(Error); + expect(startError?.message).toMatch(/did not become ready within 30000ms/); } finally { vi.useRealTimers(); errorSpy.mockRestore(); diff --git a/packages/coding-agent/test/repl-kernel-state-roundtrip.test.ts b/packages/coding-agent/test/repl-kernel-state-roundtrip.test.ts index e08ece79a58..d0c0e0e3e3b 100644 --- a/packages/coding-agent/test/repl-kernel-state-roundtrip.test.ts +++ b/packages/coding-agent/test/repl-kernel-state-roundtrip.test.ts @@ -4,6 +4,7 @@ import { homedir, tmpdir } from "node:os"; import { join, resolve } from "node:path"; import { afterAll, beforeAll, describe, expect, it } from "vitest"; import { ReplKernelManager } from "../src/core/kernel/index.js"; +import { isTestTagEnabled } from "./test-tags.js"; function resolveReplPython(): string | null { const candidates = [ @@ -20,9 +21,9 @@ function resolveReplPython(): string | null { } const python = resolveReplPython(); -const describeIfKernel = python ? describe : describe.skip; +const describeIfKernel = python && isTestTagEnabled("kernel-heavy") ? describe : describe.skip; -describeIfKernel("repl kernel state snapshot round-trip (real runtime)", { tags: ["kernel-heavy"] }, () => { +describeIfKernel("repl kernel state snapshot round-trip (real runtime)", () => { let dir = ""; let snapshotPath = ""; let manifestPath = ""; diff --git a/packages/coding-agent/test/rlm-ledger.test.ts b/packages/coding-agent/test/rlm-ledger.test.ts index 2ebb9286dc3..8bac64dc1f8 100644 --- a/packages/coding-agent/test/rlm-ledger.test.ts +++ b/packages/coding-agent/test/rlm-ledger.test.ts @@ -11,8 +11,8 @@ import { import { tmpdir } from "node:os"; const linkFailure = vi.hoisted(() => ({ code: undefined as string | undefined })); -vi.mock("node:fs", async (importOriginal) => { - const actual = await importOriginal(); +vi.mock("node:fs", () => { + const actual = require("node:fs"); return { ...actual, linkSync: ( diff --git a/packages/coding-agent/test/rpc-example.ts b/packages/coding-agent/test/rpc-example.ts index d82bd934924..be8c2df56db 100644 --- a/packages/coding-agent/test/rpc-example.ts +++ b/packages/coding-agent/test/rpc-example.ts @@ -7,7 +7,7 @@ const __dirname = dirname(fileURLToPath(import.meta.url)); /** * Interactive example of using coding-agent via RpcClient. - * Usage: npx tsx test/rpc-example.ts + * Usage: bun test/rpc-example.ts */ async function main() { diff --git a/packages/coding-agent/test/sdk-codex-cache-probe-tool-loop.ts b/packages/coding-agent/test/sdk-codex-cache-probe-tool-loop.ts index 3c71c5f2541..1bfa6328ecb 100644 --- a/packages/coding-agent/test/sdk-codex-cache-probe-tool-loop.ts +++ b/packages/coding-agent/test/sdk-codex-cache-probe-tool-loop.ts @@ -1,4 +1,4 @@ -#!/usr/bin/env tsx +#!/usr/bin/env bun /** * Manual SDK probe for OpenAI Codex prompt caching through the tool loop. * @@ -123,7 +123,7 @@ function parseArgs(argv: string[]): Args { } function printHelp(): void { - console.log(`Usage: npx tsx test/sdk-codex-cache-probe-tool-loop.ts [options] + console.log(`Usage: bun test/sdk-codex-cache-probe-tool-loop.ts [options] Options: --turns Number of turns to run. Must be between ${MIN_TURNS} and ${MAX_TURNS}. Default: ${DEFAULT_TURNS} diff --git a/packages/coding-agent/test/session-manager-flush.test.ts b/packages/coding-agent/test/session-manager-flush.test.ts index 50ddafd5559..4448bd04fce 100644 --- a/packages/coding-agent/test/session-manager-flush.test.ts +++ b/packages/coding-agent/test/session-manager-flush.test.ts @@ -30,8 +30,8 @@ const fsMocks = vi.hoisted(() => ({ renameSync: vi.fn(), writeFileSync: vi.fn(), })); -vi.mock("node:fs", async (importOriginal) => { - const actual = await importOriginal(); +vi.mock("node:fs", () => { + const actual = require("node:fs"); fsMocks.actualWriteFileSync = actual.writeFileSync; fsMocks.chmodSync.mockImplementation(actual.chmodSync); fsMocks.chownSync.mockImplementation(actual.chownSync); diff --git a/packages/coding-agent/test/stdout-cleanliness.test.ts b/packages/coding-agent/test/stdout-cleanliness.test.ts index 95b057b9acd..b889e50a440 100644 --- a/packages/coding-agent/test/stdout-cleanliness.test.ts +++ b/packages/coding-agent/test/stdout-cleanliness.test.ts @@ -6,7 +6,6 @@ import { afterEach, describe, expect, it } from "vitest"; import { ENV_AGENT_DIR } from "../src/config.js"; const cliPath = resolve(__dirname, "../src/cli.ts"); -const tsxPath = resolve(__dirname, "../../../node_modules/tsx/dist/cli.mjs"); const tempDirs: string[] = []; @@ -55,12 +54,11 @@ async function runCli(args: string[]): Promise<{ stdout: string; stderr: string; ); return await new Promise((resolvePromise, reject) => { - const child = spawn(process.execPath, [tsxPath, cliPath, ...args], { + const child = spawn(process.execPath, [cliPath, ...args], { cwd: projectDir, env: { ...process.env, [ENV_AGENT_DIR]: agentDir, - TSX_TSCONFIG_PATH: resolve(__dirname, "../../../tsconfig.json"), }, stdio: ["ignore", "pipe", "pipe"], }); diff --git a/packages/coding-agent/test/streaming-render-debug.ts b/packages/coding-agent/test/streaming-render-debug.ts index ffd4cc89b3e..322c8ef59d2 100644 --- a/packages/coding-agent/test/streaming-render-debug.ts +++ b/packages/coding-agent/test/streaming-render-debug.ts @@ -1,7 +1,7 @@ /** * Debug script to reproduce streaming rendering issues. * Uses real fixture data that caused the bug. - * Run with: npx tsx test/streaming-render-debug.ts + * Run with: bun test/streaming-render-debug.ts */ import type { AssistantMessage } from "@earendil-works/pi-ai"; diff --git a/packages/coding-agent/test/suite/acp-features.test.ts b/packages/coding-agent/test/suite/acp-features.test.ts index 5e92bc424d5..33b0406f0cf 100644 --- a/packages/coding-agent/test/suite/acp-features.test.ts +++ b/packages/coding-agent/test/suite/acp-features.test.ts @@ -765,7 +765,7 @@ describe("ACP mode preserves prime-agent features", () => { expect(init.agentInfo).toMatchObject({ name: "prime-agent" }); expect(typeof init.agentInfo?.version).toBe("string"); // Namespaced only: unknown root keys are reserved for future ACP fields. - expect(init._meta).toHaveProperty(PRIME_AGENT_META_NAMESPACE); + expect(init._meta).toHaveProperty([PRIME_AGENT_META_NAMESPACE]); expect(Object.keys(init.agentCapabilities ?? {})).not.toContain("subagents"); // close is advertised, so a client knows it may release the session slot. expect(init.agentCapabilities?.sessionCapabilities?.close).toBeDefined(); diff --git a/packages/coding-agent/test/suite/acp-mode.test.ts b/packages/coding-agent/test/suite/acp-mode.test.ts index 7f386305b74..b7bc4ebb742 100644 --- a/packages/coding-agent/test/suite/acp-mode.test.ts +++ b/packages/coding-agent/test/suite/acp-mode.test.ts @@ -212,7 +212,7 @@ describe("ACP mode end to end", () => { }); expect(init.protocolVersion).toBe(acp.PROTOCOL_VERSION); expect(init.agentInfo?.name).toBe("prime-agent"); - expect(init._meta).toHaveProperty(PRIME_AGENT_META_NAMESPACE); + expect(init._meta).toHaveProperty([PRIME_AGENT_META_NAMESPACE]); const session = await client.request("session/new", { cwd: harness.tempDir, mcpServers: [] }); expect(typeof session.sessionId).toBe("string"); diff --git a/packages/coding-agent/test/suite/agent-session-action-races.test.ts b/packages/coding-agent/test/suite/agent-session-action-races.test.ts index 8239c1f2f26..1d55728e113 100644 --- a/packages/coding-agent/test/suite/agent-session-action-races.test.ts +++ b/packages/coding-agent/test/suite/agent-session-action-races.test.ts @@ -3,7 +3,7 @@ import { afterEach, describe, expect, it, vi } from "vitest"; import type { CustomMessage } from "../../src/core/messages.js"; import type { ActionStore, SessionAction } from "../../src/core/session-action-store.js"; import { createHarness, getMessageText, getUserTexts, type Harness } from "./harness.js"; -import { createDeferred } from "./scheduling.js"; +import { createDeferred, expectPromiseRejection } from "./scheduling.js"; type ActionKind = "turn" | "command"; @@ -225,7 +225,7 @@ describe("AgentSession action commit-fence races", () => { schedule.mockRestore(); internals._scheduleSessionInputPump(); await vi.waitFor(() => expect(internals._actionStore.unfinishedActions()[0]?.lifecycle.state).toBe("selected")); - const rejection = expect(completion).rejects.toThrow("cleared before delivery"); + const rejection = expectPromiseRejection(completion, "cleared before delivery"); expect(harness.session.clearQueue().followUp).toEqual([text]); heldFence.release(); diff --git a/packages/coding-agent/test/suite/agent-session-compaction-continuation.test.ts b/packages/coding-agent/test/suite/agent-session-compaction-continuation.test.ts index 06c9a215e40..bd577548112 100644 --- a/packages/coding-agent/test/suite/agent-session-compaction-continuation.test.ts +++ b/packages/coding-agent/test/suite/agent-session-compaction-continuation.test.ts @@ -16,6 +16,7 @@ import { Type } from "typebox"; import { afterEach, describe, expect, it, vi } from "vitest"; import type { AgentSession } from "../../src/core/agent-session.js"; import { createHarness, type Harness } from "./harness.js"; +import { expectPromiseRejection } from "./scheduling.js"; type SessionInternals = { _shouldStopAfterTurn: (context: ShouldStopAfterTurnContext) => boolean | Promise; @@ -250,7 +251,7 @@ describe("compaction continuation", () => { sessionInternals._schedulePostCompactionContinue(); const idle = harness.session.waitForHeadlessIdle(); - const rejectedIdle = expect(idle).rejects.toThrow("continuation failed"); + const rejectedIdle = expectPromiseRejection(idle, "continuation failed"); await vi.advanceTimersByTimeAsync(100); await rejectedIdle; diff --git a/packages/coding-agent/test/suite/agent-session-compaction.test.ts b/packages/coding-agent/test/suite/agent-session-compaction.test.ts index f19ce5b3049..044f4715f54 100644 --- a/packages/coding-agent/test/suite/agent-session-compaction.test.ts +++ b/packages/coding-agent/test/suite/agent-session-compaction.test.ts @@ -158,10 +158,10 @@ describe("AgentSession compaction characterization", () => { tokensBefore: result.tokensBefore, fromHook: false, }); - expect(harness.session.messages[0]).toMatchObject({ - role: "compactionSummary", - summary: expect.stringContaining("model-generated summary"), - }); + const compactedMessage = harness.session.messages[0]; + expect(compactedMessage?.role).toBe("compactionSummary"); + if (compactedMessage?.role !== "compactionSummary") throw new Error("Expected compaction summary"); + expect(compactedMessage.summary).toContain("model-generated summary"); expect(harness.eventsOfType("compaction_start")).toEqual([expect.objectContaining({ reason: "manual" })]); expect(harness.eventsOfType("compaction_end")).toEqual([ expect.objectContaining({ @@ -1336,7 +1336,6 @@ describe("AgentSession compaction characterization", () => { }); it("waits for threshold-compaction autonomous continuations before finishing prompt", async () => { - vi.useFakeTimers(); const harness = await createHarness({ autonomous: { enabled: true, @@ -1356,7 +1355,6 @@ describe("AgentSession compaction characterization", () => { const promptPromise = harness.session.prompt("make the change"); await vi.waitFor(() => expect(harness.session.getAutonomousStatus().continuationsUsed).toBe(1)); - await vi.advanceTimersByTimeAsync(100); await promptPromise; expect(harness.session.getAutonomousStatus()).toMatchObject({ @@ -1475,12 +1473,12 @@ describe("AgentSession compaction characterization", () => { internals._persistCompactionOutcome("requested", "failed", "Requested compaction failed"), ).not.toThrow(); // The live outcome message discloses that it was not saved. - expect(harness.session.messages.at(-1)).toMatchObject({ - role: "custom", - customType: "compaction_outcome", - content: expect.stringContaining("could not be saved to session history"), - details: { reason: "requested", outcome: "failed" }, - }); + const liveOutcome = harness.session.messages.at(-1); + expect(liveOutcome?.role).toBe("custom"); + if (liveOutcome?.role !== "custom") throw new Error("Expected custom compaction outcome"); + expect(liveOutcome.customType).toBe("compaction_outcome"); + expect(liveOutcome.content).toContain("could not be saved to session history"); + expect(liveOutcome.details).toEqual({ reason: "requested", outcome: "failed" }); // In-memory state is fully rolled back: no outcome entry, same leaf and entries. expect(harness.sessionManager.getLeafId()).toBe(persistedLeafId); expect(harness.sessionManager.getEntries()).toEqual(persistedEntries); @@ -1497,11 +1495,11 @@ describe("AgentSession compaction characterization", () => { ); // The unpersisted disclosure survives context rebuilds (e.g. thinking toggle). const rebuilt = harness.session.buildSessionContext(); - expect(rebuilt.messages.at(-1)).toMatchObject({ - role: "custom", - customType: "compaction_outcome", - content: expect.stringContaining("could not be saved to session history"), - }); + const rebuiltOutcome = rebuilt.messages.at(-1); + expect(rebuiltOutcome?.role).toBe("custom"); + if (rebuiltOutcome?.role !== "custom") throw new Error("Expected custom compaction outcome"); + expect(rebuiltOutcome.customType).toBe("compaction_outcome"); + expect(rebuiltOutcome.content).toContain("could not be saved to session history"); // Cross a millisecond boundary so the later turn's timestamp is strictly newer. await new Promise((resolve) => setTimeout(resolve, 5)); diff --git a/packages/coding-agent/test/suite/agent-session-prompt.test.ts b/packages/coding-agent/test/suite/agent-session-prompt.test.ts index 8ade5795d07..eaf1ba69034 100644 --- a/packages/coding-agent/test/suite/agent-session-prompt.test.ts +++ b/packages/coding-agent/test/suite/agent-session-prompt.test.ts @@ -10,7 +10,7 @@ import type { PromptTemplate } from "../../src/core/prompt-templates.js"; import { createSyntheticSourceInfo } from "../../src/core/source-info.js"; import { createTestResourceLoader } from "../utilities.js"; import { createHarness, getAssistantTexts, getMessageText, getUserTexts, type Harness } from "./harness.js"; -import { createDeferred, createWaitingHarness, gatedHook } from "./scheduling.js"; +import { createDeferred, createWaitingHarness, expectPromiseRejection, gatedHook } from "./scheduling.js"; function gateNextAgentStart(harness: Harness): { reached: Promise; release(): void } { let markReached = () => {}; @@ -1166,8 +1166,8 @@ stale post-hook extension instructions`, const delivery = harness.session.waitForAgentMessagePromptDelivery(agentMessageId); const accepted = harness.session.acceptAgentMessagePrompt(agentPrompt, { expandPromptTemplates: false }); - const acceptedRejection = expect(accepted).rejects.toThrow("cleared before delivery"); - const deliveryRejection = expect(delivery).rejects.toThrow("cleared before delivery"); + const acceptedRejection = expectPromiseRejection(accepted, "cleared before delivery"); + const deliveryRejection = expectPromiseRejection(delivery, "cleared before delivery"); await admitted; expect(harness.session.clearQueuedUserMessagesMatching((text) => text.includes(agentMessageId))).toEqual({ @@ -1232,7 +1232,7 @@ stale post-hook extension instructions`, const prompt = "Agent-to-agent message received.\nSource: agent_message\nTo: Target, active target, session session-target\nMessage id: agentmsg_idle_cleanup\n\ncancel me"; const accepted = harness.session.acceptAgentMessagePrompt(prompt, { expandPromptTemplates: false }); - const rejected = expect(accepted).rejects.toThrow("cleared before delivery"); + const rejected = expectPromiseRejection(accepted, "cleared before delivery"); await Promise.all([eventQueueReached.promise, dispatchGate.reached]); harness.session.clearQueuedUserMessagesMatching((text) => text === prompt); @@ -2053,8 +2053,8 @@ stale post-hook extension instructions`, followUp: [clearedAgentPrompt], }); admission.release(); - await expect(accepted).rejects.toThrow("cleared before delivery"); - await expect(delivery).rejects.toThrow("cleared before delivery"); + await expectPromiseRejection(accepted, "cleared before delivery"); + await expectPromiseRejection(delivery, "cleared before delivery"); await harness.session.agent.waitForIdle(); await (harness.session as unknown as { _agentEventQueue: Promise })._agentEventQueue; const persistedAfter = harness.sessionManager.getEntries().filter((entry) => entry.type === "message").length; diff --git a/packages/coding-agent/test/suite/agent-session-queue.test.ts b/packages/coding-agent/test/suite/agent-session-queue.test.ts index 14e31404a16..e800414d79b 100644 --- a/packages/coding-agent/test/suite/agent-session-queue.test.ts +++ b/packages/coding-agent/test/suite/agent-session-queue.test.ts @@ -30,7 +30,13 @@ import { } from "../../src/core/refinement/index.js"; import { parseSessionSlashCommand } from "../../src/core/slash-commands.js"; import { createHarness, getAssistantTexts, getMessageText, getUserTexts, type Harness } from "./harness.js"; -import { createDeferred, createWaitingHarness, gatedHook, withStreaming } from "./scheduling.js"; +import { + createDeferred, + createWaitingHarness, + expectPromiseRejection, + gatedHook, + withStreaming, +} from "./scheduling.js"; type AutoRefineReason = "turn_interval" | "compact"; @@ -1554,7 +1560,7 @@ describe("AgentSession queue characterization", () => { { customType: "hidden-trigger", content: "hidden queued prompt", display: false }, { triggerTurn: true }, ); - const hiddenRejection = expect(hidden).rejects.toThrow("Prompt aborted before delivery."); + const hiddenRejection = expectPromiseRejection(hidden, "Prompt aborted before delivery."); await vi.waitFor(() => expect(harness.session.getSessionActionRecoverySnapshot().actions).toHaveLength(2)); expect(harness.session.getFollowUpMessages()).toEqual(["visible queued prompt"]); @@ -2023,8 +2029,8 @@ describe("AgentSession queue characterization", () => { streamingBehavior: "followUp", resumeIfIdle: true, }); - const firstCompletionRejection = expect(firstCompletion).rejects.toThrow("cleared before delivery"); - const completionRejection = expect(completion).rejects.toThrow("cleared before delivery"); + const firstCompletionRejection = expectPromiseRejection(firstCompletion, "cleared before delivery"); + const completionRejection = expectPromiseRejection(completion, "cleared before delivery"); withStreaming(harness, false); await waitForPreparation; gatePreparation = false; @@ -2062,7 +2068,7 @@ describe("AgentSession queue characterization", () => { streamingBehavior: "followUp", resumeIfIdle: true, }); - const terminalRejection = expect(terminalCompletion).rejects.toThrow("No API key"); + const terminalRejection = expectPromiseRejection(terminalCompletion, "No API key"); await vi.waitFor(() => expect(authHarness.session.getFollowUpMessages()).toEqual(["cannot start"])); withStreaming(authHarness, false); await authHarness.session.waitForSessionInputIdle(); @@ -2237,10 +2243,11 @@ describe("AgentSession queue characterization", () => { const id = "agentmsg_command_append_failed"; const delivery = harness.session.waitForAgentMessagePromptDelivery(id); const completion = harness.session.promptAndWait("/autonomous status", { agentMessageId: id }); + const deliveryFailure = expectPromiseRejection(delivery, "durable invocation append failed"); + const completionFailure = expectPromiseRejection(completion, "durable invocation append failed"); pause.release(); - await expect(delivery).rejects.toThrow("durable invocation append failed"); - await expect(completion).rejects.toThrow("durable invocation append failed"); + await Promise.all([deliveryFailure, completionFailure]); // The failed append must roll back fully: no live-only command message and // no unsaved leaf, so later durable entries persist cleanly. @@ -2900,7 +2907,7 @@ describe("AgentSession queue characterization", () => { const lateAgentMessage = harness.session.acceptAgentMessagePrompt( agentPromptText("agentmsg_after_abort", "late child result"), ); - const lateRejection = expect(lateAgentMessage).rejects.toThrow("queued session input is suspended"); + const lateRejection = expectPromiseRejection(lateAgentMessage, "queued session input is suspended"); await new Promise((resolve) => setTimeout(resolve, 0)); await harness.session.prompt("explicit user resume"); @@ -2916,7 +2923,7 @@ describe("AgentSession queue characterization", () => { const lateAgentMessage = harness.session.acceptAgentMessagePrompt( agentPromptText("agentmsg_resume_queue", "late child result"), ); - const lateRejection = expect(lateAgentMessage).rejects.toThrow("queued session input is suspended"); + const lateRejection = expectPromiseRejection(lateAgentMessage, "queued session input is suspended"); await new Promise((resolve) => setTimeout(resolve, 0)); harness.session.resumeQueuedWork(); @@ -3251,7 +3258,8 @@ describe("AgentSession scheduler scenarios", () => { await harness.session.queueAgentMessagePrompt(agentPrompt, "steer"); // Phase 3: keyed duplicates reject both agent-message outcome legs. - const dupDelivery = expect(harness.session.waitForAgentMessagePromptDelivery("agentmsg_s2_dup")).rejects.toThrow( + const dupDelivery = expectPromiseRejection( + harness.session.waitForAgentMessagePromptDelivery("agentmsg_s2_dup"), "equivalent follow-up is already pending", ); await expect( @@ -3528,9 +3536,10 @@ describe("AgentSession scheduler scenarios", () => { const failedId = "agentmsg_failed_command"; const failedDelivery = harness.session.waitForAgentMessagePromptDelivery(failedId); const failedCompletion = harness.session.promptAndWait("/refine --local", { agentMessageId: failedId }); + const failedCompletionRejection = expectPromiseRejection(failedCompletion, "refine execution failed"); failedPause.release(); await expect(failedDelivery).resolves.toBeUndefined(); - await expect(failedCompletion).rejects.toThrow("refine execution failed"); + await failedCompletionRejection; }); it("S6: auto-refine reviews after real turns, defers while busy, and drops reviews on navigation", async () => { diff --git a/packages/coding-agent/test/suite/daemon-serialized-refine-process.test.ts b/packages/coding-agent/test/suite/daemon-serialized-refine-process.test.ts index 6b25ce1178f..cd5fc9215ae 100644 --- a/packages/coding-agent/test/suite/daemon-serialized-refine-process.test.ts +++ b/packages/coding-agent/test/suite/daemon-serialized-refine-process.test.ts @@ -43,8 +43,6 @@ import { } from "../../src/modes/daemon/daemon-worker-protocol.js"; const cliPath = resolve(__dirname, "../../src/cli.ts"); -const tsxPath = resolve(__dirname, "../../../../node_modules/tsx/dist/cli.mjs"); -const repoTsconfigPath = resolve(__dirname, "../../../../tsconfig.json"); const fauxRefineExtensionPath = resolve(__dirname, "../fixtures/eng-4685-faux-refine-extension.ts"); const eventOrderExtensionPath = resolve(__dirname, "../fixtures/eng-4685-event-order-extension.ts"); const children = new Set(); @@ -83,10 +81,9 @@ async function runCli( args: string[], options: { agentDir: string; stdin?: string; environment?: NodeJS.ProcessEnv }, ): Promise<{ code: number | null; signal: NodeJS.Signals | null; stdout: string; stderr: string }> { - const child = spawn(process.execPath, [tsxPath, cliPath, ...args], { + const child = spawn(process.execPath, [cliPath, ...args], { env: { ...process.env, - TSX_TSCONFIG_PATH: repoTsconfigPath, [ENV_AGENT_DIR]: options.agentDir, PI_SKIP_VERSION_CHECK: "1", PRIME_AGENT_INTERNAL_LEGACY_OWNED_WORKER_FRONTEND: "0", diff --git a/packages/coding-agent/test/suite/regressions/2791-fswatch-error-crash.test.ts b/packages/coding-agent/test/suite/regressions/2791-fswatch-error-crash.test.ts index 81a7a268234..2fea180660e 100644 --- a/packages/coding-agent/test/suite/regressions/2791-fswatch-error-crash.test.ts +++ b/packages/coding-agent/test/suite/regressions/2791-fswatch-error-crash.test.ts @@ -50,18 +50,14 @@ describe("issue #2791 fs.watch error event crashes process", () => { writeFileSync( scriptPath, ` -import { setTheme, stopThemeWatcher } from "${themeModulePath}"; - process.env[${JSON.stringify(ENV_AGENT_DIR)}] = ${JSON.stringify(agentDir)}; +const { getActiveThemeWatcher, setTheme, stopThemeWatcher } = await import("${themeModulePath}"); setTheme("custom-test", true); - -// Find the FSWatcher among active handles -const handles = (process as any)._getActiveHandles(); -const fsWatcher = handles.find((h: any) => h.constructor?.name === "FSWatcher"); +const fsWatcher = getActiveThemeWatcher(); if (!fsWatcher) { - process.stderr.write("no FSWatcher found among active handles\\n"); + process.stderr.write("theme did not create an FSWatcher\\n"); process.exit(2); } @@ -88,7 +84,7 @@ process.exit(0); let stderr = ""; let exitCode: number; try { - _stdout = execFileSync("npx", ["tsx", scriptPath], { + _stdout = execFileSync(process.execPath, [scriptPath], { timeout: 10000, encoding: "utf-8", env: { ...process.env, [ENV_AGENT_DIR]: agentDir }, diff --git a/packages/coding-agent/test/suite/regressions/4600-supervisor-singleton.test.ts b/packages/coding-agent/test/suite/regressions/4600-supervisor-singleton.test.ts index c526a4c4435..81613ce5442 100644 --- a/packages/coding-agent/test/suite/regressions/4600-supervisor-singleton.test.ts +++ b/packages/coding-agent/test/suite/regressions/4600-supervisor-singleton.test.ts @@ -70,8 +70,6 @@ interface CleanupProcessIdentity { const fixturePath = resolve(__dirname, "../../fixtures/eng-4600-supervisor-fixture.ts"); const fauxExtensionPath = resolve(__dirname, "../../fixtures/eng-4600-faux-extension.ts"); const cliPath = resolve(__dirname, "../../../src/cli.ts"); -const tsxPath = resolve(__dirname, "../../../../../node_modules/tsx/dist/cli.mjs"); -const tsconfigPath = resolve(__dirname, "../../../../../tsconfig.json"); const supervisorRegistryDirEnv = "PRIME_AGENT_INTERNAL_DAEMON_SUPERVISOR_REGISTRY_DIR"; const handles = new Set(); const harnesses: Harness[] = []; @@ -128,7 +126,7 @@ function spawnFixture( paths: { agentDir: string; descriptorDir: string; registryDir: string; socketPath: string }, options: { extraEnv?: NodeJS.ProcessEnv; generation?: string } = {}, ): FixtureHandle { - const child = spawn(process.execPath, [tsxPath, fixturePath], { + const child = spawn(process.execPath, [fixturePath], { cwd: paths.agentDir, env: { ...process.env, @@ -142,7 +140,6 @@ function spawnFixture( ENG_4600_REGISTRY_DIR: paths.registryDir, ENG_4600_SOCKET_PATH: paths.socketPath, PI_OFFLINE: "1", - TSX_TSCONFIG_PATH: tsconfigPath, }, stdio: ["ignore", "pipe", "pipe", "ipc"], }); @@ -169,7 +166,7 @@ function spawnRealSupervisor( ): FixtureHandle { const child = spawn( process.execPath, - [tsxPath, cliPath, "--mode", "daemon", "--daemon-socket", paths.socketPath, "--offline"], + [cliPath, "--mode", "daemon", "--daemon-socket", paths.socketPath, "--offline"], { cwd: paths.agentDir, env: { @@ -178,7 +175,6 @@ function spawnRealSupervisor( [supervisorRegistryDirEnv]: paths.registryDir, [ENV_AGENT_DIR]: paths.agentDir, PI_OFFLINE: "1", - TSX_TSCONFIG_PATH: tsconfigPath, }, stdio: ["ignore", "pipe", "pipe"], }, diff --git a/packages/coding-agent/test/suite/regressions/4602-snapshot-transfer-idempotency.test.ts b/packages/coding-agent/test/suite/regressions/4602-snapshot-transfer-idempotency.test.ts index 56b99ebe0b8..0b7c6e3d30e 100644 --- a/packages/coding-agent/test/suite/regressions/4602-snapshot-transfer-idempotency.test.ts +++ b/packages/coding-agent/test/suite/regressions/4602-snapshot-transfer-idempotency.test.ts @@ -20,6 +20,8 @@ import { SnapshotTranscriptCache } from "../../../src/modes/daemon/snapshot-tran import { type PrivateFrame, PrivateFrameDecoder } from "../../../src/modes/session-worker/private-framing.js"; import { seedSupervisorRoster } from "../../fixtures/roster-seed.js"; +import { expectPromiseRejection } from "../scheduling.js"; + const activeSessionId = "active-4602"; const snapshotId = "snapshot-4602"; @@ -526,7 +528,7 @@ describe("ENG-4602 snapshot transfer containment", () => { const unhandled = vi.fn(); process.on("unhandledRejection", unhandled); try { - const validationFailure = expect(validation).rejects.toThrow("stopped during snapshot transfer"); + const validationFailure = expectPromiseRejection(validation, "stopped during snapshot transfer"); await internals.stopWorker(worker, false); await validationFailure; await catchup; diff --git a/packages/coding-agent/test/suite/regressions/4603-worker-recovery.test.ts b/packages/coding-agent/test/suite/regressions/4603-worker-recovery.test.ts index 19ab4a7f43c..f362045402f 100644 --- a/packages/coding-agent/test/suite/regressions/4603-worker-recovery.test.ts +++ b/packages/coding-agent/test/suite/regressions/4603-worker-recovery.test.ts @@ -8,6 +8,7 @@ import { readFileSync, renameSync, rmSync, + symlinkSync, writeFileSync, } from "node:fs"; import { createConnection, type Socket } from "node:net"; @@ -93,8 +94,6 @@ interface TestPaths { const fixturePath = resolve(__dirname, "../../fixtures/eng-4600-supervisor-fixture.ts"); const fauxExtensionPath = resolve(__dirname, "../../fixtures/eng-4600-faux-extension.ts"); const cliPath = resolve(__dirname, "../../../src/cli.ts"); -const tsxPath = resolve(__dirname, "../../../../../node_modules/tsx/dist/cli.mjs"); -const tsconfigPath = resolve(__dirname, "../../../../../tsconfig.json"); const supervisorRegistryDirEnv = "PRIME_AGENT_INTERNAL_DAEMON_SUPERVISOR_REGISTRY_DIR"; const handles = new Set(); const harnesses: Harness[] = []; @@ -126,7 +125,14 @@ async function createPaths(): Promise { const harness = await createHarness(); harnesses.push(harness); const executablePath = join(harness.tempDir, APP_NAME); - linkSync(process.execPath, executablePath); + try { + linkSync(process.execPath, executablePath); + } catch (error) { + if ((error as NodeJS.ErrnoException).code !== "EPERM" && (error as NodeJS.ErrnoException).code !== "EXDEV") { + throw error; + } + symlinkSync(process.execPath, executablePath); + } const socketTmpDir = `/tmp/eng-4603-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2)}`; mkdirSync(socketTmpDir, { recursive: true, mode: 0o700 }); socketTempDirs.add(socketTmpDir); @@ -147,7 +153,7 @@ async function createPaths(): Promise { function spawnSupervisor(paths: TestPaths): ProcessHandle { return trackProcess( - spawn(paths.executablePath, [tsxPath, fixturePath], { + spawn(paths.executablePath, [fixturePath], { cwd: paths.agentDir, env: { ...process.env, @@ -160,7 +166,6 @@ function spawnSupervisor(paths: TestPaths): ProcessHandle { ENG_4600_SOCKET_PATH: paths.socketPath, PI_OFFLINE: "1", TMPDIR: paths.socketTmpDir, - TSX_TSCONFIG_PATH: tsconfigPath, }, stdio: ["ignore", "pipe", "pipe", "ipc"], }), @@ -175,26 +180,21 @@ function spawnStandaloneWorker( extraEnv: NodeJS.ProcessEnv = {}, ): ProcessHandle { return trackProcess( - spawn( - paths.executablePath, - [tsxPath, cliPath, "--mode", "daemon", "--daemon-socket", workerSocketPath, "--offline"], - { - cwd: paths.agentDir, - env: { - ...process.env, - ...extraEnv, - [supervisorRegistryDirEnv]: paths.registryDir, - [ENV_AGENT_DIR]: paths.agentDir, - [DAEMON_WORKER_ROLE_ENV]: "1", - [DAEMON_WORKER_TOKEN_ENV]: token, - [DAEMON_WORKER_ACTIVE_SESSION_ID_ENV]: "eng-4603-worker", - [DAEMON_WORKER_SUPERVISOR_SOCKET_ENV]: paths.socketPath, - PI_OFFLINE: "1", - TSX_TSCONFIG_PATH: tsconfigPath, - }, - stdio: ["ignore", "pipe", "pipe"], + spawn(paths.executablePath, [cliPath, "--mode", "daemon", "--daemon-socket", workerSocketPath, "--offline"], { + cwd: paths.agentDir, + env: { + ...process.env, + ...extraEnv, + [supervisorRegistryDirEnv]: paths.registryDir, + [ENV_AGENT_DIR]: paths.agentDir, + [DAEMON_WORKER_ROLE_ENV]: "1", + [DAEMON_WORKER_TOKEN_ENV]: token, + [DAEMON_WORKER_ACTIVE_SESSION_ID_ENV]: "eng-4603-worker", + [DAEMON_WORKER_SUPERVISOR_SOCKET_ENV]: paths.socketPath, + PI_OFFLINE: "1", }, - ), + stdio: ["ignore", "pipe", "pipe"], + }), "worker", ); } @@ -677,7 +677,7 @@ async function runCli( extraEnv: NodeJS.ProcessEnv = {}, ): Promise<{ code: number; stdout: string; stderr: string }> { const handle = trackProcess( - spawn(process.execPath, [tsxPath, cliPath, ...args], { + spawn(process.execPath, [cliPath, ...args], { cwd: paths.agentDir, env: { ...process.env, @@ -686,7 +686,6 @@ async function runCli( [ENV_AGENT_DIR]: paths.agentDir, PI_OFFLINE: "1", TMPDIR: paths.socketTmpDir, - TSX_TSCONFIG_PATH: tsconfigPath, }, stdio: ["ignore", "pipe", "pipe"], }), @@ -1066,7 +1065,9 @@ describe("ENG-4603 worker recovery convergence", () => { expect(listenersBeforeShutdown).toContain(`p${successor.child.pid}`); const shutdown = await runCli(paths, ["shutdown", "--force", "--json"], 60_000, lsofEnvironment); - expect(shutdown.code).toBe(0); + if (shutdown.code !== 0) { + throw new Error(`Shutdown failed with code ${shutdown.code}: ${shutdown.stderr}\n${shutdown.stdout}`); + } const shutdownResult = JSON.parse(shutdown.stdout) as { stopped: unknown[]; failed: unknown[] }; const survivingIdentities = [ { pid: predecessor.child.pid!, processStartId: predecessorStartId }, diff --git a/packages/coding-agent/test/suite/regressions/4606-update-restart-coordinator.test.ts b/packages/coding-agent/test/suite/regressions/4606-update-restart-coordinator.test.ts index 26ce76e1b50..4546033b2eb 100644 --- a/packages/coding-agent/test/suite/regressions/4606-update-restart-coordinator.test.ts +++ b/packages/coding-agent/test/suite/regressions/4606-update-restart-coordinator.test.ts @@ -29,8 +29,6 @@ interface SupervisorOwnerRecord { const cliPath = resolve(__dirname, "../../../src/cli.ts"); const fauxExtensionPath = resolve(__dirname, "../../fixtures/eng-4600-faux-extension.ts"); const launcherFixturePath = resolve(__dirname, "../../fixtures/eng-4606-update-launcher.ts"); -const tsxPath = resolve(__dirname, "../../../../../node_modules/tsx/dist/cli.mjs"); -const tsconfigPath = resolve(__dirname, "../../../../../tsconfig.json"); const supervisorRegistryDirEnv = "PRIME_AGENT_INTERNAL_DAEMON_SUPERVISOR_REGISTRY_DIR"; const supervisors = new Set(); const harnesses: Harness[] = []; @@ -49,7 +47,7 @@ function spawnSupervisor(paths: { }): SupervisorHandle { const child = spawn( process.execPath, - [tsxPath, cliPath, "--mode", "daemon", "--daemon-socket", paths.socketPath, "--offline"], + [cliPath, "--mode", "daemon", "--daemon-socket", paths.socketPath, "--offline"], { cwd: paths.agentDir, env: { @@ -61,9 +59,7 @@ function spawnSupervisor(paths: { ENG_4606_COMPLETION_PATH: paths.completionPath, ENG_4606_PID_PATH: paths.pidPath, ENG_4606_SOCKET_PATH: paths.socketPath, - ENG_4606_TSX_PATH: tsxPath, PI_OFFLINE: "1", - TSX_TSCONFIG_PATH: tsconfigPath, }, stdio: ["ignore", "pipe", "pipe"], }, @@ -190,21 +186,11 @@ async function withSourceCliEntrypoint(action: () => Promise): Promise if (!previousEntrypoint) { throw new Error("Test process has no CLI entrypoint"); } - const previousExecArgv = [...process.execArgv]; - const previousTsconfigPath = process.env.TSX_TSCONFIG_PATH; process.argv[1] = cliPath; - process.execArgv.splice(0, process.execArgv.length, tsxPath); - process.env.TSX_TSCONFIG_PATH = tsconfigPath; try { return await action(); } finally { process.argv[1] = previousEntrypoint; - process.execArgv.splice(0, process.execArgv.length, ...previousExecArgv); - if (previousTsconfigPath === undefined) { - delete process.env.TSX_TSCONFIG_PATH; - } else { - process.env.TSX_TSCONFIG_PATH = previousTsconfigPath; - } } } @@ -276,9 +262,10 @@ describe("ENG-4606 update restart coordinator", () => { vi.useFakeTimers(); try { - vi.setSystemTime(new Date("2026-07-14T00:00:00.000Z")); - const writer = new DaemonUpdateRestartStatusWriter(statusPath, "test-request", "/tmp/daemon.sock"); + let now = new Date("2026-07-14T00:00:00.000Z"); + const writer = new DaemonUpdateRestartStatusWriter(statusPath, "test-request", "/tmp/daemon.sock", () => now); const stopHeartbeat = writer.startHeartbeat(); + now = new Date("2026-07-14T00:00:05.000Z"); vi.advanceTimersByTime(5000); stopHeartbeat(); @@ -385,7 +372,7 @@ describe("ENG-4606 update restart coordinator", () => { ), ); const originalActiveSessionId = created.activeSessionId ?? created.id; - const updateCommand = [process.execPath, tsxPath, launcherFixturePath].map(shellQuote).join(" "); + const updateCommand = [process.execPath, launcherFixturePath].map(shellQuote).join(" "); const executeResponse = await client.request( { type: "execute_bash", activeSessionId: originalActiveSessionId, command: updateCommand }, 5000, diff --git a/packages/coding-agent/test/suite/regressions/4685-daemon-client-modes.test.ts b/packages/coding-agent/test/suite/regressions/4685-daemon-client-modes.test.ts index 1dee7be89c2..2d05b0720be 100644 --- a/packages/coding-agent/test/suite/regressions/4685-daemon-client-modes.test.ts +++ b/packages/coding-agent/test/suite/regressions/4685-daemon-client-modes.test.ts @@ -4,7 +4,7 @@ import { tmpdir } from "node:os"; import { join, resolve } from "node:path"; import { fauxAssistantMessage } from "@earendil-works/pi-ai"; import { afterEach, describe, expect, it, vi } from "vitest"; -import { createCliSubprocessEnv, createCliSubprocessLaunchSpec } from "../../../src/cli/subprocess-launch.js"; +import { createCliSubprocessLaunchSpec } from "../../../src/cli/subprocess-launch.js"; import { ENV_AGENT_DIR } from "../../../src/config.js"; import type { AutonomousRuntimeState } from "../../../src/core/autonomous.js"; import type { DaemonSocketClient } from "../../../src/modes/daemon/active-session-state.js"; @@ -19,8 +19,6 @@ const fixturePath = resolve(__dirname, "../../fixtures/rpc-connection-mode-fixtu const fauxExtensionPath = resolve(__dirname, "../../fixtures/eng-4600-faux-extension.ts"); const rpcEofFauxExtensionPath = resolve(__dirname, "../../fixtures/rpc-eof-faux-extension.ts"); const cliPath = resolve(__dirname, "../../../src/cli.ts"); -const tsxPath = resolve(__dirname, "../../../../../node_modules/tsx/dist/cli.mjs"); -const repoTsconfigPath = resolve(__dirname, "../../../../../tsconfig.json"); const children = new Set(); const harnesses: Harness[] = []; const daemonSockets = new Set(); @@ -68,10 +66,9 @@ async function runCli( args: string[], options: { agentDir: string; stdin?: string; environment?: NodeJS.ProcessEnv }, ): Promise { - const child = spawn(process.execPath, [tsxPath, cliPath, ...args], { + const child = spawn(process.execPath, [cliPath, ...args], { env: { ...process.env, - TSX_TSCONFIG_PATH: repoTsconfigPath, [ENV_AGENT_DIR]: options.agentDir, PI_SKIP_VERSION_CHECK: "1", PRIME_AGENT_INTERNAL_LEGACY_OWNED_WORKER_FRONTEND: "0", @@ -114,8 +111,8 @@ async function runRpc( commands: unknown[], options: { trailingNewline?: boolean } = {}, ): Promise<{ stdout: object[]; stderr: string }> { - const child = spawn(process.execPath, [tsxPath, fixturePath], { - env: { ...process.env, TSX_TSCONFIG_PATH: repoTsconfigPath }, + const child = spawn(process.execPath, [fixturePath], { + env: { ...process.env }, stdio: ["pipe", "pipe", "pipe"], }); children.add(child); @@ -233,12 +230,9 @@ describe("ENG-4685 daemon-backed client modes", () => { }); it("resolves source subprocesses independently of a spaced runtime cwd", () => { - const entrypoint = resolve(__dirname, "../../../src/cli.ts"); const launch = createCliSubprocessLaunchSpec([], process.execPath, [], "packages/coding-agent/src/cli.ts"); - const environment = createCliSubprocessEnv({}, entrypoint, ["--import", "tsx"]); expect(launch.args[0]).toBe(resolve("packages/coding-agent/src/cli.ts")); - expect(environment.TSX_TSCONFIG_PATH).toBe(repoTsconfigPath); }); it("launches real daemon workers for every migrated client surface", async () => { diff --git a/packages/coding-agent/test/suite/regressions/617-subagent-terminal-agent-message.test.ts b/packages/coding-agent/test/suite/regressions/617-subagent-terminal-agent-message.test.ts index 7f04a5169fd..ad7b6bec6e3 100644 --- a/packages/coding-agent/test/suite/regressions/617-subagent-terminal-agent-message.test.ts +++ b/packages/coding-agent/test/suite/regressions/617-subagent-terminal-agent-message.test.ts @@ -50,7 +50,7 @@ describe("#617 subagent terminal agent messages", () => { const spawned = await parent.session.runRlmChild("finish without replying", { name: childSessionName }); - await expect.poll(() => terminalNotices(parent!.session.messages)).toHaveLength(1); + await vi.waitFor(() => expect(terminalNotices(parent!.session.messages)).toHaveLength(1)); expect(sendAgentMessage).not.toHaveBeenCalled(); expect(terminalNotices(parent.session.messages)[0]).toMatchObject({ customType: "rlm_child_terminal_notice", diff --git a/packages/coding-agent/test/suite/regressions/623-acp-canonical-cwd.test.ts b/packages/coding-agent/test/suite/regressions/623-acp-canonical-cwd.test.ts index c975a5a1165..ae0e0f8bc2c 100644 --- a/packages/coding-agent/test/suite/regressions/623-acp-canonical-cwd.test.ts +++ b/packages/coding-agent/test/suite/regressions/623-acp-canonical-cwd.test.ts @@ -88,7 +88,7 @@ describe("#623 ACP canonical cwd comparison", () => { expect(cwdMeta(created)).toBeUndefined(); }, 30_000); - it.runIf(caseVariantCwd !== undefined)( + it.skipIf(caseVariantCwd === undefined)( "uses filesystem identity when realpath preserves different component casing", async () => { if (!caseVariantCwd) throw new Error("Expected a case-only cwd variant"); diff --git a/packages/coding-agent/test/suite/scheduling.ts b/packages/coding-agent/test/suite/scheduling.ts index 7d6f8cf07ab..489eb64f35c 100644 --- a/packages/coding-agent/test/suite/scheduling.ts +++ b/packages/coding-agent/test/suite/scheduling.ts @@ -20,6 +20,22 @@ export function createDeferred(): Deferred { return { promise, resolve, reject }; } +/** Attach an immediate rejection handler without Bun's pending `.rejects` matcher deadlock. */ +export function expectPromiseRejection(promise: Promise, expected: string | RegExp): Promise { + return promise.then( + () => { + throw new Error(`Expected promise to reject with ${String(expected)}`); + }, + (error: unknown) => { + const message = error instanceof Error ? error.message : String(error); + const matches = typeof expected === "string" ? message.includes(expected) : expected.test(message); + if (!matches) { + throw new Error(`Expected rejection ${String(expected)}, received: ${message}`); + } + }, + ); +} + /** * A before_agent_start gate as an extension factory: `reached` resolves when the * hook first runs (optionally filtered by prompt), `release()` lets gated runs diff --git a/packages/coding-agent/test/suite/serialized-refine-config-integration.test.ts b/packages/coding-agent/test/suite/serialized-refine-config-integration.test.ts index 17d0d8d781c..9fb9710ddac 100644 --- a/packages/coding-agent/test/suite/serialized-refine-config-integration.test.ts +++ b/packages/coding-agent/test/suite/serialized-refine-config-integration.test.ts @@ -162,7 +162,7 @@ describe("Serialized refine controller availability (unit)", () => { expect(internals._rlmHeartbeatController).toBe(fakeController); expect(internals._ipythonKernelProvisioner).not.toBe(initialProvisioner); - expect(internals._createKernelHostHandlers()).toHaveProperty("rlm_heartbeat.create"); + expect(internals._createKernelHostHandlers()).toHaveProperty(["rlm_heartbeat.create"]); // Verify the controller is usable via host request. const result = harness.session.handleRlmHeartbeatHostRequest("rlm_heartbeat.list"); diff --git a/packages/coding-agent/test/test-tags.ts b/packages/coding-agent/test/test-tags.ts new file mode 100644 index 00000000000..6e899aa646d --- /dev/null +++ b/packages/coding-agent/test/test-tags.ts @@ -0,0 +1,6 @@ +export function isTestTagEnabled(tag: string): boolean { + return (process.env.PRIME_AGENT_TEST_TAGS ?? "") + .split(",") + .map((value) => value.trim()) + .includes(tag); +} diff --git a/packages/coding-agent/test/test-theme-colors.ts b/packages/coding-agent/test/test-theme-colors.ts index 7ee88732aa7..21e8a709f50 100644 --- a/packages/coding-agent/test/test-theme-colors.ts +++ b/packages/coding-agent/test/test-theme-colors.ts @@ -241,7 +241,7 @@ if (cmd === "contrast") { cmdTheme(cmd); } else { console.log("Usage:"); - console.log(" npx tsx test-theme-colors.ts light|dark Test built-in theme"); - console.log(" npx tsx test-theme-colors.ts contrast 4.5 Compute colors at ratio"); - console.log(" npx tsx test-theme-colors.ts test file.json Test any JSON file"); + console.log(" bun test-theme-colors.ts light|dark Test built-in theme"); + console.log(" bun test-theme-colors.ts contrast 4.5 Compute colors at ratio"); + console.log(" bun test-theme-colors.ts test file.json Test any JSON file"); } diff --git a/packages/coding-agent/vitest.config.ts b/packages/coding-agent/vitest.config.ts deleted file mode 100644 index 320ff2a9834..00000000000 --- a/packages/coding-agent/vitest.config.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { fileURLToPath } from "node:url"; -import { defineConfig } from "vitest/config"; - -const aiSrcIndex = fileURLToPath(new URL("../ai/src/index.ts", import.meta.url)); -const aiSrcOAuth = fileURLToPath(new URL("../ai/src/oauth.ts", import.meta.url)); -const aiSrcMcp = fileURLToPath(new URL("../ai/src/mcp.ts", import.meta.url)); -const agentSrcIndex = fileURLToPath(new URL("../agent/src/index.ts", import.meta.url)); -const tuiSrcIndex = fileURLToPath(new URL("../tui/src/index.ts", import.meta.url)); - -export default defineConfig({ - test: { - globals: true, - environment: "node", - testTimeout: 30000, - env: { DO_NOT_TRACK: "1" }, - tags: [ - { - name: "process-stress", - description: "Slow real-process stress and wall-clock scheduling coverage", - }, - { - name: "kernel-heavy", - description: "Boots a real Python kernel and syncs skills into the shared venv", - }, - ], - // Kernel-heavy tests are excluded from the default sharded run: several files - // booting real kernels in one shard starve the neighbouring kernel tests that - // rely on the 30s default timeout. `test:kernel` runs them on their own. - tagsFilter: ["!process-stress", "!kernel-heavy"], - server: { - deps: { - external: [/@silvia-odwyer\/photon-node/], - }, - }, - }, - resolve: { - alias: [ - { find: /^@earendil-works\/pi-ai$/, replacement: aiSrcIndex }, - { find: /^@earendil-works\/pi-ai\/oauth$/, replacement: aiSrcOAuth }, - { find: /^@earendil-works\/pi-ai\/mcp$/, replacement: aiSrcMcp }, - { find: /^@earendil-works\/pi-agent-core$/, replacement: agentSrcIndex }, - { find: /^@earendil-works\/pi-tui$/, replacement: tuiSrcIndex }, - { find: /^@mariozechner\/pi-ai$/, replacement: aiSrcIndex }, - { find: /^@mariozechner\/pi-ai\/oauth$/, replacement: aiSrcOAuth }, - { find: /^@mariozechner\/pi-ai\/mcp$/, replacement: aiSrcMcp }, - { find: /^@mariozechner\/pi-agent-core$/, replacement: agentSrcIndex }, - { find: /^@mariozechner\/pi-tui$/, replacement: tuiSrcIndex }, - ], - }, -}); diff --git a/packages/tui/package.json b/packages/tui/package.json index 7e0ecbd82df..9f991b1af87 100644 --- a/packages/tui/package.json +++ b/packages/tui/package.json @@ -8,7 +8,7 @@ "clean": "bun ../../scripts/remove-paths.ts dist", "build": "bun --bun tsgo -p tsconfig.build.json", "dev": "bun --bun tsgo -p tsconfig.build.json --watch --preserveWatchOutput", - "test": "node --test --import tsx test/*.test.ts", + "test": "bun ../../scripts/run-with-clean-env.ts bun test --isolate --timeout 30000", "prepublishOnly": "bun run clean && bun run build" }, "files": [ @@ -32,7 +32,8 @@ "directory": "packages/tui" }, "engines": { - "node": ">=20.0.0" + "node": ">=20.0.0", + "bun": "1.3.14" }, "types": "./dist/index.d.ts", "dependencies": { diff --git a/packages/tui/test/image-test.ts b/packages/tui/test/image-test.ts index 6a29d16d970..e263ce71a81 100644 --- a/packages/tui/test/image-test.ts +++ b/packages/tui/test/image-test.ts @@ -16,7 +16,7 @@ try { imageBuffer = readFileSync(testImagePath); } catch (_e) { console.error(`Failed to load image: ${testImagePath}`); - console.error("Usage: npx tsx test/image-test.ts [path-to-image.png]"); + console.error("Usage: bun test/image-test.ts [path-to-image.png]"); process.exit(1); } diff --git a/packages/tui/test/key-tester.ts b/packages/tui/test/key-tester.ts index 5103d544189..9983a1659a0 100755 --- a/packages/tui/test/key-tester.ts +++ b/packages/tui/test/key-tester.ts @@ -1,4 +1,4 @@ -#!/usr/bin/env node +#!/usr/bin/env bun import { matchesKey } from "../src/keys.js"; import { ProcessTerminal } from "../src/terminal.js"; import { type Component, TUI } from "../src/tui.js"; diff --git a/packages/tui/vitest.config.ts b/packages/tui/vitest.config.ts deleted file mode 100644 index a90c176d921..00000000000 --- a/packages/tui/vitest.config.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { defineConfig } from "vitest/config"; - -export default defineConfig({ - test: { - include: ["test/wrap-ansi.test.ts"], - }, -}); diff --git a/scripts/publish-workspaces.ts b/scripts/publish-workspaces.ts new file mode 100644 index 00000000000..ed78c440766 --- /dev/null +++ b/scripts/publish-workspaces.ts @@ -0,0 +1,18 @@ +import { dirname, join } from "node:path"; +import { fileURLToPath } from "node:url"; + +const rootDir = dirname(dirname(fileURLToPath(import.meta.url))); +const packageDirs = ["tui", "ai", "agent", "coding-agent"] as const; +const dryRun = process.argv.includes("--dry-run"); + +for (const packageDir of packageDirs) { + const command = [process.execPath, "publish", "--access", "public", "--ignore-scripts"]; + if (dryRun) command.push("--dry-run"); + const result = Bun.spawnSync(command, { + cwd: join(rootDir, "packages", packageDir), + stdin: "inherit", + stdout: "inherit", + stderr: "inherit", + }); + if (result.exitCode !== 0) process.exit(result.exitCode); +} diff --git a/scripts/run-with-clean-env.ts b/scripts/run-with-clean-env.ts new file mode 100644 index 00000000000..c99f4e3a6e2 --- /dev/null +++ b/scripts/run-with-clean-env.ts @@ -0,0 +1,34 @@ +/** Run a command without inheriting live Prime Agent orchestration state. */ + +const [requestedCommand, ...args] = process.argv.slice(2); +if (!requestedCommand) { + console.error("Usage: bun scripts/run-with-clean-env.ts [args...]"); + process.exit(2); +} + +const env = { ...process.env }; +for (const name of Object.keys(env)) { + if (name.startsWith("PRIME_AGENT_INTERNAL_") || name.startsWith("RLM_")) { + delete env[name]; + } +} +for (const name of ["PRIME_AGENT_CODING_AGENT_DIR", "PRIME_AGENT_KERNEL_OWNER_PID"]) { + delete env[name]; +} + +const command = requestedCommand === "bun" ? process.execPath : requestedCommand; +const child = Bun.spawn([command, ...args], { + cwd: process.cwd(), + env, + stdin: "inherit", + stdout: "inherit", + stderr: "inherit", +}); + +for (const signal of ["SIGINT", "SIGTERM"] as const) { + process.on(signal, () => { + child.kill(signal); + }); +} + +process.exit(await child.exited); diff --git a/scripts/set-version.ts b/scripts/set-version.ts new file mode 100644 index 00000000000..d40c1b594b5 --- /dev/null +++ b/scripts/set-version.ts @@ -0,0 +1,88 @@ +import { readFile, writeFile } from "node:fs/promises"; +import { dirname, join } from "node:path"; +import { fileURLToPath } from "node:url"; + +const rootDir = dirname(dirname(fileURLToPath(import.meta.url))); +const manifestPaths = [ + "package.json", + "packages/tui/package.json", + "packages/ai/package.json", + "packages/agent/package.json", + "packages/coding-agent/package.json", +] as const; +const target = process.argv.find((arg, index) => index >= 2 && !arg.startsWith("--")); +const dryRun = process.argv.includes("--dry-run"); +const semverPattern = /^(\d+)\.(\d+)\.(\d+)$/; + +if (!target || (!semverPattern.test(target) && !["patch", "minor", "major"].includes(target))) { + console.error("Usage: bun scripts/set-version.ts [--dry-run]"); + process.exit(2); +} + +const manifests = await Promise.all( + manifestPaths.map(async (path) => { + const text = await readFile(join(rootDir, path), "utf8"); + return { path, text, value: JSON.parse(text) }; + }), +); +const currentVersions = new Set(manifests.map(({ value }) => value.version as string)); +if (currentVersions.size !== 1) { + throw new Error(`Workspace versions are not in lockstep: ${[...currentVersions].join(", ")}`); +} +const currentVersion = manifests[0].value.version as string; +const currentMatch = currentVersion.match(semverPattern); +if (!currentMatch) { + throw new Error(`Current version is not x.y.z: ${currentVersion}`); +} + +function nextVersion(): string { + if (semverPattern.test(target!)) return target!; + let major = Number(currentMatch![1]); + let minor = Number(currentMatch![2]); + let patch = Number(currentMatch![3]); + if (target === "major") { + major += 1; + minor = 0; + patch = 0; + } else if (target === "minor") { + minor += 1; + patch = 0; + } else { + patch += 1; + } + return `${major}.${minor}.${patch}`; +} + +const version = nextVersion(); +const workspaceVersions = new Map(manifests.slice(1).map(({ value }) => [value.name as string, version])); +for (const manifest of manifests) { + manifest.value.version = version; + for (const section of ["dependencies", "devDependencies", "optionalDependencies"] as const) { + const dependencies = manifest.value[section] as Record | undefined; + if (!dependencies) continue; + for (const name of Object.keys(dependencies)) { + if (workspaceVersions.has(name)) dependencies[name] = `^${version}`; + } + } +} + +console.log(`${currentVersion} -> ${version}`); +if (dryRun) process.exit(0); + +const lockPath = join(rootDir, "bun.lock"); +const originalLock = await readFile(lockPath, "utf8"); +for (const manifest of manifests) { + await writeFile(join(rootDir, manifest.path), `${JSON.stringify(manifest.value, null, "\t")}\n`); +} +const install = Bun.spawnSync([process.execPath, "install", "--lockfile-only"], { + cwd: rootDir, + stdin: "inherit", + stdout: "inherit", + stderr: "inherit", +}); +if (install.exitCode !== 0) { + await Promise.all(manifests.map((manifest) => writeFile(join(rootDir, manifest.path), manifest.text))); + await writeFile(lockPath, originalLock); + console.error("Version update failed; restored manifests and bun.lock."); + process.exit(install.exitCode); +} diff --git a/scripts/sync-versions.js b/scripts/sync-versions.js deleted file mode 100644 index c850b4c75e2..00000000000 --- a/scripts/sync-versions.js +++ /dev/null @@ -1,90 +0,0 @@ -#!/usr/bin/env node - -/** - * Syncs all workspace package dependency versions to match their current versions. - * This ensures lockstep versioning across the monorepo. - */ - -import { readFileSync, writeFileSync, readdirSync } from 'fs'; -import { join } from 'path'; - -const packagesDir = join(process.cwd(), 'packages'); -const packageDirs = readdirSync(packagesDir, { withFileTypes: true }) - .filter(dirent => dirent.isDirectory()) - .map(dirent => dirent.name); - -const packages = {}; -const versionMap = {}; - -for (const dir of packageDirs) { - const pkgPath = join(packagesDir, dir, 'package.json'); - try { - const pkg = JSON.parse(readFileSync(pkgPath, 'utf8')); - packages[dir] = { path: pkgPath, data: pkg }; - versionMap[pkg.name] = pkg.version; - } catch (e) { - console.error(`Failed to read ${pkgPath}:`, e.message); - } -} - -console.log('Current versions:'); -for (const [name, version] of Object.entries(versionMap).sort()) { - console.log(` ${name}: ${version}`); -} - -const versions = new Set(Object.values(versionMap)); -if (versions.size > 1) { - console.error('\n❌ ERROR: Not all packages have the same version!'); - console.error('Expected lockstep versioning. Run one of:'); - console.error(' npm run version:patch'); - console.error(' npm run version:minor'); - console.error(' npm run version:major'); - process.exit(1); -} - -console.log('\n✅ All packages at same version (lockstep)'); - -let totalUpdates = 0; -for (const [dir, pkg] of Object.entries(packages)) { - let updated = false; - - if (pkg.data.dependencies) { - for (const [depName, currentVersion] of Object.entries(pkg.data.dependencies)) { - if (versionMap[depName]) { - const newVersion = `^${versionMap[depName]}`; - if (currentVersion !== newVersion) { - console.log(`\n${pkg.data.name}:`); - console.log(` ${depName}: ${currentVersion} → ${newVersion}`); - pkg.data.dependencies[depName] = newVersion; - updated = true; - totalUpdates++; - } - } - } - } - - if (pkg.data.devDependencies) { - for (const [depName, currentVersion] of Object.entries(pkg.data.devDependencies)) { - if (versionMap[depName]) { - const newVersion = `^${versionMap[depName]}`; - if (currentVersion !== newVersion) { - console.log(`\n${pkg.data.name}:`); - console.log(` ${depName}: ${currentVersion} → ${newVersion} (devDependencies)`); - pkg.data.devDependencies[depName] = newVersion; - updated = true; - totalUpdates++; - } - } - } - } - - if (updated) { - writeFileSync(pkg.path, JSON.stringify(pkg.data, null, '\t') + '\n'); - } -} - -if (totalUpdates === 0) { - console.log('\nAll inter-package dependencies already in sync.'); -} else { - console.log(`\n✅ Updated ${totalUpdates} dependency version(s)`); -} diff --git a/test.sh b/test.sh index 19b21dade1b..081b6af55ac 100755 --- a/test.sh +++ b/test.sh @@ -60,4 +60,4 @@ unset BEDROCK_EXTENSIVE_MODEL_TEST unset FIREWORKS_API_KEY echo "Running tests without API keys..." -npm test +bun scripts/run-with-clean-env.ts bun test --isolate --timeout 30000 diff --git a/tsconfig.json b/tsconfig.json index 4dfbf3d2548..a54d87e145e 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -17,7 +17,9 @@ "@earendil-works/pi-tui": ["./packages/tui/src/index.ts"], "@earendil-works/pi-tui/*": ["./packages/tui/src/*"], "@earendil-works/pi-agent-old": ["./packages/agent-old/src/index.ts"], - "@earendil-works/pi-agent-old/*": ["./packages/agent-old/src/*"] + "@earendil-works/pi-agent-old/*": ["./packages/agent-old/src/*"], + "bun:test": ["./node_modules/bun-types/test.d.ts"], + "vitest": ["./types/vitest.d.ts"] } }, "include": ["packages/*/src/**/*", "packages/*/test/**/*", "packages/coding-agent/examples/**/*"], diff --git a/types/vitest.d.ts b/types/vitest.d.ts new file mode 100644 index 00000000000..3e6c6a8be76 --- /dev/null +++ b/types/vitest.d.ts @@ -0,0 +1,45 @@ +export * from "@vitest/runner"; +export { assert, createExpect, expect } from "@vitest/expect"; +export type { Mock, MockedFunction, MockInstance, MockedObject } from "@vitest/spy"; + +import type { MockInstance } from "@vitest/spy"; + +type Awaitable = T | Promise; +type CompatMocked = T extends (...args: any[]) => any + ? T & MockInstance + : T extends object + ? { [K in keyof T]: CompatMocked } + : T; +export type Mocked = CompatMocked; + +type VitestCompat = { + advanceTimersByTime(milliseconds: number): VitestCompat; + advanceTimersByTimeAsync(milliseconds: number): Promise; + advanceTimersToNextTimer(): VitestCompat; + advanceTimersToNextTimerAsync(): Promise; + clearAllMocks(): VitestCompat; + clearAllTimers(): VitestCompat; + fn: typeof import("@vitest/spy").fn; + getTimerCount(): number; + hoisted(factory: () => T): T; + isFakeTimers(): boolean; + isMockFunction(fn: unknown): fn is MockInstance; + mock(path: string, factory?: () => unknown): void; + mocked(value: T): CompatMocked; + resetAllMocks(): VitestCompat; + restoreAllMocks(): VitestCompat; + runAllTimers(): VitestCompat; + runAllTimersAsync(): Promise; + runOnlyPendingTimers(): VitestCompat; + runOnlyPendingTimersAsync(): Promise; + setSystemTime(value: Date | number): VitestCompat; + spyOn: typeof import("@vitest/spy").spyOn; + stubEnv(name: string, value: string | undefined): VitestCompat; + stubGlobal(name: PropertyKey, value: unknown): VitestCompat; + unstubAllEnvs(): VitestCompat; + unstubAllGlobals(): VitestCompat; + useFakeTimers(options?: { now?: Date | number }): VitestCompat; + useRealTimers(): VitestCompat; + waitFor(assertion: () => Awaitable, options?: { timeout?: number; interval?: number }): Promise; +}; +export const vi: VitestCompat; From 624c83dfbeddae075147a42d22bfebd877f73e23 Mon Sep 17 00:00:00 2001 From: Seth Date: Tue, 1 Sep 2026 17:38:53 -0400 Subject: [PATCH 05/45] build: ship Bun binaries with verified sidecars --- .npmrc | 6 - install.sh | 579 +++++++++++++++++-- packages/ai/scripts/generate-models.ts | 4 +- packages/ai/scripts/generate-test-image.ts | 2 +- packages/coding-agent/scripts/bundle.mjs | 33 +- packages/coding-agent/scripts/copy-assets.ts | 10 +- scripts/assemble-release-archives.mjs | 312 ++++++++++ scripts/build-binaries.sh | 207 +++---- scripts/check-browser-smoke.mjs | 38 +- scripts/compiled-artifact-smoke.ts | 161 ++++++ scripts/pack-prime-agent-release.mjs | 376 ++++-------- scripts/release.mjs | 26 +- 12 files changed, 1222 insertions(+), 532 deletions(-) delete mode 100644 .npmrc create mode 100755 scripts/assemble-release-archives.mjs create mode 100644 scripts/compiled-artifact-smoke.ts mode change 100644 => 100755 scripts/pack-prime-agent-release.mjs diff --git a/.npmrc b/.npmrc deleted file mode 100644 index 2ebd6374a17..00000000000 --- a/.npmrc +++ /dev/null @@ -1,6 +0,0 @@ -# Supply-chain cooldown: never resolve to package versions published -# less than 7 days ago. Only affects version resolution (install/update of new -# versions); already-locked versions and `npm ci` are unaffected. -# Requires npm >= 11.10 to be enforced; older npm ignores this key. -# Urgent security patch override: npm install --min-release-age=0 -min-release-age=7 diff --git a/install.sh b/install.sh index c1b5c524580..c2bbce253f8 100755 --- a/install.sh +++ b/install.sh @@ -57,6 +57,11 @@ prime_agent_screen_status= prime_agent_screen_detail= prime_agent_screen_question= prime_agent_animation_frame=0 +prime_agent_install_method="${PRIME_AGENT_INSTALL_METHOD:-auto}" +prime_agent_binary_versions_dir="${PRIME_AGENT_VERSIONS_DIR:-${XDG_DATA_HOME:-$HOME/.local/share}/prime-agent/versions}" +prime_agent_binary_symlink="${PRIME_AGENT_BIN_DIR:-${XDG_BIN_HOME:-$HOME/.local/bin}}/prime-agent" +prime_agent_binary_rollback_version= +prime_agent_is_update=0 main() { if [ "$prime_agent_base_url" = "$prime_agent_unconfigured_base_url" ]; then @@ -65,81 +70,62 @@ main() { exit 1 fi + # Parse flags before positional args + prime_agent_install_method="${PRIME_AGENT_INSTALL_METHOD:-auto}" + prime_agent_is_update=0 + _prime_agent_positional= + for _prime_agent_arg in "$@"; do + case "$_prime_agent_arg" in + --method=*) + prime_agent_install_method="${_prime_agent_arg#--method=}" + case "$prime_agent_install_method" in + binary|npm|auto) ;; + *) + printf 'error: invalid install method: %s (must be binary, npm, or auto)\n' "$prime_agent_install_method" >&2 + exit 1 + ;; + esac + ;; + --update) + prime_agent_is_update=1 + prime_agent_install_method=binary + ;; + *) + if [ -z "$_prime_agent_positional" ]; then + _prime_agent_positional="$_prime_agent_arg" + else + _prime_agent_positional="$_prime_agent_positional $_prime_agent_arg" + fi + ;; + esac + done + prime_agent_install_traps prime_agent_init_screen - if [ "$prime_agent_screen_enabled" = 1 ]; then - prime_agent_screen "Installing Prime Agent" "" "" "" - else - printf '\n\033[1m Installing Prime Agent\033[0m\n\033[2m npm global install\033[0m\n\n' - fi - start_preflight_checks - - if finish_preflight_checks; then - check_status=0 - else - check_status=$? - fi - - if [ "$check_status" -ne 0 ]; then - if ! install_node_npm_interactive; then - exit "$check_status" - fi - - start_preflight_checks - if finish_preflight_checks; then - check_status=0 + if [ "$prime_agent_is_update" = 1 ]; then + if [ "$prime_agent_screen_enabled" = 1 ]; then + prime_agent_screen "Updating Prime Agent" "" "" "" else - check_status=$? - fi - - if [ "$check_status" -ne 0 ]; then - exit "$check_status" + printf '\n\033[1m Updating Prime Agent\033[0m\n' fi + prime_agent_binary_update "${_prime_agent_positional:-}" + return fi - version="$(resolve_prime_agent_version "$@")" - tarball_name="$prime_agent_package-$version.tgz" - tarball_url="$prime_agent_base_url/releases/v$version/$tarball_name" - - confirm_install "$version" "$tarball_url" - confirm_kernel_runtime_setup - - download_dir=$(create_temp_dir) - prime_agent_download_dir="$download_dir" - tarball_path="$download_dir/$tarball_name" - - download_prime_agent_package "$version" "$tarball_url" "$tarball_path" - install_prime_agent_package "$tarball_path" - rm -rf "$download_dir" - prime_agent_download_dir= - - if [ "${PRIME_AGENT_NODE_INSTALLED_STANDALONE:-0}" = 1 ]; then - prime_agent_screen "Prime Agent installed" "" "Checking your shell PATH." "" - configure_standalone_node_path - elif command -v "$prime_agent_cmd" >/dev/null 2>&1; then + # Default auto and explicit --method=binary both use compiled binary + if [ "$prime_agent_install_method" != npm ]; then if [ "$prime_agent_screen_enabled" = 1 ]; then - prime_agent_screen "Prime Agent installed" "" "Run it with: $prime_agent_cmd" "" + prime_agent_screen "Installing Prime Agent" "" "" "" else - printf '\nPrime Agent was installed successfully.\n' - printf '\nRun it with: %s\n' "$prime_agent_cmd" - fi - else - if [ "$prime_agent_screen_enabled" = 1 ]; then - prime_agent_screen "Prime Agent installed" "" "PATH update needed for $prime_agent_cmd." "" - prime_agent_restore_terminal - else - printf '\nPrime Agent was installed successfully.\n' + printf '\n\033[1m Installing Prime Agent\033[0m\n\033[2m compiled binary\033[0m\n\n' fi - cat </ +# and point a stable symlink at it from ~/.local/bin/prime-agent. +# On update: download to a fresh version dir, smoke-test the binary there, +# then atomically switch the symlink. Rollback by restoring the old symlink. +# Never copy runtime sidecars into user config dirs (~/.prime/*). + +prime_agent_detect_binary_platform() { + _os=$(uname -s) + _arch=$(uname -m) + case "$_os" in + Darwin) + case "$_arch" in + x86_64|amd64) printf 'darwin-x64' ;; + arm64|aarch64) printf 'darwin-arm64' ;; + *) return 1 ;; + esac + ;; + Linux) + case "$_arch" in + x86_64|amd64) printf 'linux-x64' ;; + arm64|aarch64) printf 'linux-arm64' ;; + *) return 1 ;; + esac + ;; + *) return 1 ;; + esac +} + +prime_agent_binary_artifact_name() { + _version="$1" + _platform="$2" + printf 'prime-agent-%s-%s.tar.gz' "$_version" "$_platform" +} + +prime_agent_binary_target_version_dir() { + printf '%s/v%s' "$prime_agent_binary_versions_dir" "$1" +} + +prime_agent_binary_smoke_binary() { + _binary="$1" + if [ ! -x "$_binary" ]; then + return 1 + fi + if ! "$_binary" --version >/dev/null 2>&1; then + return 1 + fi +} + +prime_agent_binary_atomic_symlink() { + _target="$1" + _link="$2" + # Create parent dir if needed + _link_dir=$(dirname "$_link") + if [ ! -d "$_link_dir" ]; then + mkdir -p "$_link_dir" + fi + # Atomic replacement with temp symlink + _tmp="${_link}.tmp.$$" + ln -sf "$_target" "$_tmp" + mv -f "$_tmp" "$_link" +} + +prime_agent_binary_fresh_install() { + _version= + _version="$(resolve_prime_agent_version "$@")" + _platform= + if ! _platform=$(prime_agent_detect_binary_platform); then + printf 'error: unsupported platform for binary install: %s %s\n' "$(uname -s)" "$(uname -m)" >&2 + exit 1 + fi + _artifact_name="$(prime_agent_binary_artifact_name "$_version" "$_platform")" + _artifact_url="$prime_agent_base_url/releases/v$_version/$_artifact_name" + _versions_dir="$prime_agent_binary_versions_dir" + _version_dir="$(prime_agent_binary_target_version_dir "$_version")" + + _download_dir=$(create_temp_dir) + prime_agent_download_dir="$_download_dir" + _artifact_path="$_download_dir/$_artifact_name" + + mkdir -p "$_versions_dir" + + # Version directories are immutable. Reuse a healthy existing install instead + # of replacing files underneath the active command symlink. + _existing_binary="$_version_dir/$prime_agent_cmd" + if [ ! -x "$_existing_binary" ] && [ -x "$_version_dir/pi" ]; then + _existing_binary="$_version_dir/pi" + fi + if [ -x "$_existing_binary" ] && prime_agent_binary_smoke_binary "$_existing_binary"; then + prime_agent_binary_atomic_symlink "$_existing_binary" "$prime_agent_binary_symlink" + prime_agent_configure_binary_path "$_version" + return + fi + + prime_agent_run_quiet_with_animation "Downloading Prime Agent v$_version" "Downloading Prime Agent v$_version" "Fetching the compiled binary for $_platform." curl -fsSL "$_artifact_url" -o "$_artifact_path" + + _checksums_url="$prime_agent_base_url/releases/v$_version/SHA256SUMS" + _checksums_path="$_download_dir/SHA256SUMS" + prime_agent_run_quiet_with_animation "Downloading checksums" "Downloading release checksums" "Prime Agent v$_version" curl -fsSL "$_checksums_url" -o "$_checksums_path" + + prime_agent_verify_binary_checksum "$_checksums_path" "$_artifact_path" + + # Extract to a clean versioned directory + rm -rf "$_version_dir" + mkdir -p "$_version_dir" + prime_agent_run_quiet_with_animation "Extracting Prime Agent" "Extracting Prime Agent v$_version" "Installing to $_version_dir" tar -xzf "$_artifact_path" -C "$_version_dir" + + # Find the binary inside the extracted archive. + # The archive may contain a wrapper dir (e.g. pi/) or be flat. + _binary_path= + if [ -f "$_version_dir/pi" ]; then + _binary_path="$_version_dir/pi" + elif [ -f "$_version_dir/$prime_agent_cmd" ]; then + _binary_path="$_version_dir/$prime_agent_cmd" + else + for _d in "$_version_dir"/*/; do + _d="${_d%/}" + if [ -f "${_d}/pi" ]; then + _binary_path="${_d}/pi" + break + elif [ -f "${_d}/$prime_agent_cmd" ]; then + _binary_path="${_d}/$prime_agent_cmd" + break + fi + done + fi + + if [ -z "$_binary_path" ]; then + printf 'error: could not find the prime-agent binary in the downloaded artifact.\n' >&2 + rm -rf "$_version_dir" "$_download_dir" + prime_agent_download_dir= + exit 1 + fi + + chmod +x "$_binary_path" 2>/dev/null || true + # Ensure bundled install.sh is executable (required sidecar for self-update) + if [ -f "$_version_dir/install.sh" ]; then + chmod +x "$_version_dir/install.sh" 2>/dev/null || true + fi + + if ! "$_binary_path" --version >/dev/null 2>&1; then + printf 'error: the downloaded binary did not run correctly.\n' >&2 + rm -rf "$_version_dir" "$_download_dir" + prime_agent_download_dir= + exit 1 + fi + + # If the binary was inside a wrapper dir, move contents up to version_dir + _binary_dir=$(dirname "$_binary_path") + if [ "$_binary_dir" != "$_version_dir" ]; then + cp -R "$_binary_dir/." "$_version_dir/" + rm -rf "$_binary_dir" + _binary_path="$_version_dir/$(basename "$_binary_path")" + fi + + # Create the stable symlink + mkdir -p "$(dirname "$prime_agent_binary_symlink")" + prime_agent_binary_atomic_symlink "$_binary_path" "$prime_agent_binary_symlink" + + rm -rf "$_download_dir" + prime_agent_download_dir= + + prime_agent_configure_binary_path "$_version" + # The Python kernel will be bootstrapped on first ipython use. +} + +prime_agent_binary_update() { + _update_version= + _update_version="$(resolve_prime_agent_version "$@")" + + # Read current version from the symlink target's package.json + _current_version= + if [ -L "$prime_agent_binary_symlink" ]; then + _symlink_target=$(readlink "$prime_agent_binary_symlink") + _pkg_dir=$(dirname "$_symlink_target") + if [ -f "$_pkg_dir/package.json" ]; then + _current_version=$(sed -n 's/.*"version": *"\([^"]*\)".*/\1/p' "$_pkg_dir/package.json" 2>/dev/null || printf '') + fi + fi + + if [ -n "$_current_version" ] && [ "$_current_version" = "$_update_version" ]; then + if [ "$prime_agent_screen_enabled" = 1 ]; then + prime_agent_screen "Prime Agent is up to date" "" "v$_current_version" "" + else + printf '\nPrime Agent v%s is already installed.\n' "$_current_version" + fi + return + fi + + _platform= + if ! _platform=$(prime_agent_detect_binary_platform); then + printf 'error: unsupported platform for binary update: %s %s\n' "$(uname -s)" "$(uname -m)" >&2 + exit 1 + fi + _artifact_name="$(prime_agent_binary_artifact_name "$_update_version" "$_platform")" + _artifact_url="$prime_agent_base_url/releases/v$_update_version/$_artifact_name" + _versions_dir="$prime_agent_binary_versions_dir" + _version_dir="$(prime_agent_binary_target_version_dir "$_update_version")" + + _download_dir=$(create_temp_dir) + prime_agent_download_dir="$_download_dir" + _artifact_path="$_download_dir/$_artifact_name" + + if [ "$prime_agent_screen_enabled" = 1 ]; then + if [ -n "$_current_version" ]; then + prime_agent_screen "Updating Prime Agent" "" "v$_current_version to v$_update_version" "" + else + prime_agent_screen "Updating Prime Agent" "" "to v$_update_version" "" + fi + fi + + prime_agent_run_quiet_with_animation "Downloading Prime Agent v$_update_version" "Downloading Prime Agent v$_update_version" "Fetching the compiled binary for $_platform." curl -fsSL "$_artifact_url" -o "$_artifact_path" + + _checksums_url="$prime_agent_base_url/releases/v$_update_version/SHA256SUMS" + _checksums_path="$_download_dir/SHA256SUMS" + prime_agent_run_quiet_with_animation "Downloading checksums" "Downloading release checksums" "Prime Agent v$_update_version" curl -fsSL "$_checksums_url" -o "$_checksums_path" + + prime_agent_verify_binary_checksum "$_checksums_path" "$_artifact_path" + + # Extract to a fresh version directory + rm -rf "$_version_dir" + mkdir -p "$_version_dir" + prime_agent_run_quiet_with_animation "Extracting Prime Agent" "Extracting Prime Agent v$_update_version" "Preparing the update." tar -xzf "$_artifact_path" -C "$_version_dir" + + # Find the binary + _binary_path= + if [ -f "$_version_dir/pi" ]; then + _binary_path="$_version_dir/pi" + elif [ -f "$_version_dir/$prime_agent_cmd" ]; then + _binary_path="$_version_dir/$prime_agent_cmd" + else + for _d in "$_version_dir"/*/; do + _d="${_d%/}" + if [ -f "${_d}/pi" ]; then + _binary_path="${_d}/pi" + break + elif [ -f "${_d}/$prime_agent_cmd" ]; then + _binary_path="${_d}/$prime_agent_cmd" + break + fi + done + fi + + if [ -z "$_binary_path" ]; then + printf 'error: could not find the prime-agent binary in the downloaded artifact.\n' >&2 + rm -rf "$_version_dir" "$_download_dir" + prime_agent_download_dir= + exit 1 + fi + + chmod +x "$_binary_path" 2>/dev/null || true + # Ensure bundled install.sh is executable (required sidecar for self-update) + if [ -f "$_version_dir/install.sh" ]; then + chmod +x "$_version_dir/install.sh" 2>/dev/null || true + fi + + # Smoke-test the new binary BEFORE switching the symlink + if ! "$_binary_path" --version >/dev/null 2>&1; then + # New binary is bad: remove it, keep old symlink + rm -rf "$_version_dir" "$_download_dir" + prime_agent_download_dir= + if [ "$prime_agent_screen_enabled" = 1 ]; then + prime_agent_screen "Update failed" "" "New binary did not run correctly." "" + else + printf '\nUpdate failed: the new binary did not run correctly.\n' + fi + exit 1 + fi + + # If the binary was inside a wrapper dir, move contents up + _binary_dir=$(dirname "$_binary_path") + if [ "$_binary_dir" != "$_version_dir" ]; then + cp -R "$_binary_dir/." "$_version_dir/" + rm -rf "$_binary_dir" + _binary_path="$_version_dir/$(basename "$_binary_path")" + fi + + # Remember old version for rollback + if [ -L "$prime_agent_binary_symlink" ]; then + _old_target=$(readlink "$prime_agent_binary_symlink" 2>/dev/null || printf '') + _old_version_dir=$(dirname "$_old_target" 2>/dev/null || printf '') + prime_agent_binary_rollback_version="$_old_version_dir" + fi + + # Atomically switch the symlink + prime_agent_binary_atomic_symlink "$_binary_path" "$prime_agent_binary_symlink" + + rm -rf "$_download_dir" + prime_agent_download_dir= + + if [ "$prime_agent_screen_enabled" = 1 ]; then + prime_agent_screen "Prime Agent updated" "" "v$_update_version installed." "" + else + printf '\nPrime Agent was updated to v%s.\n' "$_update_version" + fi + + prime_agent_configure_binary_path "$_update_version" +} + +prime_agent_binary_rollback() { + if [ -z "$prime_agent_binary_rollback_version" ] || [ ! -d "$prime_agent_binary_rollback_version" ]; then + return 1 + fi + _rollback_binary= + if [ -f "$prime_agent_binary_rollback_version/pi" ]; then + _rollback_binary="$prime_agent_binary_rollback_version/pi" + elif [ -f "$prime_agent_binary_rollback_version/$prime_agent_cmd" ]; then + _rollback_binary="$prime_agent_binary_rollback_version/$prime_agent_cmd" + fi + if [ -z "$_rollback_binary" ] || [ ! -x "$_rollback_binary" ]; then + return 1 + fi + prime_agent_binary_atomic_symlink "$_rollback_binary" "$prime_agent_binary_symlink" +} + +prime_agent_verify_binary_checksum() { + _checksums_path="$1" + _artifact_path="$2" + _checksum_dir=$(dirname "$_artifact_path") + _artifact_name=$(basename "$_artifact_path") + _selected_checksums_path="$_checksum_dir/SHA256SUMS.selected" + + if ! awk -v file="$_artifact_name" '$2 == file { print; found = 1; exit } END { if (!found) exit 1 }' "$_checksums_path" >"$_selected_checksums_path"; then + printf 'error: checksum for %s was not found in %s\n' "$_artifact_name" "$_checksums_path" >&2 + exit 1 + fi + + if command -v sha256sum >/dev/null 2>&1; then + prime_agent_run_quiet_with_animation "Verifying download" "Verifying Prime Agent download" "Checking SHA-256." prime_agent_run_checksum_check "$_checksum_dir" "$(basename "$_selected_checksums_path")" sha256sum + elif command -v shasum >/dev/null 2>&1; then + prime_agent_run_quiet_with_animation "Verifying download" "Verifying Prime Agent download" "Checking SHA-256." prime_agent_run_checksum_check "$_checksum_dir" "$(basename "$_selected_checksums_path")" shasum + else + printf 'error: sha256sum or shasum is required to verify the download.\n' >&2 + exit 1 + fi +} + +prime_agent_configure_binary_path() { + _installed_version="${1:-}" + + if command -v "$prime_agent_cmd" >/dev/null 2>&1; then + _existing_path="$(command -v "$prime_agent_cmd")" + if [ "$_existing_path" = "$prime_agent_binary_symlink" ]; then + if [ "$prime_agent_screen_enabled" = 1 ]; then + prime_agent_screen "Prime Agent installed" "" "Run it with: $prime_agent_cmd" "" + else + printf '\nRun it with: %s\n' "$prime_agent_cmd" + fi + return + fi + fi + + _bin_dir=$(dirname "$prime_agent_binary_symlink") + case ":$PATH:" in + *":$_bin_dir:"*) + if [ "$prime_agent_screen_enabled" = 1 ]; then + prime_agent_screen "Prime Agent installed" "" "Run it with: $prime_agent_cmd" "" + else + printf '\nRun it with: %s\n' "$prime_agent_cmd" + fi + return + ;; + esac + + if [ "$prime_agent_screen_enabled" = 1 ]; then + prime_agent_screen "Prime Agent installed" "" "PATH update needed for $prime_agent_cmd." "" + prime_agent_restore_terminal + else + printf '\nPrime Agent was installed to %s.\n' "$_bin_dir" + fi + + _profile=$(detect_shell_profile) + if [ -n "$_profile" ] && [ -w "$_profile" ] 2>/dev/null; then + if ! grep -q "$_bin_dir" "$_profile" 2>/dev/null; then + printf '\nexport PATH="%s:$PATH"\n' "$_bin_dir" >> "$_profile" + printf 'Added %s to %s.\n' "$_bin_dir" "$_profile" + fi + printf '\nRestart your shell or run: export PATH="%s:$PATH" && %s\n' "$_bin_dir" "$prime_agent_cmd" + else + printf '\nAdd to your shell profile:\n' + printf ' export PATH="%s:$PATH"\n' "$_bin_dir" + printf '\nThen restart your shell and run: %s\n' "$prime_agent_cmd" + fi +} + +# ============================================================================= +# NPM Install Path (extracted from main) +# ============================================================================= + +prime_agent_npm_install() { + start_preflight_checks + + if finish_preflight_checks; then + check_status=0 + else + check_status=$? + fi + + if [ "$check_status" -ne 0 ]; then + if ! install_node_npm_interactive; then + exit "$check_status" + fi + + start_preflight_checks + if finish_preflight_checks; then + check_status=0 + else + check_status=$? + fi + + if [ "$check_status" -ne 0 ]; then + exit "$check_status" + fi + fi + + version="$(resolve_prime_agent_version "$@")" + tarball_name="$prime_agent_package-$version.tgz" + tarball_url="$prime_agent_base_url/releases/v$version/$tarball_name" + + confirm_install "$version" "$tarball_url" + confirm_kernel_runtime_setup + + download_dir=$(create_temp_dir) + prime_agent_download_dir="$download_dir" + tarball_path="$download_dir/$tarball_name" + + download_prime_agent_package "$version" "$tarball_url" "$tarball_path" + install_prime_agent_package "$tarball_path" + rm -rf "$download_dir" + prime_agent_download_dir= + + if [ "${PRIME_AGENT_NODE_INSTALLED_STANDALONE:-0}" = 1 ]; then + prime_agent_screen "Prime Agent installed" "" "Checking your shell PATH." "" + configure_standalone_node_path + elif command -v "$prime_agent_cmd" >/dev/null 2>&1; then + if [ "$prime_agent_screen_enabled" = 1 ]; then + prime_agent_screen "Prime Agent installed" "" "Run it with: $prime_agent_cmd" "" + else + printf '\nPrime Agent was installed successfully.\n' + printf '\nRun it with: %s\n' "$prime_agent_cmd" + fi + else + if [ "$prime_agent_screen_enabled" = 1 ]; then + prime_agent_screen "Prime Agent installed" "" "PATH update needed for $prime_agent_cmd." "" + prime_agent_restore_terminal + else + printf '\nPrime Agent was installed successfully.\n' + fi + cat < dist/bundle/"); diff --git a/packages/coding-agent/scripts/copy-assets.ts b/packages/coding-agent/scripts/copy-assets.ts index 40b491bee06..a322995b49f 100644 --- a/packages/coding-agent/scripts/copy-assets.ts +++ b/packages/coding-agent/scripts/copy-assets.ts @@ -16,9 +16,15 @@ async function copyFiles(sourceDir: string, targetDir: string, suffix: string): } } +const excludedReleaseDirectoryNames = new Set(["node_modules", ".venv", "__pycache__", ".pytest_cache"]); + +function includeReleasePath(source: string): boolean { + return !source.split(/[\\/]/).some((part) => excludedReleaseDirectoryNames.has(part)); +} + async function replaceDirectory(source: string, target: string): Promise { await rm(target, { recursive: true, force: true }); - await cp(source, target, { recursive: true }); + await cp(source, target, { recursive: true, filter: includeReleasePath }); } async function copyFile(source: string, targetDir: string): Promise { @@ -43,6 +49,7 @@ async function copyBinaryAssets(): Promise { for (const name of ["package.json", "README.md", "CHANGELOG.md"]) { await copyFile(join(packageDir, name), distDir); } + await copyFile(join(repoDir, "install.sh"), distDir); await copyFiles(join(packageDir, "src/modes/interactive/theme"), join(distDir, "theme"), ".json"); await copyFiles(join(packageDir, "src/modes/interactive/assets"), join(distDir, "assets"), ".png"); await copyFile(join(packageDir, "src/core/export-html/template.html"), join(distDir, "export-html")); @@ -50,6 +57,7 @@ async function copyBinaryAssets(): Promise { await replaceDirectory(join(packageDir, "docs"), join(distDir, "docs")); await replaceDirectory(join(packageDir, "examples"), join(distDir, "examples")); await copyFile(join(repoDir, "node_modules/@silvia-odwyer/photon-node/photon_rs_bg.wasm"), distDir); + await replaceDirectory(join(repoDir, "prime-agent-runtime"), join(distDir, "prime-agent-runtime")); await replaceDirectory(join(packageDir, "skills"), join(distDir, "skills")); } diff --git a/scripts/assemble-release-archives.mjs b/scripts/assemble-release-archives.mjs new file mode 100755 index 00000000000..3f085a15ff0 --- /dev/null +++ b/scripts/assemble-release-archives.mjs @@ -0,0 +1,312 @@ +#!/usr/bin/env bun +/** + * Assembles Bun-compiled platform release archives with metadata. + * + * Usage: + * bun scripts/assemble-release-archives.mjs \ + * --base-url \ + * --version x.y.z \ + * --binary-dir \ + * --sidecar-dir \ + * [--channel stable|beta] \ + * [--out-dir ] + * + * Produces /artifacts/: + * prime-agent--.tar.gz (one per platform) + * SHA256SUMS (aggregate checksums) + * (plain version pointer) + * latest.json|beta.json (manifest) + * + * Platforms: darwin-arm64, darwin-x64, linux-arm64, linux-x64 + * + * Archive layout (each .tar.gz): + * prime-agent (compiled binary, executable) + * package.json + * README.md + * CHANGELOG.md + * prime-agent-runtime/ (Python runtime, recursive) + * skills/ (skill files) + * theme/ (UI theme JSON files) + * assets/ (UI assets) + * export-html/ (HTML export templates) + * docs/ (documentation) + * examples/ (example files) + * photon_rs_bg.wasm (image-processing WASM) + + */ + +import { createHash } from "node:crypto"; +import { chmodSync, cpSync, existsSync, mkdirSync, readFileSync, readdirSync, rmSync, writeFileSync } from "node:fs"; +import { spawnSync } from "node:child_process"; +import { isAbsolute, join, relative, resolve } from "node:path"; + +const root = resolve(import.meta.dirname, ".."); +const defaultOutDir = join(root, "packages", "coding-agent", "release"); +const releaseChannels = new Set(["stable", "beta"]); +const publicPackageName = "prime-agent"; +const binaryName = "prime-agent"; + +const PLATFORMS = ["darwin-arm64", "darwin-x64", "linux-arm64", "linux-x64"]; + +const REQUIRED_SIDECARS = [ + "package.json", + "README.md", + "CHANGELOG.md", + "install.sh", + "prime-agent-runtime", + "skills", + "theme", + "assets", + "export-html", + "docs", + "examples", + "photon_rs_bg.wasm", +]; + +function parseArgs(args) { + const parsed = { + baseUrl: undefined, + channel: "stable", + binaryDir: undefined, + sidecarDir: undefined, + outDir: defaultOutDir, + version: undefined, + platforms: [], + }; + + for (let i = 0; i < args.length; i += 1) { + const arg = args[i]; + switch (arg) { + case "--channel": { + const value = args[++i]; + if (!value || !releaseChannels.has(value)) throw new Error("--channel must be stable or beta"); + parsed.channel = value; + break; + } + case "--base-url": { + parsed.baseUrl = args[++i]; + if (!parsed.baseUrl) throw new Error("--base-url requires a value"); + break; + } + case "--version": { + parsed.version = args[++i]; + if (!parsed.version) throw new Error("--version requires a value"); + break; + } + case "--binary-dir": { + parsed.binaryDir = resolve(root, args[++i]); + break; + } + case "--sidecar-dir": { + parsed.sidecarDir = resolve(root, args[++i]); + break; + } + case "--out-dir": { + parsed.outDir = resolve(root, args[++i]); + break; + } + case "--platform": { + const platform = args[++i]; + if (!PLATFORMS.includes(platform)) throw new Error(`Unsupported platform: ${platform}`); + parsed.platforms.push(platform); + break; + } + case "--help": + case "-h": + printHelp(); + process.exit(0); + default: + throw new Error(`Unknown argument: ${arg}`); + } + } + + if (!parsed.baseUrl) throw new Error("--base-url is required"); + if (!parsed.version) throw new Error("--version is required"); + if (!parsed.binaryDir) throw new Error("--binary-dir is required"); + if (!parsed.sidecarDir) throw new Error("--sidecar-dir is required"); + + if (!existsSync(parsed.binaryDir)) throw new Error(`Binary dir not found: ${parsed.binaryDir}`); + if (!existsSync(parsed.sidecarDir)) throw new Error(`Sidecar dir not found: ${parsed.sidecarDir}`); + + parsed.version = normalizeVersion(parsed.version); + if (parsed.platforms.length === 0) parsed.platforms = [...PLATFORMS]; + parsed.baseUrl = parsed.baseUrl.replace(/\/+$/, ""); + return parsed; +} + +function printHelp() { + console.log(`Usage: bun scripts/assemble-release-archives.mjs --base-url --binary-dir --sidecar-dir --version x.y.z [--channel stable|beta] [--platform ] [--out-dir ] + +Assembles platform-specific release archives from a Bun-compiled binary and sidecar files. + +Output: + /artifacts/prime-agent--.tar.gz + /artifacts/SHA256SUMS + /artifacts/ + /artifacts/latest.json (stable) or beta.json (beta) +`); +} + +function normalizeVersion(version) { + const normalized = version.startsWith("v") ? version.slice(1) : version; + if (!/^[0-9A-Za-z.-]+$/.test(normalized)) throw new Error(`Invalid release version: ${version}`); + return normalized; +} + +function assertSafeOutputDir(outDir) { + const base = resolve(defaultOutDir); + const pathFromBase = relative(base, resolve(outDir)); + if (pathFromBase === "" || (!pathFromBase.startsWith("..") && !isAbsolute(pathFromBase))) return; + throw new Error(`Refusing to write output outside ${base}: ${outDir}`); +} + +function sha256File(path) { + const hash = createHash("sha256"); + hash.update(readFileSync(path)); + return hash.digest("hex"); +} + +function createArchive(sourceDir, archivePath) { + const result = spawnSync("tar", ["-czf", archivePath, "-C", sourceDir, "."], { + stdio: "pipe", + encoding: "utf8", + }); + if (result.status !== 0) throw new Error(`tar failed: ${result.stderr || result.stdout}`); +} + +function validateSidecars(sidecarDir) { + const missing = []; + for (const name of REQUIRED_SIDECARS) { + if (!existsSync(join(sidecarDir, name))) { + missing.push(name); + } + } + return missing; +} + +const FORBIDDEN_RELEASE_DIRECTORIES = new Set(["node_modules", ".venv", "__pycache__", ".pytest_cache"]); + +function findForbiddenReleaseDirectory(root, relativePath = "") { + for (const entry of readdirSync(join(root, relativePath), { withFileTypes: true })) { + const child = join(relativePath, entry.name); + if (FORBIDDEN_RELEASE_DIRECTORIES.has(entry.name)) return child; + if (entry.isDirectory()) { + const found = findForbiddenReleaseDirectory(root, child); + if (found) return found; + } + } + return undefined; +} + +function renderPackageManifest(path, version) { + const manifest = JSON.parse(readFileSync(path, "utf8")); + manifest.name = publicPackageName; + manifest.version = version; + manifest.bin = { [binaryName]: `./${binaryName}` }; + manifest.packageManager = "bun@1.3.14"; + writeFileSync(path, `${JSON.stringify(manifest, null, 2)}\n`); +} + +function renderInstaller(path, baseUrl, channel) { + const rendered = readFileSync(path, "utf8") + .replaceAll("__PRIME_AGENT_DOWNLOAD_BASE_URL__", baseUrl) + .replaceAll("__PRIME_AGENT_DEFAULT_RELEASE_CHANNEL__", channel); + if (rendered.includes("__PRIME_AGENT_DOWNLOAD_BASE_URL__") || rendered.includes("__PRIME_AGENT_DEFAULT_RELEASE_CHANNEL__")) { + throw new Error("Release installer still contains an unresolved configuration marker"); + } + writeFileSync(path, rendered); + chmodSync(path, 0o755); +} + +function main() { + const args = parseArgs(process.argv.slice(2)); + const outDir = resolve(args.outDir); + assertSafeOutputDir(outDir); + rmSync(outDir, { force: true, recursive: true }); + + const versionDir = join(outDir, "artifacts"); + mkdirSync(versionDir, { recursive: true }); + + const missing = validateSidecars(args.sidecarDir); + if (missing.length > 0) { + throw new Error( + `Missing required sidecars in ${args.sidecarDir}: ${missing.join(", ")}. ` + + `Run "bun run copy-binary-assets" first.`, + ); + } + const forbiddenDirectory = findForbiddenReleaseDirectory(args.sidecarDir); + if (forbiddenDirectory) { + throw new Error(`Release sidecars contain a forbidden dependency or cache directory: ${forbiddenDirectory}`); + } + + const archives = []; + + for (const platform of args.platforms) { + const binarySource = join(args.binaryDir, platform, "pi"); + if (!existsSync(binarySource)) { + throw new Error(`Binary not found for platform ${platform}: ${binarySource}`); + } + + const stagingDir = join(outDir, "staging", platform); + mkdirSync(stagingDir, { recursive: true }); + + // Copy binary to archive root as "prime-agent" + const dstBinary = join(stagingDir, binaryName); + cpSync(binarySource, dstBinary); + chmodSync(dstBinary, 0o755); + + // Copy all sidecars to archive root + for (const name of REQUIRED_SIDECARS) { + const src = join(args.sidecarDir, name); + cpSync(src, join(stagingDir, name), { recursive: true, dereference: true }); + } + renderPackageManifest(join(stagingDir, "package.json"), args.version); + renderInstaller(join(stagingDir, "install.sh"), args.baseUrl, args.channel); + + const archiveName = `prime-agent-${args.version}-${platform}.tar.gz`; + const archivePath = join(versionDir, archiveName); + createArchive(stagingDir, archivePath); + + const sha256 = sha256File(archivePath); + archives.push({ platform, file: archiveName, sha256 }); + console.log(`Created ${archivePath}`); + + rmSync(join(outDir, "staging"), { force: true, recursive: true }); + } + + if (archives.length === 0) throw new Error("No platform archives created"); + + archives.sort((a, b) => a.file.localeCompare(b.file)); + const sumsContent = archives.map((a) => `${a.sha256} ${a.file}`).join("\n") + "\n"; + writeFileSync(join(versionDir, "SHA256SUMS"), sumsContent); + writeFileSync(join(versionDir, args.channel), `v${args.version}\n`); + + const manifestName = args.channel === "stable" ? "latest.json" : "beta.json"; + writeFileSync( + join(versionDir, manifestName), + JSON.stringify( + { + version: `v${args.version}`, + package: publicPackageName, + platforms: archives.map((a) => ({ + platform: a.platform, + file: a.file, + sha256: a.sha256, + })), + baseUrl: `${args.baseUrl}/releases/v${args.version}`, + }, + null, + 2, + ) + "\n", + ); + + console.log(`\nSHA256SUMS -> ${join(versionDir, "SHA256SUMS")}`); + console.log(`${manifestName} -> ${join(versionDir, manifestName)}`); +} + +try { + main(); +} catch (error) { + console.error(error instanceof Error ? error.message : String(error)); + process.exit(1); +} diff --git a/scripts/build-binaries.sh b/scripts/build-binaries.sh index cbc029f81fb..b41cf1e3cc4 100755 --- a/scripts/build-binaries.sh +++ b/scripts/build-binaries.sh @@ -1,176 +1,123 @@ #!/usr/bin/env bash # -# Build pi binaries for all platforms locally. -# Mirrors .github/workflows/build-binaries.yml +# Build pi binaries for supported platforms locally. +# Mirrors the Bun-compiled release lane in .github/workflows/build-binaries.yml. # # Usage: # ./scripts/build-binaries.sh [--skip-deps] [--platform ] # # Options: -# --skip-deps Skip installing cross-platform dependencies -# --platform Build only for specified platform (darwin-arm64, darwin-x64, linux-x64, linux-arm64, windows-x64) +# --skip-deps Skip frozen dependency install and package build +# --platform Build only for specified platform +# (darwin-arm64, darwin-x64, linux-x64, linux-arm64) # # Output: # packages/coding-agent/binaries/ -# pi-darwin-arm64.tar.gz -# pi-darwin-x64.tar.gz -# pi-linux-x64.tar.gz -# pi-linux-arm64.tar.gz -# pi-windows-x64.zip +# /pi +# +# packages/coding-agent/release/artifacts/ +# prime-agent--.tar.gz +# SHA256SUMS +# stable +# latest.json set -euo pipefail cd "$(dirname "$0")/.." +# Require Bun 1.3.14 +EXPECTED_BUN_VERSION="1.3.14" +ACTUAL_BUN_VERSION=$(bun --version 2>/dev/null || echo "not-found") +if [ "$ACTUAL_BUN_VERSION" != "$EXPECTED_BUN_VERSION" ]; then + echo "ERROR: Expected Bun ${EXPECTED_BUN_VERSION}, got ${ACTUAL_BUN_VERSION}" + echo "Install Bun ${EXPECTED_BUN_VERSION}:" + echo " curl -fsSL https://bun.sh/install | bash -s -- bun-v${EXPECTED_BUN_VERSION}" + exit 1 +fi + SKIP_DEPS=false PLATFORM="" while [[ $# -gt 0 ]]; do - case $1 in - --skip-deps) - SKIP_DEPS=true - shift - ;; - --platform) - PLATFORM="$2" - shift 2 - ;; - *) - echo "Unknown option: $1" - exit 1 - ;; - esac + case $1 in + --skip-deps) + SKIP_DEPS=true + shift + ;; + --platform) + PLATFORM="$2" + shift 2 + ;; + *) + echo "Unknown option: $1" + exit 1 + ;; + esac done # Validate platform if specified if [[ -n "$PLATFORM" ]]; then - case "$PLATFORM" in - darwin-arm64|darwin-x64|linux-x64|linux-arm64|windows-x64) - ;; - *) - echo "Invalid platform: $PLATFORM" - echo "Valid platforms: darwin-arm64, darwin-x64, linux-x64, linux-arm64, windows-x64" - exit 1 - ;; - esac + case "$PLATFORM" in + darwin-arm64|darwin-x64|linux-x64|linux-arm64) ;; + *) + echo "Invalid platform: $PLATFORM" + echo "Valid platforms: darwin-arm64, darwin-x64, linux-x64, linux-arm64" + exit 1 + ;; + esac fi -echo "==> Installing dependencies..." -npm ci - if [[ "$SKIP_DEPS" == "false" ]]; then - echo "==> Installing cross-platform native bindings..." - # npm ci only installs optional deps for the current platform - # We need all platform bindings for bun cross-compilation - # Use --force to bypass platform checks (os/cpu restrictions in package.json) - # Install all in one command to avoid npm removing packages from previous installs - npm install --no-save --force \ - @mariozechner/clipboard-darwin-arm64@0.3.0 \ - @mariozechner/clipboard-darwin-x64@0.3.0 \ - @mariozechner/clipboard-linux-x64-gnu@0.3.0 \ - @mariozechner/clipboard-linux-arm64-gnu@0.3.0 \ - @mariozechner/clipboard-win32-x64-msvc@0.3.0 \ - @img/sharp-darwin-arm64@0.34.5 \ - @img/sharp-darwin-x64@0.34.5 \ - @img/sharp-linux-x64@0.34.5 \ - @img/sharp-linux-arm64@0.34.5 \ - @img/sharp-win32-x64@0.34.5 \ - @img/sharp-libvips-darwin-arm64@1.2.4 \ - @img/sharp-libvips-darwin-x64@1.2.4 \ - @img/sharp-libvips-linux-x64@1.2.4 \ - @img/sharp-libvips-linux-arm64@1.2.4 -else - echo "==> Skipping cross-platform native bindings (--skip-deps)" -fi + echo "==> Installing dependencies for all release platforms (frozen lockfile)..." + bun install --frozen-lockfile --os=* --cpu=* -echo "==> Building all packages..." -npm run build + echo "==> Building all packages..." + bun run build +fi -echo "==> Building binaries..." +echo "==> Compiling binaries..." cd packages/coding-agent # Clean previous builds rm -rf binaries -mkdir -p binaries/{darwin-arm64,darwin-x64,linux-x64,linux-arm64,windows-x64} +mkdir -p binaries # Determine which platforms to build if [[ -n "$PLATFORM" ]]; then - PLATFORMS=("$PLATFORM") + PLATFORMS=("$PLATFORM") else - PLATFORMS=(darwin-arm64 darwin-x64 linux-x64 linux-arm64 windows-x64) + PLATFORMS=(darwin-arm64 darwin-x64 linux-x64 linux-arm64) fi for platform in "${PLATFORMS[@]}"; do - echo "Building for $platform..." - # Externalize koffi to avoid embedding all 18 platform .node files (~74MB) - # into every binary. Koffi is only used on Windows for VT input and the - # call site has a try/catch fallback. For Windows builds, we copy the - # appropriate .node file alongside the binary below. - if [[ "$platform" == "windows-x64" ]]; then - bun build --compile --external koffi --target=bun-$platform ./dist/bun/cli.js --outfile binaries/$platform/pi.exe - else - bun build --compile --external koffi --target=bun-$platform ./dist/bun/cli.js --outfile binaries/$platform/pi - fi + echo "Building for $platform..." + mkdir -p "binaries/$platform" + bun build --compile --external koffi --target="bun-$platform" ./dist/bun/cli.js --outfile "binaries/$platform/pi" done -echo "==> Creating release archives..." - -# Copy shared files to each platform directory -for platform in "${PLATFORMS[@]}"; do - cp package.json binaries/$platform/ - cp README.md binaries/$platform/ - cp CHANGELOG.md binaries/$platform/ - cp ../../node_modules/@silvia-odwyer/photon-node/photon_rs_bg.wasm binaries/$platform/ - mkdir -p binaries/$platform/theme - cp dist/modes/interactive/theme/*.json binaries/$platform/theme/ - mkdir -p binaries/$platform/assets - cp dist/modes/interactive/assets/* binaries/$platform/assets/ - cp -r dist/core/export-html binaries/$platform/ - cp -r docs binaries/$platform/ - cp -r examples binaries/$platform/ - cp -r skills binaries/$platform/ - - # Copy koffi native module for Windows (needed for VT input support) - if [[ "$platform" == "windows-x64" ]]; then - mkdir -p binaries/$platform/node_modules/koffi/build/koffi/win32_x64 - cp ../../node_modules/koffi/index.js binaries/$platform/node_modules/koffi/ - cp ../../node_modules/koffi/package.json binaries/$platform/node_modules/koffi/ - cp ../../node_modules/koffi/build/koffi/win32_x64/koffi.node binaries/$platform/node_modules/koffi/build/koffi/win32_x64/ - fi -done +echo "==> Copying sidecar assets..." +bun run copy-binary-assets -# Create archives -cd binaries - -for platform in "${PLATFORMS[@]}"; do - if [[ "$platform" == "windows-x64" ]]; then - # Windows (zip) - echo "Creating pi-$platform.zip..." - (cd $platform && zip -r ../pi-$platform.zip .) - else - # Unix platforms (tar.gz) - use wrapper directory for mise compatibility - echo "Creating pi-$platform.tar.gz..." - mv $platform pi && tar -czf pi-$platform.tar.gz pi && mv pi $platform - fi -done - -# Extract archives for easy local testing -echo "==> Extracting archives for testing..." -for platform in "${PLATFORMS[@]}"; do - rm -rf $platform - if [[ "$platform" == "windows-x64" ]]; then - mkdir -p $platform && (cd $platform && unzip -q ../pi-$platform.zip) - else - tar -xzf pi-$platform.tar.gz && mv pi $platform - fi -done +echo "==> Assembling release archives..." +VERSION=$(bun -e "console.log(require('./package.json').version)") +BASE_URL="${PRIME_AGENT_DOWNLOAD_BASE_URL:-https://releases.pi.ai}" +PACK_PLATFORM_ARGS=() +if [[ -n "$PLATFORM" ]]; then + PACK_PLATFORM_ARGS=(--platform "$PLATFORM") +fi +bun ../../scripts/pack-prime-agent-release.mjs \ + --channel stable \ + --version "$VERSION" \ + --base-url "$BASE_URL" \ + --binary-base-dir "$(pwd)/binaries" \ + --sidecar-dir "$(pwd)/dist" \ + --out-dir "$(pwd)/release" \ + "${PACK_PLATFORM_ARGS[@]}" echo "" echo "==> Build complete!" -echo "Archives available in packages/coding-agent/binaries/" -ls -lh *.tar.gz *.zip 2>/dev/null || true +echo "Platform archives:" +ls -lh "release/artifacts/"*.tar.gz 2>/dev/null || true echo "" -echo "Extracted directories for testing:" -for platform in "${PLATFORMS[@]}"; do - echo " binaries/$platform/pi" -done +echo "Metadata:" +ls -lh "release/artifacts/SHA256SUMS" "release/artifacts/latest.json" "release/artifacts/stable" 2>/dev/null || true diff --git a/scripts/check-browser-smoke.mjs b/scripts/check-browser-smoke.mjs index d590988d624..fe24ff3da5b 100644 --- a/scripts/check-browser-smoke.mjs +++ b/scripts/check-browser-smoke.mjs @@ -1,36 +1,26 @@ import { writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; -import { build } from "esbuild"; -const outputPath = join(tmpdir(), "pi-browser-smoke.js"); -const errorLogPath = join(tmpdir(), "pi-browser-smoke-errors.log"); +const outputPath = join(tmpdir(), "prime-agent-browser-smoke.js"); +const errorLogPath = join(tmpdir(), "prime-agent-browser-smoke-errors.log"); try { - await build({ - entryPoints: ["scripts/browser-smoke-entry.ts"], - bundle: true, - platform: "browser", + const result = await Bun.build({ + entrypoints: ["scripts/browser-smoke-entry.ts"], + target: "browser", format: "esm", - logLevel: "silent", - outfile: outputPath, + sourcemap: "none", + write: false, + throw: false, }); - process.exit(0); -} catch (error) { - let detailedErrors = ""; - if (error && typeof error === "object" && "errors" in error && Array.isArray(error.errors)) { - detailedErrors = error.errors - .map((entry) => { - const location = entry.location - ? `${entry.location.file}:${entry.location.line}:${entry.location.column}` - : ""; - return [location, entry.text].filter(Boolean).join(" "); - }) - .join("\n"); + if (!result.success || result.outputs.length !== 1) { + throw new Error(result.logs.map((entry) => entry.message).join("\n") || "Bun browser bundle produced no output"); } - - const baseError = error instanceof Error ? (error.stack ?? error.message) : String(error); - writeFileSync(errorLogPath, [detailedErrors, baseError].filter(Boolean).join("\n\n"), "utf-8"); + await Bun.write(outputPath, result.outputs[0]); +} catch (error) { + const message = error instanceof Error ? (error.stack ?? error.message) : String(error); + writeFileSync(errorLogPath, message, "utf-8"); console.error(`Browser smoke check failed. See ${errorLogPath}`); process.exit(1); } diff --git a/scripts/compiled-artifact-smoke.ts b/scripts/compiled-artifact-smoke.ts new file mode 100644 index 00000000000..c1055f49a3c --- /dev/null +++ b/scripts/compiled-artifact-smoke.ts @@ -0,0 +1,161 @@ +#!/usr/bin/env bun + +import { spawnSync } from "node:child_process"; +import { chmodSync, copyFileSync, cpSync, existsSync, mkdtempSync, mkdirSync, readFileSync, rmSync, statSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { basename, dirname, join, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; + +const rootDir = resolve(dirname(fileURLToPath(import.meta.url)), ".."); +const defaultBinary = join(rootDir, "packages", "coding-agent", "dist", "pi"); +const defaultDist = dirname(defaultBinary); + +export const REQUIRED_BINARY_SIDECARS = [ + "package.json", + "README.md", + "CHANGELOG.md", + "install.sh", + "prime-agent-runtime", + "skills", + "theme", + "assets", + "export-html", + "docs", + "examples", + "photon_rs_bg.wasm", +] as const; + +type RunResult = { + args: string[]; + exitCode: number; + stdout: string; + stderr: string; +}; + +function run(executable: string, args: string[], cwd: string, path: string): RunResult { + const result = spawnSync(executable, args, { + cwd, + env: { ...process.env, PATH: path }, + encoding: "utf8", + }); + return { + args, + exitCode: result.status ?? 1, + stdout: result.stdout ?? "", + stderr: result.stderr ?? result.error?.message ?? "", + }; +} + +export function inspectNativeExecutable(path: string): "elf" | "mach-o" | "unknown" { + const header = readFileSync(path).subarray(0, 4).toString("hex"); + if (header === "7f454c46") return "elf"; + if ([ + "feedface", "cefaedfe", "feedfacf", "cffaedfe", + "cafebabe", "bebafeca", "cafebabf", "bfbafeca", + ].includes(header)) return "mach-o"; + return "unknown"; +} + +export function runEmptyDiagnostic(binaryPath = defaultBinary) { + if (!existsSync(binaryPath)) throw new Error(`Compiled binary not found: ${binaryPath}`); + const tempDir = mkdtempSync(join(tmpdir(), "prime-agent-binary-empty-")); + try { + const binary = join(tempDir, basename(binaryPath)); + copyFileSync(binaryPath, binary); + chmodSync(binary, 0o755); + const emptyPath = join(tempDir, "empty-path"); + mkdirSync(emptyPath); + const runResult = run(binary, ["--version"], tempDir, emptyPath); + return { + mode: "binary-only-diagnostic" as const, + nativeFormat: inspectNativeExecutable(binary), + run: runResult, + detectedMissingPackageJson: runResult.exitCode !== 0 && runResult.stderr.includes("package.json"), + }; + } finally { + rmSync(tempDir, { recursive: true, force: true }); + } +} + +export function runPackagedSmoke(binaryPath = defaultBinary, sourceDist = defaultDist) { + if (!existsSync(binaryPath)) throw new Error(`Compiled binary not found: ${binaryPath}`); + const missing = REQUIRED_BINARY_SIDECARS.filter((name) => !existsSync(join(sourceDist, name))); + if (missing.length > 0) throw new Error(`Missing compiled-binary sidecars: ${missing.join(", ")}`); + + const tempDir = mkdtempSync(join(tmpdir(), "prime-agent-binary-package-")); + try { + const binary = join(tempDir, basename(binaryPath)); + copyFileSync(binaryPath, binary); + chmodSync(binary, 0o755); + for (const name of REQUIRED_BINARY_SIDECARS) { + cpSync(join(sourceDist, name), join(tempDir, name), { recursive: true, dereference: true }); + } + + const emptyPath = join(tempDir, "empty-path"); + mkdirSync(emptyPath); + let nodeAvailable = false; + let npmAvailable = false; + try { nodeAvailable = run("node", ["--version"], tempDir, emptyPath).exitCode === 0; } catch {} + try { npmAvailable = run("npm", ["--version"], tempDir, emptyPath).exitCode === 0; } catch {} + const runs = [["--version"], ["--help"]].map((args) => run(binary, args, tempDir, emptyPath)); + const passed = + inspectNativeExecutable(binary) !== "unknown" && + !nodeAvailable && + !npmAvailable && + runs.every((result) => result.exitCode === 0 && result.stdout.trim().length > 0); + return { + mode: "packaged-layout-smoke" as const, + nativeFormat: inspectNativeExecutable(binary), + binarySizeBytes: statSync(binary).size, + path: emptyPath, + nodeAvailable, + npmAvailable, + sidecars: [...REQUIRED_BINARY_SIDECARS], + runs, + passed, + }; + } finally { + rmSync(tempDir, { recursive: true, force: true }); + } +} + +function argumentValue(name: string): string | undefined { + const index = process.argv.indexOf(name); + return index >= 0 ? process.argv[index + 1] : undefined; +} + +if (import.meta.main) { + const binary = resolve(argumentValue("--binary") ?? defaultBinary); + const sourceDist = resolve(argumentValue("--dist") ?? dirname(binary)); + try { + const empty = runEmptyDiagnostic(binary); + const packaged = runPackagedSmoke(binary, sourceDist); + console.log(JSON.stringify({ + empty: { + mode: empty.mode, + nativeFormat: empty.nativeFormat, + exitCode: empty.run.exitCode, + detectedMissingPackageJson: empty.detectedMissingPackageJson, + }, + packaged: { + mode: packaged.mode, + nativeFormat: packaged.nativeFormat, + binarySizeBytes: packaged.binarySizeBytes, + nodeAvailable: packaged.nodeAvailable, + npmAvailable: packaged.npmAvailable, + sidecars: packaged.sidecars, + runs: packaged.runs.map((result) => ({ + args: result.args, + exitCode: result.exitCode, + stdoutLength: result.stdout.length, + stderrLength: result.stderr.length, + })), + passed: packaged.passed, + }, + }, null, 2)); + if (empty.nativeFormat === "unknown" || !empty.detectedMissingPackageJson || !packaged.passed) process.exit(1); + } catch (error) { + console.error(error instanceof Error ? error.message : String(error)); + process.exit(1); + } +} diff --git a/scripts/pack-prime-agent-release.mjs b/scripts/pack-prime-agent-release.mjs old mode 100644 new mode 100755 index d264025e112..220105a68a8 --- a/scripts/pack-prime-agent-release.mjs +++ b/scripts/pack-prime-agent-release.mjs @@ -1,348 +1,166 @@ -#!/usr/bin/env node - -import { spawnSync } from "node:child_process"; -import { createHash } from "node:crypto"; -import { - cpSync, - existsSync, - mkdirSync, - renameSync, - readFileSync, - rmSync, - statSync, - writeFileSync, -} from "node:fs"; -import { basename, dirname, isAbsolute, join, relative, resolve } from "node:path"; -import { fileURLToPath } from "node:url"; - -const root = resolve(dirname(fileURLToPath(import.meta.url)), ".."); +#!/usr/bin/env bun +/** + * Produce Bun-compiled platform release archives. + * + * This script is the CI entry point for the release artifact lane. It expects + * pre-built Bun-compiled binaries for 4 platforms (darwin-arm64, darwin-x64, + * linux-arm64, linux-x64) and sidecar files, then produces platform-specific + * .tar.gz archives with checksums and channel metadata. + * + * No npm or Node.js is used during artifact assembly. + * + * Usage: + * bun scripts/pack-prime-agent-release.mjs \ + * --base-url \ + * [--channel stable|beta] \ + * [--version x.y.z] \ + * [--out-dir ] \ + * [--binary-base-dir ] \ + * [--sidecar-dir ] + * + * Output: + * /artifacts/ + * prime-agent--.tar.gz + * SHA256SUMS + * + * latest.json|beta.json + */ + +import { existsSync, readFileSync } from "node:fs"; +import { resolve, join } from "node:path"; + +const root = resolve(import.meta.dirname, ".."); const defaultOutputDir = join(root, "packages", "coding-agent", "release"); const defaultBaseUrl = process.env.PRIME_AGENT_DOWNLOAD_BASE_URL; -const publicPackageName = process.env.PRIME_AGENT_PACKAGE_NAME || "prime-agent"; -const publicCommandName = process.env.PRIME_AGENT_CMD || "prime-agent"; const releaseChannels = new Set(["stable", "beta"]); -const releasePackages = [ - { packageDir: "ai", publicName: undefined, artifactName: "prime-agent-ai" }, - { packageDir: "tui", publicName: undefined, artifactName: "prime-agent-tui" }, - { packageDir: "agent", publicName: undefined, artifactName: "prime-agent-core" }, - { packageDir: "coding-agent", publicName: publicPackageName, artifactName: publicPackageName }, -]; - function parseArgs(args) { const parsed = { baseUrl: defaultBaseUrl, channel: "stable", + binaryBaseDir: join(root, "packages", "coding-agent", "binaries"), + sidecarDir: join(root, "packages", "coding-agent", "dist"), outDir: defaultOutputDir, version: undefined, + platforms: [], }; for (let i = 0; i < args.length; i += 1) { const arg = args[i]; switch (arg) { case "--channel": { - const value = args[i + 1]; - if (!value || !releaseChannels.has(value)) { - throw new Error("--channel must be stable or beta"); - } + const value = args[++i]; + if (!value || !releaseChannels.has(value)) throw new Error("--channel must be stable or beta"); parsed.channel = value; - i += 1; break; } case "--base-url": { - const value = args[i + 1]; + const value = args[++i]; if (!value) throw new Error("--base-url requires a value"); parsed.baseUrl = value; - i += 1; + break; + } + case "--version": { + const value = args[++i]; + if (!value) throw new Error("--version requires a value"); + parsed.version = value; break; } case "--out-dir": { - const value = args[i + 1]; + const value = args[++i]; if (!value) throw new Error("--out-dir requires a value"); parsed.outDir = resolve(root, value); - i += 1; break; } - case "--version": { - const value = args[i + 1]; - if (!value) throw new Error("--version requires a value"); - parsed.version = normalizeVersion(value); - i += 1; + case "--binary-base-dir": { + const value = args[++i]; + if (!value) throw new Error("--binary-base-dir requires a value"); + parsed.binaryBaseDir = resolve(root, value); + break; + } + case "--sidecar-dir": { + const value = args[++i]; + if (!value) throw new Error("--sidecar-dir requires a value"); + parsed.sidecarDir = resolve(root, value); + break; + } + case "--platform": { + const value = args[++i]; + if (!value) throw new Error("--platform requires a value"); + parsed.platforms.push(value); break; } case "--help": case "-h": printHelp(); process.exit(0); - break; default: throw new Error(`Unknown argument: ${arg}`); } } - if (!parsed.baseUrl) { - throw new Error("--base-url or PRIME_AGENT_DOWNLOAD_BASE_URL is required"); + if (!parsed.baseUrl) throw new Error("--base-url or PRIME_AGENT_DOWNLOAD_BASE_URL is required"); + parsed.baseUrl = parsed.baseUrl.replace(/\/+$/, ""); + + // Resolve version from coding-agent package.json if not provided. + if (!parsed.version) { + const cliPkg = JSON.parse(readFileSync(join(root, "packages", "coding-agent", "package.json"), "utf8")); + parsed.version = cliPkg.version; } - parsed.baseUrl = parsed.baseUrl.replace(/\/+$/, ""); return parsed; } function printHelp() { - console.log(`Usage: node scripts/pack-prime-agent-release.mjs --base-url url [--channel stable|beta] [--version x.y.z] [--out-dir path] + console.log(`Usage: bun scripts/pack-prime-agent-release.mjs --base-url url [--channel stable|beta] [--version x.y.z] [--out-dir path] [--binary-base-dir path] [--sidecar-dir path] [--platform platform] -Creates private npm tarballs for R2 distribution: +Creates platform release archives: - /artifacts/prime-agent-.tgz - /artifacts/prime-agent-ai-.tgz - /artifacts/prime-agent-core-.tgz - /artifacts/prime-agent-tui-.tgz + /artifacts/prime-agent--.tar.gz /artifacts/SHA256SUMS /artifacts/ /artifacts/latest.json (stable) or beta.json (beta) -`); -} - -function normalizeVersion(version) { - const normalized = version.startsWith("v") ? version.slice(1) : version; - if (!/^[0-9A-Za-z.-]+$/.test(normalized)) { - throw new Error(`Invalid release version: ${version}`); - } - return normalized; -} - -function readJson(path) { - return JSON.parse(readFileSync(path, "utf8")); -} - -function writeJson(path, value) { - writeFileSync(path, `${JSON.stringify(value, null, 2)}\n`); -} - -function packagePath(packageDir) { - return join(root, "packages", packageDir); -} - -function assertSafeOutputDir(outDir) { - const relativeToReleaseRoot = relative(defaultOutputDir, outDir); - if (relativeToReleaseRoot === "" || (!relativeToReleaseRoot.startsWith("..") && !isAbsolute(relativeToReleaseRoot))) { - return; - } - throw new Error(`Refusing to remove output directory outside ${defaultOutputDir}: ${outDir}`); -} - -function packageJsonPath(packageDir) { - return join(packagePath(packageDir), "package.json"); -} - -function requireBuiltPackage(packageDir) { - const dist = join(packagePath(packageDir), "dist"); - if (!existsSync(dist)) { - throw new Error(`Missing ${dist}. Run npm run build before packing a release.`); - } -} - -function copyIfExists(source, target) { - if (existsSync(source)) { - cpSync(source, target, { recursive: true }); - } -} - -function npmTarballName(packageName, version) { - return `${packageName.replace(/^@/, "").replace("/", "-")}-${version}.tgz`; -} -function releaseTarballUrl(baseUrl, version, tarballFile) { - return `${baseUrl}/releases/v${version}/${tarballFile}`; -} - -function rewriteInternalDependencies(dependencies, internalPackageUrls) { - if (!dependencies) return undefined; - const rewritten = {}; - for (const [name, range] of Object.entries(dependencies)) { - rewritten[name] = internalPackageUrls.get(name) || range; - } - return rewritten; -} - -function releaseScripts(sourceScripts) { - if (!sourceScripts?.postinstall) return undefined; - return { - postinstall: sourceScripts.postinstall, - }; -} - -function createReleasePackageJson(sourcePackage, packageName, releaseVersion, internalPackageUrls) { - const packageJson = { - ...sourcePackage, - name: packageName, - version: releaseVersion, - dependencies: rewriteInternalDependencies(sourcePackage.dependencies, internalPackageUrls), - optionalDependencies: rewriteInternalDependencies(sourcePackage.optionalDependencies, internalPackageUrls), - scripts: releaseScripts(sourcePackage.scripts), - }; - - delete packageJson.devDependencies; - delete packageJson.overrides; - delete packageJson.private; - - if (packageName === publicPackageName) { - packageJson.bin = { - [publicCommandName]: "dist/bundle/cli.js", - }; - packageJson.piConfig = { - ...(packageJson.piConfig || {}), - name: publicCommandName, - configDir: ".prime/agent", - }; - } - - return packageJson; -} - -function copyPackageContents(sourceDir, targetDir, packageJson) { - mkdirSync(targetDir, { recursive: true }); - writeJson(join(targetDir, "package.json"), packageJson); - - for (const entry of ["dist", "docs", "examples", "skills", "postinstall.cjs", "README.md", "CHANGELOG.md"]) { - copyIfExists(join(sourceDir, entry), join(targetDir, entry)); - } -} - -function run(command, args, cwd) { - const result = spawnSync(command, args, { - cwd, - stdio: "pipe", - encoding: "utf8", - }); - - if (result.status !== 0) { - if (result.stdout) process.stdout.write(result.stdout); - if (result.stderr) process.stderr.write(result.stderr); - throw new Error(`${command} ${args.join(" ")} failed with exit code ${result.status}`); - } - - if (result.stderr) process.stderr.write(result.stderr); - return result.stdout.trim(); -} - -function sha256File(path) { - const hash = createHash("sha256"); - hash.update(readFileSync(path)); - return hash.digest("hex"); +--binary-base-dir defaults to packages/coding-agent/binaries/ +--sidecar-dir defaults to packages/coding-agent/dist/ +`); } -function main() { +async function main() { const args = parseArgs(process.argv.slice(2)); - const sourcePackages = new Map( - releasePackages.map((releasePackage) => [ - releasePackage.packageDir, - readJson(packageJsonPath(releasePackage.packageDir)), - ]), - ); - const cliPackage = sourcePackages.get("coding-agent"); - const releaseVersion = args.version || normalizeVersion(process.env.PRIME_AGENT_VERSION || cliPackage.version); - for (const releasePackage of releasePackages) { - requireBuiltPackage(releasePackage.packageDir); - } - - // Dependency keys stay on the source package names so existing compiled imports - // keep resolving, while release package names and artifact filenames are branded. - const sourcePackageNames = new Map(); - const packageNames = new Map(); - const artifactFiles = new Map(); - for (const releasePackage of releasePackages) { - const sourcePackage = sourcePackages.get(releasePackage.packageDir); - const packageName = releasePackage.publicName || releasePackage.artifactName || sourcePackage.name; - sourcePackageNames.set(releasePackage.packageDir, sourcePackage.name); - packageNames.set(releasePackage.packageDir, packageName); - artifactFiles.set( - releasePackage.packageDir, - npmTarballName(releasePackage.artifactName || packageName, releaseVersion), + if (!existsSync(args.sidecarDir)) { + throw new Error( + `Sidecar dir not found: ${args.sidecarDir}. Run "bun run build && bun run copy-binary-assets" first.`, ); } - const internalPackageUrls = new Map(); - for (const releasePackage of releasePackages) { - if (releasePackage.packageDir === "coding-agent") continue; - const sourcePackageName = sourcePackageNames.get(releasePackage.packageDir); - const artifactFile = artifactFiles.get(releasePackage.packageDir); - internalPackageUrls.set(sourcePackageName, releaseTarballUrl(args.baseUrl, releaseVersion, artifactFile)); - } - - const stagingRoot = join(args.outDir, "packages"); - const artifactsDir = join(args.outDir, "artifacts"); - assertSafeOutputDir(args.outDir); - rmSync(args.outDir, { force: true, recursive: true }); - mkdirSync(stagingRoot, { recursive: true }); - mkdirSync(artifactsDir, { recursive: true }); - - const tarballs = []; - for (const releasePackage of releasePackages) { - const sourcePackage = sourcePackages.get(releasePackage.packageDir); - const packageName = packageNames.get(releasePackage.packageDir); - const stagingDir = join(stagingRoot, releasePackage.packageDir); - const packageJson = createReleasePackageJson( - sourcePackage, - packageName, - releaseVersion, - internalPackageUrls, - ); - - copyPackageContents(packagePath(releasePackage.packageDir), stagingDir, packageJson); - - const tarballName = run("npm", ["pack", stagingDir, "--pack-destination", artifactsDir, "--silent"], root) - .split("\n") - .at(-1); - if (!tarballName) { - throw new Error(`npm pack did not report a tarball name for ${packageName}`); - } - - const tarballPath = join(artifactsDir, basename(tarballName)); - if (!existsSync(tarballPath) || !statSync(tarballPath).isFile()) { - throw new Error(`npm pack did not create ${tarballPath}`); - } - - const artifactFile = artifactFiles.get(releasePackage.packageDir); - const artifactPath = join(artifactsDir, artifactFile); - if (tarballPath !== artifactPath) { - rmSync(artifactPath, { force: true }); - renameSync(tarballPath, artifactPath); - } - - tarballs.push({ - name: packageName, - file: artifactFile, - sha256: sha256File(artifactPath), - }); + // The assembly script is in the same scripts directory. + const assemblyScript = join(import.meta.dirname, "assemble-release-archives.mjs"); + if (!existsSync(assemblyScript)) { + throw new Error(`Assembly script not found: ${assemblyScript}`); } - tarballs.sort((left, right) => left.file.localeCompare(right.file)); - writeFileSync( - join(artifactsDir, "SHA256SUMS"), - tarballs.map((tarball) => `${tarball.sha256} ${tarball.file}`).join("\n") + "\n", - ); - writeFileSync(join(artifactsDir, args.channel), `v${releaseVersion}\n`); - const manifestName = args.channel === "stable" ? "latest.json" : "beta.json"; - writeJson(join(artifactsDir, manifestName), { - version: `v${releaseVersion}`, - package: publicPackageName, - tarball: `releases/v${releaseVersion}/${artifactFiles.get("coding-agent")}`, - tarballs: tarballs.map((tarball) => ({ - package: tarball.name, - file: tarball.file, - sha256: tarball.sha256, - })), - }); + const { spawnSync } = await import("node:child_process"); + const assemblyArgs = [ + assemblyScript, + "--base-url", args.baseUrl, + "--channel", args.channel, + "--version", args.version, + "--binary-dir", args.binaryBaseDir, + "--sidecar-dir", args.sidecarDir, + "--out-dir", args.outDir, + ]; + for (const platform of args.platforms) assemblyArgs.push("--platform", platform); + const result = spawnSync(process.execPath, assemblyArgs, { stdio: "inherit", encoding: "utf8" }); - for (const tarball of tarballs) { - console.log(`Created ${join(artifactsDir, tarball.file)}`); + if (result.status !== 0) { + throw new Error(`assemble-release-archives.mjs failed with exit code ${result.status}`); } } try { - main(); + await main(); } catch (error) { console.error(error instanceof Error ? error.message : String(error)); process.exit(1); diff --git a/scripts/release.mjs b/scripts/release.mjs index 4eb4c83cba8..65adaea8039 100755 --- a/scripts/release.mjs +++ b/scripts/release.mjs @@ -1,19 +1,19 @@ -#!/usr/bin/env node +#!/usr/bin/env bun /** - * Release script for pi-mono + * Release script for Prime Agent * * Usage: - * node scripts/release.mjs - * node scripts/release.mjs - * node scripts/release.mjs --dry-run (preview changelog updates only) + * bun scripts/release.mjs + * bun scripts/release.mjs + * bun scripts/release.mjs --dry-run (preview changelog updates only) * * Steps: * 1. Check for uncommitted changes - * 2. Bump version via npm run version:xxx or set an explicit version + * 2. Bump every workspace version and refresh bun.lock * 3. Update CHANGELOG.md files: aggregate .changes/*.md fragments into a * [version] - date section, git rm the consumed fragments * 4. Commit and tag - * 5. Publish to npm + * 5. Publish the npm compatibility packages with Bun */ import { execSync } from "child_process"; @@ -27,7 +27,7 @@ const BUMP_TYPES = new Set(["major", "minor", "patch"]); const SEMVER_RE = /^\d+\.\d+\.\d+$/; if (!RELEASE_TARGET || (!BUMP_TYPES.has(RELEASE_TARGET) && !SEMVER_RE.test(RELEASE_TARGET))) { - console.error("Usage: node scripts/release.mjs [--dry-run]"); + console.error("Usage: bun scripts/release.mjs [--dry-run]"); process.exit(1); } @@ -82,7 +82,7 @@ function bumpOrSetVersion(target) { if (BUMP_TYPES.has(target)) { console.log(`Bumping version (${target})...`); - run(`npm run version:${target}`); + run(`bun run version:${target}`); return getVersion(); } @@ -92,9 +92,7 @@ function bumpOrSetVersion(target) { } console.log(`Setting explicit version (${target})...`); - run( - `npm version ${target} -ws --no-git-tag-version && node scripts/sync-versions.js && npx shx rm -rf node_modules packages/*/node_modules package-lock.json && npm install`, - ); + run(`bun scripts/set-version.ts ${target}`); return getVersion(); } @@ -214,8 +212,8 @@ run(`git commit -m "Release v${version}"`); run(`git tag v${version}`); console.log(); -console.log("Publishing to npm..."); -run("npm run publish"); +console.log("Publishing npm compatibility packages..."); +run("bun run publish"); console.log(); console.log("Pushing to remote..."); From d0e3c63c972af2020a447df6492969e6939667f4 Mon Sep 17 00:00:00 2001 From: Seth Date: Tue, 1 Sep 2026 17:39:02 -0400 Subject: [PATCH 06/45] ci: run checks and releases with pinned Bun --- .github/dependabot.yml | 2 +- .github/workflows/build-binaries.yml | 119 ++++++++-- .github/workflows/ci.yml | 218 ++++++++++++++++--- .github/workflows/nightly-process-stress.yml | 19 +- 4 files changed, 290 insertions(+), 68 deletions(-) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 6742d53aa14..21d8c43a7f9 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -1,4 +1,4 @@ -# Dependency update PRs wait out the same 7-day cooldown that .npmrc's +# Dependency update PRs wait out the same 7-day cooldown that bunfig.toml's # min-release-age enforces locally. Security-advisory PRs are exempt # from cooldown by design, so CVE fixes still arrive promptly. version: 2 diff --git a/.github/workflows/build-binaries.yml b/.github/workflows/build-binaries.yml index c140daa67fa..af5cd25a98c 100644 --- a/.github/workflows/build-binaries.yml +++ b/.github/workflows/build-binaries.yml @@ -16,7 +16,6 @@ on: concurrency: group: release-prime-agent cancel-in-progress: false - queue: max permissions: contents: read @@ -40,6 +39,11 @@ jobs: fetch-tags: true persist-credentials: false + - name: Setup Bun 1.3.14 + uses: oven-sh/setup-bun@4bc047ad259df6fc24a6c9b0f9a0cb08cf17fbe5 # v2.0.1 + with: + bun-version: 1.3.14 + - name: Resolve release context id: context env: @@ -72,7 +76,7 @@ jobs: build_ref="$REF_NAME" publish_production=true else - production_version=$(node -p "require('./package.json').version") + production_version=$(bun -e "console.log(require('./package.json').version)") build_ref="$GITHUB_SHA_VALUE" beta_version="${production_version}-beta.${RUN_NUMBER}.${RUN_ATTEMPT}.${GITHUB_SHA_VALUE::7}" publish_beta=true @@ -80,7 +84,7 @@ jobs: previous_version= if [ -n "$BEFORE_SHA" ] && ! printf '%s\n' "$BEFORE_SHA" | grep -Eq '^0+$' && git cat-file -e "${BEFORE_SHA}:package.json"; then git show "${BEFORE_SHA}:package.json" > /tmp/previous-package.json - previous_version=$(node -p "require('/tmp/previous-package.json').version") + previous_version=$(bun -e "console.log(require('/tmp/previous-package.json').version)") fi if [ -z "$previous_version" ] || [ "$production_version" != "$previous_version" ]; then @@ -133,20 +137,38 @@ jobs: ref: ${{ env.BUILD_REF }} persist-credentials: false - - name: Setup Node.js - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + - name: Setup Bun 1.3.14 + uses: oven-sh/setup-bun@4bc047ad259df6fc24a6c9b0f9a0cb08cf17fbe5 # v2.0.1 with: - node-version: '22' - registry-url: 'https://registry.npmjs.org' + bun-version: 1.3.14 + + - name: Install system dependencies + run: | + sudo apt-get update + sudo apt-get install -y libcairo2-dev libpango1.0-dev libjpeg-dev libgif-dev librsvg2-dev - - name: Install dependencies - run: npm ci + - name: Install dependencies for all release platforms + run: bun install --frozen-lockfile --os=* --cpu=* - name: Build - run: npm run build + run: bun run build - name: Check - run: npm run check + run: bun run check + + - name: Compile binaries for all platforms + working-directory: packages/coding-agent + run: | + mkdir -p binaries + for platform in darwin-arm64 darwin-x64 linux-arm64 linux-x64; do + echo "Compiling for $platform..." + mkdir -p "binaries/$platform" + bun build --compile --external koffi --target="bun-$platform" ./dist/bun/cli.js --outfile "binaries/$platform/pi" + done + + - name: Copy binary sidecar assets + working-directory: packages/coding-agent + run: bun run copy-binary-assets - name: Pack production release if: env.PUBLISH_PRODUCTION == 'true' @@ -154,7 +176,7 @@ jobs: PRIME_AGENT_DOWNLOAD_BASE_URL: ${{ vars.R2_PUBLIC_BASE_URL }} run: | test -n "$PRIME_AGENT_DOWNLOAD_BASE_URL" - npm run release:pack -- \ + bun run release:pack -- \ --channel stable \ --version "$PRODUCTION_VERSION" \ --base-url "$PRIME_AGENT_DOWNLOAD_BASE_URL" \ @@ -166,12 +188,65 @@ jobs: PRIME_AGENT_DOWNLOAD_BASE_URL: ${{ vars.R2_PUBLIC_BASE_URL }} run: | test -n "$PRIME_AGENT_DOWNLOAD_BASE_URL" - npm run release:pack -- \ + bun run release:pack -- \ --channel beta \ --version "$BETA_VERSION" \ --base-url "$PRIME_AGENT_DOWNLOAD_BASE_URL" \ --out-dir packages/coding-agent/release/beta + - name: Smoke test host-platform archive + if: env.PUBLISH_PRODUCTION == 'true' || env.PUBLISH_BETA == 'true' + run: | + # Determine which archive to test (prefer production, fall back to beta) + ARTIFACTS_DIR="" + if [ "$PUBLISH_PRODUCTION" = "true" ]; then + ARTIFACTS_DIR="packages/coding-agent/release/production/artifacts" + else + ARTIFACTS_DIR="packages/coding-agent/release/beta/artifacts" + fi + + # Detect host platform + OS=$(uname -s) + ARCH=$(uname -m) + case "$OS" in + Linux) + case "$ARCH" in + x86_64) PLATFORM="linux-x64" ;; + aarch64|arm64) PLATFORM="linux-arm64" ;; + *) echo "Unknown host arch: $ARCH"; exit 1 ;; + esac + ;; + Darwin) + case "$ARCH" in + x86_64) PLATFORM="darwin-x64" ;; + arm64) PLATFORM="darwin-arm64" ;; + *) echo "Unknown host arch: $ARCH"; exit 1 ;; + esac + ;; + *) + echo "Unknown host OS: $OS"; exit 1 ;; + esac + + # Extract host-platform archive and run --version + ARCHIVE=$(ls "$ARTIFACTS_DIR"/*-"${PLATFORM}.tar.gz" 2>/dev/null | head -1) + if [ -z "$ARCHIVE" ]; then + echo "No archive for host platform ${PLATFORM} in ${ARTIFACTS_DIR}" + exit 1 + fi + + SMOKE_DIR=$(mktemp -d) + tar -xzf "$ARCHIVE" -C "$SMOKE_DIR" + chmod +x "$SMOKE_DIR/prime-agent" + if "$SMOKE_DIR/prime-agent" --version >/dev/null 2>&1; then + echo "Smoke test passed: $("$SMOKE_DIR/prime-agent" --version 2>&1)" + else + echo "Smoke test FAILED." + "$SMOKE_DIR/prime-agent" --version 2>&1 || true + rm -rf "$SMOKE_DIR" + exit 1 + fi + rm -rf "$SMOKE_DIR" + - name: Upload production artifacts if: env.PUBLISH_PRODUCTION == 'true' uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 @@ -212,6 +287,11 @@ jobs: ref: ${{ env.BUILD_REF }} persist-credentials: false + - name: Setup Bun 1.3.14 + uses: oven-sh/setup-bun@4bc047ad259df6fc24a6c9b0f9a0cb08cf17fbe5 # v2.0.1 + with: + bun-version: 1.3.14 + - name: Download production artifacts if: env.PUBLISH_PRODUCTION == 'true' uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 @@ -232,7 +312,7 @@ jobs: export INSTALL_BASE_URL test -n "$INSTALL_BASE_URL" - node - <<'NODE' + bun -e ' const fs = require("node:fs"); const baseUrl = process.env.INSTALL_BASE_URL; if (!baseUrl) throw new Error("INSTALL_BASE_URL is required"); @@ -242,7 +322,7 @@ jobs: .replaceAll("__PRIME_AGENT_DEFAULT_RELEASE_CHANNEL__", channel); fs.writeFileSync("/tmp/prime-agent-install.sh", renderInstaller("stable")); fs.writeFileSync("/tmp/prime-agent-install-beta.sh", renderInstaller("beta")); - NODE + ' - name: Extract production release notes if: env.PUBLISH_PRODUCTION == 'true' @@ -257,16 +337,14 @@ jobs: run: | PRODUCTION_DIR=release-artifacts/production RELEASE_PREFIX="releases/v${PRODUCTION_VERSION}" - TARBALL="$PRODUCTION_DIR/prime-agent-${PRODUCTION_VERSION}.tgz" - test -f "$TARBALL" test -f "$PRODUCTION_DIR/SHA256SUMS" test -f "$PRODUCTION_DIR/stable" test -f "$PRODUCTION_DIR/latest.json" test -n "$R2_BUCKET" test -n "$R2_ENDPOINT_URL" - for artifact in "$PRODUCTION_DIR"/*.tgz; do + for artifact in "$PRODUCTION_DIR"/*.tar.gz; do aws s3 cp "$artifact" "s3://${R2_BUCKET}/${RELEASE_PREFIX}/$(basename "$artifact")" \ --endpoint-url "$R2_ENDPOINT_URL" \ --content-type application/gzip \ @@ -325,16 +403,14 @@ jobs: run: | BETA_DIR=release-artifacts/beta RELEASE_PREFIX="releases/v${BETA_VERSION}" - TARBALL="$BETA_DIR/prime-agent-${BETA_VERSION}.tgz" - test -f "$TARBALL" test -f "$BETA_DIR/SHA256SUMS" test -f "$BETA_DIR/beta" test -f "$BETA_DIR/beta.json" test -n "$R2_BUCKET" test -n "$R2_ENDPOINT_URL" - for artifact in "$BETA_DIR"/*.tgz; do + for artifact in "$BETA_DIR"/*.tar.gz; do aws s3 cp "$artifact" "s3://${R2_BUCKET}/${RELEASE_PREFIX}/$(basename "$artifact")" \ --endpoint-url "$R2_ENDPOINT_URL" \ --content-type application/gzip \ @@ -359,6 +435,7 @@ jobs: fi BETA_DIR=release-artifacts/beta + aws s3 cp "$BETA_DIR/beta.json" "s3://${R2_BUCKET}/beta.json" \ --endpoint-url "$R2_ENDPOINT_URL" \ --content-type application/json \ diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index af521a547c5..c29c8fa8c31 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -42,11 +42,10 @@ jobs: with: persist-credentials: false - - name: Setup Node.js - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + - name: Setup Bun 1.3.14 + uses: oven-sh/setup-bun@4bc047ad259df6fc24a6c9b0f9a0cb08cf17fbe5 # v2.0.1 with: - node-version: 22 - cache: npm + bun-version: 1.3.14 - name: Install system dependencies run: | @@ -54,14 +53,24 @@ jobs: sudo apt-get install -y libcairo2-dev libpango1.0-dev libjpeg-dev libgif-dev librsvg2-dev fd-find ripgrep sudo ln -s $(which fdfind) /usr/local/bin/fd - - name: Install dependencies - run: npm ci + - name: Install dependencies (frozen lockfile) + run: bun install --frozen-lockfile - name: Build - run: npm run build + run: bun run build + + - name: Check model catalog drift + run: | + if ! git diff --exit-code packages/ai/src/models.generated.ts; then + echo "ERROR: Model catalog has drifted from committed state." + echo "Run 'bun run build' in packages/ai to regenerate, then commit the updated models.generated.ts." + echo "Diff:" + git diff packages/ai/src/models.generated.ts + exit 1 + fi - name: Check - run: npm run check + run: bun run check test: name: Test (${{ matrix.name }}) @@ -75,39 +84,27 @@ jobs: include: - name: agent-core package: packages/agent - command: npm test + command: bun run test install_uv: false - name: ai package: packages/ai - command: npm test + command: bun run test install_uv: false - name: tui package: packages/tui - command: npm test + command: bun run test install_uv: false - name: coding-agent 1/3 package: packages/coding-agent - command: npm run test:ci -- --shard=1/3 + command: bun run test:ci -- --shard=1/3 install_uv: true - name: coding-agent 2/3 package: packages/coding-agent - command: npm run test:ci -- --shard=2/3 + command: bun run test:ci -- --shard=2/3 install_uv: true - name: coding-agent 3/3 package: packages/coding-agent - command: npm run test:ci -- --shard=3/3 - install_uv: true - - name: coding-agent process smoke - package: packages/coding-agent - command: npm run test:process - install_uv: true - - name: coding-agent kernel - package: packages/coding-agent - command: npm run test:kernel - install_uv: true - - name: runtime python - package: prime-agent-runtime - command: uv run python -m unittest discover -s test + command: bun run test:ci -- --shard=3/3 install_uv: true steps: - name: Checkout @@ -115,11 +112,10 @@ jobs: with: persist-credentials: false - - name: Setup Node.js - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + - name: Setup Bun 1.3.14 + uses: oven-sh/setup-bun@4bc047ad259df6fc24a6c9b0f9a0cb08cf17fbe5 # v2.0.1 with: - node-version: 22 - cache: npm + bun-version: 1.3.14 - name: Install system dependencies run: | @@ -127,32 +123,182 @@ jobs: sudo apt-get install -y libcairo2-dev libpango1.0-dev libjpeg-dev libgif-dev librsvg2-dev fd-find ripgrep sudo ln -s $(which fdfind) /usr/local/bin/fd - - name: Install dependencies - run: npm ci + - name: Install dependencies (frozen lockfile) + run: bun install --frozen-lockfile - name: Build - run: npm run build + run: bun run build - name: Install uv if: matrix.install_uv run: | - python3 -m pip install --user uv - echo "$HOME/.local/bin" >> "$GITHUB_PATH" + curl -LsSf https://astral.sh/uv/install.sh | sh + echo "$HOME/.cargo/bin" >> "$GITHUB_PATH" - name: Test working-directory: ${{ matrix.package }} run: ${{ matrix.command }} + process-stress: + name: Process stress + needs: trust + if: needs.trust.outputs.allowed == 'true' + runs-on: ubuntu-latest + timeout-minutes: 15 + steps: + - name: Checkout + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + persist-credentials: false + + - name: Setup Bun 1.3.14 + uses: oven-sh/setup-bun@4bc047ad259df6fc24a6c9b0f9a0cb08cf17fbe5 # v2.0.1 + with: + bun-version: 1.3.14 + + - name: Install system dependencies + run: | + sudo apt-get update + sudo apt-get install -y libcairo2-dev libpango1.0-dev libjpeg-dev libgif-dev librsvg2-dev fd-find ripgrep + sudo ln -s $(which fdfind) /usr/local/bin/fd + + - name: Install dependencies (frozen lockfile) + run: bun install --frozen-lockfile + + - name: Build + run: bun run build + + - name: Install uv + run: | + curl -LsSf https://astral.sh/uv/install.sh | sh + echo "$HOME/.cargo/bin" >> "$GITHUB_PATH" + + - name: Run process stress tests + working-directory: packages/coding-agent + env: + PRIME_AGENT_STRESS_WORKERS: "10" + run: bun run test:process-stress + + kernel-heavy: + name: Kernel-heavy + needs: trust + if: needs.trust.outputs.allowed == 'true' + runs-on: ubuntu-latest + timeout-minutes: 15 + steps: + - name: Checkout + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + persist-credentials: false + + - name: Setup Bun 1.3.14 + uses: oven-sh/setup-bun@4bc047ad259df6fc24a6c9b0f9a0cb08cf17fbe5 # v2.0.1 + with: + bun-version: 1.3.14 + + - name: Install system dependencies + run: | + sudo apt-get update + sudo apt-get install -y libcairo2-dev libpango1.0-dev libjpeg-dev libgif-dev librsvg2-dev fd-find ripgrep + sudo ln -s $(which fdfind) /usr/local/bin/fd + + - name: Install dependencies (frozen lockfile) + run: bun install --frozen-lockfile + + - name: Build + run: bun run build + + - name: Install uv + run: | + curl -LsSf https://astral.sh/uv/install.sh | sh + echo "$HOME/.cargo/bin" >> "$GITHUB_PATH" + + - name: Run kernel-heavy tests + working-directory: packages/coding-agent + run: bun run test:kernel + + runtime-python: + name: Runtime Python + needs: trust + if: needs.trust.outputs.allowed == 'true' + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - name: Checkout + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + persist-credentials: false + + - name: Setup Bun 1.3.14 + uses: oven-sh/setup-bun@4bc047ad259df6fc24a6c9b0f9a0cb08cf17fbe5 # v2.0.1 + with: + bun-version: 1.3.14 + + - name: Install dependencies (frozen lockfile) + run: bun install --frozen-lockfile + + - name: Install uv + run: | + curl -LsSf https://astral.sh/uv/install.sh | sh + echo "$HOME/.cargo/bin" >> "$GITHUB_PATH" + + - name: Install Python runtime dependencies + working-directory: prime-agent-runtime + run: uv sync + + - name: Run Python runtime tests + working-directory: prime-agent-runtime + run: uv run python -m unittest discover -s test + + artifact-smoke: + name: Artifact smoke + needs: trust + if: needs.trust.outputs.allowed == 'true' + runs-on: ubuntu-latest + timeout-minutes: 5 + steps: + - name: Checkout + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + persist-credentials: false + + - name: Setup Bun 1.3.14 + uses: oven-sh/setup-bun@4bc047ad259df6fc24a6c9b0f9a0cb08cf17fbe5 # v2.0.1 + with: + bun-version: 1.3.14 + + - name: Install dependencies for all release platforms + run: bun install --frozen-lockfile --os=* --cpu=* + + - name: Build + run: bun run build + + - name: Build binary + working-directory: packages/coding-agent + run: bun run build:binary + + - name: Run artifact smoke test + working-directory: packages/coding-agent + run: bun test test/compiled-artifact-smoke.test.ts + build-check-test: name: build-check-test if: always() && needs.trust.outputs.allowed == 'true' - needs: [trust, build-check, test] + needs: [trust, build-check, test, process-stress, kernel-heavy, runtime-python, artifact-smoke] runs-on: ubuntu-latest steps: - name: Verify CI results env: BUILD_CHECK_RESULT: ${{ needs.build-check.result }} TEST_RESULT: ${{ needs.test.result }} + PROCESS_STRESS_RESULT: ${{ needs.process-stress.result }} + KERNEL_HEAVY_RESULT: ${{ needs.kernel-heavy.result }} + RUNTIME_PYTHON_RESULT: ${{ needs.runtime-python.result }} + ARTIFACT_SMOKE_RESULT: ${{ needs.artifact-smoke.result }} run: | test "$BUILD_CHECK_RESULT" = success test "$TEST_RESULT" = success + test "$PROCESS_STRESS_RESULT" = success + test "$KERNEL_HEAVY_RESULT" = success + test "$RUNTIME_PYTHON_RESULT" = success + test "$ARTIFACT_SMOKE_RESULT" = success diff --git a/.github/workflows/nightly-process-stress.yml b/.github/workflows/nightly-process-stress.yml index e47f42ca854..86b197c330f 100644 --- a/.github/workflows/nightly-process-stress.yml +++ b/.github/workflows/nightly-process-stress.yml @@ -23,11 +23,10 @@ jobs: with: persist-credentials: false - - name: Setup Node.js - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + - name: Setup Bun 1.3.14 + uses: oven-sh/setup-bun@4bc047ad259df6fc24a6c9b0f9a0cb08cf17fbe5 # v2.0.1 with: - node-version: 22 - cache: npm + bun-version: 1.3.14 - name: Install system dependencies run: | @@ -35,19 +34,19 @@ jobs: sudo apt-get install -y libcairo2-dev libpango1.0-dev libjpeg-dev libgif-dev librsvg2-dev fd-find ripgrep sudo ln -s $(which fdfind) /usr/local/bin/fd - - name: Install dependencies - run: npm ci + - name: Install dependencies (frozen lockfile) + run: bun install --frozen-lockfile - name: Build - run: npm run build + run: bun run build - name: Install uv run: | - python3 -m pip install --user uv - echo "$HOME/.local/bin" >> "$GITHUB_PATH" + curl -LsSf https://astral.sh/uv/install.sh | sh + echo "$HOME/.cargo/bin" >> "$GITHUB_PATH" - name: Run process stress tests working-directory: packages/coding-agent env: PRIME_AGENT_STRESS_WORKERS: "10" - run: npm run test:process-stress + run: bun run test:process-stress From 6edda2e16c1d3db87fdd4713cb077989a1d60ff8 Mon Sep 17 00:00:00 2001 From: Seth Date: Tue, 1 Sep 2026 17:39:23 -0400 Subject: [PATCH 07/45] chore: finish Bun tooling migration --- packages/ai/src/models.generated.ts | 2 +- .../custom-provider-anthropic/index.ts | 2 +- .../custom-provider-gitlab-duo/test.ts | 8 +++--- .../examples/extensions/sandbox/index.ts | 2 +- .../examples/extensions/with-deps/index.ts | 2 +- .../coding-agent/examples/rpc-extension-ui.ts | 2 +- scripts/bench-attach-bytes.mjs | 4 +-- scripts/bench-daemon-startup.mjs | 4 +-- scripts/cost.ts | 2 +- scripts/edit-tool-stats.mjs | 4 +-- scripts/profile-coding-agent-node.mjs | 27 +++++-------------- scripts/read-tool-stats.mjs | 4 +-- scripts/session-context-stats.mjs | 4 +-- scripts/session-transcripts.ts | 4 +-- scripts/stats.ts | 2 +- scripts/tool-stats.ts | 2 +- 16 files changed, 31 insertions(+), 44 deletions(-) diff --git a/packages/ai/src/models.generated.ts b/packages/ai/src/models.generated.ts index a3945be9a11..6ecbf9e8112 100644 --- a/packages/ai/src/models.generated.ts +++ b/packages/ai/src/models.generated.ts @@ -1,5 +1,5 @@ // This file is auto-generated by scripts/generate-models.ts -// Do not edit manually - run 'npm run generate-models' to update +// Do not edit manually - run 'bun run generate-models' to update import type { Model } from "./types.js"; diff --git a/packages/coding-agent/examples/extensions/custom-provider-anthropic/index.ts b/packages/coding-agent/examples/extensions/custom-provider-anthropic/index.ts index 69267f40064..73318cd2b53 100644 --- a/packages/coding-agent/examples/extensions/custom-provider-anthropic/index.ts +++ b/packages/coding-agent/examples/extensions/custom-provider-anthropic/index.ts @@ -10,7 +10,7 @@ * * Usage: * # First install dependencies - * cd packages/coding-agent/examples/extensions/custom-provider && npm install + * cd packages/coding-agent/examples/extensions/custom-provider && bun install * * # With OAuth (run /login custom-anthropic first) * pi -e ./packages/coding-agent/examples/extensions/custom-provider diff --git a/packages/coding-agent/examples/extensions/custom-provider-gitlab-duo/test.ts b/packages/coding-agent/examples/extensions/custom-provider-gitlab-duo/test.ts index c7963732585..88f8a543e7b 100644 --- a/packages/coding-agent/examples/extensions/custom-provider-gitlab-duo/test.ts +++ b/packages/coding-agent/examples/extensions/custom-provider-gitlab-duo/test.ts @@ -1,11 +1,11 @@ /** * Test script for GitLab Duo extension - * Run: npx tsx test.ts [model-id] [--thinking] + * Run: bun test.ts [model-id] [--thinking] * * Examples: - * npx tsx test.ts # Test default (claude-sonnet-4-5-20250929) - * npx tsx test.ts gpt-5-codex # Test GPT-5 Codex - * npx tsx test.ts claude-sonnet-4-5-20250929 --thinking + * bun test.ts # Test default (claude-sonnet-4-5-20250929) + * bun test.ts gpt-5-codex # Test GPT-5 Codex + * bun test.ts claude-sonnet-4-5-20250929 --thinking */ import { type Api, type Context, type Model, registerApiProvider, streamSimple } from "@earendil-works/pi-ai"; diff --git a/packages/coding-agent/examples/extensions/sandbox/index.ts b/packages/coding-agent/examples/extensions/sandbox/index.ts index 55524872cc2..ebe6dede8ac 100644 --- a/packages/coding-agent/examples/extensions/sandbox/index.ts +++ b/packages/coding-agent/examples/extensions/sandbox/index.ts @@ -36,7 +36,7 @@ * * Setup: * 1. Copy sandbox/ directory to ~/.prime/agent/extensions/ - * 2. Run `npm install` in ~/.prime/agent/extensions/sandbox/ + * 2. Run `bun install` in ~/.prime/agent/extensions/sandbox/ * * Linux also requires: bubblewrap, socat, ripgrep */ diff --git a/packages/coding-agent/examples/extensions/with-deps/index.ts b/packages/coding-agent/examples/extensions/with-deps/index.ts index 17be7da0486..6008d65e556 100644 --- a/packages/coding-agent/examples/extensions/with-deps/index.ts +++ b/packages/coding-agent/examples/extensions/with-deps/index.ts @@ -2,7 +2,7 @@ * Example extension with its own npm dependencies. * Tests that jiti resolves modules from the extension's own node_modules. * - * Requires: npm install in this directory + * Requires: bun install in this directory */ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent"; diff --git a/packages/coding-agent/examples/rpc-extension-ui.ts b/packages/coding-agent/examples/rpc-extension-ui.ts index bcbb6ec898a..9816e8b2bf8 100644 --- a/packages/coding-agent/examples/rpc-extension-ui.ts +++ b/packages/coding-agent/examples/rpc-extension-ui.ts @@ -5,7 +5,7 @@ * Demonstrates how to build a custom UI on top of the RPC protocol, * including handling extension UI requests (select, confirm, input, editor). * - * Usage: npx tsx examples/rpc-extension-ui.ts + * Usage: bun examples/rpc-extension-ui.ts * * Slash commands: * /select - demo select dialog diff --git a/scripts/bench-attach-bytes.mjs b/scripts/bench-attach-bytes.mjs index e64434bc51d..52fd02a608e 100644 --- a/scripts/bench-attach-bytes.mjs +++ b/scripts/bench-attach-bytes.mjs @@ -1,10 +1,10 @@ -#!/usr/bin/env node +#!/usr/bin/env bun /** * Measures attach payload bytes for legacy vs slim_attach clients against a * daemon session loaded from a given session file. Uses an isolated agent dir * so no test sessions leak into the shared sessions directory. * - * node scripts/bench-attach-bytes.mjs + * bun scripts/bench-attach-bytes.mjs */ import { spawn } from "node:child_process"; import { copyFileSync, mkdtempSync, rmSync } from "node:fs"; diff --git a/scripts/bench-daemon-startup.mjs b/scripts/bench-daemon-startup.mjs index fb8e9e7f3b6..25f9dc79c3b 100644 --- a/scripts/bench-daemon-startup.mjs +++ b/scripts/bench-daemon-startup.mjs @@ -1,10 +1,10 @@ -#!/usr/bin/env node +#!/usr/bin/env bun /** * Measures daemon cold-start latency: spawn `--mode daemon` and poll the unix * socket until it accepts a connection. This is the readiness gate every * interactive cold start waits on. * - * node scripts/bench-daemon-startup.mjs [--runs N] + * bun scripts/bench-daemon-startup.mjs [--runs N] */ import { spawn } from "node:child_process"; import { mkdtempSync, rmSync } from "node:fs"; diff --git a/scripts/cost.ts b/scripts/cost.ts index 2774b8d87ed..06ef039ea5a 100755 --- a/scripts/cost.ts +++ b/scripts/cost.ts @@ -1,4 +1,4 @@ -#!/usr/bin/env npx tsx +#!/usr/bin/env bun import * as fs from "fs"; import * as path from "path"; diff --git a/scripts/edit-tool-stats.mjs b/scripts/edit-tool-stats.mjs index 2c2c7ceabc2..2e42fe96204 100644 --- a/scripts/edit-tool-stats.mjs +++ b/scripts/edit-tool-stats.mjs @@ -1,4 +1,4 @@ -#!/usr/bin/env node +#!/usr/bin/env bun import { createReadStream } from "node:fs"; import { promises as fs } from "node:fs"; @@ -62,7 +62,7 @@ function parseArgs(argv) { } function printHelp() { - console.log(`Usage: node scripts/edit-tool-stats.mjs [options] + console.log(`Usage: bun scripts/edit-tool-stats.mjs [options] Options: --sessions-dir Sessions directory (default: ~/.pi/agent/sessions) diff --git a/scripts/profile-coding-agent-node.mjs b/scripts/profile-coding-agent-node.mjs index 44ccf86b6c6..c646aac9e33 100644 --- a/scripts/profile-coding-agent-node.mjs +++ b/scripts/profile-coding-agent-node.mjs @@ -17,13 +17,11 @@ const startupBenchmarkEnvName = "PI_STARTUP_BENCHMARK"; function printHelp() { console.log(`Usage: - node scripts/profile-coding-agent-node.mjs [options] + bun scripts/profile-coding-agent-node.mjs [options] -Profiles coding-agent startup with the runtime selected below: -- npm run profile:tui -> builds packages/coding-agent and profiles TUI startup with Node -- npm run profile:rpc -> builds packages/coding-agent and profiles RPC startup with Node -- bun run profile:tui -> profiles TUI startup from src/cli.ts directly with Bun -- bun run profile:rpc -> profiles RPC startup from src/cli.ts directly with Bun +Profiles coding-agent startup with the selected runtime. bun run profile:tui +and bun run profile:rpc use Bun by default. Pass --runtime node only for +an explicit Node compatibility comparison. Options: --mode tui or rpc (default: tui) @@ -282,19 +280,8 @@ async function runBuild() { process.stdout.write("Building packages/tui, packages/ai, packages/agent, and packages/coding-agent...\n"); const startedAt = performance.now(); const child = spawn( - "npm", - [ - "run", - "build", - "--workspace", - "packages/tui", - "--workspace", - "packages/ai", - "--workspace", - "packages/agent", - "--workspace", - "packages/coding-agent", - ], + process.execPath, + ["run", "build"], { cwd: repoRoot, env: process.env, @@ -352,7 +339,7 @@ function getRuntimeCommand(runtime, mode, profileDir, profileName, cpuProfile) { } args.push(distCliPath, ...benchmarkArgs); return { - executable: process.execPath, + executable: "node", args, }; } diff --git a/scripts/read-tool-stats.mjs b/scripts/read-tool-stats.mjs index 07bedd8f905..487e11e0c9d 100755 --- a/scripts/read-tool-stats.mjs +++ b/scripts/read-tool-stats.mjs @@ -1,4 +1,4 @@ -#!/usr/bin/env node +#!/usr/bin/env bun import { createReadStream } from "node:fs"; import { promises as fs } from "node:fs"; @@ -53,7 +53,7 @@ function parseArgs(argv) { } function printHelp() { - console.log(`Usage: node scripts/read-tool-stats.mjs [options] + console.log(`Usage: bun scripts/read-tool-stats.mjs [options] Options: --sessions-dir Sessions directory (default: ~/.pi/agent/sessions) diff --git a/scripts/session-context-stats.mjs b/scripts/session-context-stats.mjs index e9ae425c302..6f47329c0e9 100755 --- a/scripts/session-context-stats.mjs +++ b/scripts/session-context-stats.mjs @@ -1,4 +1,4 @@ -#!/usr/bin/env node +#!/usr/bin/env bun import { createReadStream } from "node:fs"; import { promises as fs } from "node:fs"; @@ -34,7 +34,7 @@ function parseArgs(argv) { } function printHelp() { - console.log(`Usage: node scripts/session-context-stats.mjs [options] + console.log(`Usage: bun scripts/session-context-stats.mjs [options] Options: --sessions-dir Sessions directory (default: ~/.pi/agent/sessions) diff --git a/scripts/session-transcripts.ts b/scripts/session-transcripts.ts index e10dbff9f42..0b5a7410fca 100644 --- a/scripts/session-transcripts.ts +++ b/scripts/session-transcripts.ts @@ -1,9 +1,9 @@ -#!/usr/bin/env npx tsx +#!/usr/bin/env bun /** * Extracts session transcripts for a given cwd, splits into context-sized files, * optionally spawns subagents to analyze patterns. * - * Usage: npx tsx scripts/session-transcripts.ts [--analyze] [--output ] [cwd] + * Usage: bun scripts/session-transcripts.ts [--analyze] [--output ] [cwd] * --analyze Spawn pi subagents to analyze each transcript file * --output Output directory for transcript files (defaults to ./session-transcripts) * cwd Working directory to extract sessions for (defaults to current) diff --git a/scripts/stats.ts b/scripts/stats.ts index 15c353e382b..72597ccfaf0 100644 --- a/scripts/stats.ts +++ b/scripts/stats.ts @@ -1,4 +1,4 @@ -#!/usr/bin/env npx tsx +#!/usr/bin/env bun import { existsSync, readdirSync, readFileSync } from "node:fs"; import { homedir } from "node:os"; diff --git a/scripts/tool-stats.ts b/scripts/tool-stats.ts index 8d39b8683cf..97b59e1872d 100755 --- a/scripts/tool-stats.ts +++ b/scripts/tool-stats.ts @@ -1,4 +1,4 @@ -#!/usr/bin/env npx tsx +#!/usr/bin/env bun import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from "node:fs"; import { homedir, tmpdir } from "node:os"; From 4a65980485b23a348d97e79652500747928a558e Mon Sep 17 00:00:00 2001 From: Seth Date: Tue, 1 Sep 2026 17:39:30 -0400 Subject: [PATCH 08/45] docs: make Bun the default workflow --- AGENTS.md | 18 +++++++++--------- packages/agent/README.md | 2 +- packages/ai/README.md | 8 ++++---- packages/coding-agent/docs/daemon.md | 10 +++++----- packages/coding-agent/docs/development.md | 6 +++--- packages/coding-agent/docs/extensions.md | 6 +++--- .../coding-agent/docs/long-running-agents.md | 2 +- packages/coding-agent/docs/quickstart.md | 6 +++--- packages/coding-agent/docs/rlm.md | 2 +- packages/coding-agent/docs/sdk.md | 2 +- packages/coding-agent/docs/skills.md | 4 ++-- packages/coding-agent/docs/termux.md | 2 +- packages/coding-agent/docs/tui.md | 2 +- packages/coding-agent/docs/usage.md | 2 +- .../examples/extensions/plan-mode/README.md | 2 +- packages/coding-agent/examples/sdk/README.md | 2 +- packages/tui/README.md | 10 +++++----- 17 files changed, 43 insertions(+), 43 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 85e64ff351c..393fb51cb43 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -22,10 +22,10 @@ ## Commands -- After code changes (not documentation changes): `npm run check` (get full output, no tail). Fix all errors, warnings, and infos before committing. -- Note: `npm run check` does not run tests. -- NEVER run: `npm run dev`, `npm run build`, `npm test` -- Only run specific tests if user instructs: `npx tsx ../../node_modules/vitest/dist/cli.js --run test/specific.test.ts` +- After code changes (not documentation changes): `bun run check` (get full output, no tail). Fix all errors, warnings, and infos before committing. +- Note: `bun run check` does not run tests. +- NEVER run: `bun run dev`, `bun run build`, `bun test` +- Only run specific tests if user instructs: `bun test test/specific.test.ts` - Run tests from the package root, not the repo root. - If you create or modify a test file, you MUST run that test file and iterate until it passes. - When writing tests, run them, identify issues in either the test or implementation, and iterate until fixed. @@ -43,9 +43,9 @@ ## Dependencies -- A 7-day minimum release age applies to all dependency updates: `.npmrc` sets `min-release-age=7` and `.github/dependabot.yml` uses a matching `cooldown`. Never bypass it for routine updates. -- Enforcement requires npm >= 11.10; older npm silently ignores the setting, so use a current npm when updating dependencies. -- For an urgent security patch younger than 7 days, override explicitly: `npm install --min-release-age=0 `. +- A 7-day minimum release age applies to all dependency updates: `bunfig.toml` sets `minimumReleaseAge = 604800` and `.github/dependabot.yml` uses a matching `cooldown`. Never bypass it for routine updates. +- Bun 1.3.14 enforces this setting for dependency updates. +- For an urgent security patch younger than 7 days, override explicitly: `bun add --minimum-release-age=0 `. ## GitHub Workflow @@ -195,8 +195,8 @@ Create provider file exporting: 2. **Run release script**: ```bash - npm run release:patch # Fixes and additions - npm run release:minor # API breaking changes + bun run release:patch # Fixes and additions + bun run release:minor # API breaking changes ``` The script handles: version bump, folding `.changes/` fragments into the release section, commit, tag, and publish. diff --git a/packages/agent/README.md b/packages/agent/README.md index e28f449c899..a451cb49f51 100644 --- a/packages/agent/README.md +++ b/packages/agent/README.md @@ -18,7 +18,7 @@ Release docs use the Prime Agent package names. The source workspace manifests s ## Workspace Package ```bash -npm install prime-agent-core +bun add prime-agent-core ``` ## Quick Start diff --git a/packages/ai/README.md b/packages/ai/README.md index a35c679302b..b93a6122f20 100644 --- a/packages/ai/README.md +++ b/packages/ai/README.md @@ -91,7 +91,7 @@ Unified LLM API with automatic model discovery, provider configuration, token an ## Installation ```bash -npm install prime-agent-ai +bun add prime-agent-ai ``` TypeBox exports are re-exported from `prime-agent-ai`: `Type`, `Static`, and `TSchema`. @@ -1150,9 +1150,9 @@ Official docs: [Application Default Credentials](https://cloud.google.com/docs/a The quickest way to authenticate: ```bash -npx prime-agent-ai login # interactive provider selection -npx prime-agent-ai login anthropic # login to specific provider -npx prime-agent-ai list # list available providers +bunx prime-agent-ai login # interactive provider selection +bunx prime-agent-ai login anthropic # login to specific provider +bunx prime-agent-ai list # list available providers ``` Credentials are saved to `auth.json` in the current directory. diff --git a/packages/coding-agent/docs/daemon.md b/packages/coding-agent/docs/daemon.md index 35e4ce9dd9b..ba1cce031ef 100644 --- a/packages/coding-agent/docs/daemon.md +++ b/packages/coding-agent/docs/daemon.md @@ -156,11 +156,11 @@ If preparation or manifest validation fails, prepared workers are released and a From `packages/coding-agent`: ```sh -npx tsx test/daemon-multiclient-bench.ts -npx tsx test/daemon-multiclient-bench.ts --generated-session-mib 100 -npx tsx test/daemon-multiclient-bench.ts --generated-session-mib 500 -npx tsx test/daemon-multiclient-bench.ts --session-file /path/to/session.jsonl -PRIME_AGENT_STRESS_WORKERS=50 npx tsx ../../node_modules/vitest/dist/cli.js --run test/daemon-supervisor-process.test.ts -t "hosts resident roots" +bun test/daemon-multiclient-bench.ts +bun test/daemon-multiclient-bench.ts --generated-session-mib 100 +bun test/daemon-multiclient-bench.ts --generated-session-mib 500 +bun test/daemon-multiclient-bench.ts --session-file /path/to/session.jsonl +PRIME_AGENT_STRESS_WORKERS=50 PRIME_AGENT_TEST_TAGS=process-stress bun test --isolate test/daemon-supervisor-process.test.ts -t "hosts resident roots" ``` The benchmark compares fanout and attach paths, including serialization count, throughput, elapsed time, and sampled RSS. The stress case starts many resident roots and verifies that their schedules advance independently while sessions are busy. diff --git a/packages/coding-agent/docs/development.md b/packages/coding-agent/docs/development.md index 56aa1a74a4f..e3da5b8af9c 100644 --- a/packages/coding-agent/docs/development.md +++ b/packages/coding-agent/docs/development.md @@ -9,7 +9,7 @@ Prime Agent requires Node.js 22.8.0 or newer. ```bash git clone https://github.com/PrimeIntellect-ai/prime-agent cd prime-agent -npm ci +bun install --frozen-lockfile ``` Run from source: @@ -68,7 +68,7 @@ prime-agent shutdown After code changes, run the repository check from the root: ```bash -npm run check +bun run check ``` This performs formatting, linting, type checking, installer rendering checks, and the browser smoke check. It does not run the test suite. @@ -77,7 +77,7 @@ Run focused tests from the package root. For example: ```bash cd packages/coding-agent -npx tsx ../../node_modules/vitest/dist/cli.js --run test/specific.test.ts +bun test test/specific.test.ts ``` If you create or modify a test file, run that file and iterate until it passes. Coding-agent suite regressions belong under `test/suite/regressions/` and use the suite harness and faux provider rather than live provider credentials. diff --git a/packages/coding-agent/docs/extensions.md b/packages/coding-agent/docs/extensions.md index 4fd29128082..a16b06bbb5e 100644 --- a/packages/coding-agent/docs/extensions.md +++ b/packages/coding-agent/docs/extensions.md @@ -144,7 +144,7 @@ To share extensions via npm or git as Prime Agent packages, see [packages.md](pa | `@earendil-works/pi-ai` | AI utilities (`StringEnum` for Google-compatible enums) | | `@earendil-works/pi-tui` | TUI components for custom rendering | -npm dependencies work too. Add a `package.json` next to your extension (or in a parent directory), run `npm install`, and imports from `node_modules/` are resolved automatically. +npm dependencies work too. Add a `package.json` next to your extension (or in a parent directory), run `bun install`, and imports from `node_modules/` are resolved automatically. For distributed Prime Agent packages installed with `prime-agent package install` (npm or git), runtime dependencies must be in `dependencies`. Package installation uses production installs (`npm install --omit=dev`) by default, so `devDependencies` are not available at runtime; when `npmCommand` is configured, git packages use plain `install` for compatibility with wrappers. @@ -242,7 +242,7 @@ This pattern makes the fetched models available during normal startup and to `pr └── my-extension/ ├── package.json # Declares dependencies and entry points ├── package-lock.json - ├── node_modules/ # After npm install + ├── node_modules/ # After bun install └── src/ └── index.ts ``` @@ -261,7 +261,7 @@ This pattern makes the fetched models available during normal startup and to `pr } ``` -Run `npm install` in the extension directory, then imports from `node_modules/` work automatically. +Run `bun install` in the extension directory, then imports from `node_modules/` work automatically. ## Events diff --git a/packages/coding-agent/docs/long-running-agents.md b/packages/coding-agent/docs/long-running-agents.md index 436a40d4476..1ac2f9d1ed7 100644 --- a/packages/coding-agent/docs/long-running-agents.md +++ b/packages/coding-agent/docs/long-running-agents.md @@ -213,7 +213,7 @@ Or configure a run from the CLI: ```bash prime-agent \ --autonomous \ - --autonomous-gate "npm run check" \ + --autonomous-gate "bun run check" \ --autonomous-max-turns 20 \ "Implement and verify the requested change" ``` diff --git a/packages/coding-agent/docs/quickstart.md b/packages/coding-agent/docs/quickstart.md index e55a8375b5f..bf8db3ec31d 100644 --- a/packages/coding-agent/docs/quickstart.md +++ b/packages/coding-agent/docs/quickstart.md @@ -30,7 +30,7 @@ To run a source checkout instead, use Node.js 22.8.0 or newer: ```bash git clone https://github.com/PrimeIntellect-ai/prime-agent cd prime-agent -npm ci +bun install --frozen-lockfile ./prime-agent.sh ``` @@ -94,7 +94,7 @@ Prime Agent loads context files at startup. Add an `AGENTS.md` file to tell it h ```markdown # Project Instructions -- Run `npm run check` after code changes. +- Run `bun run check` after code changes. - Do not run production migrations locally. - Keep responses concise. ``` @@ -124,7 +124,7 @@ Images can be pasted with Ctrl+V (Alt+V on Windows) or dragged into supported te In interactive mode: ```text -!npm run lint +!bun run lint ``` The command output is sent to the model. Use `!!command` to run a command without adding its output to model context. During agent work, the model normally runs project commands from the Python REPL with `bash()`. diff --git a/packages/coding-agent/docs/rlm.md b/packages/coding-agent/docs/rlm.md index 7d558073b4b..4be00ec26cb 100644 --- a/packages/coding-agent/docs/rlm.md +++ b/packages/coding-agent/docs/rlm.md @@ -44,7 +44,7 @@ large_files = [path for path in config_files if path.stat().st_size > 10_000] Run a project's normal commands through its own environment with `bash()`: ```python -result = await bash("npm run check") +result = await bash("bun run check") print(result.output) ``` diff --git a/packages/coding-agent/docs/sdk.md b/packages/coding-agent/docs/sdk.md index a8ee9aa3727..e50f1995849 100644 --- a/packages/coding-agent/docs/sdk.md +++ b/packages/coding-agent/docs/sdk.md @@ -40,7 +40,7 @@ await session.prompt("What files are in the current directory?"); ## Installation ```bash -npm install @earendil-works/pi-coding-agent +bun add @earendil-works/pi-coding-agent ``` The SDK is included in the main package. No separate installation needed. diff --git a/packages/coding-agent/docs/skills.md b/packages/coding-agent/docs/skills.md index ef41c05f27c..8b156f0b417 100644 --- a/packages/coding-agent/docs/skills.md +++ b/packages/coding-agent/docs/skills.md @@ -278,7 +278,7 @@ description: What this skill does and when to use it. Be specific. Run once before first use: ```bash -cd /path/to/skill && npm install +cd /path/to/skill && bun install ``` ## Usage @@ -369,7 +369,7 @@ description: Web search and content extraction via Brave Search API. Use for sea ## Setup ```bash -cd /path/to/brave-search && npm install +cd /path/to/brave-search && bun install ``` ## Search diff --git a/packages/coding-agent/docs/termux.md b/packages/coding-agent/docs/termux.md index c43eab41226..db3ab9273e0 100644 --- a/packages/coding-agent/docs/termux.md +++ b/packages/coding-agent/docs/termux.md @@ -19,7 +19,7 @@ pkg install nodejs termux-api git ripgrep # Clone and install Prime Agent from source git clone https://github.com/PrimeIntellect-ai/prime-agent.git cd prime-agent -npm ci +bun install --frozen-lockfile # Run Prime Agent ./prime-agent.sh diff --git a/packages/coding-agent/docs/tui.md b/packages/coding-agent/docs/tui.md index 5e9c3712558..2d25174997b 100644 --- a/packages/coding-agent/docs/tui.md +++ b/packages/coding-agent/docs/tui.md @@ -448,7 +448,7 @@ interface MyTheme { Set `PI_TUI_WRITE_LOG` to capture the raw ANSI stream written to stdout. ```bash -PI_TUI_WRITE_LOG=/tmp/tui-ansi.log npx tsx packages/tui/test/chat-simple.ts +PI_TUI_WRITE_LOG=/tmp/tui-ansi.log bun packages/tui/test/chat-simple.ts ``` ## Performance diff --git a/packages/coding-agent/docs/usage.md b/packages/coding-agent/docs/usage.md index 3d68af75bd5..087bbf196ac 100644 --- a/packages/coding-agent/docs/usage.md +++ b/packages/coding-agent/docs/usage.md @@ -279,7 +279,7 @@ For example, this noninteractive run uses a locally available model configuratio ```bash prime-agent -p \ --autonomous \ - --autonomous-gate "npm run check" \ + --autonomous-gate "bun run check" \ --autonomous-gate-retries 2 \ --autonomous-gate-timeout-ms 300000 \ --autonomous-max-continuations 3 \ diff --git a/packages/coding-agent/examples/extensions/plan-mode/README.md b/packages/coding-agent/examples/extensions/plan-mode/README.md index 6618c808602..07835593883 100644 --- a/packages/coding-agent/examples/extensions/plan-mode/README.md +++ b/packages/coding-agent/examples/extensions/plan-mode/README.md @@ -60,6 +60,6 @@ Safe commands (allowed): Blocked commands: - File modification: `rm`, `mv`, `cp`, `mkdir`, `touch` - Git write: `git add`, `git commit`, `git push` -- Package install: `npm install`, `yarn add`, `pip install` +- Package install: `bun install`, `npm install`, `yarn add`, `pip install` - System: `sudo`, `kill`, `reboot` - Editors: `vim`, `nano`, `code` diff --git a/packages/coding-agent/examples/sdk/README.md b/packages/coding-agent/examples/sdk/README.md index aac8b4ea6bd..21d05192cd9 100644 --- a/packages/coding-agent/examples/sdk/README.md +++ b/packages/coding-agent/examples/sdk/README.md @@ -30,7 +30,7 @@ The runtime example shows how to build a recreate function that closes over proc ```bash cd packages/coding-agent -npx tsx examples/sdk/01-minimal.ts +bun examples/sdk/01-minimal.ts ``` ## Quick Reference diff --git a/packages/tui/README.md b/packages/tui/README.md index c28647549b3..120606e5839 100644 --- a/packages/tui/README.md +++ b/packages/tui/README.md @@ -770,20 +770,20 @@ See `test/chat-simple.ts` for a complete chat interface example with: Run it: ```bash -npx tsx test/chat-simple.ts +bun test/chat-simple.ts ``` ## Development ```bash # Install dependencies (from monorepo root) -npm install +bun install # Run type checking -npm run check +bun run check # Run the demo -npx tsx test/chat-simple.ts +bun test/chat-simple.ts ``` ### Debug logging @@ -791,5 +791,5 @@ npx tsx test/chat-simple.ts Set `PI_TUI_WRITE_LOG` to capture the raw ANSI stream written to stdout. ```bash -PI_TUI_WRITE_LOG=/tmp/tui-ansi.log npx tsx test/chat-simple.ts +PI_TUI_WRITE_LOG=/tmp/tui-ansi.log bun test/chat-simple.ts ``` From 9791df5a93af873ab9bf28b042e77547b6f56c87 Mon Sep 17 00:00:00 2001 From: Seth Date: Tue, 1 Sep 2026 17:43:36 -0400 Subject: [PATCH 09/45] docs: add Bun migration changelog fragments --- packages/ai/.changes/bun-primary-runtime.md | 1 + packages/coding-agent/.changes/bun-primary-runtime.md | 1 + 2 files changed, 2 insertions(+) create mode 100644 packages/ai/.changes/bun-primary-runtime.md create mode 100644 packages/coding-agent/.changes/bun-primary-runtime.md diff --git a/packages/ai/.changes/bun-primary-runtime.md b/packages/ai/.changes/bun-primary-runtime.md new file mode 100644 index 00000000000..e0b40ce40f4 --- /dev/null +++ b/packages/ai/.changes/bun-primary-runtime.md @@ -0,0 +1 @@ +- Made Bun 1.3.14 the primary development and test runtime while preserving Node-compatible package output. diff --git a/packages/coding-agent/.changes/bun-primary-runtime.md b/packages/coding-agent/.changes/bun-primary-runtime.md new file mode 100644 index 00000000000..a2feb62ae64 --- /dev/null +++ b/packages/coding-agent/.changes/bun-primary-runtime.md @@ -0,0 +1 @@ +- Added Bun-primary development, testing, native binary releases, and verified binary-first installation. From 076cd4fca9d1fd2b23fba22ca9713503b952de7e Mon Sep 17 00:00:00 2001 From: Seth Date: Tue, 1 Sep 2026 17:49:14 -0400 Subject: [PATCH 10/45] chore(ai): refresh generated model catalog --- packages/ai/src/models.generated.ts | 774 ++++++++++++++++++---------- 1 file changed, 495 insertions(+), 279 deletions(-) diff --git a/packages/ai/src/models.generated.ts b/packages/ai/src/models.generated.ts index 6ecbf9e8112..cbc6e14c3b8 100644 --- a/packages/ai/src/models.generated.ts +++ b/packages/ai/src/models.generated.ts @@ -91,6 +91,24 @@ export const MODELS = { contextWindow: 1000000, maxTokens: 128000, } satisfies Model<"bedrock-converse-stream">, + "anthropic.claude-fable-5-1": { + id: "anthropic.claude-fable-5-1", + name: "Claude Fable 5.1", + api: "bedrock-converse-stream", + provider: "amazon-bedrock", + baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", + reasoning: true, + thinkingLevelMap: {"off":null,"xhigh":"xhigh","max":"max"}, + input: ["text", "image"], + cost: { + input: 10, + output: 50, + cacheRead: 0.25, + cacheWrite: 12.5, + }, + contextWindow: 1000000, + maxTokens: 128000, + } satisfies Model<"bedrock-converse-stream">, "anthropic.claude-haiku-4-5-20251001-v1:0": { id: "anthropic.claude-haiku-4-5-20251001-v1:0", name: "Claude Haiku 4.5", @@ -637,6 +655,24 @@ export const MODELS = { contextWindow: 1000000, maxTokens: 128000, } satisfies Model<"bedrock-converse-stream">, + "global.anthropic.claude-fable-5-1": { + id: "global.anthropic.claude-fable-5-1", + name: "Claude Fable 5.1 (Global)", + api: "bedrock-converse-stream", + provider: "amazon-bedrock", + baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", + reasoning: true, + thinkingLevelMap: {"off":null,"xhigh":"xhigh","max":"max"}, + input: ["text", "image"], + cost: { + input: 10, + output: 50, + cacheRead: 0.25, + cacheWrite: 12.5, + }, + contextWindow: 1000000, + maxTokens: 128000, + } satisfies Model<"bedrock-converse-stream">, "global.anthropic.claude-haiku-4-5-20251001-v1:0": { id: "global.anthropic.claude-haiku-4-5-20251001-v1:0", name: "Claude Haiku 4.5 (Global)", @@ -1728,6 +1764,24 @@ export const MODELS = { contextWindow: 1000000, maxTokens: 128000, } satisfies Model<"bedrock-converse-stream">, + "us.anthropic.claude-fable-5-1": { + id: "us.anthropic.claude-fable-5-1", + name: "Claude Fable 5.1 (US)", + api: "bedrock-converse-stream", + provider: "amazon-bedrock", + baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", + reasoning: true, + thinkingLevelMap: {"off":null,"xhigh":"xhigh","max":"max"}, + input: ["text", "image"], + cost: { + input: 11, + output: 55, + cacheRead: 0.275, + cacheWrite: 13.75, + }, + contextWindow: 1000000, + maxTokens: 128000, + } satisfies Model<"bedrock-converse-stream">, "us.anthropic.claude-haiku-4-5-20251001-v1:0": { id: "us.anthropic.claude-haiku-4-5-20251001-v1:0", name: "Claude Haiku 4.5 (US)", @@ -2094,6 +2148,24 @@ export const MODELS = { contextWindow: 1000000, maxTokens: 128000, } satisfies Model<"anthropic-messages">, + "claude-fable-5-1": { + id: "claude-fable-5-1", + name: "Claude Fable 5.1", + api: "anthropic-messages", + provider: "anthropic", + baseUrl: "https://api.anthropic.com", + reasoning: true, + thinkingLevelMap: {"off":null,"xhigh":"xhigh","max":"max"}, + input: ["text", "image"], + cost: { + input: 10, + output: 50, + cacheRead: 0.25, + cacheWrite: 12.5, + }, + contextWindow: 1000000, + maxTokens: 128000, + } satisfies Model<"anthropic-messages">, "claude-haiku-4-5": { id: "claude-haiku-4-5", name: "Claude Haiku 4.5 (latest)", @@ -3904,6 +3976,24 @@ export const MODELS = { contextWindow: 262144, maxTokens: 256000, } satisfies Model<"openai-completions">, + "@cf/zai-org/glm-5.3": { + id: "@cf/zai-org/glm-5.3", + name: "Glm 5.3", + api: "openai-completions", + provider: "cloudflare-workers-ai", + baseUrl: "https://api.cloudflare.com/client/v4/accounts/{CLOUDFLARE_ACCOUNT_ID}/ai/v1", + compat: {"sendSessionAffinityHeaders":true}, + reasoning: true, + input: ["text"], + cost: { + input: 1.4, + output: 4.4, + cacheRead: 0.26, + cacheWrite: 0, + }, + contextWindow: 1310720, + maxTokens: 1310720, + } satisfies Model<"openai-completions">, "@cf/zai-org/glm-5.3-flash": { id: "@cf/zai-org/glm-5.3-flash", name: "Glm 5.3 Flash", @@ -3920,7 +4010,7 @@ export const MODELS = { cacheWrite: 0, }, contextWindow: 1310720, - maxTokens: 1310720, + maxTokens: 1048576, } satisfies Model<"openai-completions">, }, "deepseek": { @@ -3964,23 +4054,6 @@ export const MODELS = { } satisfies Model<"openai-completions">, }, "fireworks": { - "accounts/fireworks/models/deepseek-v4-flash": { - id: "accounts/fireworks/models/deepseek-v4-flash", - name: "DeepSeek V4 Flash", - api: "anthropic-messages", - provider: "fireworks", - baseUrl: "https://api.fireworks.ai/inference", - reasoning: true, - input: ["text"], - cost: { - input: 0.14, - output: 0.28, - cacheRead: 0.028, - cacheWrite: 0, - }, - contextWindow: 1000000, - maxTokens: 384000, - } satisfies Model<"anthropic-messages">, "accounts/fireworks/models/deepseek-v4-flash-0731": { id: "accounts/fireworks/models/deepseek-v4-flash-0731", name: "DeepSeek V4 Flash 0731", @@ -3990,9 +4063,9 @@ export const MODELS = { reasoning: true, input: ["text"], cost: { - input: 0.14, - output: 0.28, - cacheRead: 0.028, + input: 0.22, + output: 0.66, + cacheRead: 0.007, cacheWrite: 0, }, contextWindow: 1000000, @@ -4032,6 +4105,40 @@ export const MODELS = { contextWindow: 1048575, maxTokens: 131072, } satisfies Model<"anthropic-messages">, + "accounts/fireworks/models/glm-5p3": { + id: "accounts/fireworks/models/glm-5p3", + name: "GLM 5.3", + api: "anthropic-messages", + provider: "fireworks", + baseUrl: "https://api.fireworks.ai/inference", + reasoning: true, + input: ["text"], + cost: { + input: 1.4, + output: 4.4, + cacheRead: 0.26, + cacheWrite: 0, + }, + contextWindow: 1000000, + maxTokens: 131072, + } satisfies Model<"anthropic-messages">, + "accounts/fireworks/models/glm-5p3-flash": { + id: "accounts/fireworks/models/glm-5p3-flash", + name: "GLM 5.3 Flash", + api: "anthropic-messages", + provider: "fireworks", + baseUrl: "https://api.fireworks.ai/inference", + reasoning: true, + input: ["text", "image"], + cost: { + input: 0.15, + output: 0.5, + cacheRead: 0.029, + cacheWrite: 0, + }, + contextWindow: 1000000, + maxTokens: 131072, + } satisfies Model<"anthropic-messages">, "accounts/fireworks/models/gpt-oss-120b": { id: "accounts/fireworks/models/gpt-oss-120b", name: "GPT OSS 120B", @@ -5133,23 +5240,6 @@ export const MODELS = { contextWindow: 1048576, maxTokens: 65536, } satisfies Model<"google-generative-ai">, - "gemini-robotics-er-1.6-preview": { - id: "gemini-robotics-er-1.6-preview", - name: "Gemini Robotics-ER 1.6 Preview", - api: "google-generative-ai", - provider: "google", - baseUrl: "https://generativelanguage.googleapis.com/v1beta", - reasoning: true, - input: ["text", "image"], - cost: { - input: 1, - output: 5, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 65536, - } satisfies Model<"google-generative-ai">, "gemma-4-26b-a4b-it": { id: "gemma-4-26b-a4b-it", name: "Gemma 4 26B A4B IT", @@ -5517,6 +5607,23 @@ export const MODELS = { contextWindow: 131072, maxTokens: 16384, } satisfies Model<"openai-completions">, + "qwen/qwen3.8-27b": { + id: "qwen/qwen3.8-27b", + name: "Qwen3.8 27B", + api: "openai-completions", + provider: "groq", + baseUrl: "https://api.groq.com/openai/v1", + reasoning: true, + input: ["text", "image"], + cost: { + input: 0.8, + output: 4, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 131042, + maxTokens: 16384, + } satisfies Model<"openai-completions">, }, "huggingface": { "MiniMaxAI/MiniMax-M2": { @@ -6744,6 +6851,24 @@ export const MODELS = { contextWindow: 262144, maxTokens: 131072, } satisfies Model<"openai-completions">, + "zai-org/GLM-5.3": { + id: "zai-org/GLM-5.3", + name: "GLM-5.3", + api: "openai-completions", + provider: "huggingface", + baseUrl: "https://router.huggingface.co/v1", + compat: {"supportsDeveloperRole":false}, + reasoning: true, + input: ["text"], + cost: { + input: 1.4, + output: 4.4, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 1048576, + maxTokens: 131072, + } satisfies Model<"openai-completions">, "zai-org/GLM-5.3-Flash": { id: "zai-org/GLM-5.3-Flash", name: "GLM-5.3-Flash", @@ -6752,7 +6877,7 @@ export const MODELS = { baseUrl: "https://router.huggingface.co/v1", compat: {"supportsDeveloperRole":false}, reasoning: true, - input: ["text"], + input: ["text", "image"], cost: { input: 0.15, output: 0.5, @@ -8816,6 +8941,24 @@ export const MODELS = { contextWindow: 1000000, maxTokens: 128000, } satisfies Model<"anthropic-messages">, + "claude-fable-5-1": { + id: "claude-fable-5-1", + name: "Claude Fable 5.1", + api: "anthropic-messages", + provider: "opencode", + baseUrl: "https://opencode.ai/zen", + reasoning: true, + thinkingLevelMap: {"off":null,"xhigh":"xhigh","max":"max"}, + input: ["text", "image"], + cost: { + input: 10, + output: 50, + cacheRead: 0.25, + cacheWrite: 12.5, + }, + contextWindow: 1000000, + maxTokens: 128000, + } satisfies Model<"anthropic-messages">, "claude-haiku-4-5": { id: "claude-haiku-4-5", name: "Claude Haiku 4.5", @@ -9582,23 +9725,6 @@ export const MODELS = { contextWindow: 256000, maxTokens: 256000, } satisfies Model<"openai-responses">, - "hy3-free": { - id: "hy3-free", - name: "Hy3 Free", - api: "openai-completions", - provider: "opencode", - baseUrl: "https://opencode.ai/zen/v1", - reasoning: true, - input: ["text"], - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 190000, - maxTokens: 64000, - } satisfies Model<"openai-completions">, "kimi-k2.5": { id: "kimi-k2.5", name: "Kimi K2.5", @@ -9668,6 +9794,23 @@ export const MODELS = { contextWindow: 1048576, maxTokens: 131072, } satisfies Model<"openai-completions">, + "ling-3.0-flash-fin-free": { + id: "ling-3.0-flash-fin-free", + name: "Ling 3.0 Flash Fin Free", + api: "openai-completions", + provider: "opencode", + baseUrl: "https://opencode.ai/zen/v1", + reasoning: true, + input: ["text"], + cost: { + input: 0, + output: 0, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 32768, + } satisfies Model<"openai-completions">, "mimo-v2.5-free": { id: "mimo-v2.5-free", name: "MiMo V2.5 Free", @@ -10002,21 +10145,38 @@ export const MODELS = { } satisfies Model<"openai-responses">, "hy3": { id: "hy3", - name: "Hy3 (8x usage)", + name: "Hy3", api: "openai-completions", provider: "opencode-go", baseUrl: "https://opencode.ai/zen/go/v1", reasoning: true, input: ["text"], cost: { - input: 0.0175, - output: 0.0725, - cacheRead: 0.004375, + input: 0.14, + output: 0.58, + cacheRead: 0.035, cacheWrite: 0, }, contextWindow: 256000, maxTokens: 64000, } satisfies Model<"openai-completions">, + "hy4-preview": { + id: "hy4-preview", + name: "Hy4 preview", + api: "openai-completions", + provider: "opencode-go", + baseUrl: "https://opencode.ai/zen/go/v1", + reasoning: true, + input: ["text"], + cost: { + input: 0.834, + output: 2.501, + cacheRead: 0.042, + cacheWrite: 0, + }, + contextWindow: 1024000, + maxTokens: 64000, + } satisfies Model<"openai-completions">, "kimi-k2.6": { id: "kimi-k2.6", name: "Kimi K2.6", @@ -10438,6 +10598,24 @@ export const MODELS = { contextWindow: 1000000, maxTokens: 128000, } satisfies Model<"openai-completions">, + "anthropic/claude-fable-5.1": { + id: "anthropic/claude-fable-5.1", + name: "Anthropic: Claude Fable 5.1", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: true, + thinkingLevelMap: {"off":null,"minimal":null,"low":"low","medium":"medium","high":"high","xhigh":"xhigh","max":"max"}, + input: ["text", "image"], + cost: { + input: 10, + output: 50, + cacheRead: 0.25, + cacheWrite: 12.5, + }, + contextWindow: 1000000, + maxTokens: 128000, + } satisfies Model<"openai-completions">, "anthropic/claude-haiku-4.5": { id: "anthropic/claude-haiku-4.5", name: "Anthropic: Claude Haiku 4.5", @@ -10550,9 +10728,9 @@ export const MODELS = { contextWindow: 1000000, maxTokens: 128000, } satisfies Model<"openai-completions">, - "anthropic/claude-opus-4.7-fast": { - id: "anthropic/claude-opus-4.7-fast", - name: "Anthropic: Claude Opus 4.7 (Fast)", + "anthropic/claude-opus-4.8": { + id: "anthropic/claude-opus-4.8", + name: "Anthropic: Claude Opus 4.8", api: "openai-completions", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", @@ -10560,46 +10738,10 @@ export const MODELS = { thinkingLevelMap: {"minimal":null,"low":"low","medium":"medium","high":"high","xhigh":"xhigh","max":"max"}, input: ["text", "image"], cost: { - input: 30, - output: 150, - cacheRead: 3, - cacheWrite: 37.5, - }, - contextWindow: 1000000, - maxTokens: 128000, - } satisfies Model<"openai-completions">, - "anthropic/claude-opus-4.8": { - id: "anthropic/claude-opus-4.8", - name: "Anthropic: Claude Opus 4.8", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - thinkingLevelMap: {"minimal":null,"low":"low","medium":"medium","high":"high","xhigh":"xhigh","max":"max"}, - input: ["text", "image"], - cost: { - input: 5, - output: 25, - cacheRead: 0.5, - cacheWrite: 6.25, - }, - contextWindow: 1000000, - maxTokens: 128000, - } satisfies Model<"openai-completions">, - "anthropic/claude-opus-4.8-fast": { - id: "anthropic/claude-opus-4.8-fast", - name: "Anthropic: Claude Opus 4.8 (Fast)", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - thinkingLevelMap: {"minimal":null,"low":"low","medium":"medium","high":"high","xhigh":"xhigh","max":"max"}, - input: ["text", "image"], - cost: { - input: 10, - output: 50, - cacheRead: 1, - cacheWrite: 12.5, + input: 5, + output: 25, + cacheRead: 0.5, + cacheWrite: 6.25, }, contextWindow: 1000000, maxTokens: 128000, @@ -10622,24 +10764,6 @@ export const MODELS = { contextWindow: 1000000, maxTokens: 128000, } satisfies Model<"openai-completions">, - "anthropic/claude-opus-5-fast": { - id: "anthropic/claude-opus-5-fast", - name: "Claude Opus 5 (Fast)", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - thinkingLevelMap: {"minimal":null,"low":"low","medium":"medium","high":"high","xhigh":"xhigh","max":"max"}, - input: ["text", "image"], - cost: { - input: 10, - output: 50, - cacheRead: 1, - cacheWrite: 12.5, - }, - contextWindow: 1000000, - maxTokens: 128000, - } satisfies Model<"openai-completions">, "anthropic/claude-sonnet-4": { id: "anthropic/claude-sonnet-4", name: "Anthropic: Claude Sonnet 4", @@ -10725,30 +10849,13 @@ export const MODELS = { thinkingLevelMap: {"off":null,"minimal":null,"low":null,"medium":null,"high":"high","xhigh":null,"max":null}, input: ["text"], cost: { - input: 0.22, - output: 0.85, + input: 0.25, + output: 0.7999999999999999, cacheRead: 0.06, cacheWrite: 0, }, contextWindow: 262144, - maxTokens: 235929, - } satisfies Model<"openai-completions">, - "arcee-ai/virtuoso-large": { - id: "arcee-ai/virtuoso-large", - name: "Arcee AI: Virtuoso Large", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text"], - cost: { - input: 0.75, - output: 1.2, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 64000, + maxTokens: 80000, } satisfies Model<"openai-completions">, "auto": { id: "auto", @@ -11090,9 +11197,9 @@ export const MODELS = { thinkingLevelMap: {"minimal":null,"low":null,"medium":null,"high":"high","xhigh":"max","max":null}, input: ["text"], cost: { - input: 0.088606, - output: 0.177212, - cacheRead: 0.017721200000000003, + input: 0.07168000000000001, + output: 0.14336000000000002, + cacheRead: 0.014336, cacheWrite: 0, }, contextWindow: 1048576, @@ -11109,9 +11216,9 @@ export const MODELS = { thinkingLevelMap: {"minimal":null,"low":null,"medium":null,"high":"high","xhigh":"max","max":null}, input: ["text"], cost: { - input: 0.07, - output: 0.14, - cacheRead: 0.014, + input: 0.065, + output: 0.18, + cacheRead: 0.016, cacheWrite: 0, }, contextWindow: 1310720, @@ -11128,9 +11235,9 @@ export const MODELS = { thinkingLevelMap: {"minimal":null,"low":null,"medium":null,"high":"high","xhigh":"max","max":null}, input: ["text", "image"], cost: { - input: 0.44, - output: 1.32, - cacheRead: 0.014, + input: 0.22, + output: 0.66, + cacheRead: 0.007, cacheWrite: 0, }, contextWindow: 1048576, @@ -11147,9 +11254,9 @@ export const MODELS = { thinkingLevelMap: {"minimal":null,"low":null,"medium":null,"high":"high","xhigh":"max","max":null}, input: ["text"], cost: { - input: 0.87, - output: 1.74, - cacheRead: 0.0725, + input: 1.0241639999999999, + output: 2.0483279999999997, + cacheRead: 0.085347, cacheWrite: 0, }, contextWindow: 1048576, @@ -11166,9 +11273,9 @@ export const MODELS = { thinkingLevelMap: {"minimal":null,"low":null,"medium":null,"high":"high","xhigh":"max","max":null}, input: ["text"], cost: { - input: 1.32, - output: 3.9600000000000004, - cacheRead: 0.044, + input: 0.66, + output: 1.9800000000000002, + cacheRead: 0.022, cacheWrite: 0, }, contextWindow: 1048576, @@ -11461,10 +11568,10 @@ export const MODELS = { thinkingLevelMap: {"off":null,"minimal":null,"low":"low","medium":"medium","high":"high","xhigh":null,"max":null}, input: ["text", "image"], cost: { - input: 0.375, - output: 1.875, - cacheRead: 0.0375, - cacheWrite: 0.0208333333333333, + input: 0.75, + output: 3.75, + cacheRead: 0.075, + cacheWrite: 0.0416666666666667, }, contextWindow: 1048576, maxTokens: 65536, @@ -11500,7 +11607,7 @@ export const MODELS = { cacheRead: 0.04, cacheWrite: 0, }, - contextWindow: 262144, + contextWindow: 131072, maxTokens: 117964, } satisfies Model<"openai-completions">, "google/gemma-4-26b-a4b-it": { @@ -11596,6 +11703,24 @@ export const MODELS = { contextWindow: 131072, maxTokens: 117964, } satisfies Model<"openai-completions">, + "ibm-granite/granite-4.2-8b": { + id: "ibm-granite/granite-4.2-8b", + name: "IBM: Granite 4.2 8B", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: true, + thinkingLevelMap: {"minimal":null,"low":"low","medium":null,"high":"high","xhigh":null,"max":null}, + input: ["text"], + cost: { + input: 0.09999999999999999, + output: 0.15, + cacheRead: 0.049999999999999996, + cacheWrite: 0, + }, + contextWindow: 131072, + maxTokens: 117964, + } satisfies Model<"openai-completions">, "inception/mercury-2": { id: "inception/mercury-2", name: "Inception: Mercury 2", @@ -11614,6 +11739,24 @@ export const MODELS = { contextWindow: 128000, maxTokens: 50000, } satisfies Model<"openai-completions">, + "inception/mercury-2.5-preview": { + id: "inception/mercury-2.5-preview", + name: "Inception: Mercury 2.5 Preview", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: true, + thinkingLevelMap: {"minimal":null,"low":"low","medium":"medium","high":"high","xhigh":null,"max":null}, + input: ["text"], + cost: { + input: 0.04, + output: 0.15, + cacheRead: 0.004, + cacheWrite: 0, + }, + contextWindow: 260000, + maxTokens: 65536, + } satisfies Model<"openai-completions">, "inclusionai/ling-3.0-flash": { id: "inclusionai/ling-3.0-flash", name: "Ling-3.0-flash", @@ -11652,23 +11795,6 @@ export const MODELS = { contextWindow: 262144, maxTokens: 32768, } satisfies Model<"openai-completions">, - "kwaipilot/kat-coder-air-v2.5": { - id: "kwaipilot/kat-coder-air-v2.5", - name: "Kwaipilot: KAT-Coder-Air V2.5", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text"], - cost: { - input: 0.15, - output: 0.6, - cacheRead: 0.03, - cacheWrite: 0, - }, - contextWindow: 256000, - maxTokens: 80000, - } satisfies Model<"openai-completions">, "kwaipilot/kat-coder-pro-v2": { id: "kwaipilot/kat-coder-pro-v2", name: "Kwaipilot: KAT-Coder-Pro V2", @@ -11684,7 +11810,7 @@ export const MODELS = { cacheWrite: 0, }, contextWindow: 262144, - maxTokens: 80000, + maxTokens: 144000, } satisfies Model<"openai-completions">, "kwaipilot/kat-coder-pro-v2.5": { id: "kwaipilot/kat-coder-pro-v2.5", @@ -11701,7 +11827,7 @@ export const MODELS = { cacheWrite: 0, }, contextWindow: 262144, - maxTokens: 80000, + maxTokens: 235929, } satisfies Model<"openai-completions">, "liquid/lfm-2.5-2.6b:free": { id: "liquid/lfm-2.5-2.6b:free", @@ -11802,12 +11928,12 @@ export const MODELS = { input: ["text", "image"], cost: { input: 0.19999999999999998, - output: 0.7999999999999999, + output: 0.696, cacheRead: 0, cacheWrite: 0, }, contextWindow: 1048576, - maxTokens: 16384, + maxTokens: 115200, } satisfies Model<"openai-completions">, "meta-llama/llama-4-scout": { id: "meta-llama/llama-4-scout", @@ -11818,13 +11944,13 @@ export const MODELS = { reasoning: false, input: ["text", "image"], cost: { - input: 0.11, - output: 0.33999999999999997, - cacheRead: 0.055, + input: 0.09999999999999999, + output: 0.3, + cacheRead: 0, cacheWrite: 0, }, contextWindow: 1310720, - maxTokens: 8192, + maxTokens: 16384, } satisfies Model<"openai-completions">, "meta/muse-glimmer-30b": { id: "meta/muse-glimmer-30b", @@ -11836,13 +11962,13 @@ export const MODELS = { thinkingLevelMap: {"off":null,"minimal":null,"low":"low","medium":"medium","high":"high","xhigh":"xhigh","max":null}, input: ["text", "image"], cost: { - input: 0.35, - output: 1.5, + input: 0.3, + output: 1.2, cacheRead: 0.04, cacheWrite: 0, }, contextWindow: 131072, - maxTokens: 117964, + maxTokens: 16384, } satisfies Model<"openai-completions">, "meta/muse-spark-1.1": { id: "meta/muse-spark-1.1", @@ -12076,9 +12202,9 @@ export const MODELS = { reasoning: false, input: ["text"], cost: { - input: 0.44, - output: 2.2, - cacheRead: 0.044, + input: 0.39999999999999997, + output: 2, + cacheRead: 0.04, cacheWrite: 0, }, contextWindow: 262144, @@ -12338,8 +12464,8 @@ export const MODELS = { cacheRead: 0.01, cacheWrite: 0, }, - contextWindow: 32000, - maxTokens: 25600, + contextWindow: 32768, + maxTokens: 26214, } satisfies Model<"openai-completions">, "moonshotai/kimi-k2": { id: "moonshotai/kimi-k2", @@ -12628,13 +12754,13 @@ export const MODELS = { thinkingLevelMap: {"minimal":null,"low":null,"medium":null,"high":"high","xhigh":null,"max":null}, input: ["text"], cost: { - input: 0.09999999999999999, - output: 0.25, - cacheRead: 0.049999999999999996, + input: 0.08, + output: 0.19999999999999998, + cacheRead: 0.04, cacheWrite: 0, }, contextWindow: 262144, - maxTokens: 235929, + maxTokens: 131072, } satisfies Model<"openai-completions">, "nvidia/nemotron-3.5-lightning:free": { id: "nvidia/nemotron-3.5-lightning:free", @@ -14161,13 +14287,13 @@ export const MODELS = { reasoning: false, input: ["text", "image"], cost: { - input: 0.13, - output: 0.52, + input: 0.15, + output: 0.6, cacheRead: 0, cacheWrite: 0, }, contextWindow: 262144, - maxTokens: 32768, + maxTokens: 16384, } satisfies Model<"openai-completions">, "qwen/qwen3-vl-30b-a3b-thinking": { id: "qwen/qwen3-vl-30b-a3b-thinking", @@ -14252,13 +14378,13 @@ export const MODELS = { thinkingLevelMap: {"minimal":null,"low":null,"medium":null,"high":"high","xhigh":null,"max":null}, input: ["text", "image"], cost: { - input: 0.26, - output: 2.08, + input: 0.29, + output: 2.4, cacheRead: 0, cacheWrite: 0, }, contextWindow: 262144, - maxTokens: 235929, + maxTokens: 81920, } satisfies Model<"openai-completions">, "qwen/qwen3.5-27b": { id: "qwen/qwen3.5-27b", @@ -14752,9 +14878,9 @@ export const MODELS = { thinkingLevelMap: {"minimal":null,"low":"low","medium":null,"high":"high","xhigh":null,"max":null}, input: ["text"], cost: { - input: 0.13199999999999998, - output: 0.5279999999999999, - cacheRead: 0.032999999999999995, + input: 0.0825, + output: 0.33, + cacheRead: 0.020625, cacheWrite: 0, }, contextWindow: 262144, @@ -14823,13 +14949,13 @@ export const MODELS = { thinkingLevelMap: {"minimal":"minimal","low":"low","medium":"medium","high":"high","xhigh":null,"max":"max"}, input: ["text", "image"], cost: { - input: 0.95, + input: 1, output: 4.05, - cacheRead: 0.16, + cacheRead: 0.16999999999999998, cacheWrite: 0, }, contextWindow: 1048576, - maxTokens: 262144, + maxTokens: 471859, } satisfies Model<"openai-completions">, "thinkingmachines/inkling-small": { id: "thinkingmachines/inkling-small", @@ -15235,13 +15361,13 @@ export const MODELS = { thinkingLevelMap: {"minimal":null,"low":null,"medium":null,"high":"high","xhigh":null,"max":null}, input: ["text"], cost: { - input: 1.26, - output: 3.9600000000000004, - cacheRead: 0.234, + input: 0.966, + output: 3.036, + cacheRead: 0.1794, cacheWrite: 0, }, contextWindow: 204800, - maxTokens: 182476, + maxTokens: 128000, } satisfies Model<"openai-completions">, "z-ai/glm-5.2": { id: "z-ai/glm-5.2", @@ -15294,7 +15420,7 @@ export const MODELS = { cacheRead: 0.26, cacheWrite: 0, }, - contextWindow: 1048576, + contextWindow: 1310720, maxTokens: 131072, } satisfies Model<"openai-completions">, "z-ai/glm-5.3-flash": { @@ -15346,7 +15472,7 @@ export const MODELS = { cost: { input: 10, output: 50, - cacheRead: 1, + cacheRead: 0.25, cacheWrite: 12.5, }, contextWindow: 1000000, @@ -15418,9 +15544,9 @@ export const MODELS = { thinkingLevelMap: {"minimal":null,"low":null,"medium":null,"high":"high","xhigh":"max","max":null}, input: ["text"], cost: { - input: 0.03, - output: 0.09999999999999999, - cacheRead: 0.007, + input: 0.049980000000000004, + output: 0.09996000000000001, + cacheRead: 0.009996000000000001, cacheWrite: 0, }, contextWindow: 1310720, @@ -15436,10 +15562,10 @@ export const MODELS = { thinkingLevelMap: {"off":null,"minimal":null,"low":"low","medium":"medium","high":"high","xhigh":null,"max":null}, input: ["text", "image"], cost: { - input: 0.375, - output: 1.875, - cacheRead: 0.0375, - cacheWrite: 0.0208333333333333, + input: 0.75, + output: 3.75, + cacheRead: 0.075, + cacheWrite: 0.0416666666666667, }, contextWindow: 1048576, maxTokens: 65536, @@ -15544,13 +15670,13 @@ export const MODELS = { thinkingLevelMap: {"off":null,"minimal":null,"low":"low","medium":null,"high":"high","xhigh":null,"max":"max"}, input: ["text"], cost: { - input: 1.4, - output: 4.4, - cacheRead: 0.26, + input: 1.17, + output: 3.9600000000000004, + cacheRead: 0.234, cacheWrite: 0, }, - contextWindow: 1048576, - maxTokens: 131072, + contextWindow: 1310720, + maxTokens: 943718, } satisfies Model<"openai-completions">, }, "prime-inference": { @@ -16129,7 +16255,7 @@ export const MODELS = { cacheRead: 0, cacheWrite: 0, }, - contextWindow: 262144, + contextWindow: 131072, maxTokens: 117964, } satisfies Model<"openai-completions">, "meta-llama/Llama-3.2-1B-Instruct": { @@ -16202,7 +16328,26 @@ export const MODELS = { cacheWrite: 0, }, contextWindow: 1048576, - maxTokens: 16384, + maxTokens: 115200, + } satisfies Model<"openai-completions">, + "meta/muse-spark-1.2": { + id: "meta/muse-spark-1.2", + name: "Muse Spark 1.2", + api: "openai-completions", + provider: "prime-inference", + baseUrl: "https://api.pinference.ai/api/v1", + compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":true,"maxTokensField":"max_tokens","supportsStrictMode":false}, + reasoning: true, + thinkingLevelMap: {"off":null,"minimal":"minimal","low":"low","medium":"medium","high":"high","xhigh":"xhigh","max":null}, + input: ["text", "image"], + cost: { + input: 1.25, + output: 4.25, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 1048576, + maxTokens: 943718, } satisfies Model<"openai-completions">, "minimax/minimax-m2.5": { id: "minimax/minimax-m2.5", @@ -16824,8 +16969,8 @@ export const MODELS = { thinkingLevelMap: {"minimal":null,"low":"low","medium":"medium","high":"high","xhigh":"xhigh","max":"max"}, input: ["text", "image"], cost: { - input: 1, - output: 6, + input: 0.2, + output: 1.2, cacheRead: 0, cacheWrite: 0, }, @@ -17169,7 +17314,7 @@ export const MODELS = { cacheWrite: 0, }, contextWindow: 262144, - maxTokens: 32768, + maxTokens: 16384, } satisfies Model<"openai-completions">, "qwen/qwen3-vl-8b-instruct": { id: "qwen/qwen3-vl-8b-instruct", @@ -17344,6 +17489,25 @@ export const MODELS = { maxTokens: 30000, featured: true, } satisfies Model<"openai-completions">, + "x-ai/grok-4.6": { + id: "x-ai/grok-4.6", + name: "Grok 4.6", + api: "openai-completions", + provider: "prime-inference", + baseUrl: "https://api.pinference.ai/api/v1", + compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":true,"maxTokensField":"max_tokens","supportsStrictMode":false}, + reasoning: true, + thinkingLevelMap: {"off":null,"minimal":null,"low":"low","medium":"medium","high":"high","xhigh":"xhigh","max":null}, + input: ["text", "image"], + cost: { + input: 2, + output: 6, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 500000, + maxTokens: 450000, + } satisfies Model<"openai-completions">, "xiaomi/mimo-v2.5": { id: "xiaomi/mimo-v2.5", name: "Mimo V2.5", @@ -17514,7 +17678,7 @@ export const MODELS = { cacheWrite: 0, }, contextWindow: 204800, - maxTokens: 182476, + maxTokens: 128000, featured: true, } satisfies Model<"openai-completions">, "z-ai/glm-5.2": { @@ -17553,7 +17717,7 @@ export const MODELS = { cacheRead: 0, cacheWrite: 0, }, - contextWindow: 1048576, + contextWindow: 1310720, maxTokens: 131072, } satisfies Model<"openai-completions">, "z-ai/glm-5.3-flash": { @@ -18013,11 +18177,11 @@ export const MODELS = { cost: { input: 2, output: 6, - cacheRead: 0.19999999999999998, + cacheRead: 0.25, cacheWrite: 0, }, contextWindow: 262144, - maxTokens: 131072, + maxTokens: 128000, } satisfies Model<"anthropic-messages">, "alibaba/qwen3.8-27b": { id: "alibaba/qwen3.8-27b", @@ -18028,10 +18192,10 @@ export const MODELS = { reasoning: true, input: ["text", "image"], cost: { - input: 0.55, - output: 3.3000000000000003, - cacheRead: 0.11, - cacheWrite: 0, + input: 0.5, + output: 3, + cacheRead: 0.09999999999999999, + cacheWrite: 0.625, }, contextWindow: 1000000, maxTokens: 131072, @@ -18053,6 +18217,23 @@ export const MODELS = { contextWindow: 991000, maxTokens: 128000, } satisfies Model<"anthropic-messages">, + "alibaba/qwen3.8-flash-next": { + id: "alibaba/qwen3.8-flash-next", + name: "Qwen 3.8 Flash Next", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: true, + input: ["text", "image"], + cost: { + input: 0.12, + output: 0.39999999999999997, + cacheRead: 0.01, + cacheWrite: 0, + }, + contextWindow: 1048576, + maxTokens: 1048576, + } satisfies Model<"anthropic-messages">, "alibaba/qwen3.8-max": { id: "alibaba/qwen3.8-max", name: "Qwen 3.8 Max", @@ -18173,6 +18354,24 @@ export const MODELS = { contextWindow: 1000000, maxTokens: 128000, } satisfies Model<"anthropic-messages">, + "anthropic/claude-fable-5.1": { + id: "anthropic/claude-fable-5.1", + name: "Claude Fable 5.1", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: true, + thinkingLevelMap: {"off":null,"xhigh":"xhigh","max":"max"}, + input: ["text", "image"], + cost: { + input: 10, + output: 50, + cacheRead: 0.25, + cacheWrite: 12.5, + }, + contextWindow: 1000000, + maxTokens: 128000, + } satisfies Model<"anthropic-messages">, "anthropic/claude-haiku-4.5": { id: "anthropic/claude-haiku-4.5", name: "Claude Haiku 4.5", @@ -18487,23 +18686,6 @@ export const MODELS = { contextWindow: 128000, maxTokens: 8192, } satisfies Model<"anthropic-messages">, - "deepseek/deepseek-v3": { - id: "deepseek/deepseek-v3", - name: "DeepSeek V3 0324", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: false, - input: ["text"], - cost: { - input: 0.27, - output: 1.12, - cacheRead: 0.135, - cacheWrite: 0, - }, - contextWindow: 163840, - maxTokens: 163840, - } satisfies Model<"anthropic-messages">, "deepseek/deepseek-v3.1": { id: "deepseek/deepseek-v3.1", name: "DeepSeek V3.1", @@ -18620,8 +18802,8 @@ export const MODELS = { cacheRead: 0.007, cacheWrite: 0, }, - contextWindow: 1000000, - maxTokens: 384000, + contextWindow: 1048576, + maxTokens: 1048576, } satisfies Model<"anthropic-messages">, "deepseek/deepseek-v4-pro": { id: "deepseek/deepseek-v4-pro", @@ -18632,13 +18814,13 @@ export const MODELS = { reasoning: true, input: ["text"], cost: { - input: 1.74, - output: 3.48, - cacheRead: 0.14, + input: 0.66, + output: 1.9800000000000002, + cacheRead: 0.022, cacheWrite: 0, }, - contextWindow: 1048600, - maxTokens: 1048600, + contextWindow: 1000000, + maxTokens: 384000, } satisfies Model<"anthropic-messages">, "deepseek/deepseek-v4-pro-0813": { id: "deepseek/deepseek-v4-pro-0813", @@ -19271,7 +19453,7 @@ export const MODELS = { } satisfies Model<"anthropic-messages">, "minimax/minimax-m2.7": { id: "minimax/minimax-m2.7", - name: "Minimax M2.7", + name: "MiniMax M2.7", api: "anthropic-messages", provider: "vercel-ai-gateway", baseUrl: "https://ai-gateway.vercel.sh", @@ -19288,7 +19470,7 @@ export const MODELS = { } satisfies Model<"anthropic-messages">, "minimax/minimax-m2.7-free": { id: "minimax/minimax-m2.7-free", - name: "Minimax M2.7 (Free)", + name: "MiniMax M2.7 (Free)", api: "anthropic-messages", provider: "vercel-ai-gateway", baseUrl: "https://ai-gateway.vercel.sh", @@ -19334,8 +19516,8 @@ export const MODELS = { cacheRead: 0.06, cacheWrite: 0, }, - contextWindow: 1000000, - maxTokens: 1000000, + contextWindow: 512000, + maxTokens: 512000, } satisfies Model<"anthropic-messages">, "minimax/minimax-m3-free": { id: "minimax/minimax-m3-free", @@ -19637,7 +19819,7 @@ export const MODELS = { cost: { input: 0.95, output: 4, - cacheRead: 0.19, + cacheRead: 0.16, cacheWrite: 0, }, contextWindow: 256000, @@ -19757,8 +19939,8 @@ export const MODELS = { input: ["text"], cost: { input: 0.049999999999999996, - output: 0.15, - cacheRead: 0.049999999999999996, + output: 0.19999999999999998, + cacheRead: 0.01, cacheWrite: 0, }, contextWindow: 262144, @@ -21086,13 +21268,30 @@ export const MODELS = { reasoning: true, input: ["text"], cost: { - input: 0.13199999999999998, - output: 0.5279999999999999, - cacheRead: 0.032999999999999995, + input: 0.14, + output: 0.58, + cacheRead: 0.035, cacheWrite: 0, }, - contextWindow: 256000, - maxTokens: 128000, + contextWindow: 262144, + maxTokens: 262144, + } satisfies Model<"anthropic-messages">, + "tencent/hy4-preview": { + id: "tencent/hy4-preview", + name: "Tencent Hy4 Preview", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: true, + input: ["text"], + cost: { + input: 0.834, + output: 2.501, + cacheRead: 0.041999999999999996, + cacheWrite: 0, + }, + contextWindow: 1024000, + maxTokens: 64000, } satisfies Model<"anthropic-messages">, "thinkingmachines/inkling": { id: "thinkingmachines/inkling", @@ -21162,6 +21361,23 @@ export const MODELS = { contextWindow: 1050000, maxTokens: 131000, } satisfies Model<"anthropic-messages">, + "xiaomi/mimo-v2.5-pro-ultraspeed": { + id: "xiaomi/mimo-v2.5-pro-ultraspeed", + name: "MiMo V2.5 Pro UltraSpeed", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: true, + input: ["text"], + cost: { + input: 1.305, + output: 2.61, + cacheRead: 0.0108, + cacheWrite: 0, + }, + contextWindow: 1048576, + maxTokens: 131072, + } satisfies Model<"anthropic-messages">, "zai/glm-4.5": { id: "zai/glm-4.5", name: "GLM 4.5", @@ -21377,11 +21593,11 @@ export const MODELS = { cost: { input: 1.4, output: 4.4, - cacheRead: 0.26, + cacheRead: 0.14, cacheWrite: 0, }, contextWindow: 1000000, - maxTokens: 12800, + maxTokens: 1000000, } satisfies Model<"anthropic-messages">, "zai/glm-5.3-flash": { id: "zai/glm-5.3-flash", From b4613f02a91417bae6aab43faaf18a79ffbd2672 Mon Sep 17 00:00:00 2001 From: Seth Date: Tue, 1 Sep 2026 17:49:14 -0400 Subject: [PATCH 11/45] fix(coding-agent): stabilize Python skill diagnostics --- packages/coding-agent/src/core/skills.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/coding-agent/src/core/skills.ts b/packages/coding-agent/src/core/skills.ts index d8b2bd28cfd..85316240fa0 100644 --- a/packages/coding-agent/src/core/skills.ts +++ b/packages/coding-agent/src/core/skills.ts @@ -550,9 +550,10 @@ export function loadSkills(options: LoadSkillsOptions): LoadSkillsResult { if (skill.kind === "python") { const existingPythonSkill = pythonImportMap.get(skill.python.importName); if (existingPythonSkill) { + const [firstName, secondName] = [existingPythonSkill.name, skill.name].sort(); pythonImportDiagnostics.push({ type: "warning", - message: `python import name "${skill.python.importName}" is shared by skills "${existingPythonSkill.name}" and "${skill.name}"`, + message: `python import name "${skill.python.importName}" is shared by skills "${firstName}" and "${secondName}"`, path: skill.filePath, }); } else { From b80a52bf621bf534a04a780633d88b2fd6d9e62c Mon Sep 17 00:00:00 2001 From: Seth Date: Tue, 1 Sep 2026 18:21:01 -0400 Subject: [PATCH 12/45] fix: harden binary release and installer paths --- .github/workflows/build-binaries.yml | 6 +- install.sh | 40 +++--- packages/coding-agent/docs/quickstart.md | 2 +- packages/coding-agent/docs/termux.md | 2 +- packages/coding-agent/scripts/copy-assets.ts | 4 +- .../coding-agent/src/core/kernel/bootstrap.ts | 10 +- .../test/bun-installer-e2e.test.ts | 54 +++++++- .../test/bun-release-archives.test.ts | 51 ++++++- .../test/compiled-artifact-smoke.test.ts | 4 + .../test/kernel-bootstrap.test.ts | 11 ++ scripts/assemble-release-archives.mjs | 66 ++++++++-- scripts/build-binaries.sh | 5 +- scripts/pack-prime-agent-release.mjs | 124 +++++++++++++++++- 13 files changed, 335 insertions(+), 44 deletions(-) diff --git a/.github/workflows/build-binaries.yml b/.github/workflows/build-binaries.yml index af5cd25a98c..da13297546a 100644 --- a/.github/workflows/build-binaries.yml +++ b/.github/workflows/build-binaries.yml @@ -344,7 +344,8 @@ jobs: test -n "$R2_BUCKET" test -n "$R2_ENDPOINT_URL" - for artifact in "$PRODUCTION_DIR"/*.tar.gz; do + for artifact in "$PRODUCTION_DIR"/*.tar.gz "$PRODUCTION_DIR"/*.tgz; do + test -f "$artifact" || continue aws s3 cp "$artifact" "s3://${R2_BUCKET}/${RELEASE_PREFIX}/$(basename "$artifact")" \ --endpoint-url "$R2_ENDPOINT_URL" \ --content-type application/gzip \ @@ -410,7 +411,8 @@ jobs: test -n "$R2_BUCKET" test -n "$R2_ENDPOINT_URL" - for artifact in "$BETA_DIR"/*.tar.gz; do + for artifact in "$BETA_DIR"/*.tar.gz "$BETA_DIR"/*.tgz; do + test -f "$artifact" || continue aws s3 cp "$artifact" "s3://${R2_BUCKET}/${RELEASE_PREFIX}/$(basename "$artifact")" \ --endpoint-url "$R2_ENDPOINT_URL" \ --content-type application/gzip \ diff --git a/install.sh b/install.sh index c2bbce253f8..450211c37e0 100755 --- a/install.sh +++ b/install.sh @@ -1624,17 +1624,21 @@ prime_agent_detect_binary_platform() { case "$_arch" in x86_64|amd64) printf 'darwin-x64' ;; arm64|aarch64) printf 'darwin-arm64' ;; - *) return 1 ;; + *) printf 'error: unsupported macOS architecture for binary install: %s\n' "$_arch" >&2; return 1 ;; esac ;; Linux) + if [ -f /etc/alpine-release ] || { command -v ldd >/dev/null 2>&1 && ldd --version 2>&1 | grep -qi musl; }; then + printf 'error: Prime Agent binaries require glibc Linux; use --method=npm on musl systems.\n' >&2 + return 1 + fi case "$_arch" in x86_64|amd64) printf 'linux-x64' ;; arm64|aarch64) printf 'linux-arm64' ;; - *) return 1 ;; + *) printf 'error: unsupported Linux architecture for binary install: %s\n' "$_arch" >&2; return 1 ;; esac ;; - *) return 1 ;; + *) printf 'error: unsupported platform for binary install: %s %s\n' "$_os" "$_arch" >&2; return 1 ;; esac } @@ -1677,7 +1681,6 @@ prime_agent_binary_fresh_install() { _version="$(resolve_prime_agent_version "$@")" _platform= if ! _platform=$(prime_agent_detect_binary_platform); then - printf 'error: unsupported platform for binary install: %s %s\n' "$(uname -s)" "$(uname -m)" >&2 exit 1 fi _artifact_name="$(prime_agent_binary_artifact_name "$_version" "$_platform")" @@ -1800,7 +1803,6 @@ prime_agent_binary_update() { _platform= if ! _platform=$(prime_agent_detect_binary_platform); then - printf 'error: unsupported platform for binary update: %s %s\n' "$(uname -s)" "$(uname -m)" >&2 exit 1 fi _artifact_name="$(prime_agent_binary_artifact_name "$_update_version" "$_platform")" @@ -1893,8 +1895,20 @@ prime_agent_binary_update() { prime_agent_binary_rollback_version="$_old_version_dir" fi - # Atomically switch the symlink + # Atomically switch the symlink, then verify the activated path. If activation + # fails, restore the previous immutable version before returning an error. prime_agent_binary_atomic_symlink "$_binary_path" "$prime_agent_binary_symlink" + if ! prime_agent_binary_smoke_binary "$prime_agent_binary_symlink"; then + if prime_agent_binary_rollback; then + rm -rf "$_version_dir" + printf 'error: activation failed; restored the previous Prime Agent version.\n' >&2 + else + printf 'error: activation failed and no healthy rollback version was available.\n' >&2 + fi + rm -rf "$_download_dir" + prime_agent_download_dir= + exit 1 + fi rm -rf "$_download_dir" prime_agent_download_dir= @@ -1962,16 +1976,10 @@ prime_agent_configure_binary_path() { fi _bin_dir=$(dirname "$prime_agent_binary_symlink") - case ":$PATH:" in - *":$_bin_dir:"*) - if [ "$prime_agent_screen_enabled" = 1 ]; then - prime_agent_screen "Prime Agent installed" "" "Run it with: $prime_agent_cmd" "" - else - printf '\nRun it with: %s\n' "$prime_agent_cmd" - fi - return - ;; - esac + if [ -n "${_existing_path:-}" ] && [ "$_existing_path" != "$prime_agent_binary_symlink" ]; then + printf 'warning: %s at %s currently shadows the new binary at %s.\n' \ + "$prime_agent_cmd" "$_existing_path" "$prime_agent_binary_symlink" >&2 + fi if [ "$prime_agent_screen_enabled" = 1 ]; then prime_agent_screen "Prime Agent installed" "" "PATH update needed for $prime_agent_cmd." "" diff --git a/packages/coding-agent/docs/quickstart.md b/packages/coding-agent/docs/quickstart.md index bf8db3ec31d..9bf314d779d 100644 --- a/packages/coding-agent/docs/quickstart.md +++ b/packages/coding-agent/docs/quickstart.md @@ -25,7 +25,7 @@ cd /path/to/project prime-agent ``` -To run a source checkout instead, use Node.js 22.8.0 or newer: +To run a source checkout instead, use Bun 1.3.14: ```bash git clone https://github.com/PrimeIntellect-ai/prime-agent diff --git a/packages/coding-agent/docs/termux.md b/packages/coding-agent/docs/termux.md index db3ab9273e0..fd127ac0567 100644 --- a/packages/coding-agent/docs/termux.md +++ b/packages/coding-agent/docs/termux.md @@ -14,7 +14,7 @@ Prime Agent runs on Android via [Termux](https://termux.dev/), a terminal emulat pkg update && pkg upgrade # Install dependencies -pkg install nodejs termux-api git ripgrep +pkg install bun termux-api git ripgrep # Clone and install Prime Agent from source git clone https://github.com/PrimeIntellect-ai/prime-agent.git diff --git a/packages/coding-agent/scripts/copy-assets.ts b/packages/coding-agent/scripts/copy-assets.ts index a322995b49f..0fdee57c1e2 100644 --- a/packages/coding-agent/scripts/copy-assets.ts +++ b/packages/coding-agent/scripts/copy-assets.ts @@ -52,7 +52,9 @@ async function copyBinaryAssets(): Promise { await copyFile(join(repoDir, "install.sh"), distDir); await copyFiles(join(packageDir, "src/modes/interactive/theme"), join(distDir, "theme"), ".json"); await copyFiles(join(packageDir, "src/modes/interactive/assets"), join(distDir, "assets"), ".png"); - await copyFile(join(packageDir, "src/core/export-html/template.html"), join(distDir, "export-html")); + for (const name of ["template.html", "template.css", "template.js"]) { + await copyFile(join(packageDir, "src/core/export-html", name), join(distDir, "export-html")); + } await copyFiles(join(packageDir, "src/core/export-html/vendor"), join(distDir, "export-html/vendor"), ".js"); await replaceDirectory(join(packageDir, "docs"), join(distDir, "docs")); await replaceDirectory(join(packageDir, "examples"), join(distDir, "examples")); diff --git a/packages/coding-agent/src/core/kernel/bootstrap.ts b/packages/coding-agent/src/core/kernel/bootstrap.ts index 16f13fac7d9..3c33cf8426b 100644 --- a/packages/coding-agent/src/core/kernel/bootstrap.ts +++ b/packages/coding-agent/src/core/kernel/bootstrap.ts @@ -649,12 +649,12 @@ async function writeBootstrapVersion( function runtimeCandidateDirs(): string[] { const moduleDir = path.dirname(fileURLToPath(import.meta.url)); - // dist/prime-agent-runtime is listed first deliberately: it is the only path stable - // across every shipped layout (dist/, dist/bundle/, bun), where import.meta.url-relative - // resolution breaks. `bun run build` rebuilds it from live source (copy-assets does - // rm -rf + cp), so the staleness hash still refreshes on every build. The relative - // paths below cover running from source (tsx) where dist/ hasn't been built. + // Compiled binary releases place the runtime beside the executable. npm bundles + // place it under dist/. `bun run build` refreshes both layouts from live source, + // so the staleness hash still changes with runtime code. The relative paths below + // cover direct source execution where dist/ has not been built. return [ + path.join(getPackageDir(), "prime-agent-runtime"), path.join(getPackageDir(), "dist", "prime-agent-runtime"), path.resolve(moduleDir, "..", "..", "prime-agent-runtime"), path.resolve(moduleDir, "..", "..", "..", "..", "..", "prime-agent-runtime"), diff --git a/packages/coding-agent/test/bun-installer-e2e.test.ts b/packages/coding-agent/test/bun-installer-e2e.test.ts index 298278cb2b7..f9654a94305 100644 --- a/packages/coding-agent/test/bun-installer-e2e.test.ts +++ b/packages/coding-agent/test/bun-installer-e2e.test.ts @@ -31,7 +31,11 @@ function makeRelease(root: string, version: string, executable: string, checksum writeFileSync(join(releaseDir, "SHA256SUMS"), `${checksum} ${archiveName}\n`); } -function runInstaller(root: string, args: string[]): { exitCode: number; stdout: string; stderr: string } { +function runInstaller( + root: string, + args: string[], + env: Record = {}, +): { exitCode: number; stdout: string; stderr: string } { const home = join(root, "home"); const versions = join(root, "apps", "versions"); const bin = join(root, "bin"); @@ -45,6 +49,7 @@ function runInstaller(root: string, args: string[]): { exitCode: number; stdout: PRIME_AGENT_VERSIONS_DIR: versions, PRIME_AGENT_BIN_DIR: bin, TERM: "dumb", + ...env, }, encoding: "utf8", }); @@ -101,4 +106,51 @@ describe("compiled binary installer", () => { expect(readlinkSync(link)).toBe(oldTarget); expect(readFileSync(join(root, "home", ".prime", "sentinel"), "utf8")).toBe("user data"); }); + + test("rolls back when the activated symlink fails its smoke test", () => { + const root = mkdtempSync(join(tmpdir(), "prime-agent-installer-")); + temporaryRoots.push(root); + makeRelease(root, "1.2.3", goodExecutable("1.2.3")); + expect(runInstaller(root, ["1.2.3"]).exitCode).toBe(0); + const link = join(root, "bin", "prime-agent"); + const oldTarget = readlinkSync(link); + + makeRelease( + root, + "2.0.0", + '#!/bin/sh\ncase "$0" in */bin/prime-agent) exit 1 ;; esac\nif [ "$1" = "--version" ]; then echo "prime-agent 2.0.0"; exit 0; fi\nexit 0\n', + ); + const result = runInstaller(root, ["--update", "2.0.0"]); + expect(result.exitCode).not.toBe(0); + expect(result.stderr).toContain("restored the previous Prime Agent version"); + expect(readlinkSync(link)).toBe(oldTarget); + }); + + test("rejects glibc binaries on musl Linux before downloading", () => { + if (process.platform !== "linux") return; + const root = mkdtempSync(join(tmpdir(), "prime-agent-installer-")); + temporaryRoots.push(root); + const tools = join(root, "tools"); + mkdirSync(tools); + writeFileSync(join(tools, "ldd"), '#!/bin/sh\necho "musl libc"\n'); + chmodSync(join(tools, "ldd"), 0o755); + const result = runInstaller(root, ["1.2.3"], { PATH: `${tools}:${process.env.PATH}` }); + expect(result.exitCode).not.toBe(0); + expect(result.stderr).toContain("require glibc Linux"); + }); + + test("warns when an older command shadows the installed binary", () => { + const root = mkdtempSync(join(tmpdir(), "prime-agent-installer-")); + temporaryRoots.push(root); + makeRelease(root, "1.2.3", goodExecutable("1.2.3")); + const tools = join(root, "tools"); + mkdirSync(tools); + writeFileSync(join(tools, "prime-agent"), '#!/bin/sh\necho "old"\n'); + chmodSync(join(tools, "prime-agent"), 0o755); + + const result = runInstaller(root, ["1.2.3"], { PATH: `${tools}:${process.env.PATH}` }); + expect(result.exitCode, result.stderr).toBe(0); + expect(result.stderr).toContain("currently shadows the new binary"); + expect(result.stdout).toContain('export PATH="'); + }); }); diff --git a/packages/coding-agent/test/bun-release-archives.test.ts b/packages/coding-agent/test/bun-release-archives.test.ts index d92b6f9af18..6a98610e56d 100644 --- a/packages/coding-agent/test/bun-release-archives.test.ts +++ b/packages/coding-agent/test/bun-release-archives.test.ts @@ -9,6 +9,7 @@ import { readdirSync, readFileSync, rmSync, + symlinkSync, writeFileSync, } from "node:fs"; import { tmpdir } from "node:os"; @@ -89,7 +90,23 @@ describe("compiled release archives", () => { const archives = readdirSync(artifacts).filter((name) => name.endsWith(".tar.gz")); expect(archives.sort()).toEqual(platforms.map((platform) => `prime-agent-1.2.3-${platform}.tar.gz`).sort()); expect(readFileSync(join(artifacts, "stable"), "utf8")).toBe("v1.2.3\n"); - expect(readFileSync(join(artifacts, "SHA256SUMS"), "utf8").trim().split("\n")).toHaveLength(4); + expect(readFileSync(join(artifacts, "SHA256SUMS"), "utf8").trim().split("\n")).toHaveLength(8); + for (const tarball of [ + "prime-agent-1.2.3.tgz", + "prime-agent-ai-1.2.3.tgz", + "prime-agent-core-1.2.3.tgz", + "prime-agent-tui-1.2.3.tgz", + ]) { + expect(existsSync(join(artifacts, tarball)), tarball).toBe(true); + } + const npmExtracted = join(f.root, "npm-extracted"); + mkdirSync(npmExtracted); + expect(spawnSync("tar", ["-xzf", join(artifacts, "prime-agent-1.2.3.tgz"), "-C", npmExtracted]).status).toBe(0); + expect(JSON.parse(readFileSync(join(npmExtracted, "package", "package.json"), "utf8"))).toMatchObject({ + name: "prime-agent", + version: "1.2.3", + bin: { "prime-agent": "dist/bundle/cli.js" }, + }); const extracted = join(f.root, "extracted"); mkdirSync(extracted); @@ -147,4 +164,36 @@ describe("compiled release archives", () => { expect(result.status).not.toBe(0); expect(result.stderr).toContain("forbidden dependency or cache directory"); }); + test("rejects shell-active release base URLs", () => { + const f = fixture(); + const result = pack(f, ["--base-url", "https://downloads.example.test/$(touch-danger)"]); + expect(result.status).not.toBe(0); + expect(result.stderr).toContain("unsafe shell characters"); + }); + + test("preserves existing artifacts when preflight validation fails", () => { + const f = fixture(); + const sentinel = join(f.output, "artifacts", "keep.txt"); + mkdirSync(dirname(sentinel), { recursive: true }); + writeFileSync(sentinel, "keep"); + rmSync(join(f.sidecars, "theme"), { recursive: true }); + const result = pack(f); + expect(result.status).not.toBe(0); + expect(readFileSync(sentinel, "utf8")).toBe("keep"); + }); + + test("rejects output paths that traverse a symlink", () => { + const f = fixture(); + const outside = join(f.root, "outside"); + const sentinel = join(outside, "old", "keep.txt"); + mkdirSync(dirname(sentinel), { recursive: true }); + writeFileSync(sentinel, "keep"); + const link = join(releaseRoot, `link-${process.pid}-${Math.random().toString(36).slice(2)}`); + outputDirs.push(link); + symlinkSync(outside, link, "dir"); + const result = pack(f, ["--out-dir", join(link, "old")]); + expect(result.status).not.toBe(0); + expect(result.stderr).toContain("symlinked output path"); + expect(readFileSync(sentinel, "utf8")).toBe("keep"); + }); }); diff --git a/packages/coding-agent/test/compiled-artifact-smoke.test.ts b/packages/coding-agent/test/compiled-artifact-smoke.test.ts index 4215ce290f5..5bce3bb7464 100644 --- a/packages/coding-agent/test/compiled-artifact-smoke.test.ts +++ b/packages/coding-agent/test/compiled-artifact-smoke.test.ts @@ -41,6 +41,10 @@ describe("compiled Bun artifact", () => { for (const name of REQUIRED_BINARY_SIDECARS) { expect(existsSync(join(distDir, name)), name).toBe(true); } + for (const name of ["template.html", "template.css", "template.js"]) { + expect(existsSync(join(distDir, "export-html", name)), name).toBe(true); + } + expect(existsSync(join(distDir, "prime-agent-runtime", "pyproject.toml"))).toBe(true); }); it("contains no installed dependency or cache directories", () => { diff --git a/packages/coding-agent/test/kernel-bootstrap.test.ts b/packages/coding-agent/test/kernel-bootstrap.test.ts index cda17807c4f..0aff4538775 100644 --- a/packages/coding-agent/test/kernel-bootstrap.test.ts +++ b/packages/coding-agent/test/kernel-bootstrap.test.ts @@ -173,6 +173,17 @@ describe("kernel bootstrap", () => { expect(getKernelVenvDir()).toBe(venv); }); + it("resolves a runtime sidecar beside a compiled binary", async () => { + const packageDir = join(tempDir, "binary-layout"); + const runtimeDir = join(packageDir, "prime-agent-runtime"); + mkdirSync(join(runtimeDir, "src", "rlm"), { recursive: true }); + writeFileSync(join(runtimeDir, "pyproject.toml"), '[project]\nname = "prime-agent-runtime"\n'); + writeFileSync(join(runtimeDir, "src", "rlm", "__init__.py"), "VALUE = 1\n"); + process.env.PI_PACKAGE_DIR = packageDir; + + expect(await resolveRuntimeIdentity()).toMatch(/^sha256:[0-9a-f]{64}$/); + }); + it("bootstraps a missing venv with uv, prime-agent-runtime, and default extra packages", async () => { const logPath = installFakeUv(); const venv = join(tempDir, "kernel-venv"); diff --git a/scripts/assemble-release-archives.mjs b/scripts/assemble-release-archives.mjs index 3f085a15ff0..3e6475905b2 100755 --- a/scripts/assemble-release-archives.mjs +++ b/scripts/assemble-release-archives.mjs @@ -36,7 +36,17 @@ */ import { createHash } from "node:crypto"; -import { chmodSync, cpSync, existsSync, mkdirSync, readFileSync, readdirSync, rmSync, writeFileSync } from "node:fs"; +import { + chmodSync, + cpSync, + existsSync, + lstatSync, + mkdirSync, + readFileSync, + readdirSync, + rmSync, + writeFileSync, +} from "node:fs"; import { spawnSync } from "node:child_process"; import { isAbsolute, join, relative, resolve } from "node:path"; @@ -130,7 +140,7 @@ function parseArgs(args) { parsed.version = normalizeVersion(parsed.version); if (parsed.platforms.length === 0) parsed.platforms = [...PLATFORMS]; - parsed.baseUrl = parsed.baseUrl.replace(/\/+$/, ""); + parsed.baseUrl = normalizeBaseUrl(parsed.baseUrl); return parsed; } @@ -153,11 +163,41 @@ function normalizeVersion(version) { return normalized; } +function normalizeBaseUrl(value) { + if (/[\u0000-\u001f\u007f"'`$\\]/.test(value)) { + throw new Error("Release base URL contains unsafe shell characters"); + } + let parsed; + try { + parsed = new URL(value); + } catch { + throw new Error(`Invalid release base URL: ${value}`); + } + if (!new Set(["https:", "http:"]).has(parsed.protocol) || !parsed.hostname || parsed.username || parsed.password) { + throw new Error(`Invalid release base URL: ${value}`); + } + if (parsed.search || parsed.hash) throw new Error("Release base URL must not contain a query or fragment"); + return parsed.toString().replace(/\/+$/, ""); +} + function assertSafeOutputDir(outDir) { const base = resolve(defaultOutDir); - const pathFromBase = relative(base, resolve(outDir)); - if (pathFromBase === "" || (!pathFromBase.startsWith("..") && !isAbsolute(pathFromBase))) return; - throw new Error(`Refusing to write output outside ${base}: ${outDir}`); + const resolvedOutDir = resolve(outDir); + const pathFromBase = relative(base, resolvedOutDir); + if (pathFromBase !== "" && (pathFromBase.startsWith("..") || isAbsolute(pathFromBase))) { + throw new Error(`Refusing to write output outside ${base}: ${outDir}`); + } + + let current = base; + for (const part of pathFromBase.split(/[\/\\]/).filter(Boolean)) { + if (existsSync(current) && lstatSync(current).isSymbolicLink()) { + throw new Error(`Refusing to write through symlinked output path: ${current}`); + } + current = join(current, part); + } + if (existsSync(current) && lstatSync(current).isSymbolicLink()) { + throw new Error(`Refusing to write through symlinked output path: ${current}`); + } } function sha256File(path) { @@ -222,10 +262,6 @@ function main() { const args = parseArgs(process.argv.slice(2)); const outDir = resolve(args.outDir); assertSafeOutputDir(outDir); - rmSync(outDir, { force: true, recursive: true }); - - const versionDir = join(outDir, "artifacts"); - mkdirSync(versionDir, { recursive: true }); const missing = validateSidecars(args.sidecarDir); if (missing.length > 0) { @@ -239,14 +275,22 @@ function main() { throw new Error(`Release sidecars contain a forbidden dependency or cache directory: ${forbiddenDirectory}`); } - const archives = []; - + const binarySources = new Map(); for (const platform of args.platforms) { const binarySource = join(args.binaryDir, platform, "pi"); if (!existsSync(binarySource)) { throw new Error(`Binary not found for platform ${platform}: ${binarySource}`); } + binarySources.set(platform, binarySource); + } + rmSync(outDir, { force: true, recursive: true }); + const versionDir = join(outDir, "artifacts"); + mkdirSync(versionDir, { recursive: true }); + const archives = []; + + for (const platform of args.platforms) { + const binarySource = binarySources.get(platform); const stagingDir = join(outDir, "staging", platform); mkdirSync(stagingDir, { recursive: true }); diff --git a/scripts/build-binaries.sh b/scripts/build-binaries.sh index b41cf1e3cc4..b8951a7e8d8 100755 --- a/scripts/build-binaries.sh +++ b/scripts/build-binaries.sh @@ -17,6 +17,7 @@ # # packages/coding-agent/release/artifacts/ # prime-agent--.tar.gz +# prime-agent-.tgz (plus internal workspace tarballs) # SHA256SUMS # stable # latest.json @@ -116,8 +117,8 @@ bun ../../scripts/pack-prime-agent-release.mjs \ echo "" echo "==> Build complete!" -echo "Platform archives:" -ls -lh "release/artifacts/"*.tar.gz 2>/dev/null || true +echo "Release archives:" +ls -lh "release/artifacts/"*.tar.gz "release/artifacts/"*.tgz 2>/dev/null || true echo "" echo "Metadata:" ls -lh "release/artifacts/SHA256SUMS" "release/artifacts/latest.json" "release/artifacts/stable" 2>/dev/null || true diff --git a/scripts/pack-prime-agent-release.mjs b/scripts/pack-prime-agent-release.mjs index 220105a68a8..3b795dbfdd1 100755 --- a/scripts/pack-prime-agent-release.mjs +++ b/scripts/pack-prime-agent-release.mjs @@ -1,13 +1,13 @@ #!/usr/bin/env bun /** - * Produce Bun-compiled platform release archives. + * Produce Bun-compiled platform archives and npm-compatible tarballs. * * This script is the CI entry point for the release artifact lane. It expects * pre-built Bun-compiled binaries for 4 platforms (darwin-arm64, darwin-x64, * linux-arm64, linux-x64) and sidecar files, then produces platform-specific * .tar.gz archives with checksums and channel metadata. * - * No npm or Node.js is used during artifact assembly. + * Bun creates both native archives and npm-compatible package tarballs. * * Usage: * bun scripts/pack-prime-agent-release.mjs \ @@ -21,18 +21,28 @@ * Output: * /artifacts/ * prime-agent--.tar.gz + * prime-agent-.tgz (plus internal workspace tarballs) * SHA256SUMS * * latest.json|beta.json */ -import { existsSync, readFileSync } from "node:fs"; +import { createHash } from "node:crypto"; +import { cpSync, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs"; import { resolve, join } from "node:path"; const root = resolve(import.meta.dirname, ".."); const defaultOutputDir = join(root, "packages", "coding-agent", "release"); const defaultBaseUrl = process.env.PRIME_AGENT_DOWNLOAD_BASE_URL; const releaseChannels = new Set(["stable", "beta"]); +const publicPackageName = "prime-agent"; +const publicCommandName = "prime-agent"; +const releasePackages = [ + { packageDir: "ai", publicName: "prime-agent-ai" }, + { packageDir: "tui", publicName: "prime-agent-tui" }, + { packageDir: "agent", publicName: "prime-agent-core" }, + { packageDir: "coding-agent", publicName: publicPackageName }, +]; function parseArgs(args) { const parsed = { @@ -126,6 +136,113 @@ Creates platform release archives: `); } +function sha256File(path) { + const hash = createHash("sha256"); + hash.update(readFileSync(path)); + return hash.digest("hex"); +} + +function rewriteInternalDependencies(dependencies, internalPackageUrls) { + if (!dependencies) return undefined; + return Object.fromEntries( + Object.entries(dependencies).map(([name, range]) => [name, internalPackageUrls.get(name) || range]), + ); +} + +function createReleasePackageJson(sourcePackage, packageName, version, internalPackageUrls) { + const manifest = { + ...sourcePackage, + name: packageName, + version, + dependencies: rewriteInternalDependencies(sourcePackage.dependencies, internalPackageUrls), + optionalDependencies: rewriteInternalDependencies(sourcePackage.optionalDependencies, internalPackageUrls), + scripts: sourcePackage.scripts?.postinstall ? { postinstall: sourcePackage.scripts.postinstall } : undefined, + }; + delete manifest.devDependencies; + delete manifest.overrides; + delete manifest.private; + if (packageName === publicPackageName) { + manifest.bin = { [publicCommandName]: "dist/bundle/cli.js" }; + manifest.piConfig = { ...(manifest.piConfig || {}), name: publicCommandName, configDir: ".prime/agent" }; + } + return manifest; +} + +function copyPackageContents(sourceDir, targetDir, packageJson) { + mkdirSync(targetDir, { recursive: true }); + writeFileSync(join(targetDir, "package.json"), `${JSON.stringify(packageJson, null, 2)}\n`); + for (const entry of ["dist", "docs", "examples", "skills", "postinstall.cjs", "README.md", "CHANGELOG.md"]) { + const source = join(sourceDir, entry); + if (existsSync(source)) cpSync(source, join(targetDir, entry), { recursive: true }); + } +} + +function packNpmCompatibilityTarballs(args) { + const artifactsDir = join(args.outDir, "artifacts"); + const stagingRoot = join(args.outDir, "npm-packages"); + const packageSources = new Map(); + for (const releasePackage of releasePackages) { + const sourceDir = join(root, "packages", releasePackage.packageDir); + if (!existsSync(join(sourceDir, "dist"))) { + throw new Error(`Missing built package: ${sourceDir}/dist`); + } + packageSources.set(releasePackage.packageDir, JSON.parse(readFileSync(join(sourceDir, "package.json"), "utf8"))); + } + + const artifactFiles = new Map( + releasePackages.map(({ packageDir, publicName }) => [packageDir, `${publicName}-${args.version}.tgz`]), + ); + const internalPackageUrls = new Map(); + for (const { packageDir, publicName } of releasePackages) { + if (packageDir === "coding-agent") continue; + const sourceName = packageSources.get(packageDir).name; + internalPackageUrls.set(sourceName, `${args.baseUrl}/releases/v${args.version}/${artifactFiles.get(packageDir)}`); + } + + const tarballs = []; + try { + rmSync(stagingRoot, { recursive: true, force: true }); + for (const { packageDir, publicName } of releasePackages) { + const stagingDir = join(stagingRoot, packageDir); + copyPackageContents( + join(root, "packages", packageDir), + stagingDir, + createReleasePackageJson(packageSources.get(packageDir), publicName, args.version, internalPackageUrls), + ); + const result = Bun.spawnSync([process.execPath, "pm", "pack", "--destination", artifactsDir], { + cwd: stagingDir, + stdout: "pipe", + stderr: "pipe", + }); + if (result.exitCode !== 0) { + throw new Error(`bun pm pack failed for ${packageDir}: ${result.stderr.toString()}`); + } + const file = artifactFiles.get(packageDir); + const path = join(artifactsDir, file); + if (!existsSync(path)) throw new Error(`bun pm pack did not create ${path}`); + tarballs.push({ package: publicName, file, sha256: sha256File(path) }); + } + } finally { + rmSync(stagingRoot, { recursive: true, force: true }); + } + + const checksumPath = join(artifactsDir, "SHA256SUMS"); + const checksums = readFileSync(checksumPath, "utf8") + .trim() + .split("\n") + .filter(Boolean); + checksums.push(...tarballs.map(({ file, sha256 }) => `${sha256} ${file}`)); + checksums.sort((left, right) => left.split(/\s+/).at(-1).localeCompare(right.split(/\s+/).at(-1))); + writeFileSync(checksumPath, `${checksums.join("\n")}\n`); + + const manifestName = args.channel === "stable" ? "latest.json" : "beta.json"; + const manifestPath = join(artifactsDir, manifestName); + const manifest = JSON.parse(readFileSync(manifestPath, "utf8")); + manifest.tarball = `releases/v${args.version}/${artifactFiles.get("coding-agent")}`; + manifest.tarballs = tarballs; + writeFileSync(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`); +} + async function main() { const args = parseArgs(process.argv.slice(2)); @@ -157,6 +274,7 @@ async function main() { if (result.status !== 0) { throw new Error(`assemble-release-archives.mjs failed with exit code ${result.status}`); } + packNpmCompatibilityTarballs(args); } try { From a688401a3ae69692ac87c2e083b28956f858a145 Mon Sep 17 00:00:00 2001 From: Seth Date: Tue, 1 Sep 2026 18:21:18 -0400 Subject: [PATCH 13/45] fix: drain daemon sockets before shutdown --- .github/workflows/ci.yml | 32 +++++++++---------- .github/workflows/nightly-process-stress.yml | 8 ++--- .../src/modes/daemon/daemon-mode.ts | 4 +-- .../src/modes/daemon/daemon-socket.ts | 15 ++++++++- .../src/modes/daemon/daemon-supervisor.ts | 4 +-- .../coding-agent/test/daemon-socket.test.ts | 25 +++++++++++++++ 6 files changed, 63 insertions(+), 25 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c29c8fa8c31..6a6fec4ae02 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -129,11 +129,11 @@ jobs: - name: Build run: bun run build - - name: Install uv + - name: Install uv 0.12.4 if: matrix.install_uv - run: | - curl -LsSf https://astral.sh/uv/install.sh | sh - echo "$HOME/.cargo/bin" >> "$GITHUB_PATH" + uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1 + with: + version: "0.12.4" - name: Test working-directory: ${{ matrix.package }} @@ -168,10 +168,10 @@ jobs: - name: Build run: bun run build - - name: Install uv - run: | - curl -LsSf https://astral.sh/uv/install.sh | sh - echo "$HOME/.cargo/bin" >> "$GITHUB_PATH" + - name: Install uv 0.12.4 + uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1 + with: + version: "0.12.4" - name: Run process stress tests working-directory: packages/coding-agent @@ -208,10 +208,10 @@ jobs: - name: Build run: bun run build - - name: Install uv - run: | - curl -LsSf https://astral.sh/uv/install.sh | sh - echo "$HOME/.cargo/bin" >> "$GITHUB_PATH" + - name: Install uv 0.12.4 + uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1 + with: + version: "0.12.4" - name: Run kernel-heavy tests working-directory: packages/coding-agent @@ -237,10 +237,10 @@ jobs: - name: Install dependencies (frozen lockfile) run: bun install --frozen-lockfile - - name: Install uv - run: | - curl -LsSf https://astral.sh/uv/install.sh | sh - echo "$HOME/.cargo/bin" >> "$GITHUB_PATH" + - name: Install uv 0.12.4 + uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1 + with: + version: "0.12.4" - name: Install Python runtime dependencies working-directory: prime-agent-runtime diff --git a/.github/workflows/nightly-process-stress.yml b/.github/workflows/nightly-process-stress.yml index 86b197c330f..8ea0cfb4c59 100644 --- a/.github/workflows/nightly-process-stress.yml +++ b/.github/workflows/nightly-process-stress.yml @@ -40,10 +40,10 @@ jobs: - name: Build run: bun run build - - name: Install uv - run: | - curl -LsSf https://astral.sh/uv/install.sh | sh - echo "$HOME/.cargo/bin" >> "$GITHUB_PATH" + - name: Install uv 0.12.4 + uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1 + with: + version: "0.12.4" - name: Run process stress tests working-directory: packages/coding-agent diff --git a/packages/coding-agent/src/modes/daemon/daemon-mode.ts b/packages/coding-agent/src/modes/daemon/daemon-mode.ts index bcecaba035a..e9d0b1f4d34 100644 --- a/packages/coding-agent/src/modes/daemon/daemon-mode.ts +++ b/packages/coding-agent/src/modes/daemon/daemon-mode.ts @@ -183,6 +183,7 @@ import { cleanupDaemonSocketPath, type DaemonSocketIdentity, defaultDaemonSocketPath, + endDaemonSocketAfterFlush, getDaemonSocketIdentity, normalizeSocketPath, prepareDaemonSocketPath, @@ -7313,8 +7314,7 @@ export class AgentDaemon { } for (const client of this.clients) { client.detachInput(); - client.socket.end(); - client.socket.destroy(); + endDaemonSocketAfterFlush(client.socket); } await new Promise((resolveClose) => { if (!this.server) { diff --git a/packages/coding-agent/src/modes/daemon/daemon-socket.ts b/packages/coding-agent/src/modes/daemon/daemon-socket.ts index dd6d5e2cf7e..753595265c2 100644 --- a/packages/coding-agent/src/modes/daemon/daemon-socket.ts +++ b/packages/coding-agent/src/modes/daemon/daemon-socket.ts @@ -1,5 +1,5 @@ import { chmodSync, existsSync, lstatSync, mkdirSync, unlinkSync } from "node:fs"; -import { createConnection } from "node:net"; +import { createConnection, type Socket } from "node:net"; import { tmpdir } from "node:os"; import { dirname, join } from "node:path"; import lockfile from "proper-lockfile"; @@ -12,6 +12,19 @@ const DAEMON_SOCKET_RELEASE_GRACE_MS = 1000; const DAEMON_SOCKET_RELEASE_POLL_MS = 25; const DAEMON_SOCKET_LOCK_STALE_MS = 5000; const DAEMON_SOCKET_LOCK_UPDATE_MS = 1000; +const DAEMON_SOCKET_FLUSH_TIMEOUT_MS = 1000; + +export function endDaemonSocketAfterFlush(socket: Socket): void { + if (socket.destroyed) return; + const forceCloseTimer = setTimeout(() => socket.destroy(), DAEMON_SOCKET_FLUSH_TIMEOUT_MS); + forceCloseTimer.unref(); + const clearForceClose = () => clearTimeout(forceCloseTimer); + socket.once("close", clearForceClose); + socket.end(() => { + clearForceClose(); + if (!socket.destroyed) socket.destroy(); + }); +} type DaemonSocketCompromiseListener = (error: Error) => void; diff --git a/packages/coding-agent/src/modes/daemon/daemon-supervisor.ts b/packages/coding-agent/src/modes/daemon/daemon-supervisor.ts index bf92424d328..c50c81b8bbc 100644 --- a/packages/coding-agent/src/modes/daemon/daemon-supervisor.ts +++ b/packages/coding-agent/src/modes/daemon/daemon-supervisor.ts @@ -113,6 +113,7 @@ import { type DaemonSocketPathLease, defaultDaemonSocketDir, defaultDaemonSocketPath, + endDaemonSocketAfterFlush, getDaemonSocketIdentity, normalizeSocketPath, prepareDaemonSocketPath, @@ -6554,8 +6555,7 @@ export class DaemonSupervisor { await this.catalog.stop(); for (const client of this.clients) { client.detachInput(); - client.socket.end(); - client.socket.destroy(); + endDaemonSocketAfterFlush(client.socket); } await new Promise((resolveClose) => this.server?.close(() => resolveClose()) ?? resolveClose()); await this.runCleanupStep("daemon socket", () => this.cleanupSocket()); diff --git a/packages/coding-agent/test/daemon-socket.test.ts b/packages/coding-agent/test/daemon-socket.test.ts index 8237d68f231..5288c1f17e0 100644 --- a/packages/coding-agent/test/daemon-socket.test.ts +++ b/packages/coding-agent/test/daemon-socket.test.ts @@ -9,11 +9,36 @@ import { cleanupDaemonSocketPath, DaemonSocketPathLease, defaultDaemonSocketPath, + endDaemonSocketAfterFlush, getDaemonSocketIdentity, normalizeSocketPath, prepareDaemonSocketPath, } from "../src/modes/daemon/daemon-socket.js"; +describe("endDaemonSocketAfterFlush", () => { + it("delivers queued bytes before closing the socket", async () => { + const server = createServer((socket) => { + socket.write("daemon_closing\n"); + endDaemonSocketAfterFlush(socket); + }); + await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve)); + const address = server.address(); + if (!address || typeof address === "string") throw new Error("Expected TCP server address"); + const received = await new Promise((resolve, reject) => { + let data = ""; + const socket = createConnection({ host: "127.0.0.1", port: address.port }); + socket.setEncoding("utf8"); + socket.on("data", (chunk) => { + data += chunk; + }); + socket.on("end", () => resolve(data)); + socket.on("error", reject); + }); + await new Promise((resolve, reject) => server.close((error) => (error ? reject(error) : resolve()))); + expect(received).toBe("daemon_closing\n"); + }); +}); + describe("normalizeSocketPath", () => { it("normalizes equivalent Unix spellings", () => { if (process.platform === "win32") return; From 19a52a08f50993263012b945973199eae4688199 Mon Sep 17 00:00:00 2001 From: Seth Date: Tue, 1 Sep 2026 18:21:25 -0400 Subject: [PATCH 14/45] chore(ai): refresh generated model catalog --- packages/ai/src/models.generated.ts | 24 +++--------------------- 1 file changed, 3 insertions(+), 21 deletions(-) diff --git a/packages/ai/src/models.generated.ts b/packages/ai/src/models.generated.ts index cbc6e14c3b8..95bc323c017 100644 --- a/packages/ai/src/models.generated.ts +++ b/packages/ai/src/models.generated.ts @@ -8941,24 +8941,6 @@ export const MODELS = { contextWindow: 1000000, maxTokens: 128000, } satisfies Model<"anthropic-messages">, - "claude-fable-5-1": { - id: "claude-fable-5-1", - name: "Claude Fable 5.1", - api: "anthropic-messages", - provider: "opencode", - baseUrl: "https://opencode.ai/zen", - reasoning: true, - thinkingLevelMap: {"off":null,"xhigh":"xhigh","max":"max"}, - input: ["text", "image"], - cost: { - input: 10, - output: 50, - cacheRead: 0.25, - cacheWrite: 12.5, - }, - contextWindow: 1000000, - maxTokens: 128000, - } satisfies Model<"anthropic-messages">, "claude-haiku-4-5": { id: "claude-haiku-4-5", name: "Claude Haiku 4.5", @@ -11254,9 +11236,9 @@ export const MODELS = { thinkingLevelMap: {"minimal":null,"low":null,"medium":null,"high":"high","xhigh":"max","max":null}, input: ["text"], cost: { - input: 1.0241639999999999, - output: 2.0483279999999997, - cacheRead: 0.085347, + input: 1.0274699999999999, + output: 2.0549399999999998, + cacheRead: 0.0856225, cacheWrite: 0, }, contextWindow: 1048576, From 424cb85db2ba523ea179582dcc7b620701e924e2 Mon Sep 17 00:00:00 2001 From: Seth Date: Tue, 1 Sep 2026 18:30:05 -0400 Subject: [PATCH 15/45] fix: normalize release paths and require HTTPS --- install.sh | 2 ++ .../coding-agent/test/bun-installer-e2e.test.ts | 12 ++++++++++++ .../test/bun-release-archives.test.ts | 16 ++++++++++++++++ scripts/assemble-release-archives.mjs | 4 ++-- scripts/pack-prime-agent-release.mjs | 9 ++++++++- 5 files changed, 40 insertions(+), 3 deletions(-) diff --git a/install.sh b/install.sh index 450211c37e0..56367e1f7ca 100755 --- a/install.sh +++ b/install.sh @@ -1665,6 +1665,8 @@ prime_agent_binary_smoke_binary() { prime_agent_binary_atomic_symlink() { _target="$1" _link="$2" + _target_dir=$(cd "$(dirname "$_target")" 2>/dev/null && pwd -P) || return 1 + _target="$_target_dir/$(basename "$_target")" # Create parent dir if needed _link_dir=$(dirname "$_link") if [ ! -d "$_link_dir" ]; then diff --git a/packages/coding-agent/test/bun-installer-e2e.test.ts b/packages/coding-agent/test/bun-installer-e2e.test.ts index f9654a94305..772b782052d 100644 --- a/packages/coding-agent/test/bun-installer-e2e.test.ts +++ b/packages/coding-agent/test/bun-installer-e2e.test.ts @@ -42,6 +42,7 @@ function runInstaller( mkdirSync(join(home, ".prime"), { recursive: true }); writeFileSync(join(home, ".prime", "sentinel"), "user data"); const result = spawnSync("sh", [installer, ...args], { + cwd: root, env: { ...process.env, HOME: home, @@ -153,4 +154,15 @@ describe("compiled binary installer", () => { expect(result.stderr).toContain("currently shadows the new binary"); expect(result.stdout).toContain('export PATH="'); }); + test("canonicalizes relative version directories before linking", () => { + const root = mkdtempSync(join(tmpdir(), "prime-agent-installer-")); + temporaryRoots.push(root); + makeRelease(root, "1.2.3", goodExecutable("1.2.3")); + + const result = runInstaller(root, ["1.2.3"], { PRIME_AGENT_VERSIONS_DIR: "relative/versions" }); + expect(result.exitCode, result.stderr).toBe(0); + expect(readlinkSync(join(root, "bin", "prime-agent"))).toBe( + join(root, "relative", "versions", "v1.2.3", "prime-agent"), + ); + }); }); diff --git a/packages/coding-agent/test/bun-release-archives.test.ts b/packages/coding-agent/test/bun-release-archives.test.ts index 6a98610e56d..100888e1a5d 100644 --- a/packages/coding-agent/test/bun-release-archives.test.ts +++ b/packages/coding-agent/test/bun-release-archives.test.ts @@ -164,6 +164,22 @@ describe("compiled release archives", () => { expect(result.status).not.toBe(0); expect(result.stderr).toContain("forbidden dependency or cache directory"); }); + test("normalizes v-prefixed versions for npm artifact URLs", () => { + const f = fixture(); + const result = pack(f, ["--version", "v1.2.3"]); + expect(result.status, result.stderr).toBe(0); + const manifest = JSON.parse(readFileSync(join(f.output, "artifacts", "latest.json"), "utf8")); + expect(manifest.tarball).toBe("releases/v1.2.3/prime-agent-1.2.3.tgz"); + expect(existsSync(join(f.output, "artifacts", "prime-agent-1.2.3.tgz"))).toBe(true); + }); + + test("rejects insecure release base URLs", () => { + const f = fixture(); + const result = pack(f, ["--base-url", "http://downloads.example.test"]); + expect(result.status).not.toBe(0); + expect(result.stderr).toContain("must use HTTPS"); + }); + test("rejects shell-active release base URLs", () => { const f = fixture(); const result = pack(f, ["--base-url", "https://downloads.example.test/$(touch-danger)"]); diff --git a/scripts/assemble-release-archives.mjs b/scripts/assemble-release-archives.mjs index 3e6475905b2..4ac116c0548 100755 --- a/scripts/assemble-release-archives.mjs +++ b/scripts/assemble-release-archives.mjs @@ -173,8 +173,8 @@ function normalizeBaseUrl(value) { } catch { throw new Error(`Invalid release base URL: ${value}`); } - if (!new Set(["https:", "http:"]).has(parsed.protocol) || !parsed.hostname || parsed.username || parsed.password) { - throw new Error(`Invalid release base URL: ${value}`); + if (parsed.protocol !== "https:" || !parsed.hostname || parsed.username || parsed.password) { + throw new Error(`Release base URL must use HTTPS without credentials: ${value}`); } if (parsed.search || parsed.hash) throw new Error("Release base URL must not contain a query or fragment"); return parsed.toString().replace(/\/+$/, ""); diff --git a/scripts/pack-prime-agent-release.mjs b/scripts/pack-prime-agent-release.mjs index 3b795dbfdd1..e21bdce1e35 100755 --- a/scripts/pack-prime-agent-release.mjs +++ b/scripts/pack-prime-agent-release.mjs @@ -112,15 +112,22 @@ function parseArgs(args) { if (!parsed.baseUrl) throw new Error("--base-url or PRIME_AGENT_DOWNLOAD_BASE_URL is required"); parsed.baseUrl = parsed.baseUrl.replace(/\/+$/, ""); - // Resolve version from coding-agent package.json if not provided. + // Resolve and normalize the version once for archive names and embedded URLs. if (!parsed.version) { const cliPkg = JSON.parse(readFileSync(join(root, "packages", "coding-agent", "package.json"), "utf8")); parsed.version = cliPkg.version; } + parsed.version = normalizeVersion(parsed.version); return parsed; } +function normalizeVersion(version) { + const normalized = version.startsWith("v") ? version.slice(1) : version; + if (!/^[0-9A-Za-z.-]+$/.test(normalized)) throw new Error(`Invalid release version: ${version}`); + return normalized; +} + function printHelp() { console.log(`Usage: bun scripts/pack-prime-agent-release.mjs --base-url url [--channel stable|beta] [--version x.y.z] [--out-dir path] [--binary-base-dir path] [--sidecar-dir path] [--platform platform] From 2399dfb7271cdfd10cd3839cc74e9cb70dc1f3c1 Mon Sep 17 00:00:00 2001 From: Seth Date: Tue, 1 Sep 2026 18:33:14 -0400 Subject: [PATCH 16/45] fix: keep npm release postinstall Node-compatible --- packages/coding-agent/test/bun-release-archives.test.ts | 1 + scripts/pack-prime-agent-release.mjs | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/coding-agent/test/bun-release-archives.test.ts b/packages/coding-agent/test/bun-release-archives.test.ts index 100888e1a5d..f987742cc07 100644 --- a/packages/coding-agent/test/bun-release-archives.test.ts +++ b/packages/coding-agent/test/bun-release-archives.test.ts @@ -106,6 +106,7 @@ describe("compiled release archives", () => { name: "prime-agent", version: "1.2.3", bin: { "prime-agent": "dist/bundle/cli.js" }, + scripts: { postinstall: "node postinstall.cjs" }, }); const extracted = join(f.root, "extracted"); diff --git a/scripts/pack-prime-agent-release.mjs b/scripts/pack-prime-agent-release.mjs index e21bdce1e35..347ed27e7c2 100755 --- a/scripts/pack-prime-agent-release.mjs +++ b/scripts/pack-prime-agent-release.mjs @@ -163,7 +163,7 @@ function createReleasePackageJson(sourcePackage, packageName, version, internalP version, dependencies: rewriteInternalDependencies(sourcePackage.dependencies, internalPackageUrls), optionalDependencies: rewriteInternalDependencies(sourcePackage.optionalDependencies, internalPackageUrls), - scripts: sourcePackage.scripts?.postinstall ? { postinstall: sourcePackage.scripts.postinstall } : undefined, + scripts: sourcePackage.scripts?.postinstall ? { postinstall: "node postinstall.cjs" } : undefined, }; delete manifest.devDependencies; delete manifest.overrides; From f670360b8294b01961db63d1f929f47a17030699 Mon Sep 17 00:00:00 2001 From: Seth Date: Tue, 1 Sep 2026 18:36:25 -0400 Subject: [PATCH 17/45] fix: trim release base URLs before packing --- .../test/bun-release-archives.test.ts | 15 +++++++++++++++ scripts/assemble-release-archives.mjs | 1 + scripts/pack-prime-agent-release.mjs | 2 +- 3 files changed, 17 insertions(+), 1 deletion(-) diff --git a/packages/coding-agent/test/bun-release-archives.test.ts b/packages/coding-agent/test/bun-release-archives.test.ts index f987742cc07..66e0c68854a 100644 --- a/packages/coding-agent/test/bun-release-archives.test.ts +++ b/packages/coding-agent/test/bun-release-archives.test.ts @@ -174,6 +174,21 @@ describe("compiled release archives", () => { expect(existsSync(join(f.output, "artifacts", "prime-agent-1.2.3.tgz"))).toBe(true); }); + test("trims release base URLs before writing npm dependency specs", () => { + const f = fixture(); + const result = pack(f, ["--base-url", " https://downloads.example.test/ "]); + expect(result.status, result.stderr).toBe(0); + const packedManifest = spawnSync( + "tar", + ["-xOzf", join(f.output, "artifacts", "prime-agent-1.2.3.tgz"), "package/package.json"], + { encoding: "utf8" }, + ); + expect(packedManifest.status, packedManifest.stderr).toBe(0); + expect(JSON.parse(packedManifest.stdout).dependencies["@earendil-works/pi-ai"]).toBe( + "https://downloads.example.test/releases/v1.2.3/prime-agent-ai-1.2.3.tgz", + ); + }); + test("rejects insecure release base URLs", () => { const f = fixture(); const result = pack(f, ["--base-url", "http://downloads.example.test"]); diff --git a/scripts/assemble-release-archives.mjs b/scripts/assemble-release-archives.mjs index 4ac116c0548..282b5c90614 100755 --- a/scripts/assemble-release-archives.mjs +++ b/scripts/assemble-release-archives.mjs @@ -164,6 +164,7 @@ function normalizeVersion(version) { } function normalizeBaseUrl(value) { + value = value.trim(); if (/[\u0000-\u001f\u007f"'`$\\]/.test(value)) { throw new Error("Release base URL contains unsafe shell characters"); } diff --git a/scripts/pack-prime-agent-release.mjs b/scripts/pack-prime-agent-release.mjs index 347ed27e7c2..1f101d683d4 100755 --- a/scripts/pack-prime-agent-release.mjs +++ b/scripts/pack-prime-agent-release.mjs @@ -110,7 +110,7 @@ function parseArgs(args) { } if (!parsed.baseUrl) throw new Error("--base-url or PRIME_AGENT_DOWNLOAD_BASE_URL is required"); - parsed.baseUrl = parsed.baseUrl.replace(/\/+$/, ""); + parsed.baseUrl = parsed.baseUrl.trim().replace(/\/+$/, ""); // Resolve and normalize the version once for archive names and embedded URLs. if (!parsed.version) { From 06e57bc31996e99a412ace94036e1d5b9e120458 Mon Sep 17 00:00:00 2001 From: Seth Date: Tue, 1 Sep 2026 18:54:16 -0400 Subject: [PATCH 18/45] feat!: make compiled Bun binaries the only install path --- .github/workflows/build-binaries.yml | 4 +- install.sh | 914 +----------------- .../.changes/bun-primary-runtime.md | 2 +- packages/coding-agent/README.md | 2 +- packages/coding-agent/docs/development.md | 2 +- .../test/bun-installer-e2e.test.ts | 53 +- .../coding-agent/test/bun-installer.test.ts | 26 +- .../test/bun-release-archives.test.ts | 39 +- scripts/build-binaries.sh | 3 +- scripts/check-installer-render.mjs | 18 +- scripts/pack-prime-agent-release.mjs | 124 +-- scripts/release.mjs | 4 +- 12 files changed, 129 insertions(+), 1062 deletions(-) diff --git a/.github/workflows/build-binaries.yml b/.github/workflows/build-binaries.yml index da13297546a..c044db863a4 100644 --- a/.github/workflows/build-binaries.yml +++ b/.github/workflows/build-binaries.yml @@ -344,7 +344,7 @@ jobs: test -n "$R2_BUCKET" test -n "$R2_ENDPOINT_URL" - for artifact in "$PRODUCTION_DIR"/*.tar.gz "$PRODUCTION_DIR"/*.tgz; do + for artifact in "$PRODUCTION_DIR"/*.tar.gz; do test -f "$artifact" || continue aws s3 cp "$artifact" "s3://${R2_BUCKET}/${RELEASE_PREFIX}/$(basename "$artifact")" \ --endpoint-url "$R2_ENDPOINT_URL" \ @@ -411,7 +411,7 @@ jobs: test -n "$R2_BUCKET" test -n "$R2_ENDPOINT_URL" - for artifact in "$BETA_DIR"/*.tar.gz "$BETA_DIR"/*.tgz; do + for artifact in "$BETA_DIR"/*.tar.gz; do test -f "$artifact" || continue aws s3 cp "$artifact" "s3://${R2_BUCKET}/${RELEASE_PREFIX}/$(basename "$artifact")" \ --endpoint-url "$R2_ENDPOINT_URL" \ diff --git a/install.sh b/install.sh index 56367e1f7ca..41f53e530d5 100755 --- a/install.sh +++ b/install.sh @@ -57,7 +57,6 @@ prime_agent_screen_status= prime_agent_screen_detail= prime_agent_screen_question= prime_agent_animation_frame=0 -prime_agent_install_method="${PRIME_AGENT_INSTALL_METHOD:-auto}" prime_agent_binary_versions_dir="${PRIME_AGENT_VERSIONS_DIR:-${XDG_DATA_HOME:-$HOME/.local/share}/prime-agent/versions}" prime_agent_binary_symlink="${PRIME_AGENT_BIN_DIR:-${XDG_BIN_HOME:-$HOME/.local/bin}}/prime-agent" prime_agent_binary_rollback_version= @@ -70,25 +69,16 @@ main() { exit 1 fi - # Parse flags before positional args - prime_agent_install_method="${PRIME_AGENT_INSTALL_METHOD:-auto}" prime_agent_is_update=0 _prime_agent_positional= for _prime_agent_arg in "$@"; do case "$_prime_agent_arg" in --method=*) - prime_agent_install_method="${_prime_agent_arg#--method=}" - case "$prime_agent_install_method" in - binary|npm|auto) ;; - *) - printf 'error: invalid install method: %s (must be binary, npm, or auto)\n' "$prime_agent_install_method" >&2 - exit 1 - ;; - esac + printf 'error: --method is no longer supported; Prime Agent installs as a compiled Bun binary.\n' >&2 + exit 1 ;; --update) prime_agent_is_update=1 - prime_agent_install_method=binary ;; *) if [ -z "$_prime_agent_positional" ]; then @@ -113,19 +103,12 @@ main() { return fi - # Default auto and explicit --method=binary both use compiled binary - if [ "$prime_agent_install_method" != npm ]; then - if [ "$prime_agent_screen_enabled" = 1 ]; then - prime_agent_screen "Installing Prime Agent" "" "" "" - else - printf '\n\033[1m Installing Prime Agent\033[0m\n\033[2m compiled binary\033[0m\n\n' - fi - prime_agent_binary_fresh_install "${_prime_agent_positional:-}" - return + if [ "$prime_agent_screen_enabled" = 1 ]; then + prime_agent_screen "Installing Prime Agent" "" "" "" + else + printf '\n\033[1m Installing Prime Agent\033[0m\n\033[2m compiled Bun binary\033[0m\n\n' fi - - # Explicit --method=npm: original npm install path - prime_agent_npm_install "${_prime_agent_positional:-}" + prime_agent_binary_fresh_install "${_prime_agent_positional:-}" } create_temp_dir() { @@ -632,37 +615,6 @@ prime_agent_print_centered_line() { fi } -prime_agent_place_prompt_cursor() { - max_width=$((prime_agent_screen_cols - 4)) - if [ "$max_width" -lt 1 ]; then - max_width=1 - fi - prompt_text=$(prime_agent_fit_ascii "$(prime_agent_screen_primary_text)" "$max_width") - prompt_width=${#prompt_text} - content_height=$(prime_agent_content_height) - top=$(((prime_agent_screen_rows - content_height) / 2)) - if [ "$top" -lt 0 ]; then - top=0 - fi - prompt_index=0 - if prime_agent_show_logo; then - prompt_index=$((prompt_index + 15)) - fi - row=$((top + prompt_index + 1)) - col=$(((prime_agent_screen_cols - prompt_width) / 2 + prompt_width + 2)) - if [ "$col" -lt 1 ]; then - col=1 - fi - if [ "$col" -gt "$prime_agent_screen_cols" ]; then - col="$prime_agent_screen_cols" - fi - if ( : <>/dev/tty ) 2>/dev/null; then - printf '%s%s%s[%s;%sH' "$prime_agent_reset" "$prime_agent_show_cursor" "$prime_agent_esc" "$row" "$col" >/dev/tty - else - printf '%s%s%s[%s;%sH' "$prime_agent_reset" "$prime_agent_show_cursor" "$prime_agent_esc" "$row" "$col" >&2 - fi -} - prime_agent_pulse() { case $((prime_agent_screen_frame % 4)) in 0) printf '.' ;; @@ -741,15 +693,6 @@ prime_agent_run_quiet_with_animation() { prime_agent_run_quiet_with_animation_command "$title" "$status" "$detail" pulse "$@" } -prime_agent_run_quiet_with_animation_steps() { - title="$1" - status="$2" - details="$3" - shift 3 - - prime_agent_run_quiet_with_animation_command "$title" "$status" "$details" static "$@" -} - prime_agent_run_quiet_with_animation_command() { title="$1" status="$2" @@ -791,122 +734,6 @@ prime_agent_run_quiet_with_animation_command() { return "$command_status" } -prime_agent_prompt_yes_no() { - question="$1" - detail="$2" - input_prompt="$3" - - if ( : <>/dev/tty ) 2>/dev/null; then - prompt_input=tty - exec 3<>/dev/tty - elif [ -t 0 ]; then - prompt_input=stdin - else - return 2 - fi - - if [ "$prime_agent_screen_enabled" = 1 ]; then - prime_agent_screen "$question" "" "$detail" "$input_prompt" - prime_agent_place_prompt_cursor "$input_prompt" - else - printf '%s\n' "$detail" - if [ "$prompt_input" = tty ]; then - printf '%s ' "$input_prompt" >&3 - else - printf '%s ' "$input_prompt" >&2 - fi - fi - - if [ "$prompt_input" = tty ]; then - if ! IFS= read -r answer <&3; then - answer= - fi - exec 3>&- - else - if ! IFS= read -r answer; then - answer= - fi - fi - - case "$answer" in - n|N|no|NO) - return 1 - ;; - esac - return 0 -} - -start_preflight_checks() { - preflight_dir=$(create_temp_dir) - preflight_file="$preflight_dir/preflight" - run_preflight_checks >"$preflight_file" & - preflight_pid=$! -} - -finish_preflight_checks() { - if [ "$prime_agent_screen_enabled" = 1 ]; then - while kill -0 "$preflight_pid" 2>/dev/null; do - prime_agent_screen "Checking Node.js and npm$(prime_agent_pulse)" "" "" "" - sleep 0.18 - done - fi - - if wait "$preflight_pid"; then - preflight_status=0 - else - preflight_status=$? - fi - - if [ "$prime_agent_screen_enabled" = 1 ]; then - if [ "$preflight_status" -ne 0 ]; then - preflight_summary=$(sed -n '1p' "$preflight_file") - prime_agent_screen "Node.js 20.6.0 or newer is required" "" "$preflight_summary" "" - sleep 0.4 - elif [ -s "$preflight_file" ]; then - preflight_summary="Existing $prime_agent_cmd command found on PATH." - prime_agent_screen "Environment ready" "" "$preflight_summary" "" - sleep 0.4 - fi - else - cat "$preflight_file" - fi - rm -rf "$preflight_dir" - return "$preflight_status" -} - -run_preflight_checks() { - status=0 - yellow="${prime_agent_esc}[33m" - reset="${prime_agent_esc}[0m" - - if command -v node >/dev/null 2>&1; then - node_version=$(node --version) - if ! node -e 'const [major, minor, patch] = process.versions.node.split(".").map(Number); process.exit(major > 20 || (major === 20 && (minor > 6 || (minor === 6 && patch >= 0))) ? 0 : 1)' >/dev/null; then - printf 'error: Prime Agent requires Node.js 20.6.0 or newer. Found %s.\n' "$node_version" - status=1 - fi - else - printf 'error: Node.js 20.6.0 or newer is required to install Prime Agent.\n' - status=1 - fi - - if ! command -v npm >/dev/null 2>&1; then - printf 'error: npm is required to install Prime Agent.\n' - status=1 - fi - - if [ "$status" -ne 0 ]; then - printf '\n' - fi - - if prime_agent_path=$(command -v "$prime_agent_cmd" 2>/dev/null); then - printf '%sExisting %s found at: %s%s\n' "$yellow" "$prime_agent_cmd" "$prime_agent_path" "$reset" - printf '\n' - fi - - return "$status" -} - resolve_prime_agent_version() { if [ "${1:-}" ]; then case "$1" in @@ -973,387 +800,6 @@ normalize_version() { printf '%s' "$version" } -install_node_npm_interactive() { - method=$(detect_node_install_method) - case "$method" in - homebrew) label="Homebrew" ;; - apt) label="apt" ;; - apk) label="apk" ;; - standalone) label="standalone Node.js" ;; - *) - method=standalone - label="standalone Node.js" - ;; - esac - - if prime_agent_prompt_yes_no \ - "Install Node.js and npm with $label?" \ - "Required before Prime Agent can be installed." \ - "Install? [Y/n]"; then - install_node_npm "$method" "$label" - return - else - prompt_status=$? - fi - if [ "$prompt_status" -eq 2 ]; then - printf 'No terminal detected; install Node.js 20.6.0 or newer and npm, then run this installer again.\n' - else - printf '\nInstall Node.js 20.6.0 or newer and npm, then run this installer again.\n' - fi - return 1 -} - -detect_node_install_method() { - case "$(uname -s)" in - Darwin) - if command -v brew >/dev/null 2>&1; then - printf 'homebrew' - else - printf 'standalone' - fi - ;; - Linux) - if command -v apt-cache >/dev/null 2>&1 && command -v apt-get >/dev/null 2>&1 && apt_node_candidate_is_new_enough; then - printf 'apt' - elif command -v apk >/dev/null 2>&1 && apk_node_candidate_is_new_enough; then - printf 'apk' - else - printf 'standalone' - fi - ;; - *) - printf 'standalone' - ;; - esac -} - -apt_node_candidate_is_new_enough() { - version=$(apt-cache policy nodejs 2>/dev/null | awk '/Candidate:/ { print $2; exit }') - [ -n "$version" ] && [ "$version" != "(none)" ] && node_version_string_is_new_enough "$version" -} - -apk_node_candidate_is_new_enough() { - version=$(apk search -x nodejs 2>/dev/null | awk -F- '/^nodejs-/ { print $2; exit }') - [ -n "$version" ] && node_version_string_is_new_enough "$version" -} - -node_version_string_is_new_enough() { - version="${1#v}" - case "$version" in - [0-9]*) ;; - *) return 1 ;; - esac - version="${version%%[!0-9.]*}" - version_ifs=${IFS- } - IFS=. - set -- $version - IFS=$version_ifs - major="${1:-}" - minor="${2:-0}" - patch="${3:-0}" - case "$major" in ''|*[!0-9]*) return 1 ;; esac - case "$minor" in ''|*[!0-9]*) minor=0 ;; esac - case "$patch" in ''|*[!0-9]*) patch=0 ;; esac - - [ "$major" -gt 20 ] && return 0 - [ "$major" -eq 20 ] && [ "$minor" -gt 6 ] && return 0 - [ "$major" -eq 20 ] && [ "$minor" -eq 6 ] && [ "$patch" -ge 0 ] && return 0 - return 1 -} - -install_node_npm() { - method="$1" - label="$2" - - if [ "$prime_agent_screen_enabled" != 1 ]; then - printf '\nInstalling Node.js and npm with %s...\n\n' "$label" - run_node_install_method "$method" - else - prepare_sudo_for_node_install "$method" - node_install_details="Using $label. -Resolving Node.js packages. -Downloading Node.js runtime. -Installing npm. -Preparing Prime Agent setup." - prime_agent_run_quiet_with_animation_steps \ - "Installing Node.js and npm" \ - "Installing Node.js and npm" \ - "$node_install_details" \ - run_node_install_method "$method" - fi - - if [ "$method" = standalone ]; then - load_standalone_node - PRIME_AGENT_NODE_INSTALLED_STANDALONE=1 - fi - hash -r - if [ "$prime_agent_screen_enabled" = 1 ]; then - prime_agent_screen "Node.js and npm installed" "" "Continuing Prime Agent setup." "" - else - printf '\nNode.js and npm are installed.\n\n' - fi -} - -node_install_needs_sudo() { - if [ "${EUID:-$(id -u)}" -eq 0 ]; then - return 1 - fi - - case "$1" in - apt|apk) - return 0 - ;; - standalone) - [ "$(uname -s)" = Linux ] || return 1 - command -v xz >/dev/null 2>&1 && return 1 - command -v apt-get >/dev/null 2>&1 || command -v apk >/dev/null 2>&1 - ;; - *) - return 1 - ;; - esac -} - -prepare_sudo_for_node_install() { - method="$1" - if ! node_install_needs_sudo "$method"; then - return 0 - fi - - prime_agent_screen "Preparing Node.js install" "" "This may ask for your sudo password." "" - prime_agent_restore_terminal - printf '\n' - sudo -v -} - -run_node_install_method() { - case "$1" in - homebrew) install_node_with_homebrew ;; - apt) install_node_with_apt ;; - apk) install_node_with_apk ;; - standalone) install_node_standalone ;; - esac -} - -install_node_with_homebrew() { - if brew list node >/dev/null 2>&1; then - brew upgrade node - else - brew install node - fi -} - -install_node_with_apt() { - print_sudo_note - if [ "${EUID:-$(id -u)}" -eq 0 ]; then - apt-get update - apt-get install -y nodejs npm - else - sudo sh -c 'apt-get update && apt-get install -y nodejs npm' - fi -} - -install_node_with_apk() { - print_sudo_note - run_with_sudo apk add --update-cache nodejs npm -} - -install_node_standalone() { - node_platform=$(detect_node_binary_platform) || { - printf 'Unsupported operating system for automatic Node.js install: %s\n' "$(uname -s)" - return 1 - } - node_arch=$(detect_node_binary_arch) || { - printf 'Unsupported CPU architecture for automatic Node.js install: %s\n' "$(uname -m)" - return 1 - } - node_dist_base="https://nodejs.org/dist/latest-v22.x" - node_base_dir=$(node_standalone_base_dir) - node_tmp_dir=$(create_temp_dir) - - mkdir -p "$node_tmp_dir" "$node_base_dir" - - printf 'Resolving Node.js binary for %s-%s\n' "$node_platform" "$node_arch" - curl -fsSL "$node_dist_base/SHASUMS256.txt" -o "$node_tmp_dir/SHASUMS256.txt" - node_file=$(awk -v suffix="-$node_platform-$node_arch.tar.xz" ' - index($2, "node-v") == 1 && length($2) >= length(suffix) && substr($2, length($2) - length(suffix) + 1) == suffix { print $2; exit } - ' "$node_tmp_dir/SHASUMS256.txt") - if [ -z "$node_file" ]; then - printf 'No Node.js binary is available for %s-%s.\n' "$node_platform" "$node_arch" - rm -rf "$node_tmp_dir" - return 1 - fi - case "$node_file" in - */*|*\\*|*..*) - printf 'Unsafe Node.js archive name in checksum manifest: %s\n' "$node_file" - rm -rf "$node_tmp_dir" - return 1 - ;; - node-v*-"$node_platform"-"$node_arch".tar.xz) ;; - *) - printf 'Unexpected Node.js archive name in checksum manifest: %s\n' "$node_file" - rm -rf "$node_tmp_dir" - return 1 - ;; - esac - - printf 'Downloading Node.js %s\n' "${node_file%.tar.xz}" - curl -fsSL "$node_dist_base/$node_file" -o "$node_tmp_dir/$node_file" - verify_node_standalone_download "$node_tmp_dir" "$node_file" - ensure_node_standalone_extract_tools "$node_platform" - - node_dir="$node_base_dir/${node_file%.tar.xz}" - rm -rf "$node_dir" - printf 'Extracting Node.js to %s\n' "$node_dir" - tar -xf "$node_tmp_dir/$node_file" -C "$node_base_dir" - rm -f "$node_base_dir/current" - ln -s "$node_dir" "$node_base_dir/current" - rm -rf "$node_tmp_dir" - printf 'Node.js installed at %s\n' "$node_dir" -} - -verify_node_standalone_download() { - checksum_dir="$1" - checksum_file_name="$2" - awk -v file="$checksum_file_name" '$2 == file { print }' "$checksum_dir/SHASUMS256.txt" >"$checksum_dir/SHASUMS256.selected" - - if command -v sha256sum >/dev/null 2>&1; then - printf 'Verifying Node.js download\n' - (cd "$checksum_dir" && sha256sum -c SHASUMS256.selected) - elif command -v shasum >/dev/null 2>&1; then - printf 'Verifying Node.js download\n' - (cd "$checksum_dir" && shasum -a 256 -c SHASUMS256.selected) - else - printf 'error: sha256sum or shasum is required to verify the Node.js download.\n' - return 1 - fi -} - -ensure_node_standalone_extract_tools() { - extract_platform="$1" - - if [ "$extract_platform" = linux ] && ! command -v xz >/dev/null 2>&1; then - printf 'Installing xz-utils for Node.js archive extraction\n' - print_sudo_note - if command -v apt-get >/dev/null 2>&1; then - run_with_sudo apt-get update - run_with_sudo apt-get install -y xz-utils - elif command -v apk >/dev/null 2>&1; then - run_with_sudo apk add --update-cache xz - else - printf 'xz is required to extract Node.js. Install xz and run this installer again.\n' - return 1 - fi - fi -} - -load_standalone_node() { - PRIME_AGENT_STANDALONE_NODE_BIN="$(node_standalone_base_dir)/current/bin" - PATH="$PRIME_AGENT_STANDALONE_NODE_BIN:$PATH" - export PRIME_AGENT_STANDALONE_NODE_BIN PATH -} - -node_standalone_base_dir() { - if [ -n "${XDG_DATA_HOME:-}" ]; then - printf '%s/prime-agent-node' "$XDG_DATA_HOME" - else - printf '%s/.local/share/prime-agent-node' "$HOME" - fi -} - -detect_node_binary_platform() { - case "$(uname -s)" in - Darwin) printf 'darwin' ;; - Linux) printf 'linux' ;; - *) return 1 ;; - esac -} - -detect_node_binary_arch() { - case "$(uname -m)" in - x86_64|amd64) printf 'x64' ;; - arm64|aarch64) printf 'arm64' ;; - armv7l) printf 'armv7l' ;; - ppc64le) printf 'ppc64le' ;; - s390x) printf 's390x' ;; - *) return 1 ;; - esac -} - -print_sudo_note() { - if [ "${EUID:-$(id -u)}" -ne 0 ]; then - printf 'This may ask for your sudo password.\n\n' - fi -} - -run_with_sudo() { - if [ "${EUID:-$(id -u)}" -eq 0 ]; then - "$@" - else - sudo "$@" - fi -} - -configure_standalone_node_path() { - if original_prime_agent_path=$(resolve_prime_agent_with_original_path); then - case "$original_prime_agent_path" in - "$PRIME_AGENT_STANDALONE_NODE_BIN/"*) - if [ "$prime_agent_screen_enabled" = 1 ]; then - prime_agent_screen "Prime Agent installed" "" "Run it with: $prime_agent_cmd" "" - else - printf '\nRun it with: %s\n' "$prime_agent_cmd" - fi - return 0 - ;; - esac - if [ "$prime_agent_screen_enabled" = 1 ]; then - prime_agent_screen "Prime Agent installed" "" "PATH update needed for $prime_agent_cmd." "" - else - printf '%s was installed, but your shell is not using that install yet.\n' "$prime_agent_cmd" - printf 'Your shell currently resolves %s to: %s\n' "$prime_agent_cmd" "$original_prime_agent_path" - fi - else - if [ "$prime_agent_screen_enabled" = 1 ]; then - prime_agent_screen "Prime Agent installed" "" "PATH update needed for $prime_agent_cmd." "" - else - printf '%s was installed, but your shell is not using that install yet.\n' "$prime_agent_cmd" - fi - fi - - profile=$(detect_shell_profile) || { - if [ "$prime_agent_screen_enabled" = 1 ]; then - prime_agent_restore_terminal - printf '\n' - fi - print_standalone_path_manual_instructions - return 0 - } - - if shell_profile_has_standalone_node_path "$profile"; then - if [ "$prime_agent_screen_enabled" = 1 ]; then - prime_agent_screen "Prime Agent installed" "" "Run: $(prime_agent_source_profile_command "$profile")" "" - else - printf '%s already contains %s.\n' "$profile" "$PRIME_AGENT_STANDALONE_NODE_BIN" - printf 'Restart your shell or run: %s\n' "$(prime_agent_source_profile_command "$profile")" - fi - return 0 - fi - - prompt_add_standalone_node_path "$profile" -} - -resolve_prime_agent_with_original_path() { - saved_path=$PATH - PATH=$prime_agent_original_path - if command -v "$prime_agent_cmd" 2>/dev/null; then - status=0 - else - status=$? - fi - PATH=$saved_path - return "$status" -} - detect_shell_profile() { if [ -n "${PRIME_AGENT_SHELL_PROFILE:-}" ]; then printf '%s' "$PRIME_AGENT_SHELL_PROFILE" @@ -1384,119 +830,6 @@ detect_shell_profile() { esac } -shell_profile_has_standalone_node_path() { - profile="$1" - [ -f "$profile" ] && grep -F "$PRIME_AGENT_STANDALONE_NODE_BIN" "$profile" >/dev/null 2>&1 -} - -prompt_add_standalone_node_path() { - profile="$1" - path_line=$(standalone_node_path_line) - - if ! prime_agent_prompt_yes_no \ - "Add standalone Node.js to your PATH?" \ - "Updates $profile so future shells can run $prime_agent_cmd." \ - "Update PATH? [Y/n]"; then - if [ "$prime_agent_screen_enabled" = 1 ]; then - prime_agent_restore_terminal - printf '\n' - fi - print_standalone_path_manual_instructions - return 0 - fi - - mkdir -p "$(dirname "$profile")" - { - printf '\n# Prime Agent standalone Node.js\n' - printf '%s\n' "$path_line" - } >>"$profile" - if [ "$prime_agent_screen_enabled" = 1 ]; then - prime_agent_screen "Prime Agent installed" "" "Run: $(prime_agent_source_profile_command "$profile")" "" - else - printf 'Added %s to %s.\n' "$PRIME_AGENT_STANDALONE_NODE_BIN" "$profile" - printf 'Restart your shell or run: %s\n' "$(prime_agent_source_profile_command "$profile")" - fi -} - -print_standalone_path_manual_instructions() { - printf 'Add this to your shell profile to use %s from new shells:\n\n' "$prime_agent_cmd" - printf ' %s\n' "$(standalone_node_path_line)" - printf '\nThen restart your shell and run: %s\n' "$prime_agent_cmd" -} - -standalone_node_path_line() { - printf 'export PATH="%s:$PATH"' "$PRIME_AGENT_STANDALONE_NODE_BIN" -} - -prime_agent_shell_quote() { - quoted=$(printf '%s' "$1" | sed "s/'/'\\\\''/g") - printf "'%s'" "$quoted" -} - -prime_agent_source_profile_command() { - printf '. %s && %s' "$(prime_agent_shell_quote "$1")" "$prime_agent_cmd" -} - -download_prime_agent_package() { - version="$1" - tarball_url="$2" - tarball_path="$3" - download_dir=$(dirname "$tarball_path") - tarball_name=$(basename "$tarball_path") - checksums_url="$prime_agent_base_url/releases/v$version/SHA256SUMS" - checksums_path="$download_dir/SHA256SUMS" - - if ! command -v curl >/dev/null 2>&1; then - printf 'error: curl is required to download Prime Agent.\n' >&2 - exit 1 - fi - - prime_agent_run_quiet_with_animation \ - "Downloading checksums" \ - "Downloading release checksums" \ - "Prime Agent v$version" \ - curl -fsSL "$checksums_url" -o "$checksums_path" - - prime_agent_run_quiet_with_animation \ - "Downloading Prime Agent" \ - "Downloading Prime Agent v$version" \ - "Fetching the verified package." \ - curl -fsSL "$tarball_url" -o "$tarball_path" - - verify_prime_agent_package_checksum "$checksums_path" "$tarball_path" -} - -verify_prime_agent_package_checksum() { - checksums_path="$1" - tarball_path="$2" - checksum_dir=$(dirname "$tarball_path") - tarball_name=$(basename "$tarball_path") - selected_checksums_path="$checksum_dir/SHA256SUMS.selected" - - if ! awk -v file="$tarball_name" '$2 == file { print; found = 1; exit } END { if (!found) exit 1 }' \ - "$checksums_path" >"$selected_checksums_path"; then - printf 'error: checksum for %s was not found in %s\n' "$tarball_name" "$checksums_path" >&2 - exit 1 - fi - - if command -v sha256sum >/dev/null 2>&1; then - prime_agent_run_quiet_with_animation \ - "Verifying download" \ - "Verifying Prime Agent download" \ - "Checking SHA-256." \ - prime_agent_run_checksum_check "$checksum_dir" "$(basename "$selected_checksums_path")" sha256sum - elif command -v shasum >/dev/null 2>&1; then - prime_agent_run_quiet_with_animation \ - "Verifying download" \ - "Verifying Prime Agent download" \ - "Checking SHA-256." \ - prime_agent_run_checksum_check "$checksum_dir" "$(basename "$selected_checksums_path")" shasum - else - printf 'error: sha256sum or shasum is required to verify the Prime Agent download.\n' >&2 - exit 1 - fi -} - prime_agent_run_checksum_check() { checksum_dir="$1" selected_checksums_name="$2" @@ -1511,111 +844,6 @@ prime_agent_run_checksum_check() { esac } -confirm_install() { - version="$1" - tarball_url="$2" - - if prime_agent_prompt_yes_no \ - "Install Prime Agent v$version globally with npm?" \ - "Downloads the verified release and runs npm install -g." \ - "Install? [Y/n]"; then - return 0 - else - prompt_status=$? - fi - - if [ "$prompt_status" -eq 2 ]; then - printf 'This will download, verify, and install:\n\n %s\n\n' "$tarball_url" - printf 'No terminal detected; continuing without confirmation.\n' - return 0 - fi - - if [ "$prime_agent_screen_enabled" = 1 ]; then - prime_agent_screen "Installation cancelled" "" "No changes were made." "" - exit 0 - fi - printf '\nInstallation cancelled.\n' - exit 0 -} - -confirm_kernel_runtime_setup() { - case "${PRIME_AGENT_BOOTSTRAP_KERNEL_ON_INSTALL:-}" in - 1) - prime_agent_bootstrap_kernel_on_install=1 - return - ;; - 0) - prime_agent_bootstrap_kernel_on_install=0 - return - ;; - esac - - if prime_agent_prompt_yes_no \ - "Prepare Python runtime now?" \ - "Installs uv, Python 3.11, and the Prime Agent runtime." \ - "Prepare? [Y/n]"; then - prime_agent_bootstrap_kernel_on_install=1 - return - else - prompt_status=$? - fi - - if [ "$prompt_status" -eq 2 ]; then - printf 'No terminal detected; preparing the Python runtime during install.\n' - prime_agent_bootstrap_kernel_on_install=1 - return - fi - - prime_agent_bootstrap_kernel_on_install=0 - if [ "$prime_agent_screen_enabled" = 1 ]; then - prime_agent_screen "Python setup skipped" "" "The runtime can be prepared on first ipython use." "" - sleep 0.4 - else - printf '\nSkipping Python runtime setup.\n' - fi -} - -install_prime_agent_package() { - tarball_path="$1" - if [ "$prime_agent_bootstrap_kernel_on_install" = 1 ]; then - npm_install_details="Preparing global install. -Linking command binaries. -Installing runtime packages. -Preloading search tools. -Preparing Python kernel. -Finalizing npm install." - prime_agent_run_quiet_with_animation_steps \ - "Installing Prime Agent" \ - "Installing Prime Agent" \ - "$npm_install_details" \ - env PRIME_AGENT_BOOTSTRAP_TOOLS_ON_INSTALL=1 PRIME_AGENT_BOOTSTRAP_KERNEL_ON_INSTALL=1 PRIME_AGENT_INSTALL_UV=1 npm install -g --no-fund --no-audit --loglevel=error --progress=false "$tarball_path" - else - npm_install_details="Preparing global install. -Linking command binaries. -Installing runtime packages. -Preloading search tools. -Finalizing npm install." - prime_agent_run_quiet_with_animation_steps \ - "Installing Prime Agent" \ - "Installing Prime Agent" \ - "$npm_install_details" \ - env PRIME_AGENT_BOOTSTRAP_TOOLS_ON_INSTALL=1 npm install -g --no-fund --no-audit --loglevel=error --progress=false "$tarball_path" - fi -} - - - -# ============================================================================= -# Binary Install Path (Bun-compiled artifact, no Node/npm needed) -# ============================================================================= - -# Architecture: install each release into a versioned directory -# ${XDG_DATA_HOME:-$HOME/.local/share}/prime-agent/versions/v/ -# and point a stable symlink at it from ~/.local/bin/prime-agent. -# On update: download to a fresh version dir, smoke-test the binary there, -# then atomically switch the symlink. Rollback by restoring the old symlink. -# Never copy runtime sidecars into user config dirs (~/.prime/*). - prime_agent_detect_binary_platform() { _os=$(uname -s) _arch=$(uname -m) @@ -1629,7 +857,7 @@ prime_agent_detect_binary_platform() { ;; Linux) if [ -f /etc/alpine-release ] || { command -v ldd >/dev/null 2>&1 && ldd --version 2>&1 | grep -qi musl; }; then - printf 'error: Prime Agent binaries require glibc Linux; use --method=npm on musl systems.\n' >&2 + printf 'error: Prime Agent compiled binaries require glibc Linux and do not support musl systems.\n' >&2 return 1 fi case "$_arch" in @@ -1662,6 +890,31 @@ prime_agent_binary_smoke_binary() { fi } +prime_agent_binary_validate_layout() { + _root="$1" + _binary="$2" + if ! prime_agent_binary_smoke_binary "$_binary"; then + printf 'error: the downloaded Prime Agent binary did not run correctly.\n' >&2 + return 1 + fi + for _relative_path in \ + package.json README.md CHANGELOG.md install.sh photon_rs_bg.wasm \ + prime-agent-runtime/pyproject.toml \ + theme/prime.json theme/dark.json theme/light.json theme/theme-schema.json \ + export-html/template.html export-html/template.css export-html/template.js; do + if [ ! -f "$_root/$_relative_path" ]; then + printf 'error: release archive is missing required sidecar: %s\n' "$_relative_path" >&2 + return 1 + fi + done + for _relative_dir in skills assets docs examples export-html/vendor; do + if [ ! -d "$_root/$_relative_dir" ]; then + printf 'error: release archive is missing required sidecar directory: %s\n' "$_relative_dir" >&2 + return 1 + fi + done +} + prime_agent_binary_atomic_symlink() { _target="$1" _link="$2" @@ -1702,7 +955,7 @@ prime_agent_binary_fresh_install() { if [ ! -x "$_existing_binary" ] && [ -x "$_version_dir/pi" ]; then _existing_binary="$_version_dir/pi" fi - if [ -x "$_existing_binary" ] && prime_agent_binary_smoke_binary "$_existing_binary"; then + if [ -x "$_existing_binary" ] && prime_agent_binary_validate_layout "$_version_dir" "$_existing_binary"; then prime_agent_binary_atomic_symlink "$_existing_binary" "$prime_agent_binary_symlink" prime_agent_configure_binary_path "$_version" return @@ -1754,13 +1007,6 @@ prime_agent_binary_fresh_install() { chmod +x "$_version_dir/install.sh" 2>/dev/null || true fi - if ! "$_binary_path" --version >/dev/null 2>&1; then - printf 'error: the downloaded binary did not run correctly.\n' >&2 - rm -rf "$_version_dir" "$_download_dir" - prime_agent_download_dir= - exit 1 - fi - # If the binary was inside a wrapper dir, move contents up to version_dir _binary_dir=$(dirname "$_binary_path") if [ "$_binary_dir" != "$_version_dir" ]; then @@ -1768,6 +1014,11 @@ prime_agent_binary_fresh_install() { rm -rf "$_binary_dir" _binary_path="$_version_dir/$(basename "$_binary_path")" fi + if ! prime_agent_binary_validate_layout "$_version_dir" "$_binary_path"; then + rm -rf "$_version_dir" "$_download_dir" + prime_agent_download_dir= + exit 1 + fi # Create the stable symlink mkdir -p "$(dirname "$prime_agent_binary_symlink")" @@ -1869,19 +1120,6 @@ prime_agent_binary_update() { chmod +x "$_version_dir/install.sh" 2>/dev/null || true fi - # Smoke-test the new binary BEFORE switching the symlink - if ! "$_binary_path" --version >/dev/null 2>&1; then - # New binary is bad: remove it, keep old symlink - rm -rf "$_version_dir" "$_download_dir" - prime_agent_download_dir= - if [ "$prime_agent_screen_enabled" = 1 ]; then - prime_agent_screen "Update failed" "" "New binary did not run correctly." "" - else - printf '\nUpdate failed: the new binary did not run correctly.\n' - fi - exit 1 - fi - # If the binary was inside a wrapper dir, move contents up _binary_dir=$(dirname "$_binary_path") if [ "$_binary_dir" != "$_version_dir" ]; then @@ -1889,6 +1127,11 @@ prime_agent_binary_update() { rm -rf "$_binary_dir" _binary_path="$_version_dir/$(basename "$_binary_path")" fi + if ! prime_agent_binary_validate_layout "$_version_dir" "$_binary_path"; then + rm -rf "$_version_dir" "$_download_dir" + prime_agent_download_dir= + exit 1 + fi # Remember old version for rollback if [ -L "$prime_agent_binary_symlink" ]; then @@ -2008,74 +1251,5 @@ prime_agent_configure_binary_path() { # NPM Install Path (extracted from main) # ============================================================================= -prime_agent_npm_install() { - start_preflight_checks - - if finish_preflight_checks; then - check_status=0 - else - check_status=$? - fi - - if [ "$check_status" -ne 0 ]; then - if ! install_node_npm_interactive; then - exit "$check_status" - fi - - start_preflight_checks - if finish_preflight_checks; then - check_status=0 - else - check_status=$? - fi - - if [ "$check_status" -ne 0 ]; then - exit "$check_status" - fi - fi - - version="$(resolve_prime_agent_version "$@")" - tarball_name="$prime_agent_package-$version.tgz" - tarball_url="$prime_agent_base_url/releases/v$version/$tarball_name" - - confirm_install "$version" "$tarball_url" - confirm_kernel_runtime_setup - - download_dir=$(create_temp_dir) - prime_agent_download_dir="$download_dir" - tarball_path="$download_dir/$tarball_name" - - download_prime_agent_package "$version" "$tarball_url" "$tarball_path" - install_prime_agent_package "$tarball_path" - rm -rf "$download_dir" - prime_agent_download_dir= - - if [ "${PRIME_AGENT_NODE_INSTALLED_STANDALONE:-0}" = 1 ]; then - prime_agent_screen "Prime Agent installed" "" "Checking your shell PATH." "" - configure_standalone_node_path - elif command -v "$prime_agent_cmd" >/dev/null 2>&1; then - if [ "$prime_agent_screen_enabled" = 1 ]; then - prime_agent_screen "Prime Agent installed" "" "Run it with: $prime_agent_cmd" "" - else - printf '\nPrime Agent was installed successfully.\n' - printf '\nRun it with: %s\n' "$prime_agent_cmd" - fi - else - if [ "$prime_agent_screen_enabled" = 1 ]; then - prime_agent_screen "Prime Agent installed" "" "PATH update needed for $prime_agent_cmd." "" - prime_agent_restore_terminal - else - printf '\nPrime Agent was installed successfully.\n' - fi - cat < -Prime Agent began as a hard fork of [pi-mono](https://github.com/badlogic/pi-mono), but it is now developed and distributed independently. This workspace retains inherited `@earendil-works/pi-*` source package identifiers, the `pi` package manifest key, and a source-package `pi` bin entry for internal compatibility. Public releases are currently versioned tarball artifacts installed by the scripts below; release packaging rewrites the application package and command to `prime-agent`. Do not use the inherited npm package as the Prime Agent install path. +Prime Agent began as a hard fork of [pi-mono](https://github.com/badlogic/pi-mono), but it is now developed and distributed independently. This workspace retains inherited `@earendil-works/pi-*` source package identifiers, the `pi` package manifest key, and a source-package `pi` bin entry for internal compatibility. Public releases are compiled Bun binary archives installed by the scripts below; release packaging ships the application and command as `prime-agent`. Do not use the inherited npm package as the Prime Agent install path. ## Table of Contents diff --git a/packages/coding-agent/docs/development.md b/packages/coding-agent/docs/development.md index e3da5b8af9c..8a702daea8b 100644 --- a/packages/coding-agent/docs/development.md +++ b/packages/coding-agent/docs/development.md @@ -24,7 +24,7 @@ The script can be called from any directory and preserves the caller's working d Prime Agent is the product, public CLI, release artifact, and repository name. The monorepo still retains inherited `@earendil-works/pi-*` npm workspace names, a source-package `pi` bin entry, the `pi` package manifest key, and some `PI_*` compatibility environment variables. These names are source and compatibility details, not a signal that contributors should install or develop against pi-mono. -Public releases are currently versioned tarball artifacts installed by the stable and beta installer scripts. `scripts/pack-prime-agent-release.mjs` rewrites the coding-agent package name, executable, config metadata, and internal dependency URLs for that distribution. Do not document the inherited npm workspace package as the public Prime Agent install path. +Public releases are compiled Bun binary archives installed by the stable and beta installer scripts. `scripts/pack-prime-agent-release.mjs` assembles the platform archives and metadata. Do not document a package-manager install path for the public Prime Agent CLI. ## Local Configuration diff --git a/packages/coding-agent/test/bun-installer-e2e.test.ts b/packages/coding-agent/test/bun-installer-e2e.test.ts index 772b782052d..4f75734fe71 100644 --- a/packages/coding-agent/test/bun-installer-e2e.test.ts +++ b/packages/coding-agent/test/bun-installer-e2e.test.ts @@ -13,7 +13,13 @@ afterEach(() => { for (const root of temporaryRoots.splice(0)) rmSync(root, { recursive: true, force: true }); }); -function makeRelease(root: string, version: string, executable: string, checksumIsValid = true): void { +function makeRelease( + root: string, + version: string, + executable: string, + checksumIsValid = true, + omitSidecar?: string, +): void { const platform = `${process.platform === "darwin" ? "darwin" : "linux"}-${process.arch === "arm64" ? "arm64" : "x64"}`; const releaseDir = join(root, "server", "releases", `v${version}`); const stage = join(root, `stage-${version}`); @@ -21,8 +27,34 @@ function makeRelease(root: string, version: string, executable: string, checksum mkdirSync(stage, { recursive: true }); writeFileSync(join(stage, "prime-agent"), executable); chmodSync(join(stage, "prime-agent"), 0o755); - writeFileSync(join(stage, "package.json"), JSON.stringify({ name: "prime-agent", version })); - writeFileSync(join(stage, "install.sh"), "#!/bin/sh\n"); + const requiredFiles = [ + "package.json", + "README.md", + "CHANGELOG.md", + "install.sh", + "photon_rs_bg.wasm", + "prime-agent-runtime/pyproject.toml", + "theme/prime.json", + "theme/dark.json", + "theme/light.json", + "theme/theme-schema.json", + "export-html/template.html", + "export-html/template.css", + "export-html/template.js", + ]; + for (const relative of requiredFiles) { + mkdirSync(dirname(join(stage, relative)), { recursive: true }); + writeFileSync( + join(stage, relative), + relative === "package.json" ? JSON.stringify({ name: "prime-agent", version }) : "fixture", + ); + } + for (const relative of ["skills", "assets", "docs", "examples", "export-html/vendor"]) { + mkdirSync(join(stage, relative), { recursive: true }); + writeFileSync(join(stage, relative, ".keep"), "fixture"); + } + if (omitSidecar) rmSync(join(stage, omitSidecar), { recursive: true, force: true }); + chmodSync(join(stage, "install.sh"), 0o755); const archiveName = `prime-agent-${version}-${platform}.tar.gz`; const archive = join(releaseDir, archiveName); const packed = spawnSync("tar", ["-czf", archive, "-C", stage, "."], { encoding: "utf8" }); @@ -108,6 +140,21 @@ describe("compiled binary installer", () => { expect(readFileSync(join(root, "home", ".prime", "sentinel"), "utf8")).toBe("user data"); }); + test("keeps the previous version when an update is missing a required sidecar", () => { + const root = mkdtempSync(join(tmpdir(), "prime-agent-installer-")); + temporaryRoots.push(root); + makeRelease(root, "1.2.3", goodExecutable("1.2.3")); + expect(runInstaller(root, ["1.2.3"]).exitCode).toBe(0); + const link = join(root, "bin", "prime-agent"); + const oldTarget = readlinkSync(link); + + makeRelease(root, "2.0.0", goodExecutable("2.0.0"), true, "theme/prime.json"); + const result = runInstaller(root, ["--update", "2.0.0"]); + expect(result.exitCode).not.toBe(0); + expect(result.stderr).toContain("missing required sidecar: theme/prime.json"); + expect(readlinkSync(link)).toBe(oldTarget); + }); + test("rolls back when the activated symlink fails its smoke test", () => { const root = mkdtempSync(join(tmpdir(), "prime-agent-installer-")); temporaryRoots.push(root); diff --git a/packages/coding-agent/test/bun-installer.test.ts b/packages/coding-agent/test/bun-installer.test.ts index 12ba085d729..72f2294db0b 100644 --- a/packages/coding-agent/test/bun-installer.test.ts +++ b/packages/coding-agent/test/bun-installer.test.ts @@ -40,27 +40,17 @@ describe("install.sh shell syntax", () => { expect(() => execFileSync("sh", ["-n", installer], { stdio: "pipe" })).not.toThrow(); }); - it("contains the npm install function", () => { - expect(installerText).toContain("prime_agent_npm_install"); - }); - - it("supports --method and --update flags", () => { - expect(installerText).toContain("--method=*"); - expect(installerText).toContain("binary|npm|auto"); + it("supports compiled binary installs and updates only", () => { + expect(installerText).toContain("prime_agent_binary_fresh_install"); + expect(installerText).toContain("prime_agent_binary_update"); expect(installerText).toContain("--update"); + expect(installerText).not.toContain("prime_agent_npm_install"); + expect(installerText).not.toContain("install_node_npm"); + expect(installerText).not.toContain("npm install"); }); -}); -// ============================================================================ -describe("default install method (auto) routes to binary", () => { - it("routes auto and binary to binary_fresh_install, never npm", () => { - expect(installerText).toMatch(/\$prime_agent_install_method" != npm/); - }); - - it("uses npm only with explicit --method=npm", () => { - const matches = installerText.match(/\$prime_agent_install_method"\s*!=\s*npm/g); - expect(matches).not.toBeNull(); - expect(matches!.length).toBeGreaterThanOrEqual(1); + it("rejects removed package-manager method flags", () => { + expect(installerText).toContain("--method is no longer supported"); }); }); diff --git a/packages/coding-agent/test/bun-release-archives.test.ts b/packages/coding-agent/test/bun-release-archives.test.ts index 66e0c68854a..836c2bc37ba 100644 --- a/packages/coding-agent/test/bun-release-archives.test.ts +++ b/packages/coding-agent/test/bun-release-archives.test.ts @@ -90,24 +90,7 @@ describe("compiled release archives", () => { const archives = readdirSync(artifacts).filter((name) => name.endsWith(".tar.gz")); expect(archives.sort()).toEqual(platforms.map((platform) => `prime-agent-1.2.3-${platform}.tar.gz`).sort()); expect(readFileSync(join(artifacts, "stable"), "utf8")).toBe("v1.2.3\n"); - expect(readFileSync(join(artifacts, "SHA256SUMS"), "utf8").trim().split("\n")).toHaveLength(8); - for (const tarball of [ - "prime-agent-1.2.3.tgz", - "prime-agent-ai-1.2.3.tgz", - "prime-agent-core-1.2.3.tgz", - "prime-agent-tui-1.2.3.tgz", - ]) { - expect(existsSync(join(artifacts, tarball)), tarball).toBe(true); - } - const npmExtracted = join(f.root, "npm-extracted"); - mkdirSync(npmExtracted); - expect(spawnSync("tar", ["-xzf", join(artifacts, "prime-agent-1.2.3.tgz"), "-C", npmExtracted]).status).toBe(0); - expect(JSON.parse(readFileSync(join(npmExtracted, "package", "package.json"), "utf8"))).toMatchObject({ - name: "prime-agent", - version: "1.2.3", - bin: { "prime-agent": "dist/bundle/cli.js" }, - scripts: { postinstall: "node postinstall.cjs" }, - }); + expect(readFileSync(join(artifacts, "SHA256SUMS"), "utf8").trim().split("\n")).toHaveLength(4); const extracted = join(f.root, "extracted"); mkdirSync(extracted); @@ -165,28 +148,22 @@ describe("compiled release archives", () => { expect(result.status).not.toBe(0); expect(result.stderr).toContain("forbidden dependency or cache directory"); }); - test("normalizes v-prefixed versions for npm artifact URLs", () => { + test("normalizes v-prefixed versions for binary archive metadata", () => { const f = fixture(); const result = pack(f, ["--version", "v1.2.3"]); expect(result.status, result.stderr).toBe(0); const manifest = JSON.parse(readFileSync(join(f.output, "artifacts", "latest.json"), "utf8")); - expect(manifest.tarball).toBe("releases/v1.2.3/prime-agent-1.2.3.tgz"); - expect(existsSync(join(f.output, "artifacts", "prime-agent-1.2.3.tgz"))).toBe(true); + expect(manifest.version).toBe("v1.2.3"); + expect(manifest.baseUrl).toBe("https://downloads.example.test/releases/v1.2.3"); + expect(existsSync(join(f.output, "artifacts", "prime-agent-1.2.3-darwin-arm64.tar.gz"))).toBe(true); }); - test("trims release base URLs before writing npm dependency specs", () => { + test("trims release base URLs before writing binary metadata", () => { const f = fixture(); const result = pack(f, ["--base-url", " https://downloads.example.test/ "]); expect(result.status, result.stderr).toBe(0); - const packedManifest = spawnSync( - "tar", - ["-xOzf", join(f.output, "artifacts", "prime-agent-1.2.3.tgz"), "package/package.json"], - { encoding: "utf8" }, - ); - expect(packedManifest.status, packedManifest.stderr).toBe(0); - expect(JSON.parse(packedManifest.stdout).dependencies["@earendil-works/pi-ai"]).toBe( - "https://downloads.example.test/releases/v1.2.3/prime-agent-ai-1.2.3.tgz", - ); + const manifest = JSON.parse(readFileSync(join(f.output, "artifacts", "latest.json"), "utf8")); + expect(manifest.baseUrl).toBe("https://downloads.example.test/releases/v1.2.3"); }); test("rejects insecure release base URLs", () => { diff --git a/scripts/build-binaries.sh b/scripts/build-binaries.sh index b8951a7e8d8..18a6915e990 100755 --- a/scripts/build-binaries.sh +++ b/scripts/build-binaries.sh @@ -17,7 +17,6 @@ # # packages/coding-agent/release/artifacts/ # prime-agent--.tar.gz -# prime-agent-.tgz (plus internal workspace tarballs) # SHA256SUMS # stable # latest.json @@ -118,7 +117,7 @@ bun ../../scripts/pack-prime-agent-release.mjs \ echo "" echo "==> Build complete!" echo "Release archives:" -ls -lh "release/artifacts/"*.tar.gz "release/artifacts/"*.tgz 2>/dev/null || true +ls -lh "release/artifacts/"*.tar.gz echo "" echo "Metadata:" ls -lh "release/artifacts/SHA256SUMS" "release/artifacts/latest.json" "release/artifacts/stable" 2>/dev/null || true diff --git a/scripts/check-installer-render.mjs b/scripts/check-installer-render.mjs index 835f306bbca..c4b5878080a 100644 --- a/scripts/check-installer-render.mjs +++ b/scripts/check-installer-render.mjs @@ -93,9 +93,9 @@ screen_case() { } progress_case() { - progress_details="Preparing global install. -Linking command binaries. -Finalizing npm install." + progress_details="Downloading verified archive. +Extracting compiled binary. +Validating runtime sidecars." for progress_frame in 1 24 25 48 49 200; do prime_agent_animation_frame="$progress_frame" printf '__PROGRESS__ %s\t%s\t%s\\n' "$progress_frame" "$(prime_agent_animation_status "Installing Prime Agent" "$progress_details" static)" "$(prime_agent_animation_detail "$progress_details")" @@ -241,12 +241,12 @@ function assertInstallerProgress(progress) { if (progress.length !== 6) return; const expectedDetails = [ - "Preparing global install.", - "Preparing global install.", - "Linking command binaries.", - "Linking command binaries.", - "Finalizing npm install.", - "Finalizing npm install.", + "Downloading verified archive.", + "Downloading verified archive.", + "Extracting compiled binary.", + "Extracting compiled binary.", + "Validating runtime sidecars.", + "Validating runtime sidecars.", ]; for (const [index, expectedDetail] of expectedDetails.entries()) { check( diff --git a/scripts/pack-prime-agent-release.mjs b/scripts/pack-prime-agent-release.mjs index 1f101d683d4..184ac02ab2b 100755 --- a/scripts/pack-prime-agent-release.mjs +++ b/scripts/pack-prime-agent-release.mjs @@ -1,14 +1,12 @@ #!/usr/bin/env bun /** - * Produce Bun-compiled platform archives and npm-compatible tarballs. + * Produce Bun-compiled platform archives. * * This script is the CI entry point for the release artifact lane. It expects * pre-built Bun-compiled binaries for 4 platforms (darwin-arm64, darwin-x64, * linux-arm64, linux-x64) and sidecar files, then produces platform-specific * .tar.gz archives with checksums and channel metadata. * - * Bun creates both native archives and npm-compatible package tarballs. - * * Usage: * bun scripts/pack-prime-agent-release.mjs \ * --base-url \ @@ -21,28 +19,18 @@ * Output: * /artifacts/ * prime-agent--.tar.gz - * prime-agent-.tgz (plus internal workspace tarballs) * SHA256SUMS * * latest.json|beta.json */ -import { createHash } from "node:crypto"; -import { cpSync, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs"; +import { existsSync, readFileSync } from "node:fs"; import { resolve, join } from "node:path"; const root = resolve(import.meta.dirname, ".."); const defaultOutputDir = join(root, "packages", "coding-agent", "release"); const defaultBaseUrl = process.env.PRIME_AGENT_DOWNLOAD_BASE_URL; const releaseChannels = new Set(["stable", "beta"]); -const publicPackageName = "prime-agent"; -const publicCommandName = "prime-agent"; -const releasePackages = [ - { packageDir: "ai", publicName: "prime-agent-ai" }, - { packageDir: "tui", publicName: "prime-agent-tui" }, - { packageDir: "agent", publicName: "prime-agent-core" }, - { packageDir: "coding-agent", publicName: publicPackageName }, -]; function parseArgs(args) { const parsed = { @@ -143,113 +131,6 @@ Creates platform release archives: `); } -function sha256File(path) { - const hash = createHash("sha256"); - hash.update(readFileSync(path)); - return hash.digest("hex"); -} - -function rewriteInternalDependencies(dependencies, internalPackageUrls) { - if (!dependencies) return undefined; - return Object.fromEntries( - Object.entries(dependencies).map(([name, range]) => [name, internalPackageUrls.get(name) || range]), - ); -} - -function createReleasePackageJson(sourcePackage, packageName, version, internalPackageUrls) { - const manifest = { - ...sourcePackage, - name: packageName, - version, - dependencies: rewriteInternalDependencies(sourcePackage.dependencies, internalPackageUrls), - optionalDependencies: rewriteInternalDependencies(sourcePackage.optionalDependencies, internalPackageUrls), - scripts: sourcePackage.scripts?.postinstall ? { postinstall: "node postinstall.cjs" } : undefined, - }; - delete manifest.devDependencies; - delete manifest.overrides; - delete manifest.private; - if (packageName === publicPackageName) { - manifest.bin = { [publicCommandName]: "dist/bundle/cli.js" }; - manifest.piConfig = { ...(manifest.piConfig || {}), name: publicCommandName, configDir: ".prime/agent" }; - } - return manifest; -} - -function copyPackageContents(sourceDir, targetDir, packageJson) { - mkdirSync(targetDir, { recursive: true }); - writeFileSync(join(targetDir, "package.json"), `${JSON.stringify(packageJson, null, 2)}\n`); - for (const entry of ["dist", "docs", "examples", "skills", "postinstall.cjs", "README.md", "CHANGELOG.md"]) { - const source = join(sourceDir, entry); - if (existsSync(source)) cpSync(source, join(targetDir, entry), { recursive: true }); - } -} - -function packNpmCompatibilityTarballs(args) { - const artifactsDir = join(args.outDir, "artifacts"); - const stagingRoot = join(args.outDir, "npm-packages"); - const packageSources = new Map(); - for (const releasePackage of releasePackages) { - const sourceDir = join(root, "packages", releasePackage.packageDir); - if (!existsSync(join(sourceDir, "dist"))) { - throw new Error(`Missing built package: ${sourceDir}/dist`); - } - packageSources.set(releasePackage.packageDir, JSON.parse(readFileSync(join(sourceDir, "package.json"), "utf8"))); - } - - const artifactFiles = new Map( - releasePackages.map(({ packageDir, publicName }) => [packageDir, `${publicName}-${args.version}.tgz`]), - ); - const internalPackageUrls = new Map(); - for (const { packageDir, publicName } of releasePackages) { - if (packageDir === "coding-agent") continue; - const sourceName = packageSources.get(packageDir).name; - internalPackageUrls.set(sourceName, `${args.baseUrl}/releases/v${args.version}/${artifactFiles.get(packageDir)}`); - } - - const tarballs = []; - try { - rmSync(stagingRoot, { recursive: true, force: true }); - for (const { packageDir, publicName } of releasePackages) { - const stagingDir = join(stagingRoot, packageDir); - copyPackageContents( - join(root, "packages", packageDir), - stagingDir, - createReleasePackageJson(packageSources.get(packageDir), publicName, args.version, internalPackageUrls), - ); - const result = Bun.spawnSync([process.execPath, "pm", "pack", "--destination", artifactsDir], { - cwd: stagingDir, - stdout: "pipe", - stderr: "pipe", - }); - if (result.exitCode !== 0) { - throw new Error(`bun pm pack failed for ${packageDir}: ${result.stderr.toString()}`); - } - const file = artifactFiles.get(packageDir); - const path = join(artifactsDir, file); - if (!existsSync(path)) throw new Error(`bun pm pack did not create ${path}`); - tarballs.push({ package: publicName, file, sha256: sha256File(path) }); - } - } finally { - rmSync(stagingRoot, { recursive: true, force: true }); - } - - const checksumPath = join(artifactsDir, "SHA256SUMS"); - const checksums = readFileSync(checksumPath, "utf8") - .trim() - .split("\n") - .filter(Boolean); - checksums.push(...tarballs.map(({ file, sha256 }) => `${sha256} ${file}`)); - checksums.sort((left, right) => left.split(/\s+/).at(-1).localeCompare(right.split(/\s+/).at(-1))); - writeFileSync(checksumPath, `${checksums.join("\n")}\n`); - - const manifestName = args.channel === "stable" ? "latest.json" : "beta.json"; - const manifestPath = join(artifactsDir, manifestName); - const manifest = JSON.parse(readFileSync(manifestPath, "utf8")); - manifest.tarball = `releases/v${args.version}/${artifactFiles.get("coding-agent")}`; - manifest.tarballs = tarballs; - writeFileSync(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`); -} - async function main() { const args = parseArgs(process.argv.slice(2)); @@ -281,7 +162,6 @@ async function main() { if (result.status !== 0) { throw new Error(`assemble-release-archives.mjs failed with exit code ${result.status}`); } - packNpmCompatibilityTarballs(args); } try { diff --git a/scripts/release.mjs b/scripts/release.mjs index 65adaea8039..bee63bcf430 100755 --- a/scripts/release.mjs +++ b/scripts/release.mjs @@ -13,7 +13,7 @@ * 3. Update CHANGELOG.md files: aggregate .changes/*.md fragments into a * [version] - date section, git rm the consumed fragments * 4. Commit and tag - * 5. Publish the npm compatibility packages with Bun + * 5. Publish registry packages with Bun */ import { execSync } from "child_process"; @@ -212,7 +212,7 @@ run(`git commit -m "Release v${version}"`); run(`git tag v${version}`); console.log(); -console.log("Publishing npm compatibility packages..."); +console.log("Publishing registry packages with Bun..."); run("bun run publish"); console.log(); From 7deaf892816d7e41514ca421c755d03abe07b860 Mon Sep 17 00:00:00 2001 From: Seth Date: Tue, 1 Sep 2026 18:59:54 -0400 Subject: [PATCH 19/45] fix: reject ambiguous release URLs --- .../coding-agent/test/bun-installer-e2e.test.ts | 15 ++++++++++++--- .../test/bun-release-archives.test.ts | 9 +++++++++ scripts/assemble-release-archives.mjs | 4 +++- 3 files changed, 24 insertions(+), 4 deletions(-) diff --git a/packages/coding-agent/test/bun-installer-e2e.test.ts b/packages/coding-agent/test/bun-installer-e2e.test.ts index 4f75734fe71..60327ffc2ef 100644 --- a/packages/coding-agent/test/bun-installer-e2e.test.ts +++ b/packages/coding-agent/test/bun-installer-e2e.test.ts @@ -1,7 +1,16 @@ import { afterEach, describe, expect, test } from "bun:test"; import { spawnSync } from "node:child_process"; import { createHash } from "node:crypto"; -import { chmodSync, mkdirSync, mkdtempSync, readFileSync, readlinkSync, rmSync, writeFileSync } from "node:fs"; +import { + chmodSync, + mkdirSync, + mkdtempSync, + readFileSync, + readlinkSync, + realpathSync, + rmSync, + writeFileSync, +} from "node:fs"; import { tmpdir } from "node:os"; import { dirname, join } from "node:path"; import { fileURLToPath } from "node:url"; @@ -105,7 +114,7 @@ describe("compiled binary installer", () => { const result = runInstaller(root, ["1.2.3"]); expect(result.exitCode, result.stderr).toBe(0); - const target = join(root, "apps", "versions", "v1.2.3", "prime-agent"); + const target = join(realpathSync(root), "apps", "versions", "v1.2.3", "prime-agent"); expect(readlinkSync(join(root, "bin", "prime-agent"))).toBe(target); expect(readFileSync(join(root, "apps", "versions", "v1.2.3", "package.json"), "utf8")).toContain('"1.2.3"'); expect(readFileSync(join(root, "home", ".prime", "sentinel"), "utf8")).toBe("user data"); @@ -209,7 +218,7 @@ describe("compiled binary installer", () => { const result = runInstaller(root, ["1.2.3"], { PRIME_AGENT_VERSIONS_DIR: "relative/versions" }); expect(result.exitCode, result.stderr).toBe(0); expect(readlinkSync(join(root, "bin", "prime-agent"))).toBe( - join(root, "relative", "versions", "v1.2.3", "prime-agent"), + join(realpathSync(root), "relative", "versions", "v1.2.3", "prime-agent"), ); }); }); diff --git a/packages/coding-agent/test/bun-release-archives.test.ts b/packages/coding-agent/test/bun-release-archives.test.ts index 836c2bc37ba..8b35dc50ad0 100644 --- a/packages/coding-agent/test/bun-release-archives.test.ts +++ b/packages/coding-agent/test/bun-release-archives.test.ts @@ -173,6 +173,15 @@ describe("compiled release archives", () => { expect(result.stderr).toContain("must use HTTPS"); }); + test("rejects empty query and fragment delimiters in release base URLs", () => { + for (const delimiter of ["?", "#"]) { + const f = fixture(); + const result = pack(f, ["--base-url", `https://downloads.example.test/${delimiter}`]); + expect(result.status).not.toBe(0); + expect(result.stderr).toContain("must not contain a query or fragment"); + } + }); + test("rejects shell-active release base URLs", () => { const f = fixture(); const result = pack(f, ["--base-url", "https://downloads.example.test/$(touch-danger)"]); diff --git a/scripts/assemble-release-archives.mjs b/scripts/assemble-release-archives.mjs index 282b5c90614..ca175a50e64 100755 --- a/scripts/assemble-release-archives.mjs +++ b/scripts/assemble-release-archives.mjs @@ -177,7 +177,9 @@ function normalizeBaseUrl(value) { if (parsed.protocol !== "https:" || !parsed.hostname || parsed.username || parsed.password) { throw new Error(`Release base URL must use HTTPS without credentials: ${value}`); } - if (parsed.search || parsed.hash) throw new Error("Release base URL must not contain a query or fragment"); + if (parsed.search || parsed.hash || /[?#]$/.test(parsed.toString())) { + throw new Error("Release base URL must not contain a query or fragment"); + } return parsed.toString().replace(/\/+$/, ""); } From c465c92631a775c1b27478bcfa4e2a968393d36b Mon Sep 17 00:00:00 2001 From: Seth Date: Tue, 1 Sep 2026 19:05:11 -0400 Subject: [PATCH 20/45] fix: verify activated binary install paths --- install.sh | 55 ++++++++----------- .../test/bun-installer-e2e.test.ts | 33 +++++++++++ .../coding-agent/test/bun-installer.test.ts | 5 ++ 3 files changed, 60 insertions(+), 33 deletions(-) diff --git a/install.sh b/install.sh index 41f53e530d5..1e3ccf6f6be 100755 --- a/install.sh +++ b/install.sh @@ -13,10 +13,8 @@ if [ "$prime_agent_default_release_channel" = "$prime_agent_unconfigured_default prime_agent_default_release_channel=stable fi prime_agent_release_channel="${PRIME_AGENT_RELEASE_CHANNEL:-$prime_agent_default_release_channel}" -prime_agent_package="${PRIME_AGENT_PACKAGE:-prime-agent}" prime_agent_cmd="${PRIME_AGENT_CMD:-prime-agent}" prime_agent_esc=$(printf '\033') -prime_agent_original_path="${PATH:-}" prime_agent_reset="${prime_agent_esc}[0m" prime_agent_bold="${prime_agent_esc}[1m" prime_agent_italic="${prime_agent_esc}[3m" @@ -33,7 +31,7 @@ prime_agent_color_dim="${prime_agent_esc}[38;2;113;113;122m" prime_agent_color_primary="${prime_agent_esc}[38;2;127;91;213m" prime_agent_color_scan="${prime_agent_esc}[38;2;14;165;233m" prime_agent_color_warning="${prime_agent_esc}[38;2;245;158;11m" -readonly prime_agent_unconfigured_base_url prime_agent_unconfigured_default_release_channel prime_agent_base_url prime_agent_default_release_channel prime_agent_release_channel prime_agent_package prime_agent_cmd prime_agent_esc prime_agent_original_path +readonly prime_agent_unconfigured_base_url prime_agent_unconfigured_default_release_channel prime_agent_base_url prime_agent_default_release_channel prime_agent_release_channel prime_agent_cmd prime_agent_esc readonly prime_agent_reset prime_agent_bold prime_agent_italic prime_agent_hide_cursor prime_agent_show_cursor prime_agent_home_cursor prime_agent_clear_screen prime_agent_clear_line readonly prime_agent_sync_start prime_agent_sync_end readonly prime_agent_color_text prime_agent_color_muted prime_agent_color_dim prime_agent_color_primary prime_agent_color_scan prime_agent_color_warning @@ -51,11 +49,9 @@ prime_agent_screen_layout_lab_width=0 prime_agent_screen_render_lab_width=0 prime_agent_screen_compact=0 prime_agent_download_dir= -prime_agent_bootstrap_kernel_on_install=0 prime_agent_screen_title= prime_agent_screen_status= prime_agent_screen_detail= -prime_agent_screen_question= prime_agent_animation_frame=0 prime_agent_binary_versions_dir="${PRIME_AGENT_VERSIONS_DIR:-${XDG_DATA_HOME:-$HOME/.local/share}/prime-agent/versions}" prime_agent_binary_symlink="${PRIME_AGENT_BIN_DIR:-${XDG_BIN_HOME:-$HOME/.local/bin}}/prime-agent" @@ -197,7 +193,6 @@ prime_agent_screen() { fi prime_agent_screen_status= prime_agent_screen_detail="${3:-}" - prime_agent_screen_question="${4:-}" prime_agent_screen_frame=$((prime_agent_screen_frame + 1)) prime_agent_read_terminal_size prime_agent_init_screen_layout @@ -341,18 +336,12 @@ prime_agent_content_line() { fi if [ "$index" -eq 0 ]; then - if [ -n "$prime_agent_screen_question" ]; then - prime_agent_set_text_line "$(prime_agent_screen_primary_text)" "$prime_agent_bold$prime_agent_color_text" - else - prime_agent_set_title_line "$prime_agent_screen_title" - fi + prime_agent_set_title_line "$prime_agent_screen_title" return fi if [ "$index" -eq 1 ]; then - if [ -n "$prime_agent_screen_question" ]; then - prime_agent_set_text_line "Press Enter to continue; type n to cancel." "$prime_agent_color_muted" - elif [ -n "$prime_agent_screen_detail" ]; then + if [ -n "$prime_agent_screen_detail" ]; then prime_agent_set_text_line "$prime_agent_screen_detail" "$prime_agent_color_muted" else prime_agent_set_blank_line @@ -361,18 +350,6 @@ prime_agent_content_line() { fi } -prime_agent_screen_primary_text() { - if [ -z "$prime_agent_screen_question" ]; then - printf '%s' "$prime_agent_screen_title" - return - fi - - case "$prime_agent_screen_question" in - *'[Y/n]'*) printf '%s [Y/n] >' "$prime_agent_screen_title" ;; - *) printf '%s %s' "$prime_agent_screen_title" "$prime_agent_screen_question" ;; - esac -} - prime_agent_set_lab_line() { lab_row="$1" prime_agent_lab_width="$prime_agent_screen_render_lab_width" @@ -1020,9 +997,25 @@ prime_agent_binary_fresh_install() { exit 1 fi - # Create the stable symlink + # Create and verify the stable symlink. Restore a healthy prior target if the + # activated command cannot run through its public path. + _old_target= + if [ -L "$prime_agent_binary_symlink" ]; then + _old_target=$(readlink "$prime_agent_binary_symlink" 2>/dev/null || printf '') + fi mkdir -p "$(dirname "$prime_agent_binary_symlink")" prime_agent_binary_atomic_symlink "$_binary_path" "$prime_agent_binary_symlink" + if ! prime_agent_binary_smoke_binary "$prime_agent_binary_symlink"; then + if [ -n "$_old_target" ] && prime_agent_binary_smoke_binary "$_old_target"; then + prime_agent_binary_atomic_symlink "$_old_target" "$prime_agent_binary_symlink" + else + rm -f "$prime_agent_binary_symlink" + fi + rm -rf "$_version_dir" "$_download_dir" + prime_agent_download_dir= + printf 'error: the installed Prime Agent command did not run correctly.\n' >&2 + exit 1 + fi rm -rf "$_download_dir" prime_agent_download_dir= @@ -1045,7 +1038,8 @@ prime_agent_binary_update() { fi fi - if [ -n "$_current_version" ] && [ "$_current_version" = "$_update_version" ]; then + if [ -n "$_current_version" ] && [ "$_current_version" = "$_update_version" ] && + prime_agent_binary_validate_layout "$_pkg_dir" "$prime_agent_binary_symlink"; then if [ "$prime_agent_screen_enabled" = 1 ]; then prime_agent_screen "Prime Agent is up to date" "" "v$_current_version" "" else @@ -1247,9 +1241,4 @@ prime_agent_configure_binary_path() { fi } -# ============================================================================= -# NPM Install Path (extracted from main) -# ============================================================================= - - main "$@" diff --git a/packages/coding-agent/test/bun-installer-e2e.test.ts b/packages/coding-agent/test/bun-installer-e2e.test.ts index 60327ffc2ef..0dc6d18f4f1 100644 --- a/packages/coding-agent/test/bun-installer-e2e.test.ts +++ b/packages/coding-agent/test/bun-installer-e2e.test.ts @@ -124,6 +124,21 @@ describe("compiled binary installer", () => { expect(readlinkSync(join(root, "bin", "prime-agent"))).toBe(target); }); + test("rejects a fresh install that fails through the activated command symlink", () => { + const root = mkdtempSync(join(tmpdir(), "prime-agent-installer-")); + temporaryRoots.push(root); + makeRelease( + root, + "1.2.3", + '#!/bin/sh\ncase "$0" in */bin/prime-agent) exit 1 ;; esac\nif [ "$1" = "--version" ]; then exit 0; fi\nexit 0\n', + ); + + const result = runInstaller(root, ["1.2.3"]); + expect(result.exitCode).not.toBe(0); + expect(result.stderr).toContain("installed Prime Agent command did not run correctly"); + expect(() => readlinkSync(join(root, "bin", "prime-agent"))).toThrow(); + }); + test("rejects a bad checksum before changing the active version", () => { const root = mkdtempSync(join(tmpdir(), "prime-agent-installer-")); temporaryRoots.push(root); @@ -134,6 +149,24 @@ describe("compiled binary installer", () => { expect(() => readlinkSync(join(root, "bin", "prime-agent"))).toThrow(); }); + test("repairs a broken active command when updating to the same version", () => { + const root = mkdtempSync(join(tmpdir(), "prime-agent-installer-")); + temporaryRoots.push(root); + makeRelease(root, "1.2.3", goodExecutable("1.2.3")); + expect(runInstaller(root, ["1.2.3"]).exitCode).toBe(0); + const link = join(root, "bin", "prime-agent"); + const target = readlinkSync(link); + writeFileSync(target, "#!/bin/sh\nexit 1\n"); + chmodSync(target, 0o755); + makeRelease(root, "1.2.3", goodExecutable("1.2.3")); + + const result = runInstaller(root, ["--update", "1.2.3"]); + expect(result.exitCode, result.stderr).toBe(0); + const smoke = spawnSync(link, ["--version"], { encoding: "utf8" }); + expect(smoke.status).toBe(0); + expect(smoke.stdout).toContain("1.2.3"); + }); + test("keeps the previous symlink when an update fails its smoke test", () => { const root = mkdtempSync(join(tmpdir(), "prime-agent-installer-")); temporaryRoots.push(root); diff --git a/packages/coding-agent/test/bun-installer.test.ts b/packages/coding-agent/test/bun-installer.test.ts index 72f2294db0b..93f3d2f9509 100644 --- a/packages/coding-agent/test/bun-installer.test.ts +++ b/packages/coding-agent/test/bun-installer.test.ts @@ -47,6 +47,11 @@ describe("install.sh shell syntax", () => { expect(installerText).not.toContain("prime_agent_npm_install"); expect(installerText).not.toContain("install_node_npm"); expect(installerText).not.toContain("npm install"); + expect(installerText).not.toContain("NPM Install Path"); + expect(installerText).not.toContain("prime_agent_package"); + expect(installerText).not.toContain("prime_agent_original_path"); + expect(installerText).not.toContain("prime_agent_bootstrap_kernel_on_install"); + expect(installerText).not.toContain("prime_agent_screen_question"); }); it("rejects removed package-manager method flags", () => { From e53576de8b56c15144872a444725ace4c8d656c2 Mon Sep 17 00:00:00 2001 From: Seth Date: Tue, 1 Sep 2026 19:12:01 -0400 Subject: [PATCH 21/45] fix: serialize binary installer activation --- install.sh | 39 ++++++++++- packages/ai/src/models.generated.ts | 6 +- .../test/bun-installer-e2e.test.ts | 67 +++++++++++++++---- .../coding-agent/test/bun-installer.test.ts | 2 + 4 files changed, 95 insertions(+), 19 deletions(-) diff --git a/install.sh b/install.sh index 1e3ccf6f6be..e39c9047692 100755 --- a/install.sh +++ b/install.sh @@ -56,6 +56,7 @@ prime_agent_animation_frame=0 prime_agent_binary_versions_dir="${PRIME_AGENT_VERSIONS_DIR:-${XDG_DATA_HOME:-$HOME/.local/share}/prime-agent/versions}" prime_agent_binary_symlink="${PRIME_AGENT_BIN_DIR:-${XDG_BIN_HOME:-$HOME/.local/bin}}/prime-agent" prime_agent_binary_rollback_version= +prime_agent_binary_lock_dir= prime_agent_is_update=0 main() { @@ -130,6 +131,10 @@ prime_agent_cleanup() { if [ -n "${prime_agent_download_dir:-}" ] && [ -d "$prime_agent_download_dir" ]; then rm -rf "$prime_agent_download_dir" fi + if [ -n "${prime_agent_binary_lock_dir:-}" ] && [ -d "$prime_agent_binary_lock_dir" ]; then + rm -rf "$prime_agent_binary_lock_dir" + prime_agent_binary_lock_dir= + fi prime_agent_restore_terminal return "$status" } @@ -892,6 +897,29 @@ prime_agent_binary_validate_layout() { done } +prime_agent_binary_acquire_lock() { + _versions_dir="$1" + _lock_dir="$_versions_dir/.install.lock" + _timeout="${PRIME_AGENT_INSTALL_LOCK_TIMEOUT_SECONDS:-60}" + case "$_timeout" in + ''|*[!0-9]*) + printf 'error: PRIME_AGENT_INSTALL_LOCK_TIMEOUT_SECONDS must be a non-negative integer.\n' >&2 + return 1 + ;; + esac + _waited=0 + while ! mkdir "$_lock_dir" 2>/dev/null; do + if [ "$_waited" -ge "$_timeout" ]; then + printf 'error: timed out waiting for another Prime Agent install or update to finish.\n' >&2 + return 1 + fi + sleep 1 + _waited=$((_waited + 1)) + done + printf '%s\n' "$$" > "$_lock_dir/pid" + prime_agent_binary_lock_dir="$_lock_dir" +} + prime_agent_binary_atomic_symlink() { _target="$1" _link="$2" @@ -902,6 +930,10 @@ prime_agent_binary_atomic_symlink() { if [ ! -d "$_link_dir" ]; then mkdir -p "$_link_dir" fi + if [ -d "$_link" ] && [ ! -L "$_link" ]; then + printf 'error: cannot activate Prime Agent because the command path is a directory: %s\n' "$_link" >&2 + return 1 + fi # Atomic replacement with temp symlink _tmp="${_link}.tmp.$$" ln -sf "$_target" "$_tmp" @@ -925,6 +957,7 @@ prime_agent_binary_fresh_install() { _artifact_path="$_download_dir/$_artifact_name" mkdir -p "$_versions_dir" + prime_agent_binary_acquire_lock "$_versions_dir" # Version directories are immutable. Reuse a healthy existing install instead # of replacing files underneath the active command symlink. @@ -1027,6 +1060,9 @@ prime_agent_binary_fresh_install() { prime_agent_binary_update() { _update_version= _update_version="$(resolve_prime_agent_version "$@")" + _versions_dir="$prime_agent_binary_versions_dir" + mkdir -p "$_versions_dir" + prime_agent_binary_acquire_lock "$_versions_dir" # Read current version from the symlink target's package.json _current_version= @@ -1054,7 +1090,6 @@ prime_agent_binary_update() { fi _artifact_name="$(prime_agent_binary_artifact_name "$_update_version" "$_platform")" _artifact_url="$prime_agent_base_url/releases/v$_update_version/$_artifact_name" - _versions_dir="$prime_agent_binary_versions_dir" _version_dir="$(prime_agent_binary_target_version_dir "$_update_version")" _download_dir=$(create_temp_dir) @@ -1229,7 +1264,7 @@ prime_agent_configure_binary_path() { _profile=$(detect_shell_profile) if [ -n "$_profile" ] && [ -w "$_profile" ] 2>/dev/null; then - if ! grep -q "$_bin_dir" "$_profile" 2>/dev/null; then + if ! grep -F -q -- "$_bin_dir" "$_profile" 2>/dev/null; then printf '\nexport PATH="%s:$PATH"\n' "$_bin_dir" >> "$_profile" printf 'Added %s to %s.\n' "$_bin_dir" "$_profile" fi diff --git a/packages/ai/src/models.generated.ts b/packages/ai/src/models.generated.ts index 95bc323c017..68ab212bdd9 100644 --- a/packages/ai/src/models.generated.ts +++ b/packages/ai/src/models.generated.ts @@ -11179,9 +11179,9 @@ export const MODELS = { thinkingLevelMap: {"minimal":null,"low":null,"medium":null,"high":"high","xhigh":"max","max":null}, input: ["text"], cost: { - input: 0.07168000000000001, - output: 0.14336000000000002, - cacheRead: 0.014336, + input: 0.07014000000000001, + output: 0.14028000000000002, + cacheRead: 0.014028, cacheWrite: 0, }, contextWindow: 1048576, diff --git a/packages/coding-agent/test/bun-installer-e2e.test.ts b/packages/coding-agent/test/bun-installer-e2e.test.ts index 0dc6d18f4f1..c0485a24e90 100644 --- a/packages/coding-agent/test/bun-installer-e2e.test.ts +++ b/packages/coding-agent/test/bun-installer-e2e.test.ts @@ -72,27 +72,29 @@ function makeRelease( writeFileSync(join(releaseDir, "SHA256SUMS"), `${checksum} ${archiveName}\n`); } +function installerEnv(root: string, overrides: Record = {}): NodeJS.ProcessEnv { + const home = join(root, "home"); + mkdirSync(join(home, ".prime"), { recursive: true }); + writeFileSync(join(home, ".prime", "sentinel"), "user data"); + return { + ...process.env, + HOME: home, + PRIME_AGENT_DOWNLOAD_BASE_URL: `file://${join(root, "server")}`, + PRIME_AGENT_VERSIONS_DIR: join(root, "apps", "versions"), + PRIME_AGENT_BIN_DIR: join(root, "bin"), + TERM: "dumb", + ...overrides, + }; +} + function runInstaller( root: string, args: string[], env: Record = {}, ): { exitCode: number; stdout: string; stderr: string } { - const home = join(root, "home"); - const versions = join(root, "apps", "versions"); - const bin = join(root, "bin"); - mkdirSync(join(home, ".prime"), { recursive: true }); - writeFileSync(join(home, ".prime", "sentinel"), "user data"); const result = spawnSync("sh", [installer, ...args], { cwd: root, - env: { - ...process.env, - HOME: home, - PRIME_AGENT_DOWNLOAD_BASE_URL: `file://${join(root, "server")}`, - PRIME_AGENT_VERSIONS_DIR: versions, - PRIME_AGENT_BIN_DIR: bin, - TERM: "dumb", - ...env, - }, + env: installerEnv(root, env), encoding: "utf8", }); return { @@ -149,6 +151,19 @@ describe("compiled binary installer", () => { expect(() => readlinkSync(join(root, "bin", "prime-agent"))).toThrow(); }); + test("rejects a pre-existing directory at the command path", () => { + const root = mkdtempSync(join(tmpdir(), "prime-agent-installer-")); + temporaryRoots.push(root); + makeRelease(root, "1.2.3", goodExecutable("1.2.3")); + const commandPath = join(root, "bin", "prime-agent"); + mkdirSync(commandPath, { recursive: true }); + + const result = runInstaller(root, ["1.2.3"]); + expect(result.exitCode).not.toBe(0); + expect(result.stderr).toContain("command path is a directory"); + expect(readFileSync(join(root, "home", ".prime", "sentinel"), "utf8")).toBe("user data"); + }); + test("repairs a broken active command when updating to the same version", () => { const root = mkdtempSync(join(tmpdir(), "prime-agent-installer-")); temporaryRoots.push(root); @@ -182,6 +197,30 @@ describe("compiled binary installer", () => { expect(readFileSync(join(root, "home", ".prime", "sentinel"), "utf8")).toBe("user data"); }); + test("serializes concurrent updates without deleting an activated version", () => { + const root = mkdtempSync(join(tmpdir(), "prime-agent-installer-")); + temporaryRoots.push(root); + makeRelease(root, "1.2.3", goodExecutable("1.2.3")); + expect(runInstaller(root, ["1.2.3"]).exitCode).toBe(0); + makeRelease(root, "2.0.0", goodExecutable("2.0.0").replace("then echo", "then sleep 1; echo")); + + const result = spawnSync( + "sh", + [ + "-c", + 'sh "$1" --update 2.0.0 & first=$!; sh "$1" --update 2.0.0 & second=$!; wait "$first"; a=$?; wait "$second"; b=$?; [ "$a" -eq 0 ] && [ "$b" -eq 0 ]', + "--", + installer, + ], + { cwd: root, env: installerEnv(root), encoding: "utf8", timeout: 20_000 }, + ); + expect(result.status, result.stderr).toBe(0); + const link = join(root, "bin", "prime-agent"); + expect(readlinkSync(link)).toContain("v2.0.0/prime-agent"); + expect(spawnSync(link, ["--version"], { encoding: "utf8" }).status).toBe(0); + expect(() => readFileSync(join(root, "apps", "versions", ".install.lock", "pid"))).toThrow(); + }); + test("keeps the previous version when an update is missing a required sidecar", () => { const root = mkdtempSync(join(tmpdir(), "prime-agent-installer-")); temporaryRoots.push(root); diff --git a/packages/coding-agent/test/bun-installer.test.ts b/packages/coding-agent/test/bun-installer.test.ts index 93f3d2f9509..e122d4779d9 100644 --- a/packages/coding-agent/test/bun-installer.test.ts +++ b/packages/coding-agent/test/bun-installer.test.ts @@ -52,6 +52,8 @@ describe("install.sh shell syntax", () => { expect(installerText).not.toContain("prime_agent_original_path"); expect(installerText).not.toContain("prime_agent_bootstrap_kernel_on_install"); expect(installerText).not.toContain("prime_agent_screen_question"); + expect(installerText).toContain("prime_agent_binary_acquire_lock"); + expect(installerText).toContain('grep -F -q -- "$_bin_dir"'); }); it("rejects removed package-manager method flags", () => { From 1ced0af6dc6f0a02a6a2bec2bb7a5d7f04430552 Mon Sep 17 00:00:00 2001 From: Seth Date: Tue, 1 Sep 2026 19:19:10 -0400 Subject: [PATCH 22/45] fix: avoid same-version self-rollback --- install.sh | 27 ++++++++++++------- packages/ai/src/models.generated.ts | 10 +++---- .../test/bun-installer-e2e.test.ts | 20 ++++++++++++++ 3 files changed, 42 insertions(+), 15 deletions(-) diff --git a/install.sh b/install.sh index e39c9047692..8fa51070747 100755 --- a/install.sh +++ b/install.sh @@ -1039,7 +1039,12 @@ prime_agent_binary_fresh_install() { mkdir -p "$(dirname "$prime_agent_binary_symlink")" prime_agent_binary_atomic_symlink "$_binary_path" "$prime_agent_binary_symlink" if ! prime_agent_binary_smoke_binary "$prime_agent_binary_symlink"; then - if [ -n "$_old_target" ] && prime_agent_binary_smoke_binary "$_old_target"; then + _old_target_dir= + if [ -n "$_old_target" ]; then + _old_target_dir=$(dirname "$_old_target") + fi + if [ -n "$_old_target" ] && [ "$_old_target_dir" != "$_version_dir" ] && + prime_agent_binary_smoke_binary "$_old_target"; then prime_agent_binary_atomic_symlink "$_old_target" "$prime_agent_binary_symlink" else rm -f "$prime_agent_binary_symlink" @@ -1091,6 +1096,14 @@ prime_agent_binary_update() { _artifact_name="$(prime_agent_binary_artifact_name "$_update_version" "$_platform")" _artifact_url="$prime_agent_base_url/releases/v$_update_version/$_artifact_name" _version_dir="$(prime_agent_binary_target_version_dir "$_update_version")" + prime_agent_binary_rollback_version= + if [ -L "$prime_agent_binary_symlink" ]; then + _old_target=$(readlink "$prime_agent_binary_symlink" 2>/dev/null || printf '') + _old_version_dir=$(dirname "$_old_target" 2>/dev/null || printf '') + if [ "$_old_version_dir" != "$_version_dir" ]; then + prime_agent_binary_rollback_version="$_old_version_dir" + fi + fi _download_dir=$(create_temp_dir) prime_agent_download_dir="$_download_dir" @@ -1162,24 +1175,17 @@ prime_agent_binary_update() { exit 1 fi - # Remember old version for rollback - if [ -L "$prime_agent_binary_symlink" ]; then - _old_target=$(readlink "$prime_agent_binary_symlink" 2>/dev/null || printf '') - _old_version_dir=$(dirname "$_old_target" 2>/dev/null || printf '') - prime_agent_binary_rollback_version="$_old_version_dir" - fi - # Atomically switch the symlink, then verify the activated path. If activation # fails, restore the previous immutable version before returning an error. prime_agent_binary_atomic_symlink "$_binary_path" "$prime_agent_binary_symlink" if ! prime_agent_binary_smoke_binary "$prime_agent_binary_symlink"; then if prime_agent_binary_rollback; then - rm -rf "$_version_dir" printf 'error: activation failed; restored the previous Prime Agent version.\n' >&2 else + rm -f "$prime_agent_binary_symlink" printf 'error: activation failed and no healthy rollback version was available.\n' >&2 fi - rm -rf "$_download_dir" + rm -rf "$_version_dir" "$_download_dir" prime_agent_download_dir= exit 1 fi @@ -1210,6 +1216,7 @@ prime_agent_binary_rollback() { return 1 fi prime_agent_binary_atomic_symlink "$_rollback_binary" "$prime_agent_binary_symlink" + prime_agent_binary_smoke_binary "$prime_agent_binary_symlink" } prime_agent_verify_binary_checksum() { diff --git a/packages/ai/src/models.generated.ts b/packages/ai/src/models.generated.ts index 68ab212bdd9..2ab1ba6ec96 100644 --- a/packages/ai/src/models.generated.ts +++ b/packages/ai/src/models.generated.ts @@ -11236,9 +11236,9 @@ export const MODELS = { thinkingLevelMap: {"minimal":null,"low":null,"medium":null,"high":"high","xhigh":"max","max":null}, input: ["text"], cost: { - input: 1.0274699999999999, - output: 2.0549399999999998, - cacheRead: 0.0856225, + input: 1.0241639999999999, + output: 2.0483279999999997, + cacheRead: 0.085347, cacheWrite: 0, }, contextWindow: 1048576, @@ -14665,11 +14665,11 @@ export const MODELS = { cost: { input: 2, output: 6, - cacheRead: 0.25, + cacheRead: 0.19999999999999998, cacheWrite: 0, }, contextWindow: 1048576, - maxTokens: 262144, + maxTokens: 131072, } satisfies Model<"openai-completions">, "qwen/qwen3.8-27b": { id: "qwen/qwen3.8-27b", diff --git a/packages/coding-agent/test/bun-installer-e2e.test.ts b/packages/coding-agent/test/bun-installer-e2e.test.ts index c0485a24e90..bfb1021fe26 100644 --- a/packages/coding-agent/test/bun-installer-e2e.test.ts +++ b/packages/coding-agent/test/bun-installer-e2e.test.ts @@ -182,6 +182,26 @@ describe("compiled binary installer", () => { expect(smoke.stdout).toContain("1.2.3"); }); + test("does not use the repaired version as its own rollback target", () => { + const root = mkdtempSync(join(tmpdir(), "prime-agent-installer-")); + temporaryRoots.push(root); + makeRelease(root, "1.2.3", goodExecutable("1.2.3")); + expect(runInstaller(root, ["1.2.3"]).exitCode).toBe(0); + const link = join(root, "bin", "prime-agent"); + const target = readlinkSync(link); + const publicPathFailure = + '#!/bin/sh\ncase "$0" in */bin/prime-agent) exit 1 ;; esac\nif [ "$1" = "--version" ]; then exit 0; fi\nexit 0\n'; + writeFileSync(target, publicPathFailure); + chmodSync(target, 0o755); + makeRelease(root, "1.2.3", publicPathFailure); + + const result = runInstaller(root, ["--update", "1.2.3"]); + expect(result.exitCode).not.toBe(0); + expect(result.stderr).toContain("no healthy rollback version was available"); + expect(() => readlinkSync(link)).toThrow(); + expect(() => readFileSync(join(root, "apps", "versions", "v1.2.3", "package.json"))).toThrow(); + }); + test("keeps the previous symlink when an update fails its smoke test", () => { const root = mkdtempSync(join(tmpdir(), "prime-agent-installer-")); temporaryRoots.push(root); From 4f366f948b2a21e42bab96a871bd6e80d51356a9 Mon Sep 17 00:00:00 2001 From: Seth Date: Tue, 1 Sep 2026 19:26:47 -0400 Subject: [PATCH 23/45] fix: persist binary install paths --- install.sh | 62 ++++++++++++++++++- .../test/bun-installer-e2e.test.ts | 48 ++++++++++++++ .../coding-agent/test/bun-installer.test.ts | 16 ++++- 3 files changed, 123 insertions(+), 3 deletions(-) diff --git a/install.sh b/install.sh index 8fa51070747..79225148cc6 100755 --- a/install.sh +++ b/install.sh @@ -14,6 +14,23 @@ if [ "$prime_agent_default_release_channel" = "$prime_agent_unconfigured_default fi prime_agent_release_channel="${PRIME_AGENT_RELEASE_CHANNEL:-$prime_agent_default_release_channel}" prime_agent_cmd="${PRIME_AGENT_CMD:-prime-agent}" +prime_agent_persisted_versions_dir= +prime_agent_persisted_symlink= +prime_agent_script_dir=$(CDPATH= cd -P "$(dirname "$0")" 2>/dev/null && pwd) +if [ -f "$prime_agent_script_dir/.install-paths" ]; then + prime_agent_persisted_versions_dir=$(sed -n '1p' "$prime_agent_script_dir/.install-paths") + prime_agent_persisted_symlink=$(sed -n '2p' "$prime_agent_script_dir/.install-paths") +fi +prime_agent_default_data_home="${XDG_DATA_HOME:-${HOME:+$HOME/.local/share}}" +prime_agent_default_bin_home="${XDG_BIN_HOME:-${HOME:+$HOME/.local/bin}}" +prime_agent_binary_versions_dir="${PRIME_AGENT_VERSIONS_DIR:-${prime_agent_persisted_versions_dir:-${prime_agent_default_data_home:+$prime_agent_default_data_home/prime-agent/versions}}}" +if [ -n "${PRIME_AGENT_BIN_DIR:-}" ]; then + prime_agent_binary_symlink="${PRIME_AGENT_BIN_DIR%/}/prime-agent" +elif [ -n "$prime_agent_persisted_symlink" ]; then + prime_agent_binary_symlink="$prime_agent_persisted_symlink" +else + prime_agent_binary_symlink="${prime_agent_default_bin_home:+$prime_agent_default_bin_home/prime-agent}" +fi prime_agent_esc=$(printf '\033') prime_agent_reset="${prime_agent_esc}[0m" prime_agent_bold="${prime_agent_esc}[1m" @@ -53,8 +70,6 @@ prime_agent_screen_title= prime_agent_screen_status= prime_agent_screen_detail= prime_agent_animation_frame=0 -prime_agent_binary_versions_dir="${PRIME_AGENT_VERSIONS_DIR:-${XDG_DATA_HOME:-$HOME/.local/share}/prime-agent/versions}" -prime_agent_binary_symlink="${PRIME_AGENT_BIN_DIR:-${XDG_BIN_HOME:-$HOME/.local/bin}}/prime-agent" prime_agent_binary_rollback_version= prime_agent_binary_lock_dir= prime_agent_is_update=0 @@ -65,6 +80,10 @@ main() { printf 'Set PRIME_AGENT_DOWNLOAD_BASE_URL or use the installer published by the release workflow.\n' >&2 exit 1 fi + if [ -z "$prime_agent_binary_versions_dir" ] || [ -z "$prime_agent_binary_symlink" ]; then + printf 'error: HOME is not set; set HOME or explicit PRIME_AGENT_VERSIONS_DIR and PRIME_AGENT_BIN_DIR paths.\n' >&2 + exit 1 + fi prime_agent_is_update=0 _prime_agent_positional= @@ -909,6 +928,34 @@ prime_agent_binary_acquire_lock() { esac _waited=0 while ! mkdir "$_lock_dir" 2>/dev/null; do + _lock_pid= + if [ -f "$_lock_dir/pid" ]; then + _lock_pid=$(sed -n '1p' "$_lock_dir/pid" 2>/dev/null || printf '') + fi + case "$_lock_pid" in + '') + # The owner writes its PID immediately after mkdir. Give that tiny + # publication window one retry before treating the lock as abandoned. + if [ "$_waited" -eq 0 ]; then + sleep 1 + _waited=$((_waited + 1)) + continue + fi + _lock_is_stale=1 + ;; + *[!0-9]*) _lock_is_stale=1 ;; + *) + if kill -0 "$_lock_pid" 2>/dev/null; then + _lock_is_stale=0 + else + _lock_is_stale=1 + fi + ;; + esac + if [ "$_lock_is_stale" = 1 ]; then + rm -rf "$_lock_dir" + continue + fi if [ "$_waited" -ge "$_timeout" ]; then printf 'error: timed out waiting for another Prime Agent install or update to finish.\n' >&2 return 1 @@ -920,6 +967,14 @@ prime_agent_binary_acquire_lock() { prime_agent_binary_lock_dir="$_lock_dir" } +prime_agent_binary_write_install_paths() { + _version_dir="$1" + _state_path="$_version_dir/.install-paths" + _state_tmp="${_state_path}.tmp.$$" + printf '%s\n%s\n' "$prime_agent_binary_versions_dir" "$prime_agent_binary_symlink" > "$_state_tmp" + mv -f "$_state_tmp" "$_state_path" +} + prime_agent_binary_atomic_symlink() { _target="$1" _link="$2" @@ -966,6 +1021,7 @@ prime_agent_binary_fresh_install() { _existing_binary="$_version_dir/pi" fi if [ -x "$_existing_binary" ] && prime_agent_binary_validate_layout "$_version_dir" "$_existing_binary"; then + prime_agent_binary_write_install_paths "$_version_dir" prime_agent_binary_atomic_symlink "$_existing_binary" "$prime_agent_binary_symlink" prime_agent_configure_binary_path "$_version" return @@ -1029,6 +1085,7 @@ prime_agent_binary_fresh_install() { prime_agent_download_dir= exit 1 fi + prime_agent_binary_write_install_paths "$_version_dir" # Create and verify the stable symlink. Restore a healthy prior target if the # activated command cannot run through its public path. @@ -1174,6 +1231,7 @@ prime_agent_binary_update() { prime_agent_download_dir= exit 1 fi + prime_agent_binary_write_install_paths "$_version_dir" # Atomically switch the symlink, then verify the activated path. If activation # fails, restore the previous immutable version before returning an error. diff --git a/packages/coding-agent/test/bun-installer-e2e.test.ts b/packages/coding-agent/test/bun-installer-e2e.test.ts index bfb1021fe26..39827f073c6 100644 --- a/packages/coding-agent/test/bun-installer-e2e.test.ts +++ b/packages/coding-agent/test/bun-installer-e2e.test.ts @@ -126,6 +126,36 @@ describe("compiled binary installer", () => { expect(readlinkSync(join(root, "bin", "prime-agent"))).toBe(target); }); + test("self-updates the persisted custom install paths without exported overrides", () => { + const root = mkdtempSync(join(tmpdir(), "prime-agent-installer-")); + temporaryRoots.push(root); + makeRelease(root, "1.2.3", goodExecutable("1.2.3")); + expect(runInstaller(root, ["1.2.3"]).exitCode).toBe(0); + const v1Sidecar = join(root, "apps", "versions", "v1.2.3", "install.sh"); + writeFileSync(v1Sidecar, readFileSync(installer)); + chmodSync(v1Sidecar, 0o755); + makeRelease(root, "2.0.0", goodExecutable("2.0.0")); + const isolatedHome = join(root, "isolated-home"); + mkdirSync(isolatedHome); + const result = spawnSync("sh", [v1Sidecar, "--update", "2.0.0"], { + cwd: root, + env: { + ...process.env, + HOME: isolatedHome, + PRIME_AGENT_DOWNLOAD_BASE_URL: `file://${join(root, "server")}`, + PRIME_AGENT_VERSIONS_DIR: undefined, + PRIME_AGENT_BIN_DIR: undefined, + XDG_DATA_HOME: undefined, + XDG_BIN_HOME: undefined, + TERM: "dumb", + }, + encoding: "utf8", + }); + expect(result.status, result.stderr).toBe(0); + expect(readlinkSync(join(root, "bin", "prime-agent"))).toContain("v2.0.0/prime-agent"); + expect(() => readlinkSync(join(isolatedHome, ".local", "bin", "prime-agent"))).toThrow(); + }); + test("rejects a fresh install that fails through the activated command symlink", () => { const root = mkdtempSync(join(tmpdir(), "prime-agent-installer-")); temporaryRoots.push(root); @@ -241,6 +271,24 @@ describe("compiled binary installer", () => { expect(() => readFileSync(join(root, "apps", "versions", ".install.lock", "pid"))).toThrow(); }); + test("recovers an install lock whose recorded process is gone", () => { + const root = mkdtempSync(join(tmpdir(), "prime-agent-installer-")); + temporaryRoots.push(root); + makeRelease(root, "1.2.3", goodExecutable("1.2.3")); + expect(runInstaller(root, ["1.2.3"]).exitCode).toBe(0); + makeRelease(root, "2.0.0", goodExecutable("2.0.0")); + const lockDir = join(root, "apps", "versions", ".install.lock"); + mkdirSync(lockDir); + writeFileSync(join(lockDir, "pid"), "99999999\n"); + + const result = runInstaller(root, ["--update", "2.0.0"], { + PRIME_AGENT_INSTALL_LOCK_TIMEOUT_SECONDS: "1", + }); + expect(result.exitCode, result.stderr).toBe(0); + expect(readlinkSync(join(root, "bin", "prime-agent"))).toContain("v2.0.0/prime-agent"); + expect(() => readFileSync(join(lockDir, "pid"))).toThrow(); + }); + test("keeps the previous version when an update is missing a required sidecar", () => { const root = mkdtempSync(join(tmpdir(), "prime-agent-installer-")); temporaryRoots.push(root); diff --git a/packages/coding-agent/test/bun-installer.test.ts b/packages/coding-agent/test/bun-installer.test.ts index e122d4779d9..cdacafd0983 100644 --- a/packages/coding-agent/test/bun-installer.test.ts +++ b/packages/coding-agent/test/bun-installer.test.ts @@ -1,5 +1,5 @@ import { afterEach, describe, expect, it } from "bun:test"; -import { execFileSync } from "node:child_process"; +import { execFileSync, spawnSync } from "node:child_process"; import { mkdtempSync, readFileSync, rmSync } from "node:fs"; import { tmpdir } from "node:os"; import { dirname, join } from "node:path"; @@ -59,6 +59,20 @@ describe("install.sh shell syntax", () => { it("rejects removed package-manager method flags", () => { expect(installerText).toContain("--method is no longer supported"); }); + + it("reports an actionable error when HOME and explicit install paths are absent", () => { + const result = spawnSync("sh", [installer, "1.2.3"], { + encoding: "utf8", + env: { + PATH: process.env.PATH, + PRIME_AGENT_DOWNLOAD_BASE_URL: "https://downloads.example.test", + TERM: "dumb", + }, + }); + expect(result.status).not.toBe(0); + expect(result.stderr).toContain("HOME is not set"); + expect(result.stderr).not.toContain("parameter not set"); + }); }); // ============================================================================ From faf728096f0a35d65cea5725005308d8bb27bdcb Mon Sep 17 00:00:00 2001 From: Seth Date: Tue, 1 Sep 2026 19:37:51 -0400 Subject: [PATCH 24/45] fix: canonicalize binary install roots --- install.sh | 20 ++++++++++++--- .../test/bun-installer-e2e.test.ts | 25 +++++++++++++++++++ 2 files changed, 41 insertions(+), 4 deletions(-) diff --git a/install.sh b/install.sh index 79225148cc6..8bcf40a958e 100755 --- a/install.sh +++ b/install.sh @@ -916,6 +916,16 @@ prime_agent_binary_validate_layout() { done } +prime_agent_binary_canonicalize_install_paths() { + mkdir -p "$prime_agent_binary_versions_dir" + prime_agent_binary_versions_dir=$(CDPATH= cd -P "$prime_agent_binary_versions_dir" && pwd) + _link_dir=$(dirname "$prime_agent_binary_symlink") + _link_name=$(basename "$prime_agent_binary_symlink") + mkdir -p "$_link_dir" + _link_dir=$(CDPATH= cd -P "$_link_dir" && pwd) + prime_agent_binary_symlink="$_link_dir/$_link_name" +} + prime_agent_binary_acquire_lock() { _versions_dir="$1" _lock_dir="$_versions_dir/.install.lock" @@ -1004,6 +1014,7 @@ prime_agent_binary_fresh_install() { fi _artifact_name="$(prime_agent_binary_artifact_name "$_version" "$_platform")" _artifact_url="$prime_agent_base_url/releases/v$_version/$_artifact_name" + prime_agent_binary_canonicalize_install_paths _versions_dir="$prime_agent_binary_versions_dir" _version_dir="$(prime_agent_binary_target_version_dir "$_version")" @@ -1011,7 +1022,6 @@ prime_agent_binary_fresh_install() { prime_agent_download_dir="$_download_dir" _artifact_path="$_download_dir/$_artifact_name" - mkdir -p "$_versions_dir" prime_agent_binary_acquire_lock "$_versions_dir" # Version directories are immutable. Reuse a healthy existing install instead @@ -1023,8 +1033,10 @@ prime_agent_binary_fresh_install() { if [ -x "$_existing_binary" ] && prime_agent_binary_validate_layout "$_version_dir" "$_existing_binary"; then prime_agent_binary_write_install_paths "$_version_dir" prime_agent_binary_atomic_symlink "$_existing_binary" "$prime_agent_binary_symlink" - prime_agent_configure_binary_path "$_version" - return + if prime_agent_binary_smoke_binary "$prime_agent_binary_symlink"; then + prime_agent_configure_binary_path "$_version" + return + fi fi prime_agent_run_quiet_with_animation "Downloading Prime Agent v$_version" "Downloading Prime Agent v$_version" "Fetching the compiled binary for $_platform." curl -fsSL "$_artifact_url" -o "$_artifact_path" @@ -1122,8 +1134,8 @@ prime_agent_binary_fresh_install() { prime_agent_binary_update() { _update_version= _update_version="$(resolve_prime_agent_version "$@")" + prime_agent_binary_canonicalize_install_paths _versions_dir="$prime_agent_binary_versions_dir" - mkdir -p "$_versions_dir" prime_agent_binary_acquire_lock "$_versions_dir" # Read current version from the symlink target's package.json diff --git a/packages/coding-agent/test/bun-installer-e2e.test.ts b/packages/coding-agent/test/bun-installer-e2e.test.ts index 39827f073c6..52d1c7442d4 100644 --- a/packages/coding-agent/test/bun-installer-e2e.test.ts +++ b/packages/coding-agent/test/bun-installer-e2e.test.ts @@ -9,6 +9,7 @@ import { readlinkSync, realpathSync, rmSync, + symlinkSync, writeFileSync, } from "node:fs"; import { tmpdir } from "node:os"; @@ -232,6 +233,30 @@ describe("compiled binary installer", () => { expect(() => readFileSync(join(root, "apps", "versions", "v1.2.3", "package.json"))).toThrow(); }); + test("does not self-rollback through an aliased versions directory", () => { + const root = mkdtempSync(join(tmpdir(), "prime-agent-installer-")); + temporaryRoots.push(root); + const physicalVersions = join(root, "physical-versions"); + const aliasedVersions = join(root, "aliased-versions"); + mkdirSync(physicalVersions); + symlinkSync(physicalVersions, aliasedVersions, "dir"); + makeRelease(root, "1.2.3", goodExecutable("1.2.3")); + const pathEnv = { PRIME_AGENT_VERSIONS_DIR: aliasedVersions }; + expect(runInstaller(root, ["1.2.3"], pathEnv).exitCode).toBe(0); + const link = join(root, "bin", "prime-agent"); + const target = readlinkSync(link); + writeFileSync(target, "#!/bin/sh\nexit 1\n"); + chmodSync(target, 0o755); + const publicPathFailure = + '#!/bin/sh\ncase "$0" in */bin/prime-agent) exit 1 ;; esac\nif [ "$1" = "--version" ]; then exit 0; fi\nexit 0\n'; + makeRelease(root, "1.2.3", publicPathFailure); + + const result = runInstaller(root, ["1.2.3"], pathEnv); + expect(result.exitCode).not.toBe(0); + expect(() => readlinkSync(link)).toThrow(); + expect(() => readFileSync(join(physicalVersions, "v1.2.3", "package.json"))).toThrow(); + }); + test("keeps the previous symlink when an update fails its smoke test", () => { const root = mkdtempSync(join(tmpdir(), "prime-agent-installer-")); temporaryRoots.push(root); From 69ccb0e47831d87fa7c64875f423b5eaf94790d9 Mon Sep 17 00:00:00 2001 From: Seth Date: Tue, 1 Sep 2026 19:57:38 -0400 Subject: [PATCH 25/45] fix: harden installer lock ownership --- install.sh | 187 ++++++++++++++---- .../test/bun-installer-e2e.test.ts | 59 +++++- 2 files changed, 198 insertions(+), 48 deletions(-) diff --git a/install.sh b/install.sh index 8bcf40a958e..d6b78e5cea9 100755 --- a/install.sh +++ b/install.sh @@ -16,20 +16,53 @@ prime_agent_release_channel="${PRIME_AGENT_RELEASE_CHANNEL:-$prime_agent_default prime_agent_cmd="${PRIME_AGENT_CMD:-prime-agent}" prime_agent_persisted_versions_dir= prime_agent_persisted_symlink= -prime_agent_script_dir=$(CDPATH= cd -P "$(dirname "$0")" 2>/dev/null && pwd) -if [ -f "$prime_agent_script_dir/.install-paths" ]; then - prime_agent_persisted_versions_dir=$(sed -n '1p' "$prime_agent_script_dir/.install-paths") - prime_agent_persisted_symlink=$(sed -n '2p' "$prime_agent_script_dir/.install-paths") +prime_agent_persisted_paths_invalid=0 +prime_agent_script_dir= +case "$0" in + */*) + prime_agent_script_dir=$(CDPATH= cd -P "$(dirname "$0")" 2>/dev/null && pwd) + ;; +esac +if [ -n "$prime_agent_script_dir" ] && [ -f "$prime_agent_script_dir/.install-paths" ]; then + _persisted_versions=$(sed -n '1p' "$prime_agent_script_dir/.install-paths") + _persisted_symlink=$(sed -n '2p' "$prime_agent_script_dir/.install-paths") + _persisted_cmd=$(sed -n '3p' "$prime_agent_script_dir/.install-paths") + if [ -z "$_persisted_cmd" ]; then + _persisted_cmd=$(basename "$_persisted_symlink") + fi + _persisted_versions_physical= + _persisted_target_dir= + if [ -d "$_persisted_versions" ]; then + _persisted_versions_physical=$(CDPATH= cd -P "$_persisted_versions" 2>/dev/null && pwd) + fi + if [ -L "$_persisted_symlink" ]; then + _persisted_target=$(readlink "$_persisted_symlink" 2>/dev/null || printf '') + if [ -n "$_persisted_target" ] && [ -d "$(dirname "$_persisted_target")" ]; then + _persisted_target_dir=$(CDPATH= cd -P "$(dirname "$_persisted_target")" 2>/dev/null && pwd) + fi + fi + if [ -n "$_persisted_versions_physical" ] && + [ "$_persisted_versions_physical" = "$(dirname "$prime_agent_script_dir")" ] && + [ "$_persisted_target_dir" = "$prime_agent_script_dir" ] && + [ "$(basename "$_persisted_symlink")" = "$_persisted_cmd" ]; then + prime_agent_persisted_versions_dir="$_persisted_versions_physical" + prime_agent_persisted_symlink="$_persisted_symlink" + if [ -z "${PRIME_AGENT_CMD:-}" ]; then + prime_agent_cmd="$_persisted_cmd" + fi + else + prime_agent_persisted_paths_invalid=1 + fi fi prime_agent_default_data_home="${XDG_DATA_HOME:-${HOME:+$HOME/.local/share}}" prime_agent_default_bin_home="${XDG_BIN_HOME:-${HOME:+$HOME/.local/bin}}" prime_agent_binary_versions_dir="${PRIME_AGENT_VERSIONS_DIR:-${prime_agent_persisted_versions_dir:-${prime_agent_default_data_home:+$prime_agent_default_data_home/prime-agent/versions}}}" if [ -n "${PRIME_AGENT_BIN_DIR:-}" ]; then - prime_agent_binary_symlink="${PRIME_AGENT_BIN_DIR%/}/prime-agent" + prime_agent_binary_symlink="${PRIME_AGENT_BIN_DIR%/}/$prime_agent_cmd" elif [ -n "$prime_agent_persisted_symlink" ]; then prime_agent_binary_symlink="$prime_agent_persisted_symlink" else - prime_agent_binary_symlink="${prime_agent_default_bin_home:+$prime_agent_default_bin_home/prime-agent}" + prime_agent_binary_symlink="${prime_agent_default_bin_home:+$prime_agent_default_bin_home/$prime_agent_cmd}" fi prime_agent_esc=$(printf '\033') prime_agent_reset="${prime_agent_esc}[0m" @@ -72,6 +105,7 @@ prime_agent_screen_detail= prime_agent_animation_frame=0 prime_agent_binary_rollback_version= prime_agent_binary_lock_dir= +prime_agent_binary_lock_root= prime_agent_is_update=0 main() { @@ -80,6 +114,10 @@ main() { printf 'Set PRIME_AGENT_DOWNLOAD_BASE_URL or use the installer published by the release workflow.\n' >&2 exit 1 fi + if [ "$prime_agent_persisted_paths_invalid" = 1 ]; then + printf 'error: installed Prime Agent path metadata does not match this sidecar or active command.\n' >&2 + exit 1 + fi if [ -z "$prime_agent_binary_versions_dir" ] || [ -z "$prime_agent_binary_symlink" ]; then printf 'error: HOME is not set; set HOME or explicit PRIME_AGENT_VERSIONS_DIR and PRIME_AGENT_BIN_DIR paths.\n' >&2 exit 1 @@ -154,6 +192,10 @@ prime_agent_cleanup() { rm -rf "$prime_agent_binary_lock_dir" prime_agent_binary_lock_dir= fi + if [ -n "${prime_agent_binary_lock_root:-}" ] && [ -d "$prime_agent_binary_lock_root" ]; then + rmdir "$prime_agent_binary_lock_root" 2>/dev/null || true + prime_agent_binary_lock_root= + fi prime_agent_restore_terminal return "$status" } @@ -928,7 +970,7 @@ prime_agent_binary_canonicalize_install_paths() { prime_agent_binary_acquire_lock() { _versions_dir="$1" - _lock_dir="$_versions_dir/.install.lock" + _lock_root="$_versions_dir/.install-locks" _timeout="${PRIME_AGENT_INSTALL_LOCK_TIMEOUT_SECONDS:-60}" case "$_timeout" in ''|*[!0-9]*) @@ -936,36 +978,76 @@ prime_agent_binary_acquire_lock() { return 1 ;; esac - _waited=0 - while ! mkdir "$_lock_dir" 2>/dev/null; do - _lock_pid= - if [ -f "$_lock_dir/pid" ]; then - _lock_pid=$(sed -n '1p' "$_lock_dir/pid" 2>/dev/null || printf '') - fi - case "$_lock_pid" in - '') - # The owner writes its PID immediately after mkdir. Give that tiny - # publication window one retry before treating the lock as abandoned. - if [ "$_waited" -eq 0 ]; then - sleep 1 - _waited=$((_waited + 1)) - continue - fi - _lock_is_stale=1 - ;; - *[!0-9]*) _lock_is_stale=1 ;; - *) - if kill -0 "$_lock_pid" 2>/dev/null; then - _lock_is_stale=0 - else - _lock_is_stale=1 - fi - ;; - esac - if [ "$_lock_is_stale" = 1 ]; then - rm -rf "$_lock_dir" + mkdir -p "$_lock_root" + prime_agent_binary_lock_root="$_lock_root" + rm -rf "$_lock_root/choosing-$$" + for _entry in "$_lock_root"/*-"$$"; do + [ -d "$_entry" ] || continue + rm -rf "$_entry" + done + _choosing_dir="$_lock_root/choosing-$$" + mkdir "$_choosing_dir" + prime_agent_binary_lock_dir="$_choosing_dir" + + _max_ticket=0 + for _entry in "$_lock_root"/*-*; do + [ -d "$_entry" ] || continue + _entry_name=$(basename "$_entry") + case "$_entry_name" in choosing-*) continue ;; esac + _entry_ticket=${_entry_name%%-*} + _entry_pid=${_entry_name#*-} + case "$_entry_ticket:$_entry_pid" in *[!0-9:]*|:*) continue ;; esac + if ! kill -0 "$_entry_pid" 2>/dev/null; then + rm -rf "$_entry" continue fi + if [ "$_entry_ticket" -gt "$_max_ticket" ]; then + _max_ticket="$_entry_ticket" + fi + done + _my_ticket=$((_max_ticket + 1)) + while :; do + _my_contender="$_lock_root/$_my_ticket-$$" + if mkdir "$_my_contender" 2>/dev/null; then + break + fi + _my_ticket=$((_my_ticket + 1)) + done + rm -rf "$_choosing_dir" + prime_agent_binary_lock_dir="$_my_contender" + + _waited=0 + while :; do + _blocked=0 + for _entry in "$_lock_root"/choosing-*; do + [ -d "$_entry" ] || continue + _entry_pid=${_entry##*-} + case "$_entry_pid" in ''|*[!0-9]*) rm -rf "$_entry"; continue ;; esac + if kill -0 "$_entry_pid" 2>/dev/null; then + _blocked=1 + else + rm -rf "$_entry" + fi + done + for _entry in "$_lock_root"/*-*; do + [ -d "$_entry" ] || continue + _entry_name=$(basename "$_entry") + case "$_entry_name" in choosing-*|"$_my_ticket-$$") continue ;; esac + _entry_ticket=${_entry_name%%-*} + _entry_pid=${_entry_name#*-} + case "$_entry_ticket:$_entry_pid" in *[!0-9:]*|:*) continue ;; esac + if ! kill -0 "$_entry_pid" 2>/dev/null; then + rm -rf "$_entry" + continue + fi + if [ "$_entry_ticket" -lt "$_my_ticket" ] || + { [ "$_entry_ticket" -eq "$_my_ticket" ] && [ "$_entry_pid" -lt "$$" ]; }; then + _blocked=1 + fi + done + if [ "$_blocked" = 0 ]; then + return 0 + fi if [ "$_waited" -ge "$_timeout" ]; then printf 'error: timed out waiting for another Prime Agent install or update to finish.\n' >&2 return 1 @@ -973,15 +1055,13 @@ prime_agent_binary_acquire_lock() { sleep 1 _waited=$((_waited + 1)) done - printf '%s\n' "$$" > "$_lock_dir/pid" - prime_agent_binary_lock_dir="$_lock_dir" } prime_agent_binary_write_install_paths() { _version_dir="$1" _state_path="$_version_dir/.install-paths" _state_tmp="${_state_path}.tmp.$$" - printf '%s\n%s\n' "$prime_agent_binary_versions_dir" "$prime_agent_binary_symlink" > "$_state_tmp" + printf '%s\n%s\n%s\n' "$prime_agent_binary_versions_dir" "$prime_agent_binary_symlink" "$prime_agent_cmd" > "$_state_tmp" mv -f "$_state_tmp" "$_state_path" } @@ -1030,6 +1110,9 @@ prime_agent_binary_fresh_install() { if [ ! -x "$_existing_binary" ] && [ -x "$_version_dir/pi" ]; then _existing_binary="$_version_dir/pi" fi + if [ ! -x "$_existing_binary" ] && [ -x "$_version_dir/prime-agent" ]; then + _existing_binary="$_version_dir/prime-agent" + fi if [ -x "$_existing_binary" ] && prime_agent_binary_validate_layout "$_version_dir" "$_existing_binary"; then prime_agent_binary_write_install_paths "$_version_dir" prime_agent_binary_atomic_symlink "$_existing_binary" "$prime_agent_binary_symlink" @@ -1059,6 +1142,8 @@ prime_agent_binary_fresh_install() { _binary_path="$_version_dir/pi" elif [ -f "$_version_dir/$prime_agent_cmd" ]; then _binary_path="$_version_dir/$prime_agent_cmd" + elif [ -f "$_version_dir/prime-agent" ]; then + _binary_path="$_version_dir/prime-agent" else for _d in "$_version_dir"/*/; do _d="${_d%/}" @@ -1068,6 +1153,9 @@ prime_agent_binary_fresh_install() { elif [ -f "${_d}/$prime_agent_cmd" ]; then _binary_path="${_d}/$prime_agent_cmd" break + elif [ -f "${_d}/prime-agent" ]; then + _binary_path="${_d}/prime-agent" + break fi done fi @@ -1205,6 +1293,8 @@ prime_agent_binary_update() { _binary_path="$_version_dir/pi" elif [ -f "$_version_dir/$prime_agent_cmd" ]; then _binary_path="$_version_dir/$prime_agent_cmd" + elif [ -f "$_version_dir/prime-agent" ]; then + _binary_path="$_version_dir/prime-agent" else for _d in "$_version_dir"/*/; do _d="${_d%/}" @@ -1214,6 +1304,9 @@ prime_agent_binary_update() { elif [ -f "${_d}/$prime_agent_cmd" ]; then _binary_path="${_d}/$prime_agent_cmd" break + elif [ -f "${_d}/prime-agent" ]; then + _binary_path="${_d}/prime-agent" + break fi done fi @@ -1281,6 +1374,8 @@ prime_agent_binary_rollback() { _rollback_binary="$prime_agent_binary_rollback_version/pi" elif [ -f "$prime_agent_binary_rollback_version/$prime_agent_cmd" ]; then _rollback_binary="$prime_agent_binary_rollback_version/$prime_agent_cmd" + elif [ -f "$prime_agent_binary_rollback_version/prime-agent" ]; then + _rollback_binary="$prime_agent_binary_rollback_version/prime-agent" fi if [ -z "$_rollback_binary" ] || [ ! -x "$_rollback_binary" ]; then return 1 @@ -1311,6 +1406,12 @@ prime_agent_verify_binary_checksum() { fi } +prime_agent_shell_quote() { + printf "'" + printf '%s' "$1" | sed "s/'/'\\\\''/g" + printf "'" +} + prime_agent_configure_binary_path() { _installed_version="${1:-}" @@ -1340,16 +1441,18 @@ prime_agent_configure_binary_path() { fi _profile=$(detect_shell_profile) + _quoted_bin_dir=$(prime_agent_shell_quote "$_bin_dir") + _quoted_cmd=$(prime_agent_shell_quote "$prime_agent_cmd") if [ -n "$_profile" ] && [ -w "$_profile" ] 2>/dev/null; then if ! grep -F -q -- "$_bin_dir" "$_profile" 2>/dev/null; then - printf '\nexport PATH="%s:$PATH"\n' "$_bin_dir" >> "$_profile" + printf '\nexport PATH=%s:"$PATH"\n' "$_quoted_bin_dir" >> "$_profile" printf 'Added %s to %s.\n' "$_bin_dir" "$_profile" fi - printf '\nRestart your shell or run: export PATH="%s:$PATH" && %s\n' "$_bin_dir" "$prime_agent_cmd" + printf '\nRestart your shell or run: export PATH=%s:"$PATH" && %s\n' "$_quoted_bin_dir" "$_quoted_cmd" else printf '\nAdd to your shell profile:\n' - printf ' export PATH="%s:$PATH"\n' "$_bin_dir" - printf '\nThen restart your shell and run: %s\n' "$prime_agent_cmd" + printf ' export PATH=%s:"$PATH"\n' "$_quoted_bin_dir" + printf '\nThen restart your shell and run: %s\n' "$_quoted_cmd" fi } diff --git a/packages/coding-agent/test/bun-installer-e2e.test.ts b/packages/coding-agent/test/bun-installer-e2e.test.ts index 52d1c7442d4..05888a8beba 100644 --- a/packages/coding-agent/test/bun-installer-e2e.test.ts +++ b/packages/coding-agent/test/bun-installer-e2e.test.ts @@ -127,6 +127,51 @@ describe("compiled binary installer", () => { expect(readlinkSync(join(root, "bin", "prime-agent"))).toBe(target); }); + test("links a custom command name to the canonical archive executable", () => { + const root = mkdtempSync(join(tmpdir(), "prime-agent-installer-")); + temporaryRoots.push(root); + makeRelease(root, "1.2.3", goodExecutable("1.2.3")); + + const result = runInstaller(root, ["1.2.3"], { PRIME_AGENT_CMD: "pa" }); + expect(result.exitCode, result.stderr).toBe(0); + const command = join(root, "bin", "pa"); + expect(readlinkSync(command)).toContain("v1.2.3/prime-agent"); + expect(spawnSync(command, ["--version"], { encoding: "utf8" }).status).toBe(0); + }); + + test("does not trust install metadata from the working directory in a piped install", () => { + const root = mkdtempSync(join(tmpdir(), "prime-agent-installer-")); + temporaryRoots.push(root); + makeRelease(root, "1.2.3", goodExecutable("1.2.3")); + const attackerDir = join(root, "attacker"); + mkdirSync(attackerDir); + writeFileSync( + join(attackerDir, ".install-paths"), + `${join(root, "attacker-versions")}\n${join(root, "attacker-bin", "prime-agent")}\nprime-agent\n`, + ); + const home = join(root, "piped-home"); + mkdirSync(home); + + const result = spawnSync("sh", ["-s", "--", "1.2.3"], { + cwd: attackerDir, + input: readFileSync(installer), + env: { + ...process.env, + HOME: home, + PRIME_AGENT_DOWNLOAD_BASE_URL: `file://${join(root, "server")}`, + PRIME_AGENT_VERSIONS_DIR: undefined, + PRIME_AGENT_BIN_DIR: undefined, + XDG_DATA_HOME: undefined, + XDG_BIN_HOME: undefined, + TERM: "dumb", + }, + encoding: "utf8", + }); + expect(result.status, result.stderr).toBe(0); + expect(readlinkSync(join(home, ".local", "bin", "prime-agent"))).toContain("v1.2.3/prime-agent"); + expect(() => readlinkSync(join(root, "attacker-bin", "prime-agent"))).toThrow(); + }); + test("self-updates the persisted custom install paths without exported overrides", () => { const root = mkdtempSync(join(tmpdir(), "prime-agent-installer-")); temporaryRoots.push(root); @@ -278,6 +323,8 @@ describe("compiled binary installer", () => { makeRelease(root, "1.2.3", goodExecutable("1.2.3")); expect(runInstaller(root, ["1.2.3"]).exitCode).toBe(0); makeRelease(root, "2.0.0", goodExecutable("2.0.0").replace("then echo", "then sleep 1; echo")); + const staleRoot = join(root, "apps", "versions", ".install-locks"); + mkdirSync(join(staleRoot, "1-99999991"), { recursive: true }); const result = spawnSync( "sh", @@ -293,7 +340,7 @@ describe("compiled binary installer", () => { const link = join(root, "bin", "prime-agent"); expect(readlinkSync(link)).toContain("v2.0.0/prime-agent"); expect(spawnSync(link, ["--version"], { encoding: "utf8" }).status).toBe(0); - expect(() => readFileSync(join(root, "apps", "versions", ".install.lock", "pid"))).toThrow(); + expect(() => readFileSync(join(staleRoot, "1-99999991", "pid"))).toThrow(); }); test("recovers an install lock whose recorded process is gone", () => { @@ -302,16 +349,16 @@ describe("compiled binary installer", () => { makeRelease(root, "1.2.3", goodExecutable("1.2.3")); expect(runInstaller(root, ["1.2.3"]).exitCode).toBe(0); makeRelease(root, "2.0.0", goodExecutable("2.0.0")); - const lockDir = join(root, "apps", "versions", ".install.lock"); - mkdirSync(lockDir); - writeFileSync(join(lockDir, "pid"), "99999999\n"); + const lockRoot = join(root, "apps", "versions", ".install-locks"); + const staleContender = join(lockRoot, "1-99999999"); + mkdirSync(staleContender, { recursive: true }); const result = runInstaller(root, ["--update", "2.0.0"], { PRIME_AGENT_INSTALL_LOCK_TIMEOUT_SECONDS: "1", }); expect(result.exitCode, result.stderr).toBe(0); expect(readlinkSync(join(root, "bin", "prime-agent"))).toContain("v2.0.0/prime-agent"); - expect(() => readFileSync(join(lockDir, "pid"))).toThrow(); + expect(() => readFileSync(join(staleContender, "pid"))).toThrow(); }); test("keeps the previous version when an update is missing a required sidecar", () => { @@ -373,7 +420,7 @@ describe("compiled binary installer", () => { const result = runInstaller(root, ["1.2.3"], { PATH: `${tools}:${process.env.PATH}` }); expect(result.exitCode, result.stderr).toBe(0); expect(result.stderr).toContain("currently shadows the new binary"); - expect(result.stdout).toContain('export PATH="'); + expect(result.stdout).toContain("export PATH='"); }); test("canonicalizes relative version directories before linking", () => { const root = mkdtempSync(join(tmpdir(), "prime-agent-installer-")); From 0bcc0c2e471eaa839f2468d732730d1d05c47832 Mon Sep 17 00:00:00 2001 From: Seth Date: Tue, 1 Sep 2026 20:01:28 -0400 Subject: [PATCH 26/45] chore(ai): refresh generated model catalog --- packages/ai/src/models.generated.ts | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/packages/ai/src/models.generated.ts b/packages/ai/src/models.generated.ts index 2ab1ba6ec96..5c7b432e0c0 100644 --- a/packages/ai/src/models.generated.ts +++ b/packages/ai/src/models.generated.ts @@ -11179,9 +11179,9 @@ export const MODELS = { thinkingLevelMap: {"minimal":null,"low":null,"medium":null,"high":"high","xhigh":"max","max":null}, input: ["text"], cost: { - input: 0.07014000000000001, - output: 0.14028000000000002, - cacheRead: 0.014028, + input: 0.088606, + output: 0.177212, + cacheRead: 0.017721200000000003, cacheWrite: 0, }, contextWindow: 1048576, @@ -11236,9 +11236,9 @@ export const MODELS = { thinkingLevelMap: {"minimal":null,"low":null,"medium":null,"high":"high","xhigh":"max","max":null}, input: ["text"], cost: { - input: 1.0241639999999999, - output: 2.0483279999999997, - cacheRead: 0.085347, + input: 1.04226, + output: 2.08452, + cacheRead: 0.086855, cacheWrite: 0, }, contextWindow: 1048576, @@ -15526,13 +15526,13 @@ export const MODELS = { thinkingLevelMap: {"minimal":null,"low":null,"medium":null,"high":"high","xhigh":"max","max":null}, input: ["text"], cost: { - input: 0.049980000000000004, - output: 0.09996000000000001, - cacheRead: 0.009996000000000001, + input: 0.049999999999999996, + output: 0.16, + cacheRead: 0.013000000000000001, cacheWrite: 0, }, contextWindow: 1310720, - maxTokens: 131072, + maxTokens: 393216, } satisfies Model<"openai-completions">, "~google/gemini-flash-latest": { id: "~google/gemini-flash-latest", From 3acc3abc0aea911cbba766bacb34cb73c9f5930e Mon Sep 17 00:00:00 2001 From: Seth Date: Tue, 1 Sep 2026 20:09:17 -0400 Subject: [PATCH 27/45] ci: make model catalog builds deterministic --- .github/workflows/ci.yml | 10 ---------- package.json | 1 + packages/ai/README.md | 2 ++ packages/ai/package.json | 2 +- 4 files changed, 4 insertions(+), 11 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6a6fec4ae02..4228d9137be 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -59,16 +59,6 @@ jobs: - name: Build run: bun run build - - name: Check model catalog drift - run: | - if ! git diff --exit-code packages/ai/src/models.generated.ts; then - echo "ERROR: Model catalog has drifted from committed state." - echo "Run 'bun run build' in packages/ai to regenerate, then commit the updated models.generated.ts." - echo "Diff:" - git diff packages/ai/src/models.generated.ts - exit 1 - fi - - name: Check run: bun run check diff --git a/package.json b/package.json index 5009177fa15..1835df249a8 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,7 @@ "build": "bun run --cwd packages/tui build && bun run --cwd packages/ai build && bun run --cwd packages/agent build && bun run --cwd packages/coding-agent build", "dev": "bun scripts/dev.ts", "dev:tsc": "bun run --cwd packages/ai dev:tsc", + "models:refresh": "bun run --cwd packages/ai generate-models", "check": "bun --bun biome check --write --error-on-warnings . && bun run check:type && bun run check:installer && bun run check:browser-smoke", "check:installer": "bun scripts/check-installer-render.mjs", "check:browser-smoke": "bun scripts/check-browser-smoke.mjs", diff --git a/packages/ai/README.md b/packages/ai/README.md index b93a6122f20..25f5197a191 100644 --- a/packages/ai/README.md +++ b/packages/ai/README.md @@ -1272,6 +1272,8 @@ Create a new provider file (for example `amazon-bedrock.ts`) that exports: #### 4. Model Generation (`scripts/generate-models.ts`) +Normal builds compile the committed `src/models.generated.ts` snapshot and do not contact live provider APIs. Refresh the snapshot intentionally from the repository root with `bun run models:refresh`, then review and commit the generated diff. Live catalog changes must not make unrelated pull-request builds nondeterministic. + - Add logic to fetch and parse models from the provider's source (e.g., models.dev API) - Map provider model data to the standardized `Model` interface - Handle provider-specific quirks (pricing format, capability flags, model ID transformations) diff --git a/packages/ai/package.json b/packages/ai/package.json index 73ada05d790..08a54930388 100644 --- a/packages/ai/package.json +++ b/packages/ai/package.json @@ -65,7 +65,7 @@ "scripts": { "clean": "bun ../../scripts/remove-paths.ts dist", "generate-models": "bun scripts/generate-models.ts", - "build": "bun run generate-models && bun --bun tsgo -p tsconfig.build.json", + "build": "bun --bun tsgo -p tsconfig.build.json", "dev": "bun --bun tsgo -p tsconfig.build.json --watch --preserveWatchOutput", "dev:tsc": "bun --bun tsgo -p tsconfig.build.json --watch --preserveWatchOutput", "test": "bun ../../scripts/run-with-clean-env.ts bun test --isolate --timeout 30000", From 3272cec102d7d00b7e2a080531fcc4acd8e1f47a Mon Sep 17 00:00:00 2001 From: Seth Date: Tue, 1 Sep 2026 20:12:51 -0400 Subject: [PATCH 28/45] fix: retain installer lock coordination root --- install.sh | 6 ------ packages/coding-agent/test/bun-installer-e2e.test.ts | 3 ++- 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/install.sh b/install.sh index d6b78e5cea9..46a4f406935 100755 --- a/install.sh +++ b/install.sh @@ -105,7 +105,6 @@ prime_agent_screen_detail= prime_agent_animation_frame=0 prime_agent_binary_rollback_version= prime_agent_binary_lock_dir= -prime_agent_binary_lock_root= prime_agent_is_update=0 main() { @@ -192,10 +191,6 @@ prime_agent_cleanup() { rm -rf "$prime_agent_binary_lock_dir" prime_agent_binary_lock_dir= fi - if [ -n "${prime_agent_binary_lock_root:-}" ] && [ -d "$prime_agent_binary_lock_root" ]; then - rmdir "$prime_agent_binary_lock_root" 2>/dev/null || true - prime_agent_binary_lock_root= - fi prime_agent_restore_terminal return "$status" } @@ -979,7 +974,6 @@ prime_agent_binary_acquire_lock() { ;; esac mkdir -p "$_lock_root" - prime_agent_binary_lock_root="$_lock_root" rm -rf "$_lock_root/choosing-$$" for _entry in "$_lock_root"/*-"$$"; do [ -d "$_entry" ] || continue diff --git a/packages/coding-agent/test/bun-installer-e2e.test.ts b/packages/coding-agent/test/bun-installer-e2e.test.ts index 05888a8beba..e70ac65d277 100644 --- a/packages/coding-agent/test/bun-installer-e2e.test.ts +++ b/packages/coding-agent/test/bun-installer-e2e.test.ts @@ -5,6 +5,7 @@ import { chmodSync, mkdirSync, mkdtempSync, + readdirSync, readFileSync, readlinkSync, realpathSync, @@ -340,7 +341,7 @@ describe("compiled binary installer", () => { const link = join(root, "bin", "prime-agent"); expect(readlinkSync(link)).toContain("v2.0.0/prime-agent"); expect(spawnSync(link, ["--version"], { encoding: "utf8" }).status).toBe(0); - expect(() => readFileSync(join(staleRoot, "1-99999991", "pid"))).toThrow(); + expect(readdirSync(staleRoot)).toEqual([]); }); test("recovers an install lock whose recorded process is gone", () => { From df849ab3c82b08d87753dc3d2c766e567c0c875e Mon Sep 17 00:00:00 2001 From: Seth Date: Tue, 1 Sep 2026 21:45:05 -0400 Subject: [PATCH 29/45] feat: upgrade primary runtime to Bun 1.4.0 --- .bun-version | 2 +- .github/workflows/build-binaries.yml | 21 +++++++--- .github/workflows/ci.yml | 42 +++++++++++++------ .github/workflows/nightly-process-stress.yml | 7 +++- AGENTS.md | 2 +- bun.lock | 8 ++-- package.json | 9 ++-- packages/agent/package.json | 2 +- packages/ai/.changes/bun-primary-runtime.md | 2 +- packages/ai/package.json | 2 +- packages/ai/src/providers/amazon-bedrock.ts | 3 ++ packages/coding-agent/docs/quickstart.md | 2 +- packages/coding-agent/package.json | 2 +- .../test/bun-release-archives.test.ts | 2 +- packages/tui/package.json | 2 +- scripts/assemble-release-archives.mjs | 2 +- scripts/build-binaries.sh | 4 +- scripts/check-bun-version.ts | 12 ++++++ 18 files changed, 85 insertions(+), 41 deletions(-) create mode 100644 scripts/check-bun-version.ts diff --git a/.bun-version b/.bun-version index 085c0f2666a..88c5fb891dc 100644 --- a/.bun-version +++ b/.bun-version @@ -1 +1 @@ -1.3.14 +1.4.0 diff --git a/.github/workflows/build-binaries.yml b/.github/workflows/build-binaries.yml index c044db863a4..86e963be7e6 100644 --- a/.github/workflows/build-binaries.yml +++ b/.github/workflows/build-binaries.yml @@ -39,10 +39,13 @@ jobs: fetch-tags: true persist-credentials: false - - name: Setup Bun 1.3.14 + - name: Setup Bun 1.4.0 uses: oven-sh/setup-bun@4bc047ad259df6fc24a6c9b0f9a0cb08cf17fbe5 # v2.0.1 with: - bun-version: 1.3.14 + bun-version: 1.4.0 + + - name: Verify Bun 1.4.0 + run: bun run check:bun-version - name: Resolve release context id: context @@ -137,10 +140,13 @@ jobs: ref: ${{ env.BUILD_REF }} persist-credentials: false - - name: Setup Bun 1.3.14 + - name: Setup Bun 1.4.0 uses: oven-sh/setup-bun@4bc047ad259df6fc24a6c9b0f9a0cb08cf17fbe5 # v2.0.1 with: - bun-version: 1.3.14 + bun-version: 1.4.0 + + - name: Verify Bun 1.4.0 + run: bun run check:bun-version - name: Install system dependencies run: | @@ -287,10 +293,13 @@ jobs: ref: ${{ env.BUILD_REF }} persist-credentials: false - - name: Setup Bun 1.3.14 + - name: Setup Bun 1.4.0 uses: oven-sh/setup-bun@4bc047ad259df6fc24a6c9b0f9a0cb08cf17fbe5 # v2.0.1 with: - bun-version: 1.3.14 + bun-version: 1.4.0 + + - name: Verify Bun 1.4.0 + run: bun run check:bun-version - name: Download production artifacts if: env.PUBLISH_PRODUCTION == 'true' diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4228d9137be..f79c1d0e7a4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -42,10 +42,13 @@ jobs: with: persist-credentials: false - - name: Setup Bun 1.3.14 + - name: Setup Bun 1.4.0 uses: oven-sh/setup-bun@4bc047ad259df6fc24a6c9b0f9a0cb08cf17fbe5 # v2.0.1 with: - bun-version: 1.3.14 + bun-version: 1.4.0 + + - name: Verify Bun 1.4.0 + run: bun run check:bun-version - name: Install system dependencies run: | @@ -102,10 +105,13 @@ jobs: with: persist-credentials: false - - name: Setup Bun 1.3.14 + - name: Setup Bun 1.4.0 uses: oven-sh/setup-bun@4bc047ad259df6fc24a6c9b0f9a0cb08cf17fbe5 # v2.0.1 with: - bun-version: 1.3.14 + bun-version: 1.4.0 + + - name: Verify Bun 1.4.0 + run: bun run check:bun-version - name: Install system dependencies run: | @@ -141,10 +147,13 @@ jobs: with: persist-credentials: false - - name: Setup Bun 1.3.14 + - name: Setup Bun 1.4.0 uses: oven-sh/setup-bun@4bc047ad259df6fc24a6c9b0f9a0cb08cf17fbe5 # v2.0.1 with: - bun-version: 1.3.14 + bun-version: 1.4.0 + + - name: Verify Bun 1.4.0 + run: bun run check:bun-version - name: Install system dependencies run: | @@ -181,10 +190,13 @@ jobs: with: persist-credentials: false - - name: Setup Bun 1.3.14 + - name: Setup Bun 1.4.0 uses: oven-sh/setup-bun@4bc047ad259df6fc24a6c9b0f9a0cb08cf17fbe5 # v2.0.1 with: - bun-version: 1.3.14 + bun-version: 1.4.0 + + - name: Verify Bun 1.4.0 + run: bun run check:bun-version - name: Install system dependencies run: | @@ -219,10 +231,13 @@ jobs: with: persist-credentials: false - - name: Setup Bun 1.3.14 + - name: Setup Bun 1.4.0 uses: oven-sh/setup-bun@4bc047ad259df6fc24a6c9b0f9a0cb08cf17fbe5 # v2.0.1 with: - bun-version: 1.3.14 + bun-version: 1.4.0 + + - name: Verify Bun 1.4.0 + run: bun run check:bun-version - name: Install dependencies (frozen lockfile) run: bun install --frozen-lockfile @@ -252,10 +267,13 @@ jobs: with: persist-credentials: false - - name: Setup Bun 1.3.14 + - name: Setup Bun 1.4.0 uses: oven-sh/setup-bun@4bc047ad259df6fc24a6c9b0f9a0cb08cf17fbe5 # v2.0.1 with: - bun-version: 1.3.14 + bun-version: 1.4.0 + + - name: Verify Bun 1.4.0 + run: bun run check:bun-version - name: Install dependencies for all release platforms run: bun install --frozen-lockfile --os=* --cpu=* diff --git a/.github/workflows/nightly-process-stress.yml b/.github/workflows/nightly-process-stress.yml index 8ea0cfb4c59..41d049f8c9f 100644 --- a/.github/workflows/nightly-process-stress.yml +++ b/.github/workflows/nightly-process-stress.yml @@ -23,10 +23,13 @@ jobs: with: persist-credentials: false - - name: Setup Bun 1.3.14 + - name: Setup Bun 1.4.0 uses: oven-sh/setup-bun@4bc047ad259df6fc24a6c9b0f9a0cb08cf17fbe5 # v2.0.1 with: - bun-version: 1.3.14 + bun-version: 1.4.0 + + - name: Verify Bun 1.4.0 + run: bun run check:bun-version - name: Install system dependencies run: | diff --git a/AGENTS.md b/AGENTS.md index 393fb51cb43..c8fe302c1a9 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -44,7 +44,7 @@ ## Dependencies - A 7-day minimum release age applies to all dependency updates: `bunfig.toml` sets `minimumReleaseAge = 604800` and `.github/dependabot.yml` uses a matching `cooldown`. Never bypass it for routine updates. -- Bun 1.3.14 enforces this setting for dependency updates. +- Bun 1.4.0 enforces this setting for dependency updates. - For an urgent security patch younger than 7 days, override explicitly: `bun add --minimum-release-age=0 `. ## GitHub Workflow diff --git a/bun.lock b/bun.lock index dcae26ccae8..c1cfdb646d3 100644 --- a/bun.lock +++ b/bun.lock @@ -16,7 +16,7 @@ "@vitest/expect": "4.1.10", "@vitest/runner": "4.1.10", "@vitest/spy": "4.1.10", - "bun-types": "1.3.14", + "bun-types": "1.4.0", "husky": "^9.1.7", "jiti": "^2.7.0", "typescript": "^7.0.2", @@ -417,7 +417,7 @@ "buffer-equal-constant-time": ["buffer-equal-constant-time@1.0.1", "", {}, "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA=="], - "bun-types": ["bun-types@1.3.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-4N0ig0fEomHt5R0KCFWjovxow98rIoRwKolrYdCcknNwMekCXRnWEUvgu5soYV8QXtVsrUD8B95MBOZGPvr6KQ=="], + "bun-types": ["bun-types@1.4.0", "", { "dependencies": { "@types/node": "*" } }, "sha512-iIKw23BspnQQYd3prITOBxeUsxBHnwzX6YJfGMuNOZzeNcMmVqzIIVGRm1l69ogaPQmb4wB6BN8mA5bE9YuC5Q=="], "canvas": ["canvas@3.2.3", "", { "dependencies": { "node-addon-api": "^7.0.0", "prebuild-install": "^7.1.3" } }, "sha512-PzE5nJZPz72YUAfo8oTp0u3fqqY7IzlTubneAihqDYAUcBk7ryeCmBbdJBEdaH0bptSOe2VT2Zwcb3UaFyaSWw=="], @@ -665,7 +665,7 @@ "strtok3": ["strtok3@10.3.5", "", { "dependencies": { "@tokenizer/token": "^0.3.0" } }, "sha512-ki4hZQfh5rX0QDLLkOCj+h+CVNkqmp/CMf8v8kZpkNVK6jGQooMytqzLZYUVYIZcFZ6yDB70EfD8POcFXiF5oA=="], - "supports-color": ["supports-color@8.1.1", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q=="], + "supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], "tar-fs": ["tar-fs@2.1.5", "", { "dependencies": { "chownr": "^1.1.1", "mkdirp-classic": "^0.5.2", "pump": "^3.0.0", "tar-stream": "^2.1.4" } }, "sha512-OboTd8mmMhZDNPV+UjQcK9yKAatXu2aJ+r1w4im1Otd4M4fl2hwvdoXUxIYHFTHWK/3y3FarBP70v3vwmGlOxw=="], @@ -769,8 +769,6 @@ "bun-types/@types/node/undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="], - "cli-highlight/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], - "cliui/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], "protobufjs/@types/node/undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="], diff --git a/package.json b/package.json index 1835df249a8..f5dac8ce7d6 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "prime-agent", "private": true, "type": "module", - "packageManager": "bun@1.3.14", + "packageManager": "bun@1.4.0", "workspaces": [ "packages/*", "packages/coding-agent/examples/extensions/with-deps", @@ -16,7 +16,8 @@ "dev": "bun scripts/dev.ts", "dev:tsc": "bun run --cwd packages/ai dev:tsc", "models:refresh": "bun run --cwd packages/ai generate-models", - "check": "bun --bun biome check --write --error-on-warnings . && bun run check:type && bun run check:installer && bun run check:browser-smoke", + "check:bun-version": "bun scripts/check-bun-version.ts", + "check": "bun run check:bun-version && bun --bun biome check --write --error-on-warnings . && bun run check:type && bun run check:installer && bun run check:browser-smoke", "check:installer": "bun scripts/check-installer-render.mjs", "check:browser-smoke": "bun scripts/check-browser-smoke.mjs", "profile:tui": "bun scripts/profile-coding-agent-node.mjs --mode tui", @@ -46,7 +47,7 @@ "@biomejs/biome": "2.5.5", "@types/node": "^22.10.5", "@typescript/native-preview": "7.0.0-dev.20260120.1", - "bun-types": "1.3.14", + "bun-types": "1.4.0", "husky": "^9.1.7", "jiti": "^2.7.0", "typescript": "^7.0.2", @@ -56,7 +57,7 @@ }, "engines": { "node": ">=22.8.0", - "bun": "1.3.14" + "bun": "1.4.0" }, "version": "0.9.1", "dependencies": { diff --git a/packages/agent/package.json b/packages/agent/package.json index a056fb36094..1a55f80f354 100644 --- a/packages/agent/package.json +++ b/packages/agent/package.json @@ -36,7 +36,7 @@ }, "engines": { "node": ">=20.0.0", - "bun": "1.3.14" + "bun": "1.4.0" }, "devDependencies": { "@types/node": "^24.3.0", diff --git a/packages/ai/.changes/bun-primary-runtime.md b/packages/ai/.changes/bun-primary-runtime.md index e0b40ce40f4..f0b83b5388f 100644 --- a/packages/ai/.changes/bun-primary-runtime.md +++ b/packages/ai/.changes/bun-primary-runtime.md @@ -1 +1 @@ -- Made Bun 1.3.14 the primary development and test runtime while preserving Node-compatible package output. +- Made Bun 1.4.0 the primary development and test runtime while preserving Node-compatible package output. diff --git a/packages/ai/package.json b/packages/ai/package.json index 08a54930388..c6a6e323e6e 100644 --- a/packages/ai/package.json +++ b/packages/ai/package.json @@ -103,7 +103,7 @@ }, "engines": { "node": ">=20.0.0", - "bun": "1.3.14" + "bun": "1.4.0" }, "devDependencies": { "@types/node": "^24.3.0", diff --git a/packages/ai/src/providers/amazon-bedrock.ts b/packages/ai/src/providers/amazon-bedrock.ts index 0a540c337ee..76eda44a36f 100644 --- a/packages/ai/src/providers/amazon-bedrock.ts +++ b/packages/ai/src/providers/amazon-bedrock.ts @@ -208,6 +208,9 @@ export const streamBedrock: StreamFunction<"bedrock-converse-stream", BedrockOpt if (nextCommandInput !== undefined) { commandInput = nextCommandInput as typeof commandInput; } + if (options.signal?.aborted) { + throw new Error("Request was aborted"); + } const command = new ConverseStreamCommand(commandInput); const response = await client.send(command, { abortSignal: options.signal }); diff --git a/packages/coding-agent/docs/quickstart.md b/packages/coding-agent/docs/quickstart.md index 9bf314d779d..b1e614f6540 100644 --- a/packages/coding-agent/docs/quickstart.md +++ b/packages/coding-agent/docs/quickstart.md @@ -25,7 +25,7 @@ cd /path/to/project prime-agent ``` -To run a source checkout instead, use Bun 1.3.14: +To run a source checkout instead, use Bun 1.4.0: ```bash git clone https://github.com/PrimeIntellect-ai/prime-agent diff --git a/packages/coding-agent/package.json b/packages/coding-agent/package.json index 7d2896f277a..24212ce9249 100644 --- a/packages/coding-agent/package.json +++ b/packages/coding-agent/package.json @@ -102,6 +102,6 @@ }, "engines": { "node": ">=22.8.0", - "bun": "1.3.14" + "bun": "1.4.0" } } diff --git a/packages/coding-agent/test/bun-release-archives.test.ts b/packages/coding-agent/test/bun-release-archives.test.ts index 8b35dc50ad0..5bb0c74a135 100644 --- a/packages/coding-agent/test/bun-release-archives.test.ts +++ b/packages/coding-agent/test/bun-release-archives.test.ts @@ -103,7 +103,7 @@ describe("compiled release archives", () => { name: "prime-agent", version: "1.2.3", bin: { "prime-agent": "./prime-agent" }, - packageManager: "bun@1.3.14", + packageManager: "bun@1.4.0", }); for (const name of [ "prime-agent", diff --git a/packages/tui/package.json b/packages/tui/package.json index 9f991b1af87..7f6c2000c2f 100644 --- a/packages/tui/package.json +++ b/packages/tui/package.json @@ -33,7 +33,7 @@ }, "engines": { "node": ">=20.0.0", - "bun": "1.3.14" + "bun": "1.4.0" }, "types": "./dist/index.d.ts", "dependencies": { diff --git a/scripts/assemble-release-archives.mjs b/scripts/assemble-release-archives.mjs index ca175a50e64..eba30c05bc2 100755 --- a/scripts/assemble-release-archives.mjs +++ b/scripts/assemble-release-archives.mjs @@ -246,7 +246,7 @@ function renderPackageManifest(path, version) { manifest.name = publicPackageName; manifest.version = version; manifest.bin = { [binaryName]: `./${binaryName}` }; - manifest.packageManager = "bun@1.3.14"; + manifest.packageManager = "bun@1.4.0"; writeFileSync(path, `${JSON.stringify(manifest, null, 2)}\n`); } diff --git a/scripts/build-binaries.sh b/scripts/build-binaries.sh index 18a6915e990..a7dc23c0a96 100755 --- a/scripts/build-binaries.sh +++ b/scripts/build-binaries.sh @@ -25,8 +25,8 @@ set -euo pipefail cd "$(dirname "$0")/.." -# Require Bun 1.3.14 -EXPECTED_BUN_VERSION="1.3.14" +# Require Bun 1.4.0 +EXPECTED_BUN_VERSION="1.4.0" ACTUAL_BUN_VERSION=$(bun --version 2>/dev/null || echo "not-found") if [ "$ACTUAL_BUN_VERSION" != "$EXPECTED_BUN_VERSION" ]; then echo "ERROR: Expected Bun ${EXPECTED_BUN_VERSION}, got ${ACTUAL_BUN_VERSION}" diff --git a/scripts/check-bun-version.ts b/scripts/check-bun-version.ts new file mode 100644 index 00000000000..a4c56aa8956 --- /dev/null +++ b/scripts/check-bun-version.ts @@ -0,0 +1,12 @@ +import { readFileSync } from "node:fs"; +import { resolve } from "node:path"; + +const expected = readFileSync(resolve(import.meta.dir, "../.bun-version"), "utf8").trim(); +const actual = Bun.version; + +if (actual !== expected) { + console.error(`Expected Bun ${expected}, got ${actual}.`); + process.exit(1); +} + +console.log(`Validated Bun ${actual} (${process.execPath})`); From cad37bc2a28a25546fa07a3409c01edc647180a8 Mon Sep 17 00:00:00 2001 From: Seth Date: Tue, 1 Sep 2026 23:18:56 -0400 Subject: [PATCH 30/45] perf: accelerate compiled Bun startup --- .github/workflows/build-binaries.yml | 2 +- .../ai/src/providers/register-builtins.ts | 29 ++++++++++----- packages/ai/test/lazy-module-load.test.ts | 35 +++++++++++++++++++ packages/coding-agent/package.json | 2 +- .../coding-agent/src/bun/register-bedrock.ts | 8 +++-- scripts/build-binaries.sh | 2 +- 6 files changed, 63 insertions(+), 15 deletions(-) diff --git a/.github/workflows/build-binaries.yml b/.github/workflows/build-binaries.yml index 86e963be7e6..d3429868a49 100644 --- a/.github/workflows/build-binaries.yml +++ b/.github/workflows/build-binaries.yml @@ -169,7 +169,7 @@ jobs: for platform in darwin-arm64 darwin-x64 linux-arm64 linux-x64; do echo "Compiling for $platform..." mkdir -p "binaries/$platform" - bun build --compile --external koffi --target="bun-$platform" ./dist/bun/cli.js --outfile "binaries/$platform/pi" + bun build --compile --minify --keep-names --bytecode --format=esm --external koffi --target="bun-$platform" ./dist/bun/cli.js --outfile "binaries/$platform/pi" done - name: Copy binary sidecar assets diff --git a/packages/ai/src/providers/register-builtins.ts b/packages/ai/src/providers/register-builtins.ts index 6447568084a..5ed356d2857 100644 --- a/packages/ai/src/providers/register-builtins.ts +++ b/packages/ai/src/providers/register-builtins.ts @@ -115,17 +115,32 @@ let openAIResponsesProviderModulePromise: let bedrockProviderModuleOverride: | LazyProviderModule<"bedrock-converse-stream", BedrockOptions, SimpleStreamOptions> | undefined; +let bedrockProviderModuleLoaderOverride: (() => Promise) | undefined; let bedrockProviderModulePromise: | Promise> | undefined; -export function setBedrockProviderModule(module: BedrockProviderModule): void { - bedrockProviderModuleOverride = { +function toLazyBedrockProviderModule( + module: BedrockProviderModule, +): LazyProviderModule<"bedrock-converse-stream", BedrockOptions, SimpleStreamOptions> { + return { stream: module.streamBedrock, streamSimple: module.streamSimpleBedrock, }; } +export function setBedrockProviderModule(module: BedrockProviderModule): void { + bedrockProviderModuleLoaderOverride = undefined; + bedrockProviderModulePromise = undefined; + bedrockProviderModuleOverride = toLazyBedrockProviderModule(module); +} + +export function setBedrockProviderModuleLoader(loader: () => Promise): void { + bedrockProviderModuleOverride = undefined; + bedrockProviderModulePromise = undefined; + bedrockProviderModuleLoaderOverride = loader; +} + function forwardStream(target: AssistantMessageEventStream, source: AsyncIterable): void { (async () => { for await (const event of source) { @@ -310,13 +325,9 @@ function loadBedrockProviderModule(): Promise< if (bedrockProviderModuleOverride) { return Promise.resolve(bedrockProviderModuleOverride); } - bedrockProviderModulePromise ||= importNodeOnlyProvider("./amazon-bedrock.js").then((module) => { - const provider = module as BedrockProviderModule; - return { - stream: provider.streamBedrock, - streamSimple: provider.streamSimpleBedrock, - }; - }); + bedrockProviderModulePromise ||= ( + bedrockProviderModuleLoaderOverride?.() ?? importNodeOnlyProvider("./amazon-bedrock.js") + ).then((module) => toLazyBedrockProviderModule(module as BedrockProviderModule)); return bedrockProviderModulePromise; } diff --git a/packages/ai/test/lazy-module-load.test.ts b/packages/ai/test/lazy-module-load.test.ts index 7c707c86841..7b2a3e7db48 100644 --- a/packages/ai/test/lazy-module-load.test.ts +++ b/packages/ai/test/lazy-module-load.test.ts @@ -7,6 +7,7 @@ const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); const packageRoot = resolve(__dirname, ".."); const aiEntryUrl = new URL("../src/index.ts", import.meta.url).href; +const bedrockProviderEntryUrl = new URL("../src/bedrock-provider.ts", import.meta.url).href; const SDK_SPECIFIERS = [ "@anthropic-ai/sdk", @@ -58,6 +59,40 @@ describe("lazy provider module loading", () => { expect(result.loadedSpecifiers).toEqual([]); }); + it("does not load the Bedrock SDK when registering its compiled-binary loader", () => { + const result = runProbe( + `mod.setBedrockProviderModuleLoader(async () => (await import("${bedrockProviderEntryUrl}")).bedrockProviderModule);`, + ); + expect(result.loadedSpecifiers).toEqual([]); + }); + + it("loads the Bedrock SDK when the registered loader is first used", () => { + const result = runProbe( + [ + `mod.setBedrockProviderModuleLoader(async () => {`, + ` await import("${bedrockProviderEntryUrl}");`, + ` const fail = () => { throw new Error("probe"); };`, + ` return { streamBedrock: fail, streamSimpleBedrock: fail };`, + `});`, + `const model = {`, + ` id: "anthropic.claude-3-5-sonnet-20241022-v2:0",`, + ` api: "bedrock-converse-stream",`, + ` provider: "amazon-bedrock",`, + ` baseUrl: "http://127.0.0.1:9",`, + ` reasoning: false,`, + ` input: ["text"],`, + ` cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },`, + ` contextWindow: 200000,`, + ` maxTokens: 8192,`, + `};`, + `const context = { messages: [{ role: "user", content: "hi", timestamp: Date.now() }] };`, + `try { await mod.streamSimple(model, context).result(); } catch (_e) {}`, + ].join("\n"), + ); + + expect(result.loadedSpecifiers).toEqual(["@aws-sdk/client-bedrock-runtime"]); + }); + it("loads only the Anthropic SDK when calling the root lazy wrapper", () => { const result = runProbe( [ diff --git a/packages/coding-agent/package.json b/packages/coding-agent/package.json index 24212ce9249..aee7466abef 100644 --- a/packages/coding-agent/package.json +++ b/packages/coding-agent/package.json @@ -34,7 +34,7 @@ "clean": "bun ../../scripts/remove-paths.ts dist", "dev": "bun --bun tsgo -p tsconfig.build.json --watch --preserveWatchOutput", "build": "bun --bun tsgo -p tsconfig.build.json && bun scripts/copy-assets.ts package && bun run bundle", - "build:binary": "bun run --cwd ../tui build && bun run --cwd ../ai build && bun run --cwd ../agent build && bun run build && bun build --compile ./dist/bun/cli.js --outfile dist/pi && bun scripts/copy-assets.ts binary", + "build:binary": "bun run --cwd ../tui build && bun run --cwd ../ai build && bun run --cwd ../agent build && bun run build && bun build --compile --minify --keep-names --bytecode --format=esm ./dist/bun/cli.js --outfile dist/pi && bun scripts/copy-assets.ts binary", "copy-assets": "bun scripts/copy-assets.ts package", "copy-binary-assets": "bun scripts/copy-assets.ts binary", "test": "bun ../../scripts/run-with-clean-env.ts bun scripts/run-tests.ts", diff --git a/packages/coding-agent/src/bun/register-bedrock.ts b/packages/coding-agent/src/bun/register-bedrock.ts index 92af0dde280..839cbcdb2bb 100644 --- a/packages/coding-agent/src/bun/register-bedrock.ts +++ b/packages/coding-agent/src/bun/register-bedrock.ts @@ -1,4 +1,6 @@ -import { setBedrockProviderModule } from "@earendil-works/pi-ai"; -import { bedrockProviderModule } from "@earendil-works/pi-ai/bedrock-provider"; +import { setBedrockProviderModuleLoader } from "@earendil-works/pi-ai"; -setBedrockProviderModule(bedrockProviderModule); +setBedrockProviderModuleLoader(async () => { + const { bedrockProviderModule } = await import("@earendil-works/pi-ai/bedrock-provider"); + return bedrockProviderModule; +}); diff --git a/scripts/build-binaries.sh b/scripts/build-binaries.sh index a7dc23c0a96..b02ef9bc629 100755 --- a/scripts/build-binaries.sh +++ b/scripts/build-binaries.sh @@ -92,7 +92,7 @@ fi for platform in "${PLATFORMS[@]}"; do echo "Building for $platform..." mkdir -p "binaries/$platform" - bun build --compile --external koffi --target="bun-$platform" ./dist/bun/cli.js --outfile "binaries/$platform/pi" + bun build --compile --minify --keep-names --bytecode --format=esm --external koffi --target="bun-$platform" ./dist/bun/cli.js --outfile "binaries/$platform/pi" done echo "==> Copying sidecar assets..." From 4d41a6291b5fc350de7e1304cc4e8a9faf41c449 Mon Sep 17 00:00:00 2001 From: Seth Date: Wed, 2 Sep 2026 03:12:33 -0400 Subject: [PATCH 31/45] feat: add native Windows support --- .github/workflows/build-binaries.yml | 52 +++- .github/workflows/ci.yml | 75 +++++- README.md | 8 +- install.ps1 | 234 ++++++++++++++++++ package.json | 2 +- .../.changes/windows-native-support.md | 1 + packages/coding-agent/README.md | 6 + packages/coding-agent/docs/quickstart.md | 6 + packages/coding-agent/docs/windows.md | 94 ++++++- packages/coding-agent/scripts/copy-assets.ts | 1 + packages/coding-agent/src/core/exec.ts | 6 +- .../coding-agent/src/core/kernel/bootstrap.ts | 32 ++- .../src/modes/daemon/daemon-socket.ts | 97 ++++++-- .../src/modes/daemon/daemon-supervisor.ts | 3 +- .../coding-agent/src/utils/child-process.ts | 25 +- packages/coding-agent/src/utils/shell.ts | 27 +- .../coding-agent/test/bun-installer.test.ts | 7 +- .../test/bun-release-archives.test.ts | 45 +++- .../coding-agent/test/child-process.test.ts | 20 +- .../coding-agent/test/daemon-socket.test.ts | 24 +- packages/coding-agent/test/exec.test.ts | 25 ++ scripts/assemble-release-archives.mjs | 62 +++-- scripts/build-binaries.sh | 16 +- scripts/check-powershell-installer.mjs | 39 +++ scripts/compiled-artifact-smoke.ts | 4 +- scripts/test-windows-installer.ps1 | 86 +++++++ 26 files changed, 890 insertions(+), 107 deletions(-) create mode 100644 install.ps1 create mode 100644 packages/coding-agent/.changes/windows-native-support.md create mode 100644 scripts/check-powershell-installer.mjs create mode 100644 scripts/test-windows-installer.ps1 diff --git a/.github/workflows/build-binaries.yml b/.github/workflows/build-binaries.yml index d3429868a49..8185ec79f41 100644 --- a/.github/workflows/build-binaries.yml +++ b/.github/workflows/build-binaries.yml @@ -166,7 +166,7 @@ jobs: working-directory: packages/coding-agent run: | mkdir -p binaries - for platform in darwin-arm64 darwin-x64 linux-arm64 linux-x64; do + for platform in darwin-arm64 darwin-x64 linux-arm64 linux-x64 windows-x64 windows-arm64; do echo "Compiling for $platform..." mkdir -p "binaries/$platform" bun build --compile --minify --keep-names --bytecode --format=esm --external koffi --target="bun-$platform" ./dist/bun/cli.js --outfile "binaries/$platform/pi" @@ -325,12 +325,15 @@ jobs: const fs = require("node:fs"); const baseUrl = process.env.INSTALL_BASE_URL; if (!baseUrl) throw new Error("INSTALL_BASE_URL is required"); - const installer = fs.readFileSync("install.sh", "utf8"); - const renderInstaller = (channel) => installer + const renderInstaller = (source, channel) => source .replaceAll("__PRIME_AGENT_DOWNLOAD_BASE_URL__", baseUrl) .replaceAll("__PRIME_AGENT_DEFAULT_RELEASE_CHANNEL__", channel); - fs.writeFileSync("/tmp/prime-agent-install.sh", renderInstaller("stable")); - fs.writeFileSync("/tmp/prime-agent-install-beta.sh", renderInstaller("beta")); + const shellInstaller = fs.readFileSync("install.sh", "utf8"); + const powershellInstaller = fs.readFileSync("install.ps1", "utf8"); + fs.writeFileSync("/tmp/prime-agent-install.sh", renderInstaller(shellInstaller, "stable")); + fs.writeFileSync("/tmp/prime-agent-install-beta.sh", renderInstaller(shellInstaller, "beta")); + fs.writeFileSync("/tmp/prime-agent-install.ps1", renderInstaller(powershellInstaller, "stable")); + fs.writeFileSync("/tmp/prime-agent-install-beta.ps1", renderInstaller(powershellInstaller, "beta")); ' - name: Extract production release notes @@ -353,14 +356,17 @@ jobs: test -n "$R2_BUCKET" test -n "$R2_ENDPOINT_URL" - for artifact in "$PRODUCTION_DIR"/*.tar.gz; do + for artifact in "$PRODUCTION_DIR"/*.tar.gz "$PRODUCTION_DIR"/*.zip; do test -f "$artifact" || continue + content_type="application/gzip" + case "$artifact" in + *.zip) content_type="application/zip" ;; + esac aws s3 cp "$artifact" "s3://${R2_BUCKET}/${RELEASE_PREFIX}/$(basename "$artifact")" \ --endpoint-url "$R2_ENDPOINT_URL" \ - --content-type application/gzip \ + --content-type "$content_type" \ --cache-control 'public, max-age=31536000, immutable' done - aws s3 cp "$PRODUCTION_DIR/SHA256SUMS" "s3://${R2_BUCKET}/${RELEASE_PREFIX}/SHA256SUMS" \ --endpoint-url "$R2_ENDPOINT_URL" \ --content-type text/plain \ @@ -386,6 +392,16 @@ jobs: --content-type text/x-shellscript \ --cache-control no-cache + aws s3 cp /tmp/prime-agent-install.ps1 "s3://${R2_BUCKET}/install.ps1" \ + --endpoint-url "$R2_ENDPOINT_URL" \ + --content-type text/x-powershell \ + --cache-control no-cache + + aws s3 cp /tmp/prime-agent-install-beta.ps1 "s3://${R2_BUCKET}/install-beta.ps1" \ + --endpoint-url "$R2_ENDPOINT_URL" \ + --content-type text/x-powershell \ + --cache-control no-cache + - name: Create production GitHub release if: env.PUBLISH_PRODUCTION == 'true' env: @@ -420,14 +436,17 @@ jobs: test -n "$R2_BUCKET" test -n "$R2_ENDPOINT_URL" - for artifact in "$BETA_DIR"/*.tar.gz; do + for artifact in "$BETA_DIR"/*.tar.gz "$BETA_DIR"/*.zip; do test -f "$artifact" || continue + content_type="application/gzip" + case "$artifact" in + *.zip) content_type="application/zip" ;; + esac aws s3 cp "$artifact" "s3://${R2_BUCKET}/${RELEASE_PREFIX}/$(basename "$artifact")" \ --endpoint-url "$R2_ENDPOINT_URL" \ - --content-type application/gzip \ + --content-type "$content_type" \ --cache-control 'public, max-age=31536000, immutable' done - aws s3 cp "$BETA_DIR/SHA256SUMS" "s3://${R2_BUCKET}/${RELEASE_PREFIX}/SHA256SUMS" \ --endpoint-url "$R2_ENDPOINT_URL" \ --content-type text/plain \ @@ -467,6 +486,16 @@ jobs: --content-type text/x-shellscript \ --cache-control no-cache + aws s3 cp /tmp/prime-agent-install.ps1 "s3://${R2_BUCKET}/install.ps1" \ + --endpoint-url "$R2_ENDPOINT_URL" \ + --content-type text/x-powershell \ + --cache-control no-cache + + aws s3 cp /tmp/prime-agent-install-beta.ps1 "s3://${R2_BUCKET}/install-beta.ps1" \ + --endpoint-url "$R2_ENDPOINT_URL" \ + --content-type text/x-powershell \ + --cache-control no-cache + printf 'Automated beta build from `%s` (`%s`).\n' "$DEFAULT_BRANCH" "$BUILD_REF" > /tmp/beta-release-notes.md if gh api "repos/${GITHUB_REPOSITORY}/git/ref/tags/beta" >/dev/null 2>&1; then @@ -498,3 +527,4 @@ jobs: gh release upload beta "$BETA_DIR"/* --clobber echo "Beta installer: ${R2_PUBLIC_BASE_URL%/}/install-beta.sh" + echo "Windows beta installer: ${R2_PUBLIC_BASE_URL%/}/install-beta.ps1" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f79c1d0e7a4..eeb9635c742 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -4,7 +4,6 @@ on: push: branches: [main] pull_request: - branches: [main] concurrency: group: ci-${{ github.ref }} @@ -292,7 +291,7 @@ jobs: build-check-test: name: build-check-test if: always() && needs.trust.outputs.allowed == 'true' - needs: [trust, build-check, test, process-stress, kernel-heavy, runtime-python, artifact-smoke] + needs: [trust, build-check, test, process-stress, kernel-heavy, runtime-python, artifact-smoke, windows] runs-on: ubuntu-latest steps: - name: Verify CI results @@ -303,6 +302,7 @@ jobs: KERNEL_HEAVY_RESULT: ${{ needs.kernel-heavy.result }} RUNTIME_PYTHON_RESULT: ${{ needs.runtime-python.result }} ARTIFACT_SMOKE_RESULT: ${{ needs.artifact-smoke.result }} + WINDOWS_RESULT: ${{ needs.windows.result }} run: | test "$BUILD_CHECK_RESULT" = success test "$TEST_RESULT" = success @@ -310,3 +310,74 @@ jobs: test "$KERNEL_HEAVY_RESULT" = success test "$RUNTIME_PYTHON_RESULT" = success test "$ARTIFACT_SMOKE_RESULT" = success + test "$WINDOWS_RESULT" = success + + windows: + name: Windows native + needs: trust + if: needs.trust.outputs.allowed == 'true' + runs-on: windows-latest + timeout-minutes: 30 + steps: + - name: Checkout + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + persist-credentials: false + + - name: Setup Bun 1.4.0 + uses: oven-sh/setup-bun@4bc047ad259df6fc24a6c9b0f9a0cb08cf17fbe5 # v2.0.1 + with: + bun-version: 1.4.0 + + - name: Install uv 0.12.4 + uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1 + with: + version: "0.12.4" + + - name: Verify Bun 1.4.0 + run: bun run check:bun-version + + - name: Install dependencies + run: bun install --frozen-lockfile + + - name: Type check + run: bun run check:type + + - name: Validate installers + run: bun run check:installer + + - name: Run Windows behavior tests + working-directory: packages/coding-agent + run: >- + bun test + test/bash-close-hang-windows.test.ts + test/child-process.test.ts + test/daemon-socket.test.ts + test/exec.test.ts + test/kernel-bash-shell.test.ts + test/kernel-bootstrap.test.ts + + - name: Bootstrap the CPython kernel + working-directory: packages/coding-agent + env: + PRIME_AGENT_INSTALL_UV: "0" + run: bun src/core/kernel/bootstrap-cli.ts + + - name: Test the CPython runtime + working-directory: prime-agent-runtime + run: uv run python -m unittest discover -s test + + - name: Build the native Windows executable + run: bun run build:binary + + - name: Test the PowerShell installer end to end + shell: pwsh + run: ./scripts/test-windows-installer.ps1 + + - name: Smoke test the native Windows executable + shell: pwsh + run: | + & ./packages/coding-agent/dist/pi.exe --version + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + & ./packages/coding-agent/dist/pi.exe --help + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } diff --git a/README.md b/README.md index e4b77ad79f5..088b1f1dcbf 100644 --- a/README.md +++ b/README.md @@ -59,7 +59,13 @@ Install the latest stable release on macOS or Linux: curl -fsSL https://app.primeintellect.ai/prime-agent/install.sh | sh ``` -The installer downloads a versioned release, verifies its SHA-256 checksum, installs the `prime-agent` command, and can prepare the Python runtime used by the agent. +On Windows, use PowerShell. WSL is not required; Git Bash is required for shell commands. + +```powershell +irm https://app.primeintellect.ai/prime-agent/install.ps1 | iex +``` + +The installers download a versioned release, verify its SHA-256 checksum, install the `prime-agent` command, and can prepare the Python runtime used by the agent. See [Windows Setup](packages/coding-agent/docs/windows.md) for PATH, update, uninstall, and troubleshooting guidance. Start Prime Agent from the repository or directory you want it to work in: diff --git a/install.ps1 b/install.ps1 new file mode 100644 index 00000000000..f7871b737b3 --- /dev/null +++ b/install.ps1 @@ -0,0 +1,234 @@ +#!/usr/bin/env pwsh +<# +.SYNOPSIS +Install Prime Agent on Windows without WSL. +.DESCRIPTION +Downloads a checksummed Windows release archive, installs it under the current +user's LocalAppData directory, and adds the Prime Agent command directory to the +user PATH. Git Bash is required by Prime Agent's shell tool after installation. +.PARAMETER Version +Install a specific release version. A leading v is accepted. +.PARAMETER Channel +Resolve the current stable or beta release when Version is omitted. +.PARAMETER Update +Install and activate the requested release over the current command shim. +.PARAMETER Uninstall +Remove Prime Agent and its user PATH entry. +#> + +[CmdletBinding()] +param( + [string]$Version, + [ValidateSet("stable", "beta")] + [string]$Channel, + [switch]$Update, + [switch]$Uninstall +) + +Set-StrictMode -Version Latest +$ErrorActionPreference = "Stop" +$ProgressPreference = "SilentlyContinue" + +$DownloadBaseUrl = if ($env:PRIME_AGENT_DOWNLOAD_BASE_URL) { + $env:PRIME_AGENT_DOWNLOAD_BASE_URL.TrimEnd("/") +} else { + "__PRIME_AGENT_DOWNLOAD_BASE_URL__" +} +$DefaultChannel = "__PRIME_AGENT_DEFAULT_RELEASE_CHANNEL__" +if ($DefaultChannel -eq "__PRIME_AGENT_DEFAULT_RELEASE_CHANNEL__") { + $DefaultChannel = "stable" +} +if (-not $Channel) { + $Channel = $DefaultChannel +} + +$CommandName = if ($env:PRIME_AGENT_CMD) { $env:PRIME_AGENT_CMD } else { "prime-agent" } +if ($CommandName -notmatch "^[A-Za-z0-9._-]+$") { + throw "Invalid PRIME_AGENT_CMD value: $CommandName" +} +if (-not $env:LOCALAPPDATA) { + throw "LOCALAPPDATA is required for a per-user Prime Agent installation." +} + +$InstallRoot = Join-Path $env:LOCALAPPDATA "PrimeAgent" +$VersionsDir = Join-Path $InstallRoot "versions" +$BinDir = Join-Path $InstallRoot "bin" +$CommandShim = Join-Path $BinDir "$CommandName.cmd" + +function Write-Step([string]$Message) { + Write-Host "prime-agent: $Message" -ForegroundColor Cyan +} + +function Get-ReleasePlatform { + $architecture = if ($env:PROCESSOR_ARCHITEW6432) { + $env:PROCESSOR_ARCHITEW6432 + } else { + $env:PROCESSOR_ARCHITECTURE + } + switch ($architecture.ToUpperInvariant()) { + "AMD64" { return "windows-x64" } + "ARM64" { return "windows-arm64" } + default { throw "Unsupported Windows architecture: $architecture" } + } +} + +function Assert-ReleaseVersion([string]$Value) { + $normalized = $Value.Trim().TrimStart("v") + if ($normalized -notmatch "^[0-9]+\.[0-9]+\.[0-9]+(?:-[0-9A-Za-z.-]+)?$") { + throw "Invalid Prime Agent release version: $Value" + } + return $normalized +} + +function Resolve-ReleaseVersion { + if ($Version) { + return Assert-ReleaseVersion $Version + } + if ($env:PRIME_AGENT_VERSION) { + return Assert-ReleaseVersion $env:PRIME_AGENT_VERSION + } + $channelUrl = "$DownloadBaseUrl/$Channel" + Write-Step "resolving the $Channel release" + $response = Invoke-WebRequest -Uri $channelUrl -UseBasicParsing -TimeoutSec 30 + return Assert-ReleaseVersion $response.Content +} + +function Add-UserPathEntry([string]$Entry) { + $userPath = [Environment]::GetEnvironmentVariable("PATH", "User") + $entries = @($userPath -split ";" | ForEach-Object { $_.Trim() } | Where-Object { $_ }) + $present = $entries | Where-Object { [string]::Equals($_, $Entry, [System.StringComparison]::OrdinalIgnoreCase) } + if (-not $present) { + $newPath = (@($entries) + $Entry) -join ";" + [Environment]::SetEnvironmentVariable("PATH", $newPath, "User") + } + if (-not (($env:PATH -split ";") | Where-Object { [string]::Equals($_, $Entry, [System.StringComparison]::OrdinalIgnoreCase) })) { + $env:PATH = "$env:PATH;$Entry" + } +} + +function Remove-UserPathEntry([string]$Entry) { + $userPath = [Environment]::GetEnvironmentVariable("PATH", "User") + $entries = @($userPath -split ";" | ForEach-Object { $_.Trim() } | Where-Object { + $_ -and -not [string]::Equals($_, $Entry, [System.StringComparison]::OrdinalIgnoreCase) + }) + [Environment]::SetEnvironmentVariable("PATH", ($entries -join ";"), "User") +} + +function Get-ExpectedChecksum([string]$ChecksumsPath, [string]$ArtifactName) { + foreach ($line in Get-Content -LiteralPath $ChecksumsPath) { + if ($line -match "^([0-9A-Fa-f]{64})\s+\*?(.+)$" -and $Matches[2].Trim() -eq $ArtifactName) { + return $Matches[1].ToLowerInvariant() + } + } + throw "SHA256SUMS does not contain $ArtifactName" +} + +function Assert-ArchiveLayout([string]$Directory) { + $required = @( + "prime-agent.exe", + "package.json", + "README.md", + "CHANGELOG.md", + "install.sh", + "install.ps1", + "photon_rs_bg.wasm", + "prime-agent-runtime", + "skills", + "theme", + "assets", + "export-html", + "docs", + "examples" + ) + foreach ($name in $required) { + if (-not (Test-Path -LiteralPath (Join-Path $Directory $name))) { + throw "Release archive is missing required sidecar: $name" + } + } +} + +function Set-ActiveVersion([string]$VersionDirectory) { + New-Item -ItemType Directory -Path $BinDir -Force | Out-Null + $binaryPath = Join-Path $VersionDirectory "prime-agent.exe" + $temporaryShim = Join-Path $BinDir "$CommandName.cmd.$([guid]::NewGuid().ToString("N")).tmp" + $shim = "@echo off`r`n`"$binaryPath`" %*`r`n" + $backupShim = "$CommandShim.backup" + try { + Set-Content -LiteralPath $temporaryShim -Value $shim -Encoding Ascii -NoNewline + if (Test-Path -LiteralPath $CommandShim) { + [System.IO.File]::Replace($temporaryShim, $CommandShim, $backupShim, $true) + Remove-Item -LiteralPath $backupShim -Force -ErrorAction SilentlyContinue + } else { + Move-Item -LiteralPath $temporaryShim -Destination $CommandShim + } + } finally { + Remove-Item -LiteralPath $temporaryShim -Force -ErrorAction SilentlyContinue + } + Add-UserPathEntry $BinDir +} + +function Install-Release([string]$TargetVersion) { + $platform = Get-ReleasePlatform + $artifactName = "prime-agent-$TargetVersion-$platform.zip" + $releaseUrl = "$DownloadBaseUrl/releases/v$TargetVersion" + $versionDirectory = Join-Path $VersionsDir "v$TargetVersion" + + if ((Test-Path -LiteralPath (Join-Path $versionDirectory "prime-agent.exe")) -and + (Test-Path -LiteralPath (Join-Path $versionDirectory "package.json"))) { + Set-ActiveVersion $versionDirectory + Write-Step "Prime Agent v$TargetVersion is active" + return + } + + $temporaryRoot = Join-Path ([System.IO.Path]::GetTempPath()) "prime-agent-install-$([guid]::NewGuid().ToString("N"))" + $archivePath = Join-Path $temporaryRoot $artifactName + $checksumsPath = Join-Path $temporaryRoot "SHA256SUMS" + $stagingDirectory = Join-Path $temporaryRoot "staging" + + New-Item -ItemType Directory -Path $temporaryRoot -Force | Out-Null + try { + Write-Step "downloading Prime Agent v$TargetVersion for $platform" + Invoke-WebRequest -Uri "$releaseUrl/$artifactName" -OutFile $archivePath -UseBasicParsing -TimeoutSec 120 + Invoke-WebRequest -Uri "$releaseUrl/SHA256SUMS" -OutFile $checksumsPath -UseBasicParsing -TimeoutSec 30 + + $expectedHash = Get-ExpectedChecksum $checksumsPath $artifactName + $actualHash = (Get-FileHash -LiteralPath $archivePath -Algorithm SHA256).Hash.ToLowerInvariant() + if ($actualHash -ne $expectedHash) { + throw "SHA-256 mismatch for ${artifactName}: expected $expectedHash, got $actualHash" + } + + Expand-Archive -LiteralPath $archivePath -DestinationPath $stagingDirectory -Force + Assert-ArchiveLayout $stagingDirectory + + New-Item -ItemType Directory -Path $VersionsDir -Force | Out-Null + if (Test-Path -LiteralPath $versionDirectory) { + Remove-Item -LiteralPath $versionDirectory -Recurse -Force + } + Move-Item -LiteralPath $stagingDirectory -Destination $versionDirectory + Set-ActiveVersion $versionDirectory + Write-Step "installed Prime Agent v$TargetVersion" + } finally { + Remove-Item -LiteralPath $temporaryRoot -Recurse -Force -ErrorAction SilentlyContinue + } +} + +function Uninstall-PrimeAgent { + Remove-UserPathEntry $BinDir + Remove-Item -LiteralPath $InstallRoot -Recurse -Force -ErrorAction SilentlyContinue + Write-Step "uninstalled Prime Agent" +} + +if ($Uninstall) { + Uninstall-PrimeAgent + exit 0 +} +if ($DownloadBaseUrl -eq "__PRIME_AGENT_DOWNLOAD_BASE_URL__") { + throw "Installer download URL is not configured. Use the published installer or set PRIME_AGENT_DOWNLOAD_BASE_URL." +} + +$targetVersion = Resolve-ReleaseVersion +Install-Release $targetVersion +if ($Update) { + Write-Step "update complete" +} +Write-Host "Run '$CommandName' in a new terminal. Git Bash is required for shell commands." -ForegroundColor Green diff --git a/package.json b/package.json index f5dac8ce7d6..dbbab7e11d4 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,7 @@ "models:refresh": "bun run --cwd packages/ai generate-models", "check:bun-version": "bun scripts/check-bun-version.ts", "check": "bun run check:bun-version && bun --bun biome check --write --error-on-warnings . && bun run check:type && bun run check:installer && bun run check:browser-smoke", - "check:installer": "bun scripts/check-installer-render.mjs", + "check:installer": "bun scripts/check-installer-render.mjs && bun scripts/check-powershell-installer.mjs", "check:browser-smoke": "bun scripts/check-browser-smoke.mjs", "profile:tui": "bun scripts/profile-coding-agent-node.mjs --mode tui", "profile:rpc": "bun scripts/profile-coding-agent-node.mjs --mode rpc", diff --git a/packages/coding-agent/.changes/windows-native-support.md b/packages/coding-agent/.changes/windows-native-support.md new file mode 100644 index 00000000000..64c4909d3bd --- /dev/null +++ b/packages/coding-agent/.changes/windows-native-support.md @@ -0,0 +1 @@ +- Added native Windows installation, release archives, process cleanup, daemon named-pipe coordination, CPython bootstrap, and CI coverage without requiring WSL. diff --git a/packages/coding-agent/README.md b/packages/coding-agent/README.md index 21af060ac1f..0fdd4b2417f 100644 --- a/packages/coding-agent/README.md +++ b/packages/coding-agent/README.md @@ -46,6 +46,12 @@ Prime Agent began as a hard fork of [pi-mono](https://github.com/badlogic/pi-mon curl -fsSL https://app.primeintellect.ai/prime-agent/install.sh | sh ``` +On Windows, install natively from PowerShell. WSL is not required; Git Bash is required for shell commands. + +```powershell +irm https://app.primeintellect.ai/prime-agent/install.ps1 | iex +``` + To install the beta built from the latest commit on `main`: ```bash diff --git a/packages/coding-agent/docs/quickstart.md b/packages/coding-agent/docs/quickstart.md index b1e614f6540..d6fb8d0f182 100644 --- a/packages/coding-agent/docs/quickstart.md +++ b/packages/coding-agent/docs/quickstart.md @@ -10,6 +10,12 @@ Install the latest stable release on Linux or macOS: curl -fsSL https://app.primeintellect.ai/prime-agent/install.sh | sh ``` +On Windows, use PowerShell. This is a native install and does not require WSL. Install Git Bash for shell commands. + +```powershell +irm https://app.primeintellect.ai/prime-agent/install.ps1 | iex +``` + To try the latest beta built from `main`: ```bash diff --git a/packages/coding-agent/docs/windows.md b/packages/coding-agent/docs/windows.md index 3f7da6c61dc..33d70d9171d 100644 --- a/packages/coding-agent/docs/windows.md +++ b/packages/coding-agent/docs/windows.md @@ -1,17 +1,97 @@ # Windows Setup -Prime Agent requires a bash shell on Windows. Checked locations (in order): +Prime Agent supports native Windows 10 and 11 on x64 and Arm64. WSL is not required. -1. Custom path from `~/.prime/agent/settings.json` -2. Git Bash (`C:\Program Files\Git\bin\bash.exe`) -3. `bash.exe` on PATH (Cygwin, MSYS2, WSL) +## Requirements -For most users, [Git for Windows](https://git-scm.com/download/win) is sufficient. +- PowerShell 5.1 or newer for installation and updates +- [Git for Windows](https://git-scm.com/download/win), including Git Bash, for shell commands +- Windows Terminal is recommended -## Custom Shell Path +Prime Agent itself runs as a native Bun-compiled Windows executable. Git Bash is the command shell used by the `bash` tool; it does not run Prime Agent under WSL. + +## Install + +Open PowerShell and run: + +```powershell +irm https://app.primeintellect.ai/prime-agent/install.ps1 | iex +``` + +The installer downloads the x64 or Arm64 ZIP archive, requires a matching SHA-256 entry, installs versioned files under `%LOCALAPPDATA%\PrimeAgent`, and adds `%LOCALAPPDATA%\PrimeAgent\bin` to your user `PATH`. + +Open a new terminal after the first install, then run: + +```powershell +prime-agent +``` + +To install the beta channel: + +```powershell +irm https://app.primeintellect.ai/prime-agent/install-beta.ps1 | iex +``` + +## Update or select a version + +```powershell +& ([scriptblock]::Create((irm https://app.primeintellect.ai/prime-agent/install.ps1))) -Update +& ([scriptblock]::Create((irm https://app.primeintellect.ai/prime-agent/install.ps1))) -Version 0.9.1 +``` + +Downloads and extraction finish before the command shim changes. A failed download, checksum, extraction, or activation leaves the previous version active. + +## Uninstall + +```powershell +& ([scriptblock]::Create((irm https://app.primeintellect.ai/prime-agent/install.ps1))) -Uninstall +``` + +This removes `%LOCALAPPDATA%\PrimeAgent` and its entry from your user `PATH`. Open a new terminal to see the updated `PATH`. + +## Git Bash discovery + +Prime Agent checks shell locations in this order: + +1. `shellPath` in `~/.prime/agent/settings.json` +2. `C:\Program Files\Git\bin\bash.exe` +3. `C:\Program Files (x86)\Git\bin\bash.exe` +4. `bash.exe` on `PATH` + +A custom shell can be configured as follows: ```json { - "shellPath": "C:\\cygwin64\\bin\\bash.exe" + "shellPath": "C:\\Program Files\\Git\\bin\\bash.exe" } ``` + +Git Bash is the supported default. Cygwin and MSYS2 can be selected explicitly, but they are not part of the primary Windows validation path. WSL paths are not used by the native installer. + +## Python kernel + +The persistent CPython kernel is prepared automatically with `uv`. The Windows bootstrap uses PowerShell and a virtual environment at `~/.prime/agent/kernel-venv`, whose interpreter is under `Scripts\python.exe`. + +On managed systems that block the uv installer, install uv separately or set `PRIME_AGENT_KERNEL_PYTHON` to a CPython environment that already contains `prime-agent-runtime`. + +## Troubleshooting + +### `prime-agent` is not recognized + +Open a new terminal. Confirm that `%LOCALAPPDATA%\PrimeAgent\bin` appears in your user `PATH` and that `prime-agent.cmd` exists there. + +### No bash shell found + +Install Git for Windows with Git Bash. If it is installed in a custom directory, set `shellPath` in `~/.prime/agent/settings.json`. + +### PowerShell blocks the installer + +Run the command in a normal interactive PowerShell session. Organization policy can block downloaded scripts or remote content. In that case, download `install.ps1`, review it, and run it according to your organization's policy. + +### A child process survives cancellation + +Run `prime-agent shutdown --force`. Prime Agent uses Windows process-tree termination and CPython Job Objects, but a process moved into a separately managed Windows service or job can require manual termination. + +### Terminal input or colors are incorrect + +Use an updated Windows Terminal profile. See [Terminal Setup](terminal-setup.md) for the recommended key mappings. diff --git a/packages/coding-agent/scripts/copy-assets.ts b/packages/coding-agent/scripts/copy-assets.ts index 0fdee57c1e2..b0be2b34af0 100644 --- a/packages/coding-agent/scripts/copy-assets.ts +++ b/packages/coding-agent/scripts/copy-assets.ts @@ -50,6 +50,7 @@ async function copyBinaryAssets(): Promise { await copyFile(join(packageDir, name), distDir); } await copyFile(join(repoDir, "install.sh"), distDir); + await copyFile(join(repoDir, "install.ps1"), distDir); await copyFiles(join(packageDir, "src/modes/interactive/theme"), join(distDir, "theme"), ".json"); await copyFiles(join(packageDir, "src/modes/interactive/assets"), join(distDir, "assets"), ".png"); for (const name of ["template.html", "template.css", "template.js"]) { diff --git a/packages/coding-agent/src/core/exec.ts b/packages/coding-agent/src/core/exec.ts index c3cc4080683..47fb0597bb7 100644 --- a/packages/coding-agent/src/core/exec.ts +++ b/packages/coding-agent/src/core/exec.ts @@ -3,7 +3,7 @@ */ import { spawn } from "node:child_process"; -import { waitForChildProcess } from "../utils/child-process.js"; +import { signalProcessGroupOrProcess, waitForChildProcess } from "../utils/child-process.js"; /** * Options for executing shell commands. @@ -80,7 +80,9 @@ export async function execCommand( forceKillTimeoutId = setTimeout(() => { forceKillTimeoutId = undefined; if (proc.exitCode === null && proc.signalCode === null) { - proc.kill("SIGKILL"); + if (proc.pid) { + signalProcessGroupOrProcess(proc.pid, "SIGKILL"); + } } }, 5000); } diff --git a/packages/coding-agent/src/core/kernel/bootstrap.ts b/packages/coding-agent/src/core/kernel/bootstrap.ts index 3c33cf8426b..a2533875c1f 100644 --- a/packages/coding-agent/src/core/kernel/bootstrap.ts +++ b/packages/coding-agent/src/core/kernel/bootstrap.ts @@ -34,7 +34,10 @@ const DEFAULT_RLM_EXTRA_PACKAGES = [ export const DEFAULT_RLM_EXTRA_UV_ARGS = DEFAULT_RLM_EXTRA_PACKAGES.map((pkg) => pkg.uvArg); export const DEFAULT_RLM_EXTRA_IMPORT_NAMES = DEFAULT_RLM_EXTRA_PACKAGES.map((pkg) => pkg.importName); export const DEFAULT_RLM_EXTRA_IMPORT_LABELS = DEFAULT_RLM_EXTRA_PACKAGES.map((pkg) => pkg.promptLabel); -const UV_INSTALL_COMMAND = "curl -LsSf https://astral.sh/uv/install.sh | sh"; +const IS_WINDOWS = process.platform === "win32"; +const UV_INSTALL_COMMAND_POSIX = "curl -LsSf https://astral.sh/uv/install.sh | sh"; +const UV_INSTALL_COMMAND_WINDOWS = + 'powershell -NoProfile -ExecutionPolicy Bypass -Command "irm https://astral.sh/uv/install.ps1 | iex"'; const REQUIRED_HARNESS_METHODS = [ "create_memory", "update_memory", @@ -340,6 +343,11 @@ export function getKernelVenvDir(): string { return path.join(os.homedir(), ".prime", "agent", "kernel-venv"); } +/** Python executable path inside a venv directory, respecting Windows layout. */ +function venvPythonPath(venv: string): string { + return IS_WINDOWS ? path.join(venv, "Scripts", "python.exe") : path.join(venv, "bin", "python"); +} + function getXdgKernelVenvDir(): string { const dataHome = process.env.XDG_DATA_HOME ? path.resolve(expandHome(process.env.XDG_DATA_HOME)) @@ -515,25 +523,35 @@ async function ensureUv(options: EnsureKernelPythonOptions): Promise { const shouldInstallUv = process.env.PRIME_AGENT_INSTALL_UV === "1" || (!options.onProgress && (await confirmUvInstall())); if (!shouldInstallUv) { + const command = IS_WINDOWS ? UV_INSTALL_COMMAND_WINDOWS : UV_INSTALL_COMMAND_POSIX; throw new Error( - `uv is required to set up the Python kernel. Install uv yourself: ${UV_INSTALL_COMMAND}, ` + + `uv is required to set up the Python kernel. Install uv yourself: ${command}, ` + "or set PRIME_AGENT_INSTALL_UV=1 to let prime-agent run that installer.", ); } reportProgress(options, "› installing uv (one-time)…"); try { - await run("sh", ["-c", UV_INSTALL_COMMAND], { stdio: options.onProgress ? "ignore" : "inherit" }); + if (IS_WINDOWS) { + await run( + "powershell", + ["-NoProfile", "-ExecutionPolicy", "Bypass", "-Command", "irm https://astral.sh/uv/install.ps1 | iex"], + { stdio: options.onProgress ? "ignore" : "inherit" }, + ); + } else { + await run("sh", ["-c", UV_INSTALL_COMMAND_POSIX], { stdio: options.onProgress ? "ignore" : "inherit" }); + } } catch (error) { + const command = IS_WINDOWS ? UV_INSTALL_COMMAND_WINDOWS : UV_INSTALL_COMMAND_POSIX; throw new Error( - `couldn't install uv from astral.sh; install it yourself: ${UV_INSTALL_COMMAND}, then re-run prime-agent. ${errorMessage(error)}`, + `couldn't install uv from astral.sh; install it yourself: ${command}, then re-run prime-agent. ${errorMessage(error)}`, ); } if (await isExecutable(localUv)) return localUv; const installedFromPath = await findExecutable("uv"); if (installedFromPath) return installedFromPath; - throw new Error("uv install completed but binary not found at ~/.local/bin/uv"); + throw new Error(`uv install completed but binary not found at ${localUv}`); } async function confirmUvInstall(): Promise { @@ -716,7 +734,7 @@ async function bootstrapVenv( ): Promise { await mkdir(path.dirname(venv), { recursive: true }); const uv = await ensureUv(options); - const python = path.join(venv, "bin", "python"); + const python = venvPythonPath(venv); const sourceDir = await resolveRuntimeSourceDir(); const runtimeRequirement = sourceDir ?? RUNTIME_REQUIREMENT; const runtimeIdentity = await resolveRuntimeIdentity(); @@ -873,7 +891,7 @@ async function ensureKernelPythonUncached( } const venv = await resolveWritableKernelVenvDir(); - const python = path.join(venv, "bin", "python"); + const python = venvPythonPath(venv); const runtimeIdentity = await resolveRuntimeIdentity(); if (await kernelReady(python, venv, runtimeIdentity, pythonSkills)) return python; diff --git a/packages/coding-agent/src/modes/daemon/daemon-socket.ts b/packages/coding-agent/src/modes/daemon/daemon-socket.ts index 753595265c2..4e18f219f09 100644 --- a/packages/coding-agent/src/modes/daemon/daemon-socket.ts +++ b/packages/coding-agent/src/modes/daemon/daemon-socket.ts @@ -1,6 +1,7 @@ +import { createHash } from "node:crypto"; import { chmodSync, existsSync, lstatSync, mkdirSync, unlinkSync } from "node:fs"; import { createConnection, type Socket } from "node:net"; -import { tmpdir } from "node:os"; +import { tmpdir, userInfo } from "node:os"; import { dirname, join } from "node:path"; import lockfile from "proper-lockfile"; @@ -14,6 +15,31 @@ const DAEMON_SOCKET_LOCK_STALE_MS = 5000; const DAEMON_SOCKET_LOCK_UPDATE_MS = 1000; const DAEMON_SOCKET_FLUSH_TIMEOUT_MS = 1000; +/** Map a Windows named pipe to a per-user filesystem lock target. */ +function windowsPipeLockPath(socketPath: string): string { + const pipeHash = createHash("sha256").update(socketPath.toLowerCase()).digest("hex").slice(0, 24); + return join(defaultDaemonSocketDir(), `pipe-${pipeHash}`); +} + +/** Probe a Windows named pipe by attempting a short-lived connection. */ +function canConnectToWindowsPipe(pipePath: string): Promise { + return new Promise((resolveConnect) => { + const socket = createConnection(pipePath); + let settled = false; + const finish = (canConnect: boolean) => { + if (settled) return; + settled = true; + clearTimeout(timeoutId); + socket.removeAllListeners(); + socket.destroy(); + resolveConnect(canConnect); + }; + const timeoutId = setTimeout(() => finish(false), 250); + socket.once("connect", () => finish(true)); + socket.once("error", () => finish(false)); + }); +} + export function endDaemonSocketAfterFlush(socket: Socket): void { if (socket.destroyed) return; const forceCloseTimer = setTimeout(() => socket.destroy(), DAEMON_SOCKET_FLUSH_TIMEOUT_MS); @@ -80,9 +106,14 @@ export interface DaemonSocketIdentity { ino: number; } +export function windowsNamedPipeUserScope(): string { + const user = userInfo(); + return createHash("sha256").update(`${user.username}\0${user.homedir}`).digest("hex").slice(0, 16); +} + export function defaultDaemonSocketPath(): string { if (process.platform === "win32") { - return "\\\\.\\pipe\\prime-agent-daemon"; + return `\\\\.\\pipe\\prime-agent-daemon-${windowsNamedPipeUserScope()}`; } return join(defaultDaemonSocketDir(), "daemon.sock"); } @@ -90,7 +121,29 @@ export function defaultDaemonSocketPath(): string { export async function acquireDaemonSocketPathLease(socketPath: string): Promise { ensureDefaultDaemonSocketDir(socketPath); if (process.platform === "win32") { - return undefined; + // Use a file-based lock for Windows named pipes since proper-lockfile + // requires a filesystem path. + const lockPath = windowsPipeLockPath(socketPath); + let lease: DaemonSocketPathLease | undefined; + let pendingCompromise: Error | undefined; + const releaseLock = await lockfile.lock(lockPath, { + realpath: false, + stale: DAEMON_SOCKET_LOCK_STALE_MS, + update: DAEMON_SOCKET_LOCK_UPDATE_MS, + onCompromised: (error) => { + if (lease) lease.recordCompromise(error); + else pendingCompromise = error; + }, + retries: { + retries: 600, + factor: 1, + minTimeout: DAEMON_SOCKET_RELEASE_POLL_MS, + maxTimeout: DAEMON_SOCKET_RELEASE_POLL_MS, + }, + }); + lease = new DaemonSocketPathLease(socketPath, releaseLock); + if (pendingCompromise) lease.recordCompromise(pendingCompromise); + return lease; } let lease: DaemonSocketPathLease | undefined; let pendingCompromise: Error | undefined; @@ -118,6 +171,11 @@ export async function prepareDaemonSocketPath(socketPath: string, lease?: Daemon ensureDefaultDaemonSocketDir(socketPath); if (process.platform === "win32") { + // Windows named pipes have no filesystem artifact. Check if a server + // is already listening on this pipe name; reject if so. + if (await canConnectToWindowsPipe(socketPath)) { + throw new Error(`Daemon socket already in use: ${socketPath}`); + } return; } if (lease) { @@ -191,6 +249,7 @@ async function prepareUnixDaemonSocketPath(socketPath: string, lease?: DaemonSoc export function restrictDaemonSocketPath(socketPath: string): void { if (process.platform === "win32") { + // Named pipes have no chmod-equivalent filesystem mode. return; } chmodSync(socketPath, DAEMON_SOCKET_MODE); @@ -198,6 +257,7 @@ export function restrictDaemonSocketPath(socketPath: string): void { export function getDaemonSocketIdentity(socketPath: string): DaemonSocketIdentity | undefined { if (process.platform === "win32") { + // Windows named pipes do not expose inode identity. return undefined; } const stat = lstatSync(socketPath); @@ -210,6 +270,8 @@ export function cleanupDaemonSocketPath( lease?: DaemonSocketPathLease, ): void { if (process.platform === "win32") { + // Named pipes have no filesystem artifact to clean up. The lock file + // is released when the lease is released. return; } if (lease) { @@ -290,7 +352,12 @@ function assertSocketLeaseHeld(socketPath: string, lease: DaemonSocketPathLease) } export function defaultDaemonSocketDir(): string { - const suffix = typeof process.getuid === "function" ? String(process.getuid()) : "user"; + const suffix = + process.platform === "win32" + ? windowsNamedPipeUserScope() + : typeof process.getuid === "function" + ? String(process.getuid()) + : "user"; // Bun caches os.tmpdir() at startup. Read the standard overrides here so // isolated processes and tests can select a runtime temporary directory. const runtimeTmpDir = process.env.TMPDIR || process.env.TMP || process.env.TEMP || tmpdir(); @@ -298,24 +365,24 @@ export function defaultDaemonSocketDir(): string { } function ensureDefaultDaemonSocketDir(socketPath: string): void { - if (process.platform === "win32" || dirname(socketPath) !== defaultDaemonSocketDir()) { - return; - } + const socketDir = defaultDaemonSocketDir(); + if (process.platform !== "win32" && dirname(socketPath) !== socketDir) return; - if (!existsSync(defaultDaemonSocketDir())) { - mkdirSync(defaultDaemonSocketDir(), { recursive: true, mode: DAEMON_SOCKET_DIR_MODE }); + if (!existsSync(socketDir)) { + mkdirSync(socketDir, { recursive: true, mode: DAEMON_SOCKET_DIR_MODE }); } - const stat = lstatSync(defaultDaemonSocketDir()); + const stat = lstatSync(socketDir); if (!stat.isDirectory()) { - throw new Error(`Daemon socket directory exists and is not a directory: ${defaultDaemonSocketDir()}`); + throw new Error(`Daemon socket directory exists and is not a directory: ${socketDir}`); } - if (typeof process.getuid === "function" && stat.uid !== process.getuid()) { - throw new Error(`Daemon socket directory is not owned by the current user: ${defaultDaemonSocketDir()}`); + if (process.platform !== "win32") { + if (typeof process.getuid === "function" && stat.uid !== process.getuid()) { + throw new Error(`Daemon socket directory is not owned by the current user: ${socketDir}`); + } + chmodSync(socketDir, DAEMON_SOCKET_DIR_MODE); } - - chmodSync(defaultDaemonSocketDir(), DAEMON_SOCKET_DIR_MODE); } function canConnectToUnixSocket(socketPath: string): Promise { diff --git a/packages/coding-agent/src/modes/daemon/daemon-supervisor.ts b/packages/coding-agent/src/modes/daemon/daemon-supervisor.ts index c50c81b8bbc..3b9fd058d5e 100644 --- a/packages/coding-agent/src/modes/daemon/daemon-supervisor.ts +++ b/packages/coding-agent/src/modes/daemon/daemon-supervisor.ts @@ -118,6 +118,7 @@ import { normalizeSocketPath, prepareDaemonSocketPath, restrictDaemonSocketPath, + windowsNamedPipeUserScope, } from "./daemon-socket.js"; import { acquireDaemonSupervisorOwnership, @@ -646,7 +647,7 @@ export function idleEvictionSweepIntervalMs(idleEvictionMinutes: IdleEvictionMin function workerSocketPath(supervisorSocketPath: string, workerId: string): string { const key = descriptorKey(supervisorSocketPath); if (process.platform === "win32") { - return `\\\\.\\pipe\\prime-agent-worker-${key}-${workerId.slice(0, 12)}`; + return `\\\\.\\pipe\\prime-agent-worker-${windowsNamedPipeUserScope()}-${key}-${workerId.slice(0, 12)}`; } return join(defaultDaemonSocketDir(), `worker-${key}-${workerId.slice(0, 12)}.sock`); } diff --git a/packages/coding-agent/src/utils/child-process.ts b/packages/coding-agent/src/utils/child-process.ts index 3f587327f8d..cd4cfbec39e 100644 --- a/packages/coding-agent/src/utils/child-process.ts +++ b/packages/coding-agent/src/utils/child-process.ts @@ -1,4 +1,4 @@ -import { type ChildProcess, execFileSync } from "node:child_process"; +import { type ChildProcess, execFileSync, spawn } from "node:child_process"; import { readFileSync } from "node:fs"; import { constants } from "node:os"; import { basename } from "node:path"; @@ -52,6 +52,29 @@ export function isProcessAlive(pid: number): boolean { } export function signalProcessGroupOrProcess(pid: number, signal: NodeJS.Signals): void { + if (process.platform === "win32") { + const fallbackSignal = signal === "SIGKILL" ? "SIGTERM" : signal; + const fallback = () => { + try { + process.kill(pid, fallbackSignal); + } catch { + // Process may already be dead. + } + }; + try { + const args = ["/PID", String(pid), "/T"]; + if (signal === "SIGKILL") args.push("/F"); + const taskkill = spawn("taskkill", args, { stdio: "ignore", windowsHide: true }); + taskkill.once("error", fallback); + taskkill.once("exit", (code) => { + if (code !== 0 && isProcessAlive(pid)) fallback(); + }); + taskkill.unref(); + } catch { + fallback(); + } + return; + } try { process.kill(-pid, signal); return; diff --git a/packages/coding-agent/src/utils/shell.ts b/packages/coding-agent/src/utils/shell.ts index 3a854400111..88ff899c47c 100644 --- a/packages/coding-agent/src/utils/shell.ts +++ b/packages/coding-agent/src/utils/shell.ts @@ -1,8 +1,9 @@ import { existsSync } from "node:fs"; import { delimiter } from "node:path"; -import { spawn, spawnSync } from "child_process"; +import { spawnSync } from "child_process"; import { getBinDir } from "../config.js"; import { recordOrphanProcessState } from "../core/orphan-process-journal.js"; +import { signalProcessGroupOrProcess } from "./child-process.js"; export interface ShellConfig { shell: string; @@ -216,27 +217,5 @@ export function killTrackedDetachedChildren(): void { * Kill a process and all its children (cross-platform) */ export function killProcessTree(pid: number): void { - if (process.platform === "win32") { - // Use taskkill on Windows to kill process tree - try { - spawn("taskkill", ["/F", "/T", "/PID", String(pid)], { - stdio: "ignore", - detached: true, - }); - } catch { - // Ignore errors if taskkill fails - } - } else { - // Use SIGKILL on Unix/Linux/Mac - try { - process.kill(-pid, "SIGKILL"); - } catch { - // Fallback to killing just the child if process group kill fails - try { - process.kill(pid, "SIGKILL"); - } catch { - // Process already dead - } - } - } + signalProcessGroupOrProcess(pid, "SIGKILL"); } diff --git a/packages/coding-agent/test/bun-installer.test.ts b/packages/coding-agent/test/bun-installer.test.ts index cdacafd0983..438d17ce93d 100644 --- a/packages/coding-agent/test/bun-installer.test.ts +++ b/packages/coding-agent/test/bun-installer.test.ts @@ -1,6 +1,6 @@ import { afterEach, describe, expect, it } from "bun:test"; import { execFileSync, spawnSync } from "node:child_process"; -import { mkdtempSync, readFileSync, rmSync } from "node:fs"; +import { mkdtempSync, readFileSync, realpathSync, rmSync } from "node:fs"; import { tmpdir } from "node:os"; import { dirname, join } from "node:path"; import { fileURLToPath } from "node:url"; @@ -146,6 +146,7 @@ describe("atomic symlink function", () => { const v1 = `${tmp}/versions/v1`; const v2 = `${tmp}/versions/v2`; const link = `${tmp}/bin/prime-agent`; + const expectedTarget = `${realpathSync(tmp)}/versions/v2/pi`; const cmd = 'eval "$(sed \'s/^main "$@"$/# &/\' "$1")" 2>/dev/null' + "; mkdir -p " + @@ -178,8 +179,8 @@ describe("atomic symlink function", () => { '; [ "$(readlink ' + link + ')" = "' + - v2 + - "/pi\" ] && printf 'OK'"; + expectedTarget + + "\" ] && printf 'OK'"; const result = execFileSync("sh", ["-c", cmd, "--", installer], { encoding: "utf-8", env: process.env as any, diff --git a/packages/coding-agent/test/bun-release-archives.test.ts b/packages/coding-agent/test/bun-release-archives.test.ts index 5bb0c74a135..837c3cbe10c 100644 --- a/packages/coding-agent/test/bun-release-archives.test.ts +++ b/packages/coding-agent/test/bun-release-archives.test.ts @@ -22,7 +22,7 @@ const packScript = join(repoRoot, "scripts", "pack-prime-agent-release.mjs"); const releaseRoot = join(packageDir, "release"); const temporaryRoots: string[] = []; const outputDirs: string[] = []; -const platforms = ["darwin-arm64", "darwin-x64", "linux-arm64", "linux-x64"]; +const platforms = ["darwin-arm64", "darwin-x64", "linux-arm64", "linux-x64", "windows-x64", "windows-arm64"]; function fixture(): { root: string; binaries: string; sidecars: string; output: string } { const root = mkdtempSync(join(tmpdir(), "prime-agent-release-")); @@ -32,8 +32,10 @@ function fixture(): { root: string; binaries: string; sidecars: string; output: for (const platform of platforms) { const dir = join(binaries, platform); mkdirSync(dir, { recursive: true }); - cpSync("/bin/echo", join(dir, "pi")); - chmodSync(join(dir, "pi"), 0o755); + const binaryName = platform.startsWith("windows") ? "pi.exe" : "pi"; + const binaryPath = join(dir, binaryName); + cpSync("/bin/echo", binaryPath); + chmodSync(binaryPath, 0o755); } mkdirSync(sidecars, { recursive: true }); for (const name of ["prime-agent-runtime", "skills", "theme", "assets", "export-html", "docs", "examples"]) { @@ -48,6 +50,10 @@ function fixture(): { root: string; binaries: string; sidecars: string; output: '#!/bin/sh\nbase="__PRIME_AGENT_DOWNLOAD_BASE_URL__"\nchannel="__PRIME_AGENT_DEFAULT_RELEASE_CHANNEL__"\n', ); writeFileSync(join(sidecars, "photon_rs_bg.wasm"), "wasm"); + writeFileSync( + join(sidecars, "install.ps1"), + '#!/usr/bin/env pwsh\n# Prime Agent Windows installer\n$Script:PrimeAgentBaseUrl = "__PRIME_AGENT_DOWNLOAD_BASE_URL__"\n$Script:DefaultReleaseChannel = "__PRIME_AGENT_DEFAULT_RELEASE_CHANNEL__"\n', + ); const output = join(releaseRoot, `test-${process.pid}-${Math.random().toString(36).slice(2)}`); outputDirs.push(output); return { root, binaries, sidecars, output }; @@ -87,17 +93,34 @@ describe("compiled release archives", () => { const result = pack(f); expect(result.status, result.stderr).toBe(0); const artifacts = join(f.output, "artifacts"); - const archives = readdirSync(artifacts).filter((name) => name.endsWith(".tar.gz")); - expect(archives.sort()).toEqual(platforms.map((platform) => `prime-agent-1.2.3-${platform}.tar.gz`).sort()); + const archives = readdirSync(artifacts).filter((name) => name.endsWith(".tar.gz") || name.endsWith(".zip")); + const expectedArchives = [ + ...["darwin-arm64", "darwin-x64", "linux-arm64", "linux-x64"].map((p) => `prime-agent-1.2.3-${p}.tar.gz`), + ...["windows-x64", "windows-arm64"].map((p) => `prime-agent-1.2.3-${p}.zip`), + ]; + expect(archives.sort()).toEqual(expectedArchives.sort()); expect(readFileSync(join(artifacts, "stable"), "utf8")).toBe("v1.2.3\n"); - expect(readFileSync(join(artifacts, "SHA256SUMS"), "utf8").trim().split("\n")).toHaveLength(4); + expect(readFileSync(join(artifacts, "SHA256SUMS"), "utf8").trim().split("\n")).toHaveLength(6); const extracted = join(f.root, "extracted"); mkdirSync(extracted); const archive = join(artifacts, "prime-agent-1.2.3-darwin-arm64.tar.gz"); expect(spawnSync("tar", ["-xzf", archive, "-C", extracted]).status).toBe(0); + + const winExtracted = join(f.root, "win-extracted"); + mkdirSync(winExtracted); + const winArchive = join(artifacts, "prime-agent-1.2.3-windows-x64.zip"); + expect(spawnSync("unzip", ["-q", winArchive, "-d", winExtracted]).status).toBe(0); + expect(existsSync(join(winExtracted, "prime-agent.exe"))).toBe(true); expect(readFileSync(join(extracted, "install.sh"), "utf8")).toContain('base="https://downloads.example.test"'); expect(readFileSync(join(extracted, "install.sh"), "utf8")).toContain('channel="stable"'); + const powershellInstaller = readFileSync(join(winExtracted, "install.ps1"), "utf8"); + expect(powershellInstaller).toContain('$Script:PrimeAgentBaseUrl = "https://downloads.example.test"'); + expect(powershellInstaller).toContain('$Script:DefaultReleaseChannel = "stable"'); + expect(powershellInstaller).not.toContain("__PRIME_AGENT_DOWNLOAD_BASE_URL__"); + expect(powershellInstaller).not.toContain("__PRIME_AGENT_DEFAULT_RELEASE_CHANNEL__"); + const windowsManifest = JSON.parse(readFileSync(join(winExtracted, "package.json"), "utf8")); + expect(windowsManifest.bin).toEqual({ "prime-agent": "./prime-agent.exe" }); const manifest = JSON.parse(readFileSync(join(extracted, "package.json"), "utf8")); expect(manifest).toMatchObject({ name: "prime-agent", @@ -119,6 +142,7 @@ describe("compiled release archives", () => { "docs", "examples", "photon_rs_bg.wasm", + "install.ps1", ]) { expect(existsSync(join(extracted, name))).toBe(true); } @@ -155,6 +179,15 @@ describe("compiled release archives", () => { const manifest = JSON.parse(readFileSync(join(f.output, "artifacts", "latest.json"), "utf8")); expect(manifest.version).toBe("v1.2.3"); expect(manifest.baseUrl).toBe("https://downloads.example.test/releases/v1.2.3"); + expect(manifest.platforms.length).toBe(6); + expect(manifest.platforms.map((p: any) => p.platform).sort()).toEqual([ + "darwin-arm64", + "darwin-x64", + "linux-arm64", + "linux-x64", + "windows-arm64", + "windows-x64", + ]); expect(existsSync(join(f.output, "artifacts", "prime-agent-1.2.3-darwin-arm64.tar.gz"))).toBe(true); }); diff --git a/packages/coding-agent/test/child-process.test.ts b/packages/coding-agent/test/child-process.test.ts index 08a87793f26..93ff6cc1416 100644 --- a/packages/coding-agent/test/child-process.test.ts +++ b/packages/coding-agent/test/child-process.test.ts @@ -1,7 +1,12 @@ import { type ChildProcess, spawn } from "node:child_process"; import { EventEmitter } from "node:events"; import { describe, expect, it } from "vitest"; -import { isProcessAlive, isZombieProcess, waitForChildProcess } from "../src/utils/child-process.js"; +import { + isProcessAlive, + isZombieProcess, + signalProcessGroupOrProcess, + waitForChildProcess, +} from "../src/utils/child-process.js"; describe("waitForChildProcess", () => { it("reports signaled already-exited children as failures", async () => { @@ -16,6 +21,19 @@ describe("waitForChildProcess", () => { }); }); +describe("signalProcessGroupOrProcess", () => { + it("does not throw for a running child pid", async () => { + const child = spawn(process.execPath, ["--eval", "setTimeout(() => {}, 1000)"], { stdio: "ignore" }); + await new Promise((resolve) => child.once("spawn", () => resolve())); + expect(() => signalProcessGroupOrProcess(child.pid!, "SIGTERM")).not.toThrow(); + await new Promise((resolve) => child.once("exit", () => resolve())); + }); + + it("does not throw for a nonexistent pid", () => { + expect(() => signalProcessGroupOrProcess(999999999, "SIGKILL")).not.toThrow(); + }); +}); + describe("process liveness", () => { it("treats the current process as alive and not a zombie", () => { expect(isProcessAlive(process.pid)).toBe(true); diff --git a/packages/coding-agent/test/daemon-socket.test.ts b/packages/coding-agent/test/daemon-socket.test.ts index 5288c1f17e0..d36c22746d9 100644 --- a/packages/coding-agent/test/daemon-socket.test.ts +++ b/packages/coding-agent/test/daemon-socket.test.ts @@ -13,6 +13,7 @@ import { getDaemonSocketIdentity, normalizeSocketPath, prepareDaemonSocketPath, + windowsNamedPipeUserScope, } from "../src/modes/daemon/daemon-socket.js"; describe("endDaemonSocketAfterFlush", () => { @@ -47,12 +48,31 @@ describe("normalizeSocketPath", () => { }); describe("defaultDaemonSocketPath", () => { - it("uses a fixed Windows named pipe path", () => { + it("uses a per-user Windows named pipe path", () => { if (process.platform !== "win32") { return; } - expect(defaultDaemonSocketPath()).toBe("\\\\.\\pipe\\prime-agent-daemon"); + expect(defaultDaemonSocketPath()).toBe(`\\\\.\\pipe\\prime-agent-daemon-${windowsNamedPipeUserScope()}`); + }); + + it("uses a stable opaque Windows user scope", () => { + expect(windowsNamedPipeUserScope()).toMatch(/^[0-9a-f]{16}$/); + expect(windowsNamedPipeUserScope()).toBe(windowsNamedPipeUserScope()); + }); + + it("detects a live Windows named pipe", async () => { + if (process.platform !== "win32") return; + const pipePath = `\\\\.\\pipe\\prime-agent-test-${process.pid}-${Date.now()}`; + const server = createServer(); + await new Promise((resolveListen) => server.listen(pipePath, resolveListen)); + try { + await expect(prepareDaemonSocketPath(pipePath)).rejects.toThrow("already in use"); + } finally { + await new Promise((resolveClose, rejectClose) => + server.close((error) => (error ? rejectClose(error) : resolveClose())), + ); + } }); it("uses a per-user Unix socket directory", () => { diff --git a/packages/coding-agent/test/exec.test.ts b/packages/coding-agent/test/exec.test.ts index 2f2ab6ea025..6c52abc48ea 100644 --- a/packages/coding-agent/test/exec.test.ts +++ b/packages/coding-agent/test/exec.test.ts @@ -50,3 +50,28 @@ describe.skipIf(process.platform === "win32")("execCommand", () => { } }); }); + +describe.skipIf(process.platform !== "win32")("execCommand on Windows", () => { + it("cancels a running process tree", async () => { + const testDir = mkdtempSync(join(tmpdir(), "prime-agent-exec-windows-test-")); + const readyFile = join(testDir, "ready"); + const controller = new AbortController(); + let resultPromise: Promise>> | undefined; + try { + resultPromise = execCommand( + process.execPath, + ["-e", `require("node:fs").writeFileSync(process.argv[1], ""); setInterval(() => {}, 1000);`, readyFile], + process.cwd(), + { signal: controller.signal }, + ); + await waitForFile(readyFile); + controller.abort(); + const result = await resultPromise; + expect(result.killed).toBe(true); + } finally { + controller.abort(); + await resultPromise; + rmSync(testDir, { recursive: true, force: true }); + } + }); +}); diff --git a/scripts/assemble-release-archives.mjs b/scripts/assemble-release-archives.mjs index eba30c05bc2..d8af8bfa13a 100755 --- a/scripts/assemble-release-archives.mjs +++ b/scripts/assemble-release-archives.mjs @@ -17,10 +17,10 @@ * (plain version pointer) * latest.json|beta.json (manifest) * - * Platforms: darwin-arm64, darwin-x64, linux-arm64, linux-x64 + * Platforms: darwin-arm64, darwin-x64, linux-arm64, linux-x64, windows-x64, windows-arm64 * - * Archive layout (each .tar.gz): - * prime-agent (compiled binary, executable) + * Archive layout (each archive): + * prime-agent[.exe] (compiled binary, executable) * package.json * README.md * CHANGELOG.md @@ -56,13 +56,14 @@ const releaseChannels = new Set(["stable", "beta"]); const publicPackageName = "prime-agent"; const binaryName = "prime-agent"; -const PLATFORMS = ["darwin-arm64", "darwin-x64", "linux-arm64", "linux-x64"]; +const PLATFORMS = ["darwin-arm64", "darwin-x64", "linux-arm64", "linux-x64", "windows-x64", "windows-arm64"]; const REQUIRED_SIDECARS = [ "package.json", "README.md", "CHANGELOG.md", "install.sh", + "install.ps1", "prime-agent-runtime", "skills", "theme", @@ -150,7 +151,8 @@ function printHelp() { Assembles platform-specific release archives from a Bun-compiled binary and sidecar files. Output: - /artifacts/prime-agent--.tar.gz + /artifacts/prime-agent--.tar.gz (macOS/Linux) + /artifacts/prime-agent--.zip (Windows) /artifacts/SHA256SUMS /artifacts/ /artifacts/latest.json (stable) or beta.json (beta) @@ -210,11 +212,32 @@ function sha256File(path) { } function createArchive(sourceDir, archivePath) { - const result = spawnSync("tar", ["-czf", archivePath, "-C", sourceDir, "."], { - stdio: "pipe", - encoding: "utf8", - }); - if (result.status !== 0) throw new Error(`tar failed: ${result.stderr || result.stdout}`); + if (archivePath.endsWith(".zip")) { + const result = spawnSync("zip", ["-q", "-r", archivePath, "."], { + cwd: sourceDir, + stdio: "pipe", + encoding: "utf8", + }); + if (result.status !== 0) throw new Error(`zip failed: ${result.stderr || result.stdout}`); + } else { + const result = spawnSync("tar", ["-czf", archivePath, "-C", sourceDir, "."], { + stdio: "pipe", + encoding: "utf8", + }); + if (result.status !== 0) throw new Error(`tar failed: ${result.stderr || result.stdout}`); + } +} + +function isWindowsPlatform(platform) { + return platform === "windows-x64" || platform === "windows-arm64"; +} + +function archiveExtension(platform) { + return isWindowsPlatform(platform) ? "zip" : "tar.gz"; +} + +function binaryNameForPlatform(platform) { + return isWindowsPlatform(platform) ? "prime-agent.exe" : "prime-agent"; } function validateSidecars(sidecarDir) { @@ -241,11 +264,11 @@ function findForbiddenReleaseDirectory(root, relativePath = "") { return undefined; } -function renderPackageManifest(path, version) { +function renderPackageManifest(path, version, platform) { const manifest = JSON.parse(readFileSync(path, "utf8")); manifest.name = publicPackageName; manifest.version = version; - manifest.bin = { [binaryName]: `./${binaryName}` }; + manifest.bin = { [binaryName]: `./${binaryNameForPlatform(platform)}` }; manifest.packageManager = "bun@1.4.0"; writeFileSync(path, `${JSON.stringify(manifest, null, 2)}\n`); } @@ -280,7 +303,8 @@ function main() { const binarySources = new Map(); for (const platform of args.platforms) { - const binarySource = join(args.binaryDir, platform, "pi"); + const binaryName = isWindowsPlatform(platform) ? "pi.exe" : "pi"; + const binarySource = join(args.binaryDir, platform, binaryName); if (!existsSync(binarySource)) { throw new Error(`Binary not found for platform ${platform}: ${binarySource}`); } @@ -297,8 +321,9 @@ function main() { const stagingDir = join(outDir, "staging", platform); mkdirSync(stagingDir, { recursive: true }); - // Copy binary to archive root as "prime-agent" - const dstBinary = join(stagingDir, binaryName); + // Copy binary to archive root as platform-specific name + const archiveBinaryName = binaryNameForPlatform(platform); + const dstBinary = join(stagingDir, archiveBinaryName); cpSync(binarySource, dstBinary); chmodSync(dstBinary, 0o755); @@ -307,10 +332,12 @@ function main() { const src = join(args.sidecarDir, name); cpSync(src, join(stagingDir, name), { recursive: true, dereference: true }); } - renderPackageManifest(join(stagingDir, "package.json"), args.version); + renderPackageManifest(join(stagingDir, "package.json"), args.version, platform); renderInstaller(join(stagingDir, "install.sh"), args.baseUrl, args.channel); + renderInstaller(join(stagingDir, "install.ps1"), args.baseUrl, args.channel); - const archiveName = `prime-agent-${args.version}-${platform}.tar.gz`; + const archiveExt = archiveExtension(platform); + const archiveName = `prime-agent-${args.version}-${platform}.${archiveExt}`; const archivePath = join(versionDir, archiveName); createArchive(stagingDir, archivePath); @@ -339,6 +366,7 @@ function main() { platform: a.platform, file: a.file, sha256: a.sha256, + binaryName: binaryNameForPlatform(a.platform), })), baseUrl: `${args.baseUrl}/releases/v${args.version}`, }, diff --git a/scripts/build-binaries.sh b/scripts/build-binaries.sh index b02ef9bc629..ed65571c3ff 100755 --- a/scripts/build-binaries.sh +++ b/scripts/build-binaries.sh @@ -58,10 +58,10 @@ done # Validate platform if specified if [[ -n "$PLATFORM" ]]; then case "$PLATFORM" in - darwin-arm64|darwin-x64|linux-x64|linux-arm64) ;; + darwin-arm64|darwin-x64|linux-x64|linux-arm64|windows-x64|windows-arm64) ;; *) echo "Invalid platform: $PLATFORM" - echo "Valid platforms: darwin-arm64, darwin-x64, linux-x64, linux-arm64" + echo "Valid platforms: darwin-arm64, darwin-x64, linux-x64, linux-arm64, windows-x64, windows-arm64" exit 1 ;; esac @@ -86,7 +86,7 @@ mkdir -p binaries if [[ -n "$PLATFORM" ]]; then PLATFORMS=("$PLATFORM") else - PLATFORMS=(darwin-arm64 darwin-x64 linux-x64 linux-arm64) + PLATFORMS=(darwin-arm64 darwin-x64 linux-x64 linux-arm64 windows-x64 windows-arm64) fi for platform in "${PLATFORMS[@]}"; do @@ -117,7 +117,13 @@ bun ../../scripts/pack-prime-agent-release.mjs \ echo "" echo "==> Build complete!" echo "Release archives:" -ls -lh "release/artifacts/"*.tar.gz +shopt -s nullglob +archives=(release/artifacts/*.tar.gz release/artifacts/*.zip) +if (( ${#archives[@]} == 0 )); then + echo "No release archives were created" >&2 + exit 1 +fi +ls -lh "${archives[@]}" echo "" echo "Metadata:" -ls -lh "release/artifacts/SHA256SUMS" "release/artifacts/latest.json" "release/artifacts/stable" 2>/dev/null || true +ls -lh "release/artifacts/SHA256SUMS" "release/artifacts/latest.json" "release/artifacts/stable" diff --git a/scripts/check-powershell-installer.mjs b/scripts/check-powershell-installer.mjs new file mode 100644 index 00000000000..0a9f7214e66 --- /dev/null +++ b/scripts/check-powershell-installer.mjs @@ -0,0 +1,39 @@ +import { spawnSync } from "node:child_process"; +import { readFileSync } from "node:fs"; + +const source = readFileSync("install.ps1", "utf8"); +const baseMarker = "__PRIME_AGENT_DOWNLOAD_BASE_URL__"; +const channelMarker = "__PRIME_AGENT_DEFAULT_RELEASE_CHANNEL__"; + +for (const channel of ["stable", "beta"]) { + const rendered = source.replaceAll(baseMarker, "https://releases.example.test").replaceAll(channelMarker, channel); + if (rendered.includes(baseMarker) || rendered.includes(channelMarker)) { + throw new Error(`PowerShell installer has unresolved markers for ${channel}`); + } + if (!rendered.includes(`$DefaultChannel = "${channel}"`)) { + throw new Error(`PowerShell installer did not render the ${channel} channel`); + } +} + +const parserCommand = [ + "$allErrors = @()", + "foreach ($path in @('install.ps1', 'scripts/test-windows-installer.ps1')) {", + "$tokens = $null", + "$errors = $null", + "[System.Management.Automation.Language.Parser]::ParseFile((Resolve-Path $path), [ref]$tokens, [ref]$errors) | Out-Null", + "$allErrors += $errors", + "}", + "if ($allErrors.Count -gt 0) { $allErrors | ForEach-Object { Write-Error $_.Message }; exit 1 }", +].join("; "); +const parser = spawnSync("pwsh", ["-NoProfile", "-NonInteractive", "-Command", parserCommand], { + encoding: "utf8", +}); +if (parser.error && (parser.error).code !== "ENOENT") { + throw parser.error; +} +if (!parser.error && parser.status !== 0) { + throw new Error(`PowerShell installer parse failed: +${parser.stderr}${parser.stdout}`); +} + +console.log(parser.error ? "PowerShell installer markers passed; pwsh parser unavailable." : "PowerShell installer check passed."); diff --git a/scripts/compiled-artifact-smoke.ts b/scripts/compiled-artifact-smoke.ts index c1055f49a3c..ff4408086f9 100644 --- a/scripts/compiled-artifact-smoke.ts +++ b/scripts/compiled-artifact-smoke.ts @@ -15,6 +15,7 @@ export const REQUIRED_BINARY_SIDECARS = [ "README.md", "CHANGELOG.md", "install.sh", + "install.ps1", "prime-agent-runtime", "skills", "theme", @@ -46,9 +47,10 @@ function run(executable: string, args: string[], cwd: string, path: string): Run }; } -export function inspectNativeExecutable(path: string): "elf" | "mach-o" | "unknown" { +export function inspectNativeExecutable(path: string): "elf" | "mach-o" | "pe" | "unknown" { const header = readFileSync(path).subarray(0, 4).toString("hex"); if (header === "7f454c46") return "elf"; + if (header.startsWith("4d5a")) return "pe"; if ([ "feedface", "cefaedfe", "feedfacf", "cffaedfe", "cafebabe", "bebafeca", "cafebabf", "bfbafeca", diff --git a/scripts/test-windows-installer.ps1 b/scripts/test-windows-installer.ps1 new file mode 100644 index 00000000000..c5eb17a303d --- /dev/null +++ b/scripts/test-windows-installer.ps1 @@ -0,0 +1,86 @@ +#!/usr/bin/env pwsh +[CmdletBinding()] +param() + +Set-StrictMode -Version Latest +$ErrorActionPreference = "Stop" +$ProgressPreference = "SilentlyContinue" + +if (-not $IsWindows -and $PSVersionTable.PSEdition -eq "Core") { + throw "Windows installer smoke test must run on Windows." +} + +$repoRoot = Split-Path -Parent $PSScriptRoot +$packageDir = Join-Path $repoRoot "packages\coding-agent" +$distDir = Join-Path $packageDir "dist" +$version = (Get-Content (Join-Path $packageDir "package.json") -Raw | ConvertFrom-Json).version +$testRoot = Join-Path ([System.IO.Path]::GetTempPath()) "prime-agent-windows-installer-$([guid]::NewGuid().ToString("N"))" +$serverRoot = Join-Path $testRoot "server" +$releaseDir = Join-Path $serverRoot "releases\v$version" +$stagingDir = Join-Path $testRoot "archive" +$localAppData = Join-Path $testRoot "local-app-data" +$artifactName = "prime-agent-$version-windows-x64.zip" +$artifactPath = Join-Path $releaseDir $artifactName +$originalLocalAppData = $env:LOCALAPPDATA +$originalDownloadBaseUrl = $env:PRIME_AGENT_DOWNLOAD_BASE_URL +$originalUserPath = [Environment]::GetEnvironmentVariable("PATH", "User") +$server = $null + +try { + New-Item -ItemType Directory -Path $releaseDir, $stagingDir, $localAppData -Force | Out-Null + Copy-Item -Path (Join-Path $distDir "*") -Destination $stagingDir -Recurse -Force + Move-Item -LiteralPath (Join-Path $stagingDir "pi.exe") -Destination (Join-Path $stagingDir "prime-agent.exe") + Compress-Archive -Path (Join-Path $stagingDir "*") -DestinationPath $artifactPath -CompressionLevel Optimal + $hash = (Get-FileHash -LiteralPath $artifactPath -Algorithm SHA256).Hash.ToLowerInvariant() + Set-Content -LiteralPath (Join-Path $releaseDir "SHA256SUMS") -Value "$hash $artifactName" -Encoding Ascii + Set-Content -LiteralPath (Join-Path $serverRoot "stable") -Value $version -Encoding Ascii + + $listener = [System.Net.Sockets.TcpListener]::new([System.Net.IPAddress]::Loopback, 0) + $listener.Start() + $port = ([System.Net.IPEndPoint]$listener.LocalEndpoint).Port + $listener.Stop() + + $server = Start-Process -FilePath "python" -ArgumentList @( + "-m", "http.server", "$port", "--bind", "127.0.0.1", "--directory", $serverRoot + ) -PassThru -WindowStyle Hidden + $baseUrl = "http://127.0.0.1:$port" + $deadline = [DateTime]::UtcNow.AddSeconds(15) + while ($true) { + try { + Invoke-WebRequest -Uri "$baseUrl/stable" -UseBasicParsing -TimeoutSec 1 | Out-Null + break + } catch { + if ([DateTime]::UtcNow -ge $deadline) { throw "Local release server did not become ready." } + Start-Sleep -Milliseconds 100 + } + } + + $env:LOCALAPPDATA = $localAppData + $env:PRIME_AGENT_DOWNLOAD_BASE_URL = $baseUrl + & (Join-Path $repoRoot "install.ps1") -Version $version + + $shim = Join-Path $localAppData "PrimeAgent\bin\prime-agent.cmd" + $binary = Join-Path $localAppData "PrimeAgent\versions\v$version\prime-agent.exe" + if (-not (Test-Path -LiteralPath $shim)) { throw "Installer did not create prime-agent.cmd" } + if (-not (Test-Path -LiteralPath $binary)) { throw "Installer did not create the versioned executable" } + + & $shim --version + if ($LASTEXITCODE -ne 0) { throw "Installed prime-agent --version failed with $LASTEXITCODE" } + & $shim --help | Out-Null + if ($LASTEXITCODE -ne 0) { throw "Installed prime-agent --help failed with $LASTEXITCODE" } + + & (Join-Path $repoRoot "install.ps1") -Version $version -Update + & (Join-Path $repoRoot "install.ps1") -Uninstall + if (Test-Path -LiteralPath (Join-Path $localAppData "PrimeAgent")) { + throw "Uninstall left the PrimeAgent install directory behind" + } + Write-Host "Windows installer end-to-end smoke passed." +} finally { + if ($server -and -not $server.HasExited) { + Stop-Process -Id $server.Id -Force -ErrorAction SilentlyContinue + } + [Environment]::SetEnvironmentVariable("PATH", $originalUserPath, "User") + $env:LOCALAPPDATA = $originalLocalAppData + $env:PRIME_AGENT_DOWNLOAD_BASE_URL = $originalDownloadBaseUrl + Remove-Item -LiteralPath $testRoot -Recurse -Force -ErrorAction SilentlyContinue +} From 2af76a987c907c9c35e51ec46ec77281adf69e31 Mon Sep 17 00:00:00 2001 From: Seth Date: Wed, 2 Sep 2026 03:21:48 -0400 Subject: [PATCH 32/45] ci: exercise the real Windows Python kernel --- .github/workflows/ci.yml | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index eeb9635c742..edb14e5b535 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -355,14 +355,28 @@ jobs: test/daemon-socket.test.ts test/exec.test.ts test/kernel-bash-shell.test.ts - test/kernel-bootstrap.test.ts - name: Bootstrap the CPython kernel working-directory: packages/coding-agent env: PRIME_AGENT_INSTALL_UV: "0" + PRIME_AGENT_KERNEL_VENV: ${{ runner.temp }}\Prime Agent ø\kernel-venv run: bun src/core/kernel/bootstrap-cli.ts + - name: Test persistent CPython kernel behavior + working-directory: packages/coding-agent + env: + PRIME_AGENT_KERNEL_VENV: ${{ runner.temp }}\Prime Agent ø\kernel-venv + PRIME_AGENT_TEST_TAGS: kernel-heavy + run: >- + bun ../../scripts/run-with-clean-env.ts bun test --isolate --timeout 30000 + test/acp-kernel-features.test.ts + test/acp-cold-cli.test.ts + test/kernel-goal-skill.test.ts + test/repl-kernel-state-roundtrip.test.ts + test/repl-kernel-mcp-shutdown.test.ts + test/repl-kernel-parent-watchdog.test.ts + - name: Test the CPython runtime working-directory: prime-agent-runtime run: uv run python -m unittest discover -s test From 9cfb56cff7e26eab0e2fc939712844ac03e64d1e Mon Sep 17 00:00:00 2001 From: Seth Date: Wed, 2 Sep 2026 03:37:28 -0400 Subject: [PATCH 33/45] fix: validate Windows terminal and kernel cleanup --- .github/workflows/ci.yml | 12 +++ .../coding-agent/test/acp-cold-cli.test.ts | 10 ++- .../test/repl-kernel-parent-watchdog.test.ts | 33 ++++++-- packages/tui/src/terminal.ts | 77 ++++++++++++++----- packages/tui/test/terminal.test.ts | 49 ++++++++++++ 5 files changed, 152 insertions(+), 29 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index edb14e5b535..f2d690d2a93 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -356,6 +356,18 @@ jobs: test/exec.test.ts test/kernel-bash-shell.test.ts + - name: Test terminal behavior + working-directory: packages/tui + run: >- + bun test + test/fullscreen.test.ts + test/input.test.ts + test/stdin-buffer.test.ts + test/terminal.test.ts + test/terminal-image.test.ts + test/tui-cell-size-input.test.ts + test/tui-render.test.ts + - name: Bootstrap the CPython kernel working-directory: packages/coding-agent env: diff --git a/packages/coding-agent/test/acp-cold-cli.test.ts b/packages/coding-agent/test/acp-cold-cli.test.ts index a91ca9039b5..d20c9c57b7c 100644 --- a/packages/coding-agent/test/acp-cold-cli.test.ts +++ b/packages/coding-agent/test/acp-cold-cli.test.ts @@ -5,6 +5,7 @@ import { tmpdir } from "node:os"; import { join, resolve } from "node:path"; import { afterEach, describe, expect, it } from "vitest"; import { ENV_AGENT_DIR } from "../src/config.js"; +import { signalProcessGroupOrProcess } from "../src/utils/child-process.js"; import { isTestTagEnabled } from "./test-tags.js"; /** @@ -171,13 +172,16 @@ async function driveAcpTurn(baseUrl: string): Promise { exited.then(() => true), new Promise((resolveTimeout) => setTimeout(() => resolveTimeout(false), 10_000)), ]); - if (!exitedInTime) { - child.kill("SIGTERM"); + if (!exitedInTime && child.pid !== undefined) { + signalProcessGroupOrProcess(child.pid, "SIGTERM"); const stoppedInTime = await Promise.race([ exited.then(() => true), new Promise((resolveTimeout) => setTimeout(() => resolveTimeout(false), 5_000)), ]); - if (!stoppedInTime) child.kill("SIGKILL"); + if (!stoppedInTime) { + signalProcessGroupOrProcess(child.pid, "SIGKILL"); + await Promise.race([exited, new Promise((resolveTimeout) => setTimeout(resolveTimeout, 5_000))]); + } } } return { responses, updates }; diff --git a/packages/coding-agent/test/repl-kernel-parent-watchdog.test.ts b/packages/coding-agent/test/repl-kernel-parent-watchdog.test.ts index e3a6e897f51..03e70b34544 100644 --- a/packages/coding-agent/test/repl-kernel-parent-watchdog.test.ts +++ b/packages/coding-agent/test/repl-kernel-parent-watchdog.test.ts @@ -1,6 +1,6 @@ import { spawn, spawnSync } from "node:child_process"; import { EventEmitter } from "node:events"; -import { chmodSync, existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"; +import { chmodSync, existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"; import { homedir, tmpdir } from "node:os"; import { join, resolve } from "node:path"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; @@ -18,11 +18,32 @@ vi.mock("../src/core/kernel/bootstrap.js", () => { let tempDir = ""; const savedJournalPath = process.env[ORPHAN_PROCESS_JOURNAL_ENV]; -function writeFakePython(script: string[]): string { +function writeEarlyExitKernel(envDump: string): { python: string; env?: Record } { + if (process.platform === "win32") { + const venv = process.env.PRIME_AGENT_KERNEL_VENV; + if (!venv) throw new Error("PRIME_AGENT_KERNEL_VENV is required for this Windows kernel test"); + const python = join(venv, "Scripts", "python.exe"); + const runtimeRoot = join(tempDir, "early-exit-runtime"); + const rlmDir = join(runtimeRoot, "rlm"); + mkdirSync(rlmDir, { recursive: true }); + writeFileSync(join(rlmDir, "__init__.py"), ""); + writeFileSync( + join(rlmDir, "repl.py"), + [ + "import os", + "from pathlib import Path", + `Path(${JSON.stringify(envDump)}).write_text("\n".join(f"{key}={value}" for key, value in os.environ.items()))`, + "raise SystemExit(42)", + "", + ].join("\n"), + ); + return { python, env: { PYTHONPATH: runtimeRoot } }; + } + const python = join(tempDir, "python"); - writeFileSync(python, script.join("\n")); + writeFileSync(python, ["#!/bin/sh", `env > "${envDump}"`, "exit 42", ""].join("\n")); chmodSync(python, 0o755); - return python; + return { python }; } interface JournalRecord { @@ -55,11 +76,11 @@ describe("repl kernel parent watchdog", () => { it("spawn sets PRIME_AGENT_KERNEL_OWNER_PID and journals the kernel pid", async () => { const envDump = join(tempDir, "kernel-env"); - const python = writeFakePython(["#!/bin/sh", `env > "${envDump}"`, "exit 42", ""]); + const { python, env } = writeEarlyExitKernel(envDump); const journalPath = join(tempDir, "orphans.jsonl"); process.env[ORPHAN_PROCESS_JOURNAL_ENV] = journalPath; const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {}); - const manager = new ReplKernelManager({ python, cwd: tempDir }); + const manager = new ReplKernelManager({ python, cwd: tempDir, env }); try { await expect(manager.execute("x")).rejects.toThrow(/Kernel exited before ready/); diff --git a/packages/tui/src/terminal.ts b/packages/tui/src/terminal.ts index c770eacee37..5a0f5ebf7d3 100644 --- a/packages/tui/src/terminal.ts +++ b/packages/tui/src/terminal.ts @@ -13,6 +13,61 @@ import { const cjsRequire = createRequire(import.meta.url); +interface WindowsKernel32 { + symbols: { + GetStdHandle(handle: number): unknown; + GetConsoleMode(handle: unknown, mode: unknown): boolean; + SetConsoleMode(handle: unknown, mode: number): boolean; + }; + close(): void; +} + +interface BunFfi { + dlopen(library: string, symbols: Record): WindowsKernel32; + ptr(buffer: Uint32Array): unknown; +} + +function enableWindowsVirtualTerminalInput(): void { + if (process.platform !== "win32") return; + try { + const STD_INPUT_HANDLE = -10; + const ENABLE_VIRTUAL_TERMINAL_INPUT = 0x0200; + + const bunRuntime = Reflect.get(globalThis, "Bun") as { FFI?: BunFfi } | undefined; + const bunFfi = bunRuntime?.FFI; + if (bunFfi) { + const kernel32 = bunFfi.dlopen("kernel32.dll", { + GetStdHandle: { args: ["i32"], returns: "ptr" }, + GetConsoleMode: { args: ["ptr", "ptr"], returns: "bool" }, + SetConsoleMode: { args: ["ptr", "u32"], returns: "bool" }, + }); + try { + const handle = kernel32.symbols.GetStdHandle(STD_INPUT_HANDLE); + const mode = new Uint32Array(1); + if (handle && kernel32.symbols.GetConsoleMode(handle, bunFfi.ptr(mode))) { + kernel32.symbols.SetConsoleMode(handle, mode[0]! | ENABLE_VIRTUAL_TERMINAL_INPUT); + } + } finally { + kernel32.close(); + } + return; + } + + const koffi = cjsRequire("koffi"); + const kernel32 = koffi.load("kernel32.dll"); + const GetStdHandle = kernel32.func("void* __stdcall GetStdHandle(int)"); + const GetConsoleMode = kernel32.func("bool __stdcall GetConsoleMode(void*, _Out_ uint32_t*)"); + const SetConsoleMode = kernel32.func("bool __stdcall SetConsoleMode(void*, uint32_t)"); + const handle = GetStdHandle(STD_INPUT_HANDLE); + const mode = new Uint32Array(1); + if (handle && GetConsoleMode(handle, mode)) { + SetConsoleMode(handle, mode[0]! | ENABLE_VIRTUAL_TERMINAL_INPUT); + } + } catch { + // The terminal remains usable, but modified key events may be unavailable. + } +} + const TERMINAL_PROGRESS_KEEPALIVE_MS = 1000; const TERMINAL_PROGRESS_ACTIVE_SEQUENCE = "\x1b]9;4;3\x07"; const TERMINAL_PROGRESS_CLEAR_SEQUENCE = "\x1b]9;4;0;\x07"; @@ -356,27 +411,9 @@ export class ProcessTerminal implements Terminal { * (e.g. \x1b[Z for Shift+Tab). Without this, libuv's ReadConsoleInputW * discards modifier state and Shift+Tab arrives as plain \t. */ + private enableWindowsVTInput(): void { - if (process.platform !== "win32") return; - try { - // Dynamic require to avoid bundling koffi's 74MB of cross-platform - // native binaries into every compiled binary. Koffi is only needed - // on Windows for VT input support. - const koffi = cjsRequire("koffi"); - const k32 = koffi.load("kernel32.dll"); - const GetStdHandle = k32.func("void* __stdcall GetStdHandle(int)"); - const GetConsoleMode = k32.func("bool __stdcall GetConsoleMode(void*, _Out_ uint32_t*)"); - const SetConsoleMode = k32.func("bool __stdcall SetConsoleMode(void*, uint32_t)"); - - const STD_INPUT_HANDLE = -10; - const ENABLE_VIRTUAL_TERMINAL_INPUT = 0x0200; - const handle = GetStdHandle(STD_INPUT_HANDLE); - const mode = new Uint32Array(1); - GetConsoleMode(handle, mode); - SetConsoleMode(handle, mode[0]! | ENABLE_VIRTUAL_TERMINAL_INPUT); - } catch { - // koffi not available — Shift+Tab won't be distinguishable from Tab - } + enableWindowsVirtualTerminalInput(); } async drainInput(maxMs = 1000, idleMs = 50): Promise { diff --git a/packages/tui/test/terminal.test.ts b/packages/tui/test/terminal.test.ts index 59d1f921329..cbf2f1a8f4a 100644 --- a/packages/tui/test/terminal.test.ts +++ b/packages/tui/test/terminal.test.ts @@ -44,6 +44,55 @@ describe("ProcessTerminal dimensions", () => { }); }); +describe("ProcessTerminal Windows console input", () => { + it("enables virtual terminal input through Bun FFI", () => { + const originalPlatform = Object.getOwnPropertyDescriptor(process, "platform"); + const bunRuntime = Reflect.get(globalThis, "Bun") as { FFI: unknown }; + const originalBunFfi = bunRuntime.FFI; + const handle = {}; + let setMode: number | undefined; + let closed = false; + + Object.defineProperty(process, "platform", { configurable: true, value: "win32" }); + bunRuntime.FFI = { + dlopen: (library: string) => { + assert.equal(library, "kernel32.dll"); + return { + symbols: { + GetStdHandle: (identifier: number) => { + assert.equal(identifier, -10); + return handle; + }, + GetConsoleMode: (actualHandle: unknown, mode: Uint32Array) => { + assert.equal(actualHandle, handle); + mode[0] = 0x10; + return true; + }, + SetConsoleMode: (actualHandle: unknown, mode: number) => { + assert.equal(actualHandle, handle); + setMode = mode; + return true; + }, + }, + close: () => { + closed = true; + }, + }; + }, + ptr: (buffer: Uint32Array) => buffer, + }; + try { + const terminal = new ProcessTerminal() as unknown as { enableWindowsVTInput(): void }; + terminal.enableWindowsVTInput(); + assert.equal(setMode, 0x210); + assert.equal(closed, true); + } finally { + restoreProperty(process, "platform", originalPlatform); + bunRuntime.FFI = originalBunFfi; + } + }); +}); + describe("ProcessTerminal alternate screen handoff", () => { it("keeps raw input active and discards keys until the next fullscreen TUI starts", () => { const originalWrite = process.stdout.write; From 3145d1711553331b793bf0bf013c85188d871e60 Mon Sep 17 00:00:00 2001 From: Seth Date: Wed, 2 Sep 2026 03:43:39 -0400 Subject: [PATCH 34/45] ci: focus Windows kernel validation --- .github/workflows/ci.yml | 3 --- packages/coding-agent/test/repl-kernel-parent-watchdog.test.ts | 2 +- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f2d690d2a93..876a494a3d1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -383,10 +383,7 @@ jobs: run: >- bun ../../scripts/run-with-clean-env.ts bun test --isolate --timeout 30000 test/acp-kernel-features.test.ts - test/acp-cold-cli.test.ts test/kernel-goal-skill.test.ts - test/repl-kernel-state-roundtrip.test.ts - test/repl-kernel-mcp-shutdown.test.ts test/repl-kernel-parent-watchdog.test.ts - name: Test the CPython runtime diff --git a/packages/coding-agent/test/repl-kernel-parent-watchdog.test.ts b/packages/coding-agent/test/repl-kernel-parent-watchdog.test.ts index 03e70b34544..431a8214a96 100644 --- a/packages/coding-agent/test/repl-kernel-parent-watchdog.test.ts +++ b/packages/coding-agent/test/repl-kernel-parent-watchdog.test.ts @@ -32,7 +32,7 @@ function writeEarlyExitKernel(envDump: string): { python: string; env?: Record Date: Wed, 2 Sep 2026 03:54:54 -0400 Subject: [PATCH 35/45] fix: support Windows kernel interrupts --- .github/workflows/ci.yml | 13 +++++++++-- .../test/repl-kernel-execute.test.ts | 7 ++++-- prime-agent-runtime/src/rlm/repl.py | 22 ++++++------------- prime-agent-runtime/test/test_repl.py | 4 ++-- 4 files changed, 25 insertions(+), 21 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 876a494a3d1..bcbb89b1aa1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -378,17 +378,26 @@ jobs: - name: Test persistent CPython kernel behavior working-directory: packages/coding-agent env: + PRIME_AGENT_BASH_SHELL: C:\Program Files\Git\bin\bash.exe PRIME_AGENT_KERNEL_VENV: ${{ runner.temp }}\Prime Agent ø\kernel-venv PRIME_AGENT_TEST_TAGS: kernel-heavy run: >- bun ../../scripts/run-with-clean-env.ts bun test --isolate --timeout 30000 test/acp-kernel-features.test.ts test/kernel-goal-skill.test.ts + test/repl-kernel-execute.test.ts test/repl-kernel-parent-watchdog.test.ts - - name: Test the CPython runtime + - name: Test the Windows CPython runtime working-directory: prime-agent-runtime - run: uv run python -m unittest discover -s test + env: + PRIME_AGENT_BASH_SHELL: C:\Program Files\Git\bin\bash.exe + run: >- + uv run python -m unittest + test.test_winjob + test.test_repl.ReplTest.test_interrupt_without_pthread_kill_cancels_awaited_cell + test.test_repl.ReplTest.test_interrupt_sync_blocking + test.test_repl.ReplTest.test_interrupt_await_suspended - name: Build the native Windows executable run: bun run build:binary diff --git a/packages/coding-agent/test/repl-kernel-execute.test.ts b/packages/coding-agent/test/repl-kernel-execute.test.ts index c653bce98f3..eaca159a0af 100644 --- a/packages/coding-agent/test/repl-kernel-execute.test.ts +++ b/packages/coding-agent/test/repl-kernel-execute.test.ts @@ -12,10 +12,13 @@ import { } from "../src/core/kernel/index.js"; function resolveReplPython(): string | null { + const venvPython = (...parts: string[]) => + process.platform === "win32" ? join(...parts, "Scripts", "python.exe") : join(...parts, "bin", "python"); const candidates = [ process.env.PRIME_AGENT_KERNEL_PYTHON, - resolve(__dirname, "..", "..", "..", "prime-agent-runtime", ".venv", "bin", "python"), - join(homedir(), ".prime", "agent", "kernel-venv", "bin", "python"), + process.env.PRIME_AGENT_KERNEL_VENV ? venvPython(process.env.PRIME_AGENT_KERNEL_VENV) : undefined, + venvPython(resolve(__dirname, "..", "..", "..", "prime-agent-runtime", ".venv")), + venvPython(homedir(), ".prime", "agent", "kernel-venv"), ].filter((p): p is string => Boolean(p)); for (const python of candidates) { if (!existsSync(python)) continue; diff --git a/prime-agent-runtime/src/rlm/repl.py b/prime-agent-runtime/src/rlm/repl.py index ea04ca1766c..1b982f762af 100644 --- a/prime-agent-runtime/src/rlm/repl.py +++ b/prime-agent-runtime/src/rlm/repl.py @@ -7,6 +7,7 @@ from __future__ import annotations +import _thread import ast import asyncio import codecs @@ -366,24 +367,15 @@ def _request_interrupt(target: str | None) -> None: else: return # SIGINT must land on the main thread, where cells execute. Windows has no - # signal.pthread_kill: fall back to cancelling the active task on the loop - # (sync-blocked cells and the finishing repr/drain cannot be broken there; - # best-effort parity). + # signal.pthread_kill, but CPython's interrupt_main schedules the installed + # SIGINT handler on that thread. Wake the loop as well so await-suspended + # cells observe the interrupt without waiting for selector activity. if hasattr(signal, "pthread_kill"): signal.pthread_kill(threading.main_thread().ident, signal.SIGINT) - if _loop is not None: - # Wake the selector so a cancel scheduled by the handler runs promptly. - _loop.call_soon_threadsafe(lambda: None) - return + else: + _thread.interrupt_main() if _loop is not None: - - def cancel_active() -> None: - current = _active["task"] - if current is task and current is not None and not current.done(): - _active["interrupted"] = True - current.cancel() - - _loop.call_soon_threadsafe(cancel_active) + _loop.call_soon_threadsafe(lambda: None) def _consume_pending_interrupt(rid: str) -> bool: diff --git a/prime-agent-runtime/test/test_repl.py b/prime-agent-runtime/test/test_repl.py index 26b7aca89d3..14a99a1b41b 100644 --- a/prime-agent-runtime/test/test_repl.py +++ b/prime-agent-runtime/test/test_repl.py @@ -347,8 +347,8 @@ def test_large_buffer_write_survives_short_pipe_writes(self): self.assertIn("wrote 262144", tagged) def test_interrupt_without_pthread_kill_cancels_awaited_cell(self): - # Windows fallback seam: with pthread_kill absent the reader cancels - # the active task on the loop; an await-suspended cell still interrupts. + # Windows fallback seam: interrupt_main runs the SIGINT handler on the + # main thread; an await-suspended cell still interrupts. code = "\n".join( [ "import signal", From 438dd80ef5ea0b0713e1e0fbd70eb032aff2d1ef Mon Sep 17 00:00:00 2001 From: Seth Date: Wed, 2 Sep 2026 04:01:51 -0400 Subject: [PATCH 36/45] ci: validate Windows Arm64 executable --- .github/workflows/ci.yml | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index bcbb89b1aa1..5925ef9be71 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -402,6 +402,27 @@ jobs: - name: Build the native Windows executable run: bun run build:binary + - name: Cross-compile and inspect the Windows Arm64 executable + working-directory: packages/coding-agent + shell: pwsh + run: | + bun build --compile --minify --keep-names --bytecode --format=esm --external koffi --target=bun-windows-arm64 ./dist/bun/cli.js --outfile "$env:RUNNER_TEMP\prime-agent-arm64.exe" + $artifacts = @( + @{ Path = "dist\pi.exe"; Machine = 0x8664 }, + @{ Path = "$env:RUNNER_TEMP\prime-agent-arm64.exe"; Machine = 0xAA64 } + ) + foreach ($artifact in $artifacts) { + $bytes = [System.IO.File]::ReadAllBytes($artifact.Path) + if ($bytes.Length -lt 64 -or $bytes[0] -ne 0x4D -or $bytes[1] -ne 0x5A) { + throw "$($artifact.Path) is not a PE executable" + } + $peOffset = [BitConverter]::ToInt32($bytes, 0x3C) + $machine = [BitConverter]::ToUInt16($bytes, $peOffset + 4) + if ($machine -ne $artifact.Machine) { + throw "$($artifact.Path) has PE machine 0x$($machine.ToString('X4')), expected 0x$($artifact.Machine.ToString('X4'))" + } + } + - name: Test the PowerShell installer end to end shell: pwsh run: ./scripts/test-windows-installer.ps1 From 0510c6ca406dc3ba1ffc6d668c2eb04187e77896 Mon Sep 17 00:00:00 2001 From: Seth Date: Sun, 6 Sep 2026 17:46:31 -0400 Subject: [PATCH 37/45] fix: retain FIFO release ordering in #1982 --- .github/workflows/build-binaries.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/build-binaries.yml b/.github/workflows/build-binaries.yml index 63bb8c85d0c..1bb108c6f06 100644 --- a/.github/workflows/build-binaries.yml +++ b/.github/workflows/build-binaries.yml @@ -16,6 +16,7 @@ on: concurrency: group: release-prime-agent cancel-in-progress: false + queue: max permissions: contents: read From 75e5e99ce33788ca5f4cdafbbcfff30576f631e8 Mon Sep 17 00:00:00 2001 From: Seth Date: Sun, 6 Sep 2026 17:49:33 -0400 Subject: [PATCH 38/45] docs: record native Windows terminal support for #1982 --- packages/tui/.changes/windows-native-support.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 packages/tui/.changes/windows-native-support.md diff --git a/packages/tui/.changes/windows-native-support.md b/packages/tui/.changes/windows-native-support.md new file mode 100644 index 00000000000..a695a5ff429 --- /dev/null +++ b/packages/tui/.changes/windows-native-support.md @@ -0,0 +1 @@ +- Fixed modified-key input in native Windows terminals running the compiled executable. From 9680ac3dfa8bdafae0f9e53286e2274d8b9c9090 Mon Sep 17 00:00:00 2001 From: Seth Date: Sun, 6 Sep 2026 18:09:16 -0400 Subject: [PATCH 39/45] fix: harden native Windows installation and cleanup --- .github/workflows/ci.yml | 4 +- install.ps1 | 46 +++++++++--- packages/coding-agent/src/core/exec.ts | 6 +- .../coding-agent/src/utils/child-process.ts | 6 +- .../coding-agent/test/child-process.test.ts | 74 ++++++++++++++++++- packages/coding-agent/test/exec.test.ts | 33 ++++++++- scripts/test-windows-installer.ps1 | 14 +++- 7 files changed, 162 insertions(+), 21 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5925ef9be71..a7a446ba206 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -423,8 +423,8 @@ jobs: } } - - name: Test the PowerShell installer end to end - shell: pwsh + - name: Test the PowerShell 5.1 installer end to end + shell: powershell run: ./scripts/test-windows-installer.ps1 - name: Smoke test the native Windows executable diff --git a/install.ps1 b/install.ps1 index f7871b737b3..d0cc0e61ab2 100644 --- a/install.ps1 +++ b/install.ps1 @@ -60,15 +60,41 @@ function Write-Step([string]$Message) { } function Get-ReleasePlatform { - $architecture = if ($env:PROCESSOR_ARCHITEW6432) { - $env:PROCESSOR_ARCHITEW6432 - } else { - $env:PROCESSOR_ARCHITECTURE + if (-not ("PrimeAgent.WindowsNativeMethods" -as [type])) { + Add-Type -TypeDefinition @" +using System; +using System.Runtime.InteropServices; +namespace PrimeAgent { + public static class WindowsNativeMethods { + [DllImport("kernel32.dll", SetLastError = true)] + public static extern bool IsWow64Process2( + IntPtr process, + out ushort processMachine, + out ushort nativeMachine); } - switch ($architecture.ToUpperInvariant()) { - "AMD64" { return "windows-x64" } - "ARM64" { return "windows-arm64" } - default { throw "Unsupported Windows architecture: $architecture" } +} +"@ + } + + [UInt16]$processMachine = 0 + [UInt16]$nativeMachine = 0 + $process = [System.Diagnostics.Process]::GetCurrentProcess() + try { + if (-not [PrimeAgent.WindowsNativeMethods]::IsWow64Process2( + $process.Handle, + [ref]$processMachine, + [ref]$nativeMachine)) { + $errorCode = [System.Runtime.InteropServices.Marshal]::GetLastWin32Error() + throw [System.ComponentModel.Win32Exception]::new($errorCode) + } + } finally { + $process.Dispose() + } + + switch ($nativeMachine) { + 0x8664 { return "windows-x64" } + 0xAA64 { return "windows-arm64" } + default { throw "Unsupported native Windows machine type: 0x$($nativeMachine.ToString('X4'))" } } } @@ -149,9 +175,9 @@ function Assert-ArchiveLayout([string]$Directory) { function Set-ActiveVersion([string]$VersionDirectory) { New-Item -ItemType Directory -Path $BinDir -Force | Out-Null - $binaryPath = Join-Path $VersionDirectory "prime-agent.exe" + $versionName = Split-Path -Leaf $VersionDirectory $temporaryShim = Join-Path $BinDir "$CommandName.cmd.$([guid]::NewGuid().ToString("N")).tmp" - $shim = "@echo off`r`n`"$binaryPath`" %*`r`n" + $shim = "@echo off`r`nsetlocal DisableDelayedExpansion`r`n`"%~dp0..\versions\$versionName\prime-agent.exe`" %*`r`n" $backupShim = "$CommandShim.backup" try { Set-Content -LiteralPath $temporaryShim -Value $shim -Encoding Ascii -NoNewline diff --git a/packages/coding-agent/src/core/exec.ts b/packages/coding-agent/src/core/exec.ts index 47fb0597bb7..458e8c490db 100644 --- a/packages/coding-agent/src/core/exec.ts +++ b/packages/coding-agent/src/core/exec.ts @@ -76,7 +76,11 @@ export async function execCommand( const killProcess = () => { if (!killed) { killed = true; - proc.kill("SIGTERM"); + if (process.platform === "win32" && proc.pid) { + signalProcessGroupOrProcess(proc.pid, "SIGTERM"); + } else { + proc.kill("SIGTERM"); + } forceKillTimeoutId = setTimeout(() => { forceKillTimeoutId = undefined; if (proc.exitCode === null && proc.signalCode === null) { diff --git a/packages/coding-agent/src/utils/child-process.ts b/packages/coding-agent/src/utils/child-process.ts index cd4cfbec39e..72c189b13f1 100644 --- a/packages/coding-agent/src/utils/child-process.ts +++ b/packages/coding-agent/src/utils/child-process.ts @@ -64,7 +64,11 @@ export function signalProcessGroupOrProcess(pid: number, signal: NodeJS.Signals) try { const args = ["/PID", String(pid), "/T"]; if (signal === "SIGKILL") args.push("/F"); - const taskkill = spawn("taskkill", args, { stdio: "ignore", windowsHide: true }); + const taskkill = spawn("taskkill", args, { + stdio: "ignore", + windowsHide: true, + detached: true, + }); taskkill.once("error", fallback); taskkill.once("exit", (code) => { if (code !== 0 && isProcessAlive(pid)) fallback(); diff --git a/packages/coding-agent/test/child-process.test.ts b/packages/coding-agent/test/child-process.test.ts index 93ff6cc1416..51ebc3ba86b 100644 --- a/packages/coding-agent/test/child-process.test.ts +++ b/packages/coding-agent/test/child-process.test.ts @@ -1,5 +1,9 @@ -import { type ChildProcess, spawn } from "node:child_process"; +import { type ChildProcess, execFileSync, spawn } from "node:child_process"; import { EventEmitter } from "node:events"; +import { existsSync, mkdtempSync, readFileSync, rmSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join, resolve } from "node:path"; +import { pathToFileURL } from "node:url"; import { describe, expect, it } from "vitest"; import { isProcessAlive, @@ -8,6 +12,31 @@ import { waitForChildProcess, } from "../src/utils/child-process.js"; +async function waitForFile(path: string): Promise { + const deadline = Date.now() + 10_000; + while (!existsSync(path)) { + if (Date.now() >= deadline) throw new Error(`File was not created: ${path}`); + await new Promise((resolveDelay) => setTimeout(resolveDelay, 25)); + } +} + +async function waitForProcessExit(pid: number): Promise { + const deadline = Date.now() + 10_000; + while (isProcessAlive(pid)) { + if (Date.now() >= deadline) throw new Error(`Process ${pid} did not exit`); + await new Promise((resolveDelay) => setTimeout(resolveDelay, 25)); + } +} + +function forceKillOnWindows(pid: number | undefined): void { + if (!pid || !isProcessAlive(pid)) return; + try { + execFileSync("taskkill", ["/PID", String(pid), "/T", "/F"], { stdio: "ignore" }); + } catch { + // The process may have exited between the liveness check and taskkill. + } +} + describe("waitForChildProcess", () => { it("reports signaled already-exited children as failures", async () => { const child = Object.assign(new EventEmitter(), { @@ -32,6 +61,49 @@ describe("signalProcessGroupOrProcess", () => { it("does not throw for a nonexistent pid", () => { expect(() => signalProcessGroupOrProcess(999999999, "SIGKILL")).not.toThrow(); }); + + it.skipIf(process.platform !== "win32")("finishes a tree kill after the signaling process exits", async () => { + const testDir = mkdtempSync(join(tmpdir(), "prime-agent-taskkill-test-")); + const grandchildPidFile = join(testDir, "grandchild.pid"); + let target: ChildProcess | undefined; + let grandchildPid: number | undefined; + try { + target = spawn( + process.execPath, + [ + "--eval", + `const { spawn } = require("node:child_process"); const { writeFileSync } = require("node:fs"); const child = spawn(process.execPath, ["--eval", "setInterval(() => {}, 1000)"], { stdio: "ignore" }); writeFileSync(process.argv[1], String(child.pid)); setInterval(() => {}, 1000);`, + grandchildPidFile, + ], + { stdio: "ignore" }, + ); + await waitForFile(grandchildPidFile); + grandchildPid = Number.parseInt(readFileSync(grandchildPidFile, "utf8"), 10); + expect(isProcessAlive(target.pid!)).toBe(true); + expect(isProcessAlive(grandchildPid)).toBe(true); + + const helperUrl = pathToFileURL(resolve(import.meta.dirname, "../src/utils/child-process.ts")).href; + const killer = spawn( + process.execPath, + [ + "--eval", + `import { signalProcessGroupOrProcess } from ${JSON.stringify(helperUrl)}; signalProcessGroupOrProcess(${target.pid}, "SIGKILL");`, + ], + { stdio: "ignore" }, + ); + const killerCode = await new Promise((resolveExit, rejectExit) => { + killer.once("error", rejectExit); + killer.once("exit", resolveExit); + }); + expect(killerCode).toBe(0); + await waitForProcessExit(target.pid!); + await waitForProcessExit(grandchildPid); + } finally { + forceKillOnWindows(target?.pid); + forceKillOnWindows(grandchildPid); + rmSync(testDir, { recursive: true, force: true }); + } + }); }); describe("process liveness", () => { diff --git a/packages/coding-agent/test/exec.test.ts b/packages/coding-agent/test/exec.test.ts index 6c52abc48ea..1b6226cd1b7 100644 --- a/packages/coding-agent/test/exec.test.ts +++ b/packages/coding-agent/test/exec.test.ts @@ -1,8 +1,9 @@ -import { existsSync, mkdtempSync, rmSync } from "node:fs"; +import { existsSync, mkdtempSync, readFileSync, rmSync } from "node:fs"; import { constants, tmpdir } from "node:os"; import { join } from "node:path"; import { describe, expect, it, vi } from "vitest"; import { execCommand } from "../src/core/exec.js"; +import { isProcessAlive } from "../src/utils/child-process.js"; const SIGKILL_EXIT_CODE = 128 + constants.signals.SIGKILL; @@ -14,6 +15,14 @@ async function waitForFile(path: string): Promise { } } +async function waitForProcessExit(pid: number): Promise { + const deadline = Date.now() + 10_000; + while (isProcessAlive(pid)) { + if (Date.now() >= deadline) throw new Error(`Process ${pid} did not exit`); + await new Promise((resolve) => setTimeout(resolve, 25)); + } +} + describe.skipIf(process.platform === "win32")("execCommand", () => { it("force kills a process that ignores SIGTERM and cleans up the fallback timer", async () => { const testDir = mkdtempSync(join(tmpdir(), "prime-agent-exec-test-")); @@ -54,23 +63,39 @@ describe.skipIf(process.platform === "win32")("execCommand", () => { describe.skipIf(process.platform !== "win32")("execCommand on Windows", () => { it("cancels a running process tree", async () => { const testDir = mkdtempSync(join(tmpdir(), "prime-agent-exec-windows-test-")); - const readyFile = join(testDir, "ready"); + const grandchildPidFile = join(testDir, "grandchild.pid"); const controller = new AbortController(); + let grandchildPid: number | undefined; let resultPromise: Promise>> | undefined; try { resultPromise = execCommand( process.execPath, - ["-e", `require("node:fs").writeFileSync(process.argv[1], ""); setInterval(() => {}, 1000);`, readyFile], + [ + "-e", + `const { spawn } = require("node:child_process"); const { writeFileSync } = require("node:fs"); const child = spawn(process.execPath, ["-e", "setInterval(() => {}, 1000)"], { stdio: "ignore" }); writeFileSync(process.argv[1], String(child.pid)); setInterval(() => {}, 1000);`, + grandchildPidFile, + ], process.cwd(), { signal: controller.signal }, ); - await waitForFile(readyFile); + await waitForFile(grandchildPidFile); + grandchildPid = Number.parseInt(readFileSync(grandchildPidFile, "utf8"), 10); + expect(isProcessAlive(grandchildPid)).toBe(true); + controller.abort(); const result = await resultPromise; expect(result.killed).toBe(true); + await waitForProcessExit(grandchildPid); } finally { controller.abort(); await resultPromise; + if (grandchildPid && isProcessAlive(grandchildPid)) { + try { + process.kill(grandchildPid, "SIGKILL"); + } catch { + // The process may have exited after the liveness check. + } + } rmSync(testDir, { recursive: true, force: true }); } }); diff --git a/scripts/test-windows-installer.ps1 b/scripts/test-windows-installer.ps1 index c5eb17a303d..f52a714a95f 100644 --- a/scripts/test-windows-installer.ps1 +++ b/scripts/test-windows-installer.ps1 @@ -6,7 +6,7 @@ Set-StrictMode -Version Latest $ErrorActionPreference = "Stop" $ProgressPreference = "SilentlyContinue" -if (-not $IsWindows -and $PSVersionTable.PSEdition -eq "Core") { +if ($PSVersionTable.PSEdition -eq "Core" -and -not $IsWindows) { throw "Windows installer smoke test must run on Windows." } @@ -18,11 +18,13 @@ $testRoot = Join-Path ([System.IO.Path]::GetTempPath()) "prime-agent-windows-ins $serverRoot = Join-Path $testRoot "server" $releaseDir = Join-Path $serverRoot "releases\v$version" $stagingDir = Join-Path $testRoot "archive" -$localAppData = Join-Path $testRoot "local-app-data" +$localAppData = Join-Path $testRoot ("local app {0}{1} & % ! (test)" -f ([char]0x6D4B), ([char]0x8BD5)) $artifactName = "prime-agent-$version-windows-x64.zip" $artifactPath = Join-Path $releaseDir $artifactName $originalLocalAppData = $env:LOCALAPPDATA $originalDownloadBaseUrl = $env:PRIME_AGENT_DOWNLOAD_BASE_URL +$originalProcessorArchitecture = $env:PROCESSOR_ARCHITECTURE +$originalProcessorArchitectureW6432 = $env:PROCESSOR_ARCHITEW6432 $originalUserPath = [Environment]::GetEnvironmentVariable("PATH", "User") $server = $null @@ -57,7 +59,13 @@ try { $env:LOCALAPPDATA = $localAppData $env:PRIME_AGENT_DOWNLOAD_BASE_URL = $baseUrl + # Environment variables describe an emulated process, not the native OS. + # Poison both values so this x64 runner proves the installer uses the Win32 native-machine API. + $env:PROCESSOR_ARCHITECTURE = "ARM64" + $env:PROCESSOR_ARCHITEW6432 = "ARM64" & (Join-Path $repoRoot "install.ps1") -Version $version + $env:PROCESSOR_ARCHITECTURE = $originalProcessorArchitecture + $env:PROCESSOR_ARCHITEW6432 = $originalProcessorArchitectureW6432 $shim = Join-Path $localAppData "PrimeAgent\bin\prime-agent.cmd" $binary = Join-Path $localAppData "PrimeAgent\versions\v$version\prime-agent.exe" @@ -82,5 +90,7 @@ try { [Environment]::SetEnvironmentVariable("PATH", $originalUserPath, "User") $env:LOCALAPPDATA = $originalLocalAppData $env:PRIME_AGENT_DOWNLOAD_BASE_URL = $originalDownloadBaseUrl + $env:PROCESSOR_ARCHITECTURE = $originalProcessorArchitecture + $env:PROCESSOR_ARCHITEW6432 = $originalProcessorArchitectureW6432 Remove-Item -LiteralPath $testRoot -Recurse -Force -ErrorAction SilentlyContinue } From 90d3f184994733265b35f0fdbbd305f56b2356a1 Mon Sep 17 00:00:00 2001 From: Seth Date: Mon, 7 Sep 2026 19:20:22 -0400 Subject: [PATCH 40/45] Fix Windows installer and process identity handling Preserve child-ID ambiguity and passivation during message lookup. Verify deferred Windows termination identity and report final kill failures. Exercise rendered installers and document runtime requirements. --- .github/workflows/ci.yml | 11 + install.ps1 | 4 +- packages/coding-agent/docs/windows.md | 6 +- packages/coding-agent/src/core/exec.ts | 61 ++-- .../src/modes/daemon/daemon-mode.ts | 46 ++- .../coding-agent/src/utils/child-process.ts | 41 +-- .../src/utils/windows-process-signal.ts | 122 ++++++++ .../coding-agent/test/daemon-mode.test.ts | 292 ++++++++++++++++++ .../fixtures/windows-process-signal-native.ts | 198 ++++++++++++ .../test/windows-process-signal.test.ts | 259 ++++++++++++++++ scripts/test-windows-installer.ps1 | 18 +- 11 files changed, 991 insertions(+), 67 deletions(-) create mode 100644 packages/coding-agent/src/utils/windows-process-signal.ts create mode 100644 packages/coding-agent/test/fixtures/windows-process-signal-native.ts create mode 100644 packages/coding-agent/test/windows-process-signal.test.ts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a7a446ba206..36c1b2c0ec0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -355,6 +355,17 @@ jobs: test/daemon-socket.test.ts test/exec.test.ts test/kernel-bash-shell.test.ts + test/windows-process-signal.test.ts + + - name: Test identity-pinned tree signals under Bun and Node + working-directory: packages/coding-agent + run: | + bun ./test/fixtures/windows-process-signal-native.ts + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + bun build ./test/fixtures/windows-process-signal-native.ts --target=node --external koffi --outfile=./node_modules/.cache/windows-process-signal-native.mjs + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + node node_modules/.cache/windows-process-signal-native.mjs + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } - name: Test terminal behavior working-directory: packages/tui diff --git a/install.ps1 b/install.ps1 index d0cc0e61ab2..4d83c2c0ad5 100644 --- a/install.ps1 +++ b/install.ps1 @@ -35,7 +35,7 @@ $DownloadBaseUrl = if ($env:PRIME_AGENT_DOWNLOAD_BASE_URL) { "__PRIME_AGENT_DOWNLOAD_BASE_URL__" } $DefaultChannel = "__PRIME_AGENT_DEFAULT_RELEASE_CHANNEL__" -if ($DefaultChannel -eq "__PRIME_AGENT_DEFAULT_RELEASE_CHANNEL__") { +if ($DefaultChannel -eq ("__PRIME_AGENT_DEFAULT_RELEASE_" + "CHANNEL__")) { $DefaultChannel = "stable" } if (-not $Channel) { @@ -248,7 +248,7 @@ if ($Uninstall) { Uninstall-PrimeAgent exit 0 } -if ($DownloadBaseUrl -eq "__PRIME_AGENT_DOWNLOAD_BASE_URL__") { +if ($DownloadBaseUrl -eq ("__PRIME_AGENT_DOWNLOAD_BASE" + "_URL__")) { throw "Installer download URL is not configured. Use the published installer or set PRIME_AGENT_DOWNLOAD_BASE_URL." } diff --git a/packages/coding-agent/docs/windows.md b/packages/coding-agent/docs/windows.md index 33d70d9171d..8df17dd5f6f 100644 --- a/packages/coding-agent/docs/windows.md +++ b/packages/coding-agent/docs/windows.md @@ -1,6 +1,6 @@ # Windows Setup -Prime Agent supports native Windows 10 and 11 on x64 and Arm64. WSL is not required. +Prime Agent supports native Windows 10 version 1809 or later and Windows 11 on x64 and Arm64. WSL is not required. ## Requirements @@ -72,7 +72,7 @@ Git Bash is the supported default. Cygwin and MSYS2 can be selected explicitly, The persistent CPython kernel is prepared automatically with `uv`. The Windows bootstrap uses PowerShell and a virtual environment at `~/.prime/agent/kernel-venv`, whose interpreter is under `Scripts\python.exe`. -On managed systems that block the uv installer, install uv separately or set `PRIME_AGENT_KERNEL_PYTHON` to a CPython environment that already contains `prime-agent-runtime`. +On managed systems that block the uv installer, install uv separately or set `PRIME_AGENT_KERNEL_PYTHON` to a CPython environment that already contains a current `prime-agent-runtime` and the default Python packages, including `PyYAML`. ## Troubleshooting @@ -92,6 +92,8 @@ Run the command in a normal interactive PowerShell session. Organization policy Run `prime-agent shutdown --force`. Prime Agent uses Windows process-tree termination and CPython Job Objects, but a process moved into a separately managed Windows service or job can require manual termination. +Verified process-tree termination requires permitted Windows PowerShell. Under Node, it also requires the optional `koffi` package. The .NET `Process.Handle` check requires `ALL_ACCESS`; narrower process DACLs can cause refusal even when `taskkill` alone could work. Failed verification or helper launch does not fall back to a PID-only kill. `execCommand` reports final termination failure as `code: 1` and `killed: false`. It continues watching and draining the live child until it exits. + ### Terminal input or colors are incorrect Use an updated Windows Terminal profile. See [Terminal Setup](terminal-setup.md) for the recommended key mappings. diff --git a/packages/coding-agent/src/core/exec.ts b/packages/coding-agent/src/core/exec.ts index ff5606c2bd9..b6ed96be0b9 100644 --- a/packages/coding-agent/src/core/exec.ts +++ b/packages/coding-agent/src/core/exec.ts @@ -69,6 +69,7 @@ export async function execCommand( let stdout = ""; let stderr = ""; let killed = false; + let settled = false; let timeoutId: NodeJS.Timeout | undefined; let forceKillTimeoutId: NodeJS.Timeout | undefined; @@ -84,33 +85,27 @@ export async function execCommand( forceKillTimeoutId = undefined; if (proc.exitCode === null && proc.signalCode === null) { if (proc.pid) { - signalProcessGroupOrProcess(proc.pid, "SIGKILL"); + signalProcessGroupOrProcess(proc.pid, "SIGKILL", (error) => { + if (proc.exitCode !== null || proc.signalCode !== null) return; + finish({ + stdout, + stderr: `${stderr}\nCould not terminate owned process ${proc.pid}; it may still be running: ${error.message}`, + code: 1, + killed: false, + }); + }); } } }, 5000); } }; - if (options?.signal) { - if (options.signal.aborted) { - killProcess(); - } else { - options.signal.addEventListener("abort", killProcess, { once: true }); - } - } - - if (options?.timeout && options.timeout > 0) { - timeoutId = setTimeout(() => { - killProcess(); - }, options.timeout); - } - proc.stdout?.on("data", (data) => { - stdout += data.toString(); + if (!settled) stdout += data.toString(); }); proc.stderr?.on("data", (data) => { - stderr += data.toString(); + if (!settled) stderr += data.toString(); }); const cleanup = () => { @@ -121,16 +116,36 @@ export async function execCommand( } }; - // Wait for process termination without hanging on inherited stdio handles - // held open by detached descendants. + const finish = (result: ExecResult) => { + if (settled) return; + settled = true; + cleanup(); + resolve(result); + stdout = ""; + stderr = ""; + }; + + // Failed cancellation settles the result, not ownership: keep draining and waiting for real exit. waitForChildProcess(proc) .then((code) => { - cleanup(); - resolve({ stdout, stderr, code: code ?? 0, killed }); + finish({ stdout, stderr, code: code ?? 0, killed }); }) .catch((_err) => { - cleanup(); - resolve({ stdout, stderr, code: 1, killed }); + finish({ stdout, stderr, code: 1, killed }); }); + + if (options?.signal) { + if (options.signal.aborted) { + killProcess(); + } else { + options.signal.addEventListener("abort", killProcess, { once: true }); + } + } + + if (options?.timeout && options.timeout > 0) { + timeoutId = setTimeout(() => { + killProcess(); + }, options.timeout); + } }); } diff --git a/packages/coding-agent/src/modes/daemon/daemon-mode.ts b/packages/coding-agent/src/modes/daemon/daemon-mode.ts index fe07a6cf682..ce1f6bc39a7 100644 --- a/packages/coding-agent/src/modes/daemon/daemon-mode.ts +++ b/packages/coding-agent/src/modes/daemon/daemon-mode.ts @@ -6139,29 +6139,41 @@ export class AgentDaemon { const resolved = this.resolveAgentFamilySessionName(options.fromState, targetSelector, error); targetState = await this.getOrHydrateBoundSessionState(resolved.activeSessionId); } else { - const residentChild = [...this.sessions.values()].find( + const initialResidentMatches = [...this.sessions.values()].filter( (state) => state.runtime.metadata.kind === "subagent" && state.runtime.metadata.rlmChildId === targetSelector, ); - const passiveSubagent = residentChild ? undefined : await this.findPassiveRlmSubagent(targetSelector); - if (residentChild) { - targetState = await this.waitForHydratingChild(residentChild, targetSelector); - } else if (passiveSubagent) { - if (options.origin === "agent" && options.fromState) { - assertAgentFamilyReach( - this.agentFamilyEntry(options.fromState), - this.passiveAgentFamilyEntry(passiveSubagent), - ); - } - targetState = await this.hydratePassiveRlmSubagent(passiveSubagent); - } else { - const hydratingChild = [...this.sessions.values()].find( + if (initialResidentMatches.length > 1) { + throw new Error(`Session selector "${targetSelector}" is ambiguous`); + } + const reservedResident = initialResidentMatches[0]; + if (reservedResident) reservedResident.pendingAttaches++; + try { + const passiveSubagent = await this.findPassiveRlmSubagent(targetSelector, true); + const residentMatches = [...this.sessions.values()].filter( (state) => state.runtime.metadata.kind === "subagent" && state.runtime.metadata.rlmChildId === targetSelector, ); - if (hydratingChild) { - targetState = await this.waitForHydratingChild(hydratingChild, targetSelector); + const residentChild = residentMatches[0]; + if ( + residentMatches.length > 1 || + (residentChild && + passiveSubagent && + this.findSessionBySessionFile(passiveSubagent.entry.sessionFile) !== residentChild) + ) { + throw new Error(`Session selector "${targetSelector}" is ambiguous`); + } + if (passiveSubagent) { + if (options.origin === "agent" && options.fromState) { + assertAgentFamilyReach( + this.agentFamilyEntry(options.fromState), + this.passiveAgentFamilyEntry(passiveSubagent), + ); + } + targetState = await this.hydratePassiveRlmSubagent(passiveSubagent); + } else if (residentChild) { + targetState = await this.waitForHydratingChild(residentChild, targetSelector); } else if (this.options.worker && options.fromState) { // The supervisor can resolve and wake a saved worker even when it is no longer // present in this worker's resident peer snapshot. @@ -6169,6 +6181,8 @@ export class AgentDaemon { } else { throw error; } + } finally { + if (reservedResident) reservedResident.pendingAttaches--; } } } diff --git a/packages/coding-agent/src/utils/child-process.ts b/packages/coding-agent/src/utils/child-process.ts index 923dafb71a7..eb437c63a2c 100644 --- a/packages/coding-agent/src/utils/child-process.ts +++ b/packages/coding-agent/src/utils/child-process.ts @@ -19,6 +19,7 @@ import { import { readFileSync } from "node:fs"; import { constants } from "node:os"; import { basename } from "node:path"; +import { createWindowsProcessTreeSignal } from "./windows-process-signal.js"; const EXIT_STDIO_GRACE_MS = 100; @@ -167,30 +168,30 @@ export function signalProcessGroupIfHeld(pgid: number, signal: NodeJS.Signals): return true; } -export function signalProcessGroupOrProcess(pid: number, signal: NodeJS.Signals): void { +export function signalProcessGroupOrProcess( + pid: number, + signal: NodeJS.Signals, + onFailure?: (error: Error) => void, +): void { if (process.platform === "win32") { - const fallbackSignal = signal === "SIGKILL" ? "SIGTERM" : signal; - const fallback = () => { - try { - process.kill(pid, fallbackSignal); - } catch { - // Process may already be dead. - } + let reported = false; + const reportFailure = (reason: unknown) => { + if (reported) return; + reported = true; + const error = reason instanceof Error ? reason : new Error(String(reason)); + if (onFailure) onFailure(error); + else console.error(`Could not signal Windows process tree ${pid} (${signal}): ${error.message}`); }; try { - const args = ["/PID", String(pid), "/T"]; - if (signal === "SIGKILL") args.push("/F"); - const taskkill = spawnHidden("taskkill", args, { - stdio: "ignore", - detached: true, - }); - taskkill.once("error", fallback); - taskkill.once("exit", (code) => { - if (code !== 0 && isProcessAlive(pid)) fallback(); + const { command, args } = createWindowsProcessTreeSignal(pid, signal); + const helper = spawnHidden(command, args, { stdio: "ignore", detached: true }); + helper.once("error", reportFailure); + helper.once("exit", (code) => { + if (code !== 0 && isProcessAlive(pid)) reportFailure(`identity helper exited with code ${code}`); }); - taskkill.unref(); - } catch { - fallback(); + helper.unref(); + } catch (error) { + reportFailure(error); } return; } diff --git a/packages/coding-agent/src/utils/windows-process-signal.ts b/packages/coding-agent/src/utils/windows-process-signal.ts new file mode 100644 index 00000000000..c18b77fd2e1 --- /dev/null +++ b/packages/coding-agent/src/utils/windows-process-signal.ts @@ -0,0 +1,122 @@ +import { createRequire } from "node:module"; +import { win32 } from "node:path"; + +const cjsRequire = createRequire(import.meta.url); + +interface ProcessSymbols { + OpenProcess(access: number, inherit: boolean, pid: number): unknown; + GetProcessTimes(handle: unknown, creation: unknown, exit: unknown, kernel: unknown, user: unknown): boolean; + CloseHandle(handle: unknown): boolean; +} + +interface ProcessLibrary { + symbols: ProcessSymbols; + close(): void; +} + +interface BunFfi { + dlopen(library: string, symbols: Record): ProcessLibrary; + ptr(buffer: Uint32Array): unknown; +} + +function openProcessLibrary(): ProcessLibrary & { ptr(buffer: Uint32Array): unknown } { + const ffi = (Reflect.get(globalThis, "Bun") as { FFI?: BunFfi } | undefined)?.FFI; + if (ffi) { + const library = ffi.dlopen("kernel32.dll", { + OpenProcess: { args: ["u32", "bool", "u32"], returns: "ptr" }, + GetProcessTimes: { args: ["ptr", "ptr", "ptr", "ptr", "ptr"], returns: "bool" }, + CloseHandle: { args: ["ptr"], returns: "bool" }, + }); + return { symbols: library.symbols, close: () => library.close(), ptr: (buffer) => ffi.ptr(buffer) }; + } + // koffi belongs to pi-tui and is optional; do not rely on workspace hoisting. + const tuiRequire = createRequire(cjsRequire.resolve("@earendil-works/pi-tui/package.json")); + const koffi = tuiRequire("koffi"); + const library = koffi.load("kernel32.dll"); + const openProcess = library.func("void* __stdcall OpenProcess(uint32_t, int, uint32_t)"); + return { + symbols: { + OpenProcess: (access, inherit, pid) => openProcess(access, Number(inherit), pid), + GetProcessTimes: library.func( + "int __stdcall GetProcessTimes(void*, _Out_ uint32_t*, _Out_ uint32_t*, _Out_ uint32_t*, _Out_ uint32_t*)", + ), + CloseHandle: library.func("int __stdcall CloseHandle(void*)"), + }, + close: () => library.unload(), + ptr: (buffer) => buffer, + }; +} + +/** Capture from one native HANDLE before scheduling any PID-based helper. */ +export function captureWindowsProcessCreationTime(pid: number): string { + if (!Number.isInteger(pid) || pid <= 0 || pid > 0xffffffff) throw new Error("Invalid process ID"); + const library = openProcessLibrary(); + let handle: unknown; + let creationTime: string; + let closed = true; + try { + handle = library.symbols.OpenProcess(0x1000, false, pid); // PROCESS_QUERY_LIMITED_INFORMATION + if (!handle) throw new Error("Cannot open process for identity verification"); + const times = Array.from({ length: 4 }, () => new Uint32Array(2)); + const [creation, exit, kernel, user] = times.map((time) => library.ptr(time)); + if (!library.symbols.GetProcessTimes(handle, creation, exit, kernel, user)) { + throw new Error("Cannot read process creation time"); + } + creationTime = ((BigInt(times[0]![1]!) << 32n) | BigInt(times[0]![0]!)).toString(); + } finally { + try { + if (handle) closed = library.symbols.CloseHandle(handle); + } finally { + library.close(); + } + } + if (!closed) throw new Error("Cannot close process identity handle"); + return creationTime; +} + +export function createWindowsProcessTreeSignal( + pid: number, + signal: NodeJS.Signals, +): { command: string; args: string[] } { + const creationTime = captureWindowsProcessCreationTime(pid); + // Handle must be cached BEFORE StartTime: .NET then queries the same process object. + // .Handle requires ALL_ACCESS; denied access, missing koffi, or blocked PowerShell fail closed. + const script = ` +$ErrorActionPreference = 'Stop' +$targetId = ${pid} +$process = $null +$code = 2 +try { + $process = [System.Diagnostics.Process]::GetProcessById($targetId) + $null = $process.Handle + $actual = $process.StartTime.ToUniversalTime().ToFileTimeUtc().ToString([System.Globalization.CultureInfo]::InvariantCulture) + if ($actual -eq '${creationTime}') { + & "$env:SystemRoot\\System32\\taskkill.exe" /PID $targetId /T ${signal === "SIGKILL" ? "/F" : ""} + $code = $LASTEXITCODE + } else { + $code = 3 + } +} catch { + $code = 2 +} finally { + if ($null -ne $process) { $process.Dispose() } +} +exit $code +`; + return { + command: win32.join( + process.env.SystemRoot ?? "C:\\Windows", + "System32", + "WindowsPowerShell", + "v1.0", + "powershell.exe", + ), + args: [ + "-NoLogo", + "-NoProfile", + "-NonInteractive", + "-EncodedCommand", + Buffer.from(script, "utf16le").toString("base64"), + ], + }; +} diff --git a/packages/coding-agent/test/daemon-mode.test.ts b/packages/coding-agent/test/daemon-mode.test.ts index 27440e5b155..c75d09af1e0 100644 --- a/packages/coding-agent/test/daemon-mode.test.ts +++ b/packages/coding-agent/test/daemon-mode.test.ts @@ -3390,6 +3390,110 @@ describe("daemon mode helpers", () => { expect(unrelatedHelper.acceptAgentMessagePrompt).not.toHaveBeenCalled(); }); + it.each(["cli", "agent"] as const)( + "rejects duplicate resident child IDs across parents for %s messages", + async (origin) => { + const daemon = new AgentDaemon("/tmp/prime-agent-child-id-ambiguity.sock", { + defaultSessionConfig: { agentDir: "/tmp", cwd: "/tmp" }, + createRuntime: vi.fn(), + }); + const otherRoot = makeAgentFamilyState("other-root", "other-root"); + const unrelatedChild = makeAgentFamilyState("unrelated-child", "unrelated-child", otherRoot.state); + const observer = makeAgentFamilyState("observer", "observer"); + const reachableChild = makeAgentFamilyState("reachable-child", "reachable-child", observer.state); + for (const child of [unrelatedChild, reachableChild]) { + Object.assign(child.state.runtime.metadata, { rlmChildId: "shared-child-id" }); + } + const internals = daemon as unknown as { + sessions: Map; + sendAgentSessionMessage(options: { + targetSelector: string; + message: string; + origin: "cli" | "agent"; + fromState?: ActiveSessionState; + }): Promise; + }; + for (const fixture of [otherRoot, unrelatedChild, observer, reachableChild]) { + internals.sessions.set(fixture.state.activeSessionId, fixture.state); + } + await expect( + internals.sendAgentSessionMessage({ + targetSelector: "shared-child-id", + message: "report progress", + origin, + ...(origin === "agent" ? { fromState: observer.state } : {}), + }), + ).rejects.toThrow(/ambiguous/i); + expect(unrelatedChild.acceptAgentMessagePrompt).not.toHaveBeenCalled(); + expect(reachableChild.acceptAgentMessagePrompt).not.toHaveBeenCalled(); + expect(unrelatedChild.state.pendingAttaches).toBe(0); + expect(reachableChild.state.pendingAttaches).toBe(0); + }, + ); + + it.each([ + { passiveCount: 2, ledgerResident: true }, + { passiveCount: 1, ledgerResident: false }, + ])( + "rejects child-ID ambiguity across resident and passive sessions ($passiveCount passive, ledger=$ledgerResident)", + async ({ passiveCount, ledgerResident }) => { + const tempDir = mkdtempSync(join(tmpdir(), "prime-agent-mixed-child-id-")); + try { + const fixtures = Array.from({ length: passiveCount + (ledgerResident ? 1 : 0) }, (_, index) => + makePersistedRlmDaemonFixture(join(tempDir, `family-${index}`)), + ); + const fixture = fixtures[0]!; + const internals = fixture.daemon as unknown as { + sessions: Map; + createRuntime(command: Extract): Promise; + rlmSpawnLedger(): RlmSpawnLedger; + listPassiveRlmSubagents(): Promise>; + sendAgentSessionMessage(options: { + targetSelector: string; + message: string; + origin: "cli"; + }): Promise; + }; + for (const family of fixtures) { + await internals.createRuntime({ type: "create", sessionPath: family.parentSessionFile }); + await internals.rlmSpawnLedger().appendSpawn({ + childId: family.childId, + parent: family.parentSessionFile, + child: family.childSessionFile, + depth: 1, + name: "renamed-worker", + }); + } + if (ledgerResident) { + await internals.createRuntime({ type: "create", sessionPath: fixtures.at(-1)!.childSessionFile }); + } else { + const parent = makeAgentFamilyState("ephemeral-parent", "ephemeral-parent"); + const child = makeAgentFamilyState("ephemeral-child", "ephemeral-child", parent.state); + const childManager = SessionManager.create(tempDir, join(tempDir, "ephemeral-child")); + childManager.appendSessionInfo("ephemeral-child"); + Object.assign(child.state.runtime.metadata, { rlmChildId: fixture.childId }); + Object.assign(child.state.runtime.session, { sessionFile: childManager.getSessionFile() }); + internals.sessions.set(parent.state.activeSessionId, parent.state); + internals.sessions.set(child.state.activeSessionId, child.state); + } + expect( + (await internals.listPassiveRlmSubagents()).filter(({ entry }) => entry.childId === fixture.childId), + ).toHaveLength(passiveCount); + await expect( + internals.sendAgentSessionMessage({ + targetSelector: fixture.childId, + message: "ambiguous target", + origin: "cli", + }), + ).rejects.toThrow(/ambiguous/i); + expect(fixture.acceptAgentMessagePrompt).not.toHaveBeenCalled(); + for (const state of internals.sessions.values()) expect(state.pendingAttaches).toBe(0); + } finally { + rmSync(tempDir, { recursive: true, force: true }); + } + }, + ); + it("keeps a session ID ambiguous when a reachable agent uses it as its name", async () => { const daemon = new AgentDaemon("/tmp/prime-agent-family-id-ambiguity.sock", { defaultSessionConfig: { agentDir: "/tmp", cwd: "/tmp" }, @@ -6871,6 +6975,193 @@ describe("daemon mode helpers", () => { } }); + it("keeps an ephemeral parent's child resident while resolving its child-ID message", async () => { + const tempDir = mkdtempSync(join(tmpdir(), "prime-agent-ephemeral-child-message-")); + let releaseLookup = () => {}; + const lookupGate = new Promise((resolve) => { + releaseLookup = resolve; + }); + let markLookup = () => {}; + const lookupStarted = new Promise((resolve) => { + markLookup = resolve; + }); + let releaseAdmission = () => {}; + const admissionGate = new Promise((resolve) => { + releaseAdmission = resolve; + }); + let markAdmission = () => {}; + const admissionStarted = new Promise((resolve) => { + markAdmission = resolve; + }); + let admissionPending = false; + let delivery: Promise | undefined; + try { + const createRuntime = vi.fn(async (options: Parameters[0]) => { + const session = makeRuntimeSession(options.sessionManager); + Object.assign(session, { + isStreaming: false, + isCompacting: false, + isBashRunning: false, + isRetrying: false, + isSessionActive: false, + unfinishedActionCount: 0, + state: { pendingToolCalls: new Set() }, + hasRunningRlmChildren: () => false, + getSessionActionSnapshot: () => ({ queuedCount: 0, steering: [], followUps: [] }), + acceptAgentMessagePrompt: vi.fn( + async (_message: string, options?: { preflightResult?: (ok: boolean) => void }) => { + admissionPending = true; + markAdmission(); + try { + await admissionGate; + options?.preflightResult?.(true); + } finally { + admissionPending = false; + } + }, + ), + }); + Object.defineProperty(session, "hasPendingAdmissionWaiters", { get: () => admissionPending }); + return { + session, + extensionsResult: { extensions: [], errors: [], runtime: {} } as unknown as Awaited< + ReturnType + >["extensionsResult"], + services: { cwd: options.cwd, agentDir: options.agentDir } as Awaited< + ReturnType + >["services"], + diagnostics: [], + }; + }); + const daemon = new AgentDaemon(join(tempDir, "daemon.sock"), { + defaultSessionConfig: { agentDir: tempDir, cwd: tempDir, sessionDir: join(tempDir, "sessions") }, + createRuntime, + }); + const internals = daemon as unknown as { + sessions: Map; + createRuntime(command: Extract): Promise; + createRlmSubagentRuntime( + parent: ActiveSessionState, + options: CreateRlmSubagentRuntimeOptions, + ): Promise; + createSubagentRuntimeHost(parent: ActiveSessionState): SubagentRuntimeHost; + rlmSpawnLedger(): RlmSpawnLedger; + findPassiveRlmSubagent(target: string, includeResident?: boolean): Promise; + passivateIdleChildren(threshold: number, now: number, limit: number): Promise; + createAgentMessageController( + getCurrentState: () => ActiveSessionState | undefined, + ): AgentSessionMessageController; + }; + const parent = await internals.createRuntime({ type: "create", noSession: true }); + expect(parent.runtime.session.sessionFile).toBeUndefined(); + const childId = "ephemeral-child-id"; + const childRuntime = await internals.createRlmSubagentRuntime(parent, { + parentSession: parent.runtime.session, + id: childId, + prompt: "complete", + sessionName: "ephemeral-worker", + sessionDir: join(tempDir, "child"), + model: { provider: "test", id: "model" } as Model, + thinkingLevel: "off", + serviceTier: null, + scopedModels: [], + activeToolNames: [], + customTools: [], + includeGoals: false, + includeCompactSkill: false, + rlmDepth: 1, + rlmMaxDepth: 4, + rlmParentNodeId: childId, + }); + const child = [...internals.sessions.values()].find((state) => state.runtime.session === childRuntime.session); + if (!child?.runtime.session.sessionFile) throw new Error("Missing child state"); + expect( + internals.createSubagentRuntimeHost(parent).completeRlmSubagentRuntime?.(childId, childRuntime.session), + ).toBe(true); + expect( + (await internals.rlmSpawnLedger().edges()).some( + (edge) => edge.child === canonicalSessionPath(child.runtime.session.sessionFile!), + ), + ).toBe(false); + const realLookup = internals.findPassiveRlmSubagent.bind(internals); + vi.spyOn(internals, "findPassiveRlmSubagent").mockImplementation(async (target, includeResident) => { + const result = await realLookup(target, includeResident); + if (target === childId) { + markLookup(); + await lookupGate; + } + return result; + }); + delivery = internals + .createAgentMessageController(() => parent) + .sendAgentMessage({ target: childId, message: "stay resident for this message" }); + void delivery.catch(() => undefined); + await Promise.race([lookupStarted, admissionStarted]); + const passivated = await internals.passivateIdleChildren(90, Date.parse("2036-08-01T12:00:00Z"), 1); + const resident = internals.sessions.get(child.activeSessionId) === child; + releaseLookup(); + releaseAdmission(); + const outcome = await delivery.then( + (receipt) => receipt, + (error: Error) => ({ error: error.message }), + ); + expect({ passivated, resident, outcome }).toMatchObject({ + passivated: 0, + resident: true, + outcome: { deliveryStatus: "delivered" }, + }); + expect(child.pendingAttaches).toBe(0); + } finally { + releaseLookup(); + releaseAdmission(); + await delivery?.catch(() => undefined); + rmSync(tempDir, { recursive: true, force: true }); + } + }); + + it.each(["lookup", "authorization"] as const)( + "releases a child-message reservation after %s failure", + async (failure) => { + const tempDir = mkdtempSync(join(tmpdir(), "prime-agent-child-message-reservation-")); + let restoreLookup = () => {}; + try { + const fixture = makePersistedRlmDaemonFixture(tempDir); + const internals = fixture.daemon as unknown as { + sessions: Map; + createRuntime(command: Extract): Promise; + findPassiveRlmSubagent(target: string, includeResident?: boolean): Promise; + sendAgentSessionMessage(options: { + targetSelector: string; + message: string; + origin: "agent"; + fromState: ActiveSessionState; + }): Promise; + }; + await internals.createRuntime({ type: "create", sessionPath: fixture.parentSessionFile }); + const child = await internals.createRuntime({ type: "create", sessionPath: fixture.childSessionFile }); + const sender = makeAgentFamilyState("unrelated-sender", "unrelated-sender"); + internals.sessions.set(sender.state.activeSessionId, sender.state); + if (failure === "lookup") { + const spy = vi.spyOn(internals, "findPassiveRlmSubagent").mockRejectedValue(new Error("lookup failed")); + restoreLookup = () => spy.mockRestore(); + } + await expect( + internals.sendAgentSessionMessage({ + targetSelector: fixture.childId, + message: "denied", + origin: "agent", + fromState: sender.state, + }), + ).rejects.toThrow(failure === "lookup" ? "lookup failed" : AGENT_FAMILY_REACH_ERROR); + expect(child.pendingAttaches).toBe(0); + expect(fixture.acceptAgentMessagePrompt).not.toHaveBeenCalled(); + } finally { + restoreLookup(); + rmSync(tempDir, { recursive: true, force: true }); + } + }, + ); + it("waits for passivation before rehydrating and delivering a racing a2a message", async () => { const tempDir = mkdtempSync(join(tmpdir(), "prime-agent-daemon-passivation-race-")); let releaseDispose!: () => void; @@ -10107,6 +10398,7 @@ function makeAgentFamilyState( parent?: ActiveSessionState, ): { state: ActiveSessionState; acceptAgentMessagePrompt: ReturnType } { const state = makeState(activeSessionId, parent?.activeSessionId); + state.pendingAttaches = 0; const acceptAgentMessagePrompt = vi.fn( (_message: string, options?: { preflightResult?: (didSucceed: boolean) => void }) => { options?.preflightResult?.(true); diff --git a/packages/coding-agent/test/fixtures/windows-process-signal-native.ts b/packages/coding-agent/test/fixtures/windows-process-signal-native.ts new file mode 100644 index 00000000000..134c1bbc20b --- /dev/null +++ b/packages/coding-agent/test/fixtures/windows-process-signal-native.ts @@ -0,0 +1,198 @@ +import assert from "node:assert/strict"; +import type { ChildProcess } from "node:child_process"; +import { existsSync, mkdtempSync, readFileSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { execCommand } from "../../src/core/exec.js"; +import { isProcessAlive, spawnHidden, waitForChildProcess } from "../../src/utils/child-process.js"; +import { + captureWindowsProcessCreationTime, + createWindowsProcessTreeSignal, +} from "../../src/utils/windows-process-signal.js"; + +assert.equal(process.platform, "win32", "This fixture requires native Windows"); +const nodeVersion = process.versions.node.split(".").map(Number); +assert(nodeVersion[0]! > 22 || (nodeVersion[0] === 22 && nodeVersion[1]! >= 8)); + +const quote = (text: string) => `'${text.replaceAll("'", "''")}'`; +const decode = (command: { args: string[] }) => Buffer.from(command.args.at(-1)!, "base64").toString("utf16le"); +const encode = (command: { args: string[] }, script: string) => { + command.args[command.args.length - 1] = Buffer.from(script, "utf16le").toString("base64"); +}; +const gate = (path: string) => `while (!(Test-Path -LiteralPath ${quote(path)})) { Start-Sleep -Milliseconds 25 }`; + +if (process.argv[2] === "--exit-caller") { + const command = createWindowsProcessTreeSignal(Number(process.argv[3]), "SIGKILL"); + encode(command, `${gate(process.argv[4]!)}\n${decode(command)}`); + const helper = spawnHidden(command.command, command.args, { detached: true, stdio: "ignore" }); + writeFileSync(process.argv[5]!, String(helper.pid)); + process.exit(0); +} + +async function until(condition: () => boolean): Promise { + const deadline = Date.now() + 15000; + while (!condition()) { + assert(Date.now() < deadline, "Native process condition timed out"); + await new Promise((resolve) => setTimeout(resolve, 25)); + } +} + +const directory = mkdtempSync(join(tmpdir(), "prime-agent-identity-native-")); +const stopMarker = join(directory, "stop-owned-tree"); +const leafScript = `const { existsSync } = require("node:fs"); setInterval(() => { if (existsSync(process.argv[1])) process.exit(0); }, 25);`; +const treeScript = `const { spawn } = require("node:child_process"); const { existsSync, writeFileSync } = require("node:fs"); const child = spawn(process.execPath, ["-e", ${JSON.stringify(leafScript)}, process.argv[2]], { stdio: "ignore" }); child.on("spawn", () => writeFileSync(process.argv[1], JSON.stringify([process.pid, child.pid]))); setInterval(() => { if (existsSync(process.argv[2])) process.exit(0); }, 25);`; +const processes: Array<{ pid: number; identity?: string }> = []; +const gates: string[] = []; +const cleanupErrors: unknown[] = []; +const owners: ChildProcess[] = []; +let execFinished = true; +let testError: unknown; + +async function readTree(path: string): Promise<[number, number]> { + await until(() => existsSync(path)); + const pids = JSON.parse(readFileSync(path, "utf8")) as [number, number]; + const records = pids.map((pid) => ({ pid, identity: undefined as string | undefined })); + processes.push(...records); + for (const record of records) { + assert(isProcessAlive(record.pid)); + record.identity = captureWindowsProcessCreationTime(record.pid); + } + return pids; +} + +async function liveTree(name: string): Promise<{ root: ChildProcess; pids: [number, number] }> { + const path = join(directory, name); + const root = spawnHidden(process.execPath, ["-e", treeScript, path, stopMarker], { stdio: "ignore" }); + owners.push(root); + root.once("error", (error) => cleanupErrors.push(error)); + return { root, pids: await readTree(path) }; +} + +async function runHelper(command: { command: string; args: string[] }): Promise { + return waitForChildProcess(spawnHidden(command.command, command.args, { stdio: "ignore", detached: true })); +} + +try { + // A real abort must remove both live processes; a root-only fallback would cancel escalation. + const ready = join(directory, "exec-tree"); + const controller = new AbortController(); + execFinished = false; + const result = execCommand(process.execPath, ["-e", treeScript, ready, stopMarker], process.cwd(), { + signal: controller.signal, + }); + result.then( + () => { + execFinished = true; + }, + () => { + execFinished = true; + }, + ); + const owned = await readTree(ready); + controller.abort(); + assert.equal((await result).killed, true); + await until(() => owned.every((pid) => !isProcessAlive(pid))); + console.log("PASS owned exec removes a live whole tree"); + + const mismatched = await liveTree("mismatched"); + const mismatchCommand = createWindowsProcessTreeSignal(mismatched.pids[0], "SIGKILL"); + encode(mismatchCommand, decode(mismatchCommand).replace(/\$actual -eq '\d+'/, "$actual -eq '1'")); + assert.equal(await runHelper(mismatchCommand), 3); + assert(mismatched.pids.every(isProcessAlive)); + console.log("PASS mismatched identity leaves the whole tree alive"); + + const departing = await liveTree("departing"); + const release = join(directory, "caller-exit-gate"); + const helperPidFile = join(directory, "caller-exit-helper"); + gates.push(release); + const caller = spawnHidden( + process.execPath, + [process.argv[1]!, "--exit-caller", String(departing.pids[0]), release, helperPidFile], + { stdio: "ignore" }, + ); + owners.push(caller); + assert.equal(await waitForChildProcess(caller), 0); + assert(departing.pids.every(isProcessAlive)); + const helperPid = Number(readFileSync(helperPidFile, "utf8")); + assert(isProcessAlive(helperPid)); + processes.push({ pid: helperPid, identity: captureWindowsProcessCreationTime(helperPid) }); + writeFileSync(release, "release"); + await until(() => departing.pids.every((pid) => !isProcessAlive(pid)) && !isProcessAlive(helperPid)); + console.log("PASS verification and tree kill after the original caller exits"); + + // Delay after .Handle acquisition, then exit the target before .StartTime is read. + const held = await liveTree("held"); + const identity = captureWindowsProcessCreationTime(held.pids[0]); + const pinReady = join(directory, "pin-ready"); + const pinRelease = join(directory, "pin-release"); + const verified = join(directory, "verified-identity"); + gates.push(pinRelease); + const heldCommand = createWindowsProcessTreeSignal(held.pids[0], "SIGKILL"); + const heldScript = decode(heldCommand) + .replace( + "$null = $process.Handle", + `$null = $process.Handle\n $pin = $process.SafeHandle\n [IO.File]::WriteAllText(${quote(pinReady)}, 'ready')\n ${gate(pinRelease)}`, + ) + .replace(" if ($actual -eq", ` [IO.File]::WriteAllText(${quote(verified)}, $actual)\n if ($actual -eq`) + .replace("exit $code", "if (!$pin.IsClosed) { exit 97 }\nexit $code"); + encode(heldCommand, heldScript); + const heldHelper = spawnHidden(heldCommand.command, heldCommand.args, { stdio: "ignore", detached: true }); + owners.push(heldHelper); + const heldResult = waitForChildProcess(heldHelper); + heldResult.catch((error) => cleanupErrors.push(error)); + processes.push({ pid: heldHelper.pid!, identity: captureWindowsProcessCreationTime(heldHelper.pid!) }); + await until(() => existsSync(pinReady)); + held.root.kill("SIGKILL"); + await waitForChildProcess(held.root); + writeFileSync(pinRelease, "release"); + assert.notEqual(await heldResult, 97, "Process.Dispose must close the helper's own pin"); + assert.equal(readFileSync(verified, "utf8"), identity, "StartTime must query the pinned object after its exit"); + console.log("PASS cached HANDLE lifetime and exact FILETIME after target exit"); + + const gone = await liveTree("gone"); + const goneCommand = createWindowsProcessTreeSignal(gone.pids[0], "SIGKILL"); + gone.root.kill("SIGKILL"); + await waitForChildProcess(gone.root); + assert.notEqual(await runHelper(goneCommand), 0); + assert(isProcessAlive(gone.pids[1])); + console.log("PASS target gone before verification fails closed"); +} catch (error) { + testError = error; +} finally { + const recorded = JSON.stringify(processes.map(({ pid, identity }) => ({ pid, identity: identity ?? null }))); + console.error(`Native fixture records before cleanup (${directory}): ${recorded}`); + try { + writeFileSync(join(directory, "process-records.json"), recorded); + } catch (error) { + cleanupErrors.push(error); + } + // Only these fixture processes observe this unique marker, including orphaned leaves. + for (const path of [...gates, stopMarker]) { + try { + writeFileSync(path, "release"); + } catch (error) { + cleanupErrors.push(error); + } + } + try { + await until( + () => + execFinished && + owners.every((child) => child.exitCode !== null || child.signalCode !== null) && + processes.every(({ pid }) => !isProcessAlive(pid)), + ); + } catch (error) { + cleanupErrors.push(error); + } + // Preserve the marker and records: a late-starting descendant must still stop itself. + for (const error of cleanupErrors) console.error("Native fixture cleanup error:", error); +} +if (testError !== undefined || cleanupErrors.length) { + throw new AggregateError( + testError === undefined ? cleanupErrors : [testError, ...cleanupErrors], + "Native fixture failed", + ); +} +console.log( + `PASS native identity-pinned signals under ${Reflect.get(globalThis, "Bun") ? "Bun" : "Node"} ${process.version}`, +); diff --git a/packages/coding-agent/test/windows-process-signal.test.ts b/packages/coding-agent/test/windows-process-signal.test.ts new file mode 100644 index 00000000000..21df31cf1b5 --- /dev/null +++ b/packages/coding-agent/test/windows-process-signal.test.ts @@ -0,0 +1,259 @@ +import type * as ChildProcessModule from "node:child_process"; +import { EventEmitter } from "node:events"; +import { createRequire } from "node:module"; +import { PassThrough } from "node:stream"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { execCommand } from "../src/core/exec.js"; +import { signalProcessGroupOrProcess } from "../src/utils/child-process.js"; +import { + captureWindowsProcessCreationTime, + createWindowsProcessTreeSignal, +} from "../src/utils/windows-process-signal.js"; + +const calls = vi.hoisted(() => ({ spawn: vi.fn() })); +const actualChildProcess = createRequire(import.meta.url)("node:child_process") as typeof ChildProcessModule; +vi.mock("node:child_process", () => ({ ...actualChildProcess, spawn: calls.spawn })); + +const ffi = ( + Reflect.get(globalThis, "Bun") as { + FFI: { dlopen: (...args: unknown[]) => unknown; ptr: (buffer: Uint32Array) => unknown }; + } +).FFI; +const platform = process.platform; +const creationTime = 134909123456789013n; +const handle = {}; +let helper: EventEmitter & { unref: ReturnType }; +let open: ReturnType; +let query: ReturnType; +let close: ReturnType; +let unload: ReturnType; + +beforeEach(() => { + Object.defineProperty(process, "platform", { value: "win32" }); + helper = Object.assign(new EventEmitter(), { unref: vi.fn() }); + calls.spawn.mockReset().mockReturnValue(helper); + open = vi.fn(() => handle); + query = vi.fn((_handle, creation: Uint32Array) => { + creation[0] = Number(creationTime & 0xffffffffn); + creation[1] = Number(creationTime >> 32n); + return true; + }); + close = vi.fn(() => true); + unload = vi.fn(); + vi.spyOn(ffi, "dlopen").mockReturnValue({ + symbols: { OpenProcess: open, GetProcessTimes: query, CloseHandle: close }, + close: unload, + }); + vi.spyOn(ffi, "ptr").mockImplementation((buffer) => buffer); +}); + +afterEach(() => { + Object.defineProperty(process, "platform", { value: platform }); + vi.useRealTimers(); + vi.restoreAllMocks(); +}); + +describe("Windows process identity capture", () => { + it("preserves FILETIME precision and closes the single queried handle", () => { + expect(captureWindowsProcessCreationTime(42)).toBe(creationTime.toString()); + expect(open).toHaveBeenCalledWith(0x1000, false, 42); + expect(query.mock.calls[0]![0]).toBe(handle); + expect(close.mock.calls).toEqual([[handle]]); + expect(unload).toHaveBeenCalledTimes(1); + }); + + it("closes the pin if querying fails and does not close a denied handle", () => { + query.mockReturnValueOnce(false); + expect(() => captureWindowsProcessCreationTime(42)).toThrow("creation time"); + expect(close.mock.calls).toEqual([[handle]]); + close.mockClear(); + open.mockReturnValueOnce(null); + expect(() => captureWindowsProcessCreationTime(42)).toThrow("identity verification"); + expect(close).not.toHaveBeenCalled(); + expect(unload).toHaveBeenCalledTimes(2); + }); + + it("fails closed if releasing the capture handle fails", () => { + close.mockReturnValueOnce(false); + const failure = vi.fn(); + signalProcessGroupOrProcess(42, "SIGKILL", failure); + expect(failure.mock.calls[0]![0].message).toContain("Cannot close"); + expect(calls.spawn).not.toHaveBeenCalled(); + expect(unload).toHaveBeenCalledTimes(1); + }); + + it("rejects invalid IDs before opening native state", () => { + for (const pid of [0, -1, 1.5, 0x100000000, Number.NaN]) { + expect(() => captureWindowsProcessCreationTime(pid)).toThrow("Invalid process ID"); + } + expect(open).not.toHaveBeenCalled(); + }); + + it("passes the exact identity to canonical PowerShell without changing force policy", () => { + for (const signal of ["SIGTERM", "SIGKILL"] as const) { + const command = createWindowsProcessTreeSignal(42, signal); + expect(command.command).toMatch(/\\System32\\WindowsPowerShell\\v1\.0\\powershell\.exe$/i); + const script = Buffer.from(command.args.at(-1)!, "base64").toString("utf16le"); + expect(script).toContain(`'${creationTime}'`); + expect(script).toContain("/PID $targetId /T"); + expect(script.includes("/F")).toBe(signal === "SIGKILL"); + } + }); +}); + +describe("Windows shared signal failures", () => { + it("fails closed without throwing or spawning when the native adapter is unavailable", () => { + vi.mocked(ffi.dlopen).mockImplementation(() => { + throw new Error("adapter unavailable"); + }); + const failure = vi.fn(); + const kill = vi.spyOn(process, "kill"); + expect(() => signalProcessGroupOrProcess(42, "SIGTERM", failure)).not.toThrow(); + expect(failure.mock.calls[0]![0].message).toBe("adapter unavailable"); + expect(calls.spawn).not.toHaveBeenCalled(); + expect(kill).not.toHaveBeenCalled(); + }); + + it("reports synchronous spawn failure without a PID-only fallback", () => { + calls.spawn.mockImplementationOnce(() => { + throw new Error("helper blocked"); + }); + const failure = vi.fn(); + const kill = vi.spyOn(process, "kill"); + expect(() => signalProcessGroupOrProcess(42, "SIGKILL", failure)).not.toThrow(); + expect(failure.mock.calls[0]![0].message).toBe("helper blocked"); + expect(kill).not.toHaveBeenCalled(); + expect(close.mock.calls).toEqual([[handle]]); + }); + + it.each(["error", "nonzero"])("reports %s only once and never retries a raw PID", (event) => { + const failure = vi.fn(); + const kill = vi.spyOn(process, "kill").mockReturnValue(true); + signalProcessGroupOrProcess(42, "SIGKILL", failure); + if (event === "error") helper.emit("error", new Error("ENOENT")); + helper.emit("exit", 3); + expect(failure).toHaveBeenCalledTimes(1); + expect(kill.mock.calls.every(([, signal]) => signal === 0)).toBe(true); + expect(calls.spawn).toHaveBeenCalledTimes(1); + expect(calls.spawn.mock.calls[0]![2]).toEqual({ stdio: "ignore", detached: true, windowsHide: true }); + expect(helper.unref).toHaveBeenCalledTimes(1); + }); +}); + +function ownedProcess() { + return Object.assign(new EventEmitter(), { + pid: 4242, + exitCode: null as number | null, + signalCode: null as NodeJS.Signals | null, + stdout: new PassThrough(), + stderr: new PassThrough(), + kill: vi.fn(), + unref: vi.fn(), + }); +} + +function finishOwned(proc: ReturnType, code = 0) { + proc.exitCode = code; + proc.emit("exit", code, null); + proc.emit("close", code, null); +} + +describe("owned exec failure reporting", () => { + it.each([false, true])( + "reports two failed attempts without abandoning a live child (already aborted: %s)", + async (aborted) => { + vi.useFakeTimers(); + vi.spyOn(console, "error").mockImplementation(() => {}); + const proc = ownedProcess(); + const blocked = () => { + throw new Error("PowerShell blocked"); + }; + calls.spawn.mockReturnValueOnce(proc).mockImplementationOnce(blocked).mockImplementationOnce(blocked); + const controller = new AbortController(); + if (aborted) controller.abort(); + const removeAbort = vi.spyOn(controller.signal, "removeEventListener"); + let result: Awaited> | undefined; + const pending = execCommand("owned", [], process.cwd(), { signal: controller.signal, timeout: 60000 }); + pending.then((value) => { + result = value; + }); + try { + proc.stdout.write("before failure"); + controller.abort(); + await vi.advanceTimersByTimeAsync(4999); + expect(result).toBeUndefined(); + expect(calls.spawn).toHaveBeenCalledTimes(2); + await vi.advanceTimersByTimeAsync(1); + expect(result?.code).toBe(1); + expect(result?.killed).toBe(false); + expect(result?.stderr).toContain("owned process 4242"); + expect(result?.stderr).toContain("PowerShell blocked"); + expect(result?.stdout).toBe("before failure"); + expect(proc.exitCode).toBeNull(); + expect(proc.listenerCount("exit")).toBeGreaterThan(0); + expect(proc.listenerCount("error")).toBeGreaterThan(0); + expect(proc.listenerCount("close")).toBeGreaterThan(0); + expect(proc.stdout.destroyed).toBe(false); + expect(proc.kill).not.toHaveBeenCalled(); + expect(proc.unref).not.toHaveBeenCalled(); + expect(vi.getTimerCount()).toBe(0); + expect(removeAbort).toHaveBeenCalled(); + const chunk = { toString: vi.fn(() => "discard this output") }; + proc.stdout.emit("data", chunk); + proc.stderr.emit("data", chunk); + expect(chunk.toString).not.toHaveBeenCalled(); + } finally { + finishOwned(proc); + await pending; + } + expect(proc.stdout.destroyed).toBe(true); + expect(proc.stderr.destroyed).toBe(true); + for (const event of ["error", "exit", "close"]) expect(proc.listenerCount(event)).toBe(0); + }, + ); + + it("retains the five-second escalation after TERM failure and waits for successful KILL exit", async () => { + vi.useFakeTimers(); + vi.spyOn(console, "error").mockImplementation(() => {}); + const proc = ownedProcess(); + calls.spawn.mockReturnValueOnce(proc).mockImplementationOnce(() => { + throw new Error("TERM blocked"); + }); + const controller = new AbortController(); + let result: Awaited> | undefined; + const pending = execCommand("owned", [], process.cwd(), { signal: controller.signal }); + pending.then((value) => { + result = value; + }); + controller.abort(); + await vi.advanceTimersByTimeAsync(5000); + expect(calls.spawn).toHaveBeenCalledTimes(3); + const script = Buffer.from(calls.spawn.mock.calls[2]![1].at(-1), "base64").toString("utf16le"); + expect(script).toContain("/T /F"); + expect(result).toBeUndefined(); + helper.emit("exit", 0); + finishOwned(proc, 137); + expect(await pending).toMatchObject({ code: 137, killed: true }); + expect(vi.getTimerCount()).toBe(0); + }); + + it("does not replace a real owned exit with a late helper failure", async () => { + vi.useFakeTimers(); + vi.spyOn(console, "error").mockImplementation(() => {}); + const proc = ownedProcess(); + calls.spawn.mockReturnValueOnce(proc).mockImplementationOnce(() => { + throw new Error("TERM blocked"); + }); + const controller = new AbortController(); + const pending = execCommand("owned", [], process.cwd(), { signal: controller.signal }); + controller.abort(); + await vi.advanceTimersByTimeAsync(5000); + proc.exitCode = 0; + proc.emit("exit", 0, null); + helper.emit("error", new Error("late helper failure")); + proc.emit("close", 0, null); + expect(await pending).toMatchObject({ code: 0, killed: true, stderr: "" }); + expect(proc.stdout.destroyed).toBe(true); + expect(vi.getTimerCount()).toBe(0); + }); +}); diff --git a/scripts/test-windows-installer.ps1 b/scripts/test-windows-installer.ps1 index f52a714a95f..d58f7a7f17c 100644 --- a/scripts/test-windows-installer.ps1 +++ b/scripts/test-windows-installer.ps1 @@ -23,6 +23,7 @@ $artifactName = "prime-agent-$version-windows-x64.zip" $artifactPath = Join-Path $releaseDir $artifactName $originalLocalAppData = $env:LOCALAPPDATA $originalDownloadBaseUrl = $env:PRIME_AGENT_DOWNLOAD_BASE_URL +$originalVersion = $env:PRIME_AGENT_VERSION $originalProcessorArchitecture = $env:PROCESSOR_ARCHITECTURE $originalProcessorArchitectureW6432 = $env:PROCESSOR_ARCHITEW6432 $originalUserPath = [Environment]::GetEnvironmentVariable("PATH", "User") @@ -58,12 +59,18 @@ try { } $env:LOCALAPPDATA = $localAppData - $env:PRIME_AGENT_DOWNLOAD_BASE_URL = $baseUrl + $installerSource = Get-Content -LiteralPath (Join-Path $repoRoot "install.ps1") -Raw + foreach ($channel in @("stable", "beta")) { + $rendered = $installerSource.Replace("__PRIME_AGENT_DOWNLOAD_BASE_URL__", $baseUrl).Replace("__PRIME_AGENT_DEFAULT_RELEASE_CHANNEL__", $channel) + Set-Content -LiteralPath (Join-Path $testRoot "install-$channel.ps1") -Value $rendered -Encoding UTF8 + } + $env:PRIME_AGENT_DOWNLOAD_BASE_URL = $null + $env:PRIME_AGENT_VERSION = $null # Environment variables describe an emulated process, not the native OS. # Poison both values so this x64 runner proves the installer uses the Win32 native-machine API. $env:PROCESSOR_ARCHITECTURE = "ARM64" $env:PROCESSOR_ARCHITEW6432 = "ARM64" - & (Join-Path $repoRoot "install.ps1") -Version $version + & (Join-Path $testRoot "install-stable.ps1") $env:PROCESSOR_ARCHITECTURE = $originalProcessorArchitecture $env:PROCESSOR_ARCHITEW6432 = $originalProcessorArchitectureW6432 @@ -77,8 +84,10 @@ try { & $shim --help | Out-Null if ($LASTEXITCODE -ne 0) { throw "Installed prime-agent --help failed with $LASTEXITCODE" } - & (Join-Path $repoRoot "install.ps1") -Version $version -Update - & (Join-Path $repoRoot "install.ps1") -Uninstall + Remove-Item -LiteralPath (Join-Path $serverRoot "stable") + Set-Content -LiteralPath (Join-Path $serverRoot "beta") -Value $version -Encoding Ascii + & (Join-Path $testRoot "install-beta.ps1") -Update + & (Join-Path $testRoot "install-beta.ps1") -Uninstall if (Test-Path -LiteralPath (Join-Path $localAppData "PrimeAgent")) { throw "Uninstall left the PrimeAgent install directory behind" } @@ -90,6 +99,7 @@ try { [Environment]::SetEnvironmentVariable("PATH", $originalUserPath, "User") $env:LOCALAPPDATA = $originalLocalAppData $env:PRIME_AGENT_DOWNLOAD_BASE_URL = $originalDownloadBaseUrl + $env:PRIME_AGENT_VERSION = $originalVersion $env:PROCESSOR_ARCHITECTURE = $originalProcessorArchitecture $env:PROCESSOR_ARCHITEW6432 = $originalProcessorArchitectureW6432 Remove-Item -LiteralPath $testRoot -Recurse -Force -ErrorAction SilentlyContinue From 30dfa53a1996724ceae8bbdf067cf61c621018f5 Mon Sep 17 00:00:00 2001 From: Seth Date: Mon, 7 Sep 2026 19:56:11 -0400 Subject: [PATCH 41/45] Bound Windows process tests and preserve failure diagnostics Add phase budgets and cooperative cleanup without changing termination code. Run native Bun and Node probes independently after prerequisite setup. --- .github/workflows/ci.yml | 28 +++- .../coding-agent/test/child-process.test.ts | 152 +++++++++++------- packages/coding-agent/test/exec.test.ts | 112 +++++++++---- .../fixtures/windows-process-observation.ts | 27 ++++ .../fixtures/windows-process-signal-native.ts | 117 +++++++++++--- 5 files changed, 306 insertions(+), 130 deletions(-) create mode 100644 packages/coding-agent/test/fixtures/windows-process-observation.ts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 36c1b2c0ec0..a28028b5db3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -325,6 +325,7 @@ jobs: persist-credentials: false - name: Setup Bun 1.4.0 + id: windows_bun uses: oven-sh/setup-bun@4bc047ad259df6fc24a6c9b0f9a0cb08cf17fbe5 # v2.0.1 with: bun-version: 1.4.0 @@ -335,9 +336,11 @@ jobs: version: "0.12.4" - name: Verify Bun 1.4.0 + id: windows_bun_verified run: bun run check:bun-version - name: Install dependencies + id: windows_dependencies run: bun install --frozen-lockfile - name: Type check @@ -357,15 +360,24 @@ jobs: test/kernel-bash-shell.test.ts test/windows-process-signal.test.ts - - name: Test identity-pinned tree signals under Bun and Node + - name: Build the Node identity signal fixture + id: windows_node_fixture + if: ${{ !cancelled() && steps.windows_bun.outcome == 'success' && steps.windows_bun_verified.outcome == 'success' && steps.windows_dependencies.outcome == 'success' }} + timeout-minutes: 3 working-directory: packages/coding-agent - run: | - bun ./test/fixtures/windows-process-signal-native.ts - if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } - bun build ./test/fixtures/windows-process-signal-native.ts --target=node --external koffi --outfile=./node_modules/.cache/windows-process-signal-native.mjs - if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } - node node_modules/.cache/windows-process-signal-native.mjs - if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + run: bun build ./test/fixtures/windows-process-signal-native.ts --target=node --external koffi --outfile=./node_modules/.cache/windows-process-signal-native.mjs + + - name: Test identity-pinned tree signals under Bun + if: ${{ !cancelled() && steps.windows_bun.outcome == 'success' && steps.windows_bun_verified.outcome == 'success' && steps.windows_dependencies.outcome == 'success' }} + timeout-minutes: 3 + working-directory: packages/coding-agent + run: bun ./test/fixtures/windows-process-signal-native.ts + + - name: Test identity-pinned tree signals under Node + if: ${{ !cancelled() && steps.windows_bun.outcome == 'success' && steps.windows_bun_verified.outcome == 'success' && steps.windows_dependencies.outcome == 'success' && steps.windows_node_fixture.outcome == 'success' }} + timeout-minutes: 3 + working-directory: packages/coding-agent + run: node ./node_modules/.cache/windows-process-signal-native.mjs - name: Test terminal behavior working-directory: packages/tui diff --git a/packages/coding-agent/test/child-process.test.ts b/packages/coding-agent/test/child-process.test.ts index 89d71efeb92..bc5f117ed95 100644 --- a/packages/coding-agent/test/child-process.test.ts +++ b/packages/coding-agent/test/child-process.test.ts @@ -1,7 +1,7 @@ import type * as ChildProcessModule from "node:child_process"; -import { type ChildProcess, execFileSync, spawn } from "node:child_process"; +import { type ChildProcess, spawn } from "node:child_process"; import { EventEmitter } from "node:events"; -import { existsSync, mkdtempSync, readFileSync, rmSync } from "node:fs"; +import { existsSync, mkdtempSync, readFileSync, writeFileSync } from "node:fs"; import { createRequire } from "node:module"; import { tmpdir } from "node:os"; import { join, resolve } from "node:path"; @@ -22,6 +22,9 @@ import { waitForChildProcess, } from "../src/utils/child-process.js"; +import { captureWindowsProcessCreationTime } from "../src/utils/windows-process-signal.js"; +import { cooperativeTreeScript, observe, waitUntil } from "./fixtures/windows-process-observation.js"; + import { spawnZombieProcess } from "./fixtures/zombie-process.js"; const recordedWindowsHide = vi.hoisted(() => [] as Array); @@ -53,23 +56,6 @@ async function waitForFile(path: string): Promise { } } -async function waitForProcessExit(pid: number): Promise { - const deadline = Date.now() + 10_000; - while (isProcessAlive(pid)) { - if (Date.now() >= deadline) throw new Error(`Process ${pid} did not exit`); - await new Promise((resolveDelay) => setTimeout(resolveDelay, 25)); - } -} - -function forceKillOnWindows(pid: number | undefined): void { - if (!pid || !isProcessAlive(pid)) return; - try { - execFileSync("taskkill", ["/PID", String(pid), "/T", "/F"], { stdio: "ignore" }); - } catch { - // The process may have exited between the liveness check and taskkill. - } -} - describe("waitForChildProcess", () => { it("reports signaled already-exited children as failures", async () => { const child = Object.assign(new EventEmitter(), { @@ -95,48 +81,92 @@ describe("signalProcessGroupOrProcess", () => { expect(() => signalProcessGroupOrProcess(999999999, "SIGKILL")).not.toThrow(); }); - it.skipIf(process.platform !== "win32")("finishes a tree kill after the signaling process exits", async () => { - const testDir = mkdtempSync(join(tmpdir(), "prime-agent-taskkill-test-")); - const grandchildPidFile = join(testDir, "grandchild.pid"); - let target: ChildProcess | undefined; - let grandchildPid: number | undefined; - try { - target = spawn( - process.execPath, - [ - "--eval", - `const { spawn } = require("node:child_process"); const { writeFileSync } = require("node:fs"); const child = spawn(process.execPath, ["--eval", "setInterval(() => {}, 1000)"], { stdio: "ignore" }); writeFileSync(process.argv[1], String(child.pid)); setInterval(() => {}, 1000);`, - grandchildPidFile, - ], - { stdio: "ignore" }, - ); - await waitForFile(grandchildPidFile); - grandchildPid = Number.parseInt(readFileSync(grandchildPidFile, "utf8"), 10); - expect(isProcessAlive(target.pid!)).toBe(true); - expect(isProcessAlive(grandchildPid)).toBe(true); - - const helperUrl = pathToFileURL(resolve(import.meta.dirname, "../src/utils/child-process.ts")).href; - const killer = spawn( - process.execPath, - [ - "--eval", - `import { signalProcessGroupOrProcess } from ${JSON.stringify(helperUrl)}; signalProcessGroupOrProcess(${target.pid}, "SIGKILL");`, - ], - { stdio: "ignore" }, - ); - const killerCode = await new Promise((resolveExit, rejectExit) => { - killer.once("error", rejectExit); - killer.once("exit", resolveExit); - }); - expect(killerCode).toBe(0); - await waitForProcessExit(target.pid!); - await waitForProcessExit(grandchildPid); - } finally { - forceKillOnWindows(target?.pid); - forceKillOnWindows(grandchildPid); - rmSync(testDir, { recursive: true, force: true }); - } - }); + it.skipIf(process.platform !== "win32")( + "finishes a tree kill after the signaling process exits", + async () => { + const started = Date.now(); + const testDir = mkdtempSync(join(tmpdir(), "prime-agent-taskkill-test-")); + const ready = join(testDir, "ready"); + const stop = join(testDir, "stop"); + const records: Array<{ pid: number; identity: string | null }> = []; + const errors: unknown[] = []; + let target: ChildProcess | undefined; + let killer: ChildProcess | undefined; + let killerStderr = ""; + const phase = (name: string, detail: unknown = null) => + console.error( + JSON.stringify({ + case: "caller-exit", + ms: Date.now() - started, + phase: name, + records, + alive: records.map(({ pid }) => isProcessAlive(pid)), + detail, + killerStderr, + }), + ); + try { + target = spawn(process.execPath, ["-e", cooperativeTreeScript, ready, stop], { stdio: "ignore" }); + target.once("error", (error) => { + errors.push(error); + phase("target-error", String(error)); + }); + await waitForFile(ready); // <=10s readiness. + const pids = JSON.parse(readFileSync(ready, "utf8")) as number[]; + records.push(...pids.map((pid) => ({ pid, identity: null }))); + phase("ready-before-capture"); + for (const record of records) record.identity = captureWindowsProcessCreationTime(record.pid); + phase("identity-captured"); + expect(records.map(({ pid }) => isProcessAlive(pid))).toEqual([true, true]); + const helperUrl = pathToFileURL(resolve(import.meta.dirname, "../src/utils/child-process.ts")).href; + killer = spawn( + process.execPath, + [ + "--eval", + `import { signalProcessGroupOrProcess } from ${JSON.stringify(helperUrl)}; console.error("signal-enter", Date.now()); signalProcessGroupOrProcess(${target.pid}, "SIGKILL", error => console.error("signal-failure", Date.now(), error.message)); console.error("signal-return", Date.now());`, + ], + { stdio: ["ignore", "ignore", "pipe"] }, + ); + killer.stderr?.on("data", (data: Buffer) => { + killerStderr += data.toString().slice(0, Math.max(0, 4096 - killerStderr.length)); + }); + killer.once("exit", (code, signal) => phase("caller-exit", { code, signal })); + phase("signal-requested"); + expect(await observe(waitForChildProcess(killer), 5000, "caller exit")).toBe(0); + await waitUntil(() => records.every(({ pid }) => !isProcessAlive(pid)), 15000, "both tree processes exit"); + expect(records.map(({ pid }) => isProcessAlive(pid))).toEqual([false, false]); + phase("whole-tree-gone"); + } catch (error) { + errors.push(error); + } finally { + phase("before-cleanup"); + try { + writeFileSync(join(testDir, "records.json"), JSON.stringify(records)); + } catch (error) { + errors.push(error); + } + try { + writeFileSync(stop, "stop"); + } catch (error) { + errors.push(error); + } + try { + await waitUntil( + () => + records.every(({ pid }) => !isProcessAlive(pid)) && + [target, killer].every((child) => !child || child.exitCode !== null || child.signalCode !== null), + 10000, + "cooperative cleanup", + ); + } catch (error) { + errors.push(error); + } + phase("cleanup-complete", { errors: errors.map(String), preserved: testDir }); + } + if (errors.length) throw new AggregateError(errors, "Caller-exit tree test failed"); + }, + 45000, + ); }); describe("process liveness", () => { diff --git a/packages/coding-agent/test/exec.test.ts b/packages/coding-agent/test/exec.test.ts index 1b6226cd1b7..41ec087696d 100644 --- a/packages/coding-agent/test/exec.test.ts +++ b/packages/coding-agent/test/exec.test.ts @@ -1,10 +1,13 @@ -import { existsSync, mkdtempSync, readFileSync, rmSync } from "node:fs"; +import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"; import { constants, tmpdir } from "node:os"; import { join } from "node:path"; import { describe, expect, it, vi } from "vitest"; import { execCommand } from "../src/core/exec.js"; import { isProcessAlive } from "../src/utils/child-process.js"; +import { captureWindowsProcessCreationTime } from "../src/utils/windows-process-signal.js"; +import { cooperativeTreeScript, observe, waitUntil } from "./fixtures/windows-process-observation.js"; + const SIGKILL_EXIT_CODE = 128 + constants.signals.SIGKILL; async function waitForFile(path: string): Promise { @@ -15,14 +18,6 @@ async function waitForFile(path: string): Promise { } } -async function waitForProcessExit(pid: number): Promise { - const deadline = Date.now() + 10_000; - while (isProcessAlive(pid)) { - if (Date.now() >= deadline) throw new Error(`Process ${pid} did not exit`); - await new Promise((resolve) => setTimeout(resolve, 25)); - } -} - describe.skipIf(process.platform === "win32")("execCommand", () => { it("force kills a process that ignores SIGTERM and cleans up the fallback timer", async () => { const testDir = mkdtempSync(join(tmpdir(), "prime-agent-exec-test-")); @@ -62,41 +57,88 @@ describe.skipIf(process.platform === "win32")("execCommand", () => { describe.skipIf(process.platform !== "win32")("execCommand on Windows", () => { it("cancels a running process tree", async () => { + const started = Date.now(); const testDir = mkdtempSync(join(tmpdir(), "prime-agent-exec-windows-test-")); - const grandchildPidFile = join(testDir, "grandchild.pid"); + const ready = join(testDir, "ready"); + const stop = join(testDir, "stop"); const controller = new AbortController(); - let grandchildPid: number | undefined; - let resultPromise: Promise>> | undefined; + const records: Array<{ pid: number; identity: string | null }> = []; + const errors: unknown[] = []; + let completed = false; + const phase = (name: string, detail: unknown = null) => + console.error( + JSON.stringify({ + case: "exec-tree", + ms: Date.now() - started, + phase: name, + records, + alive: records.map(({ pid }) => isProcessAlive(pid)), + detail, + }), + ); try { - resultPromise = execCommand( + const resultPromise = execCommand( process.execPath, - [ - "-e", - `const { spawn } = require("node:child_process"); const { writeFileSync } = require("node:fs"); const child = spawn(process.execPath, ["-e", "setInterval(() => {}, 1000)"], { stdio: "ignore" }); writeFileSync(process.argv[1], String(child.pid)); setInterval(() => {}, 1000);`, - grandchildPidFile, - ], + ["-e", cooperativeTreeScript, ready, stop], process.cwd(), - { signal: controller.signal }, + { + signal: controller.signal, + }, ); - await waitForFile(grandchildPidFile); - grandchildPid = Number.parseInt(readFileSync(grandchildPidFile, "utf8"), 10); - expect(isProcessAlive(grandchildPid)).toBe(true); - + resultPromise.then( + (result) => { + completed = true; + phase("exec-result", { ...result, stderr: result.stderr.slice(0, 4096) }); + }, + (error) => { + completed = true; + phase("exec-error", String(error)); + }, + ); + await waitForFile(ready); // <=10s readiness. + const pids = JSON.parse(readFileSync(ready, "utf8")) as number[]; + records.push(...pids.map((pid) => ({ pid, identity: null }))); + phase("ready-before-capture"); + for (const record of records) record.identity = captureWindowsProcessCreationTime(record.pid); + phase("identity-captured"); + expect(records.map(({ pid }) => isProcessAlive(pid))).toEqual([true, true]); + const deadline = Date.now() + 15000; + phase("abort-requested"); controller.abort(); - const result = await resultPromise; + const result = await observe(resultPromise, 15000, "exec cancellation result"); expect(result.killed).toBe(true); - await waitForProcessExit(grandchildPid); + await waitUntil( + () => records.every(({ pid }) => !isProcessAlive(pid)), + Math.max(0, deadline - Date.now()), + "whole-tree exit", + ); + expect(records.map(({ pid }) => isProcessAlive(pid))).toEqual([false, false]); + phase("whole-tree-gone"); + } catch (error) { + errors.push(error); } finally { - controller.abort(); - await resultPromise; - if (grandchildPid && isProcessAlive(grandchildPid)) { - try { - process.kill(grandchildPid, "SIGKILL"); - } catch { - // The process may have exited after the liveness check. - } + phase("before-cleanup"); + try { + writeFileSync(join(testDir, "records.json"), JSON.stringify(records)); + } catch (error) { + errors.push(error); } - rmSync(testDir, { recursive: true, force: true }); + try { + writeFileSync(stop, "stop"); + } catch (error) { + errors.push(error); + } + try { + await waitUntil( + () => completed && records.every(({ pid }) => !isProcessAlive(pid)), + 10000, + "cooperative cleanup", + ); + } catch (error) { + errors.push(error); + } + phase("cleanup-complete", { errors: errors.map(String), preserved: testDir }); } - }); + if (errors.length) throw new AggregateError(errors, "Exec tree test failed"); + }, 45000); }); diff --git a/packages/coding-agent/test/fixtures/windows-process-observation.ts b/packages/coding-agent/test/fixtures/windows-process-observation.ts new file mode 100644 index 00000000000..e1a597e94fc --- /dev/null +++ b/packages/coding-agent/test/fixtures/windows-process-observation.ts @@ -0,0 +1,27 @@ +import assert from "node:assert/strict"; + +const leafScript = `const { existsSync } = require("node:fs"); setInterval(() => { if (existsSync(process.argv[1])) process.exit(0); }, 25);`; +export const cooperativeTreeScript = `const { spawn } = require("node:child_process"); const { existsSync, writeFileSync } = require("node:fs"); const child = spawn(process.execPath, ["-e", ${JSON.stringify(leafScript)}, process.argv[2]], { stdio: "ignore" }); child.on("spawn", () => writeFileSync(process.argv[1], JSON.stringify([process.pid, child.pid]))); setInterval(() => { if (existsSync(process.argv[2])) process.exit(0); }, 25);`; + +export async function waitUntil(condition: () => boolean, milliseconds: number, phase: string): Promise { + const deadline = Date.now() + milliseconds; + while (!condition()) { + assert(Date.now() < deadline, `${phase} timed out after ${milliseconds}ms`); + await new Promise((resolve) => setTimeout(resolve, 25)); + } +} + +/** Observe without abandoning the real operation's exit/error handlers on timeout. */ +export async function observe(operation: Promise, milliseconds: number, phase: string): Promise { + let settled = false; + operation.then( + () => { + settled = true; + }, + () => { + settled = true; + }, + ); + await waitUntil(() => settled, milliseconds, phase); + return operation; +} diff --git a/packages/coding-agent/test/fixtures/windows-process-signal-native.ts b/packages/coding-agent/test/fixtures/windows-process-signal-native.ts index 134c1bbc20b..78f6b9ab9d9 100644 --- a/packages/coding-agent/test/fixtures/windows-process-signal-native.ts +++ b/packages/coding-agent/test/fixtures/windows-process-signal-native.ts @@ -9,6 +9,7 @@ import { captureWindowsProcessCreationTime, createWindowsProcessTreeSignal, } from "../../src/utils/windows-process-signal.js"; +import { cooperativeTreeScript, observe, waitUntil } from "./windows-process-observation.js"; assert.equal(process.platform, "win32", "This fixture requires native Windows"); const nodeVersion = process.versions.node.split(".").map(Number); @@ -21,30 +22,71 @@ const encode = (command: { args: string[] }, script: string) => { }; const gate = (path: string) => `while (!(Test-Path -LiteralPath ${quote(path)})) { Start-Sleep -Milliseconds 25 }`; +const began = Date.now(); +const phase = (name: string, detail: unknown = null) => + console.error( + JSON.stringify({ + phase: name, + ms: Date.now() - began, + pid: process.pid, + detail, + }), + ); +function traceCommand(command: { args: string[] }, path: string): void { + const emit = (name: string) => + `Add-Content -LiteralPath ${quote(path)} -Encoding UTF8 -Value ("$([DateTime]::UtcNow.ToString('o')) ${name}")`; + const script = decode(command) + .replace( + "$null = $process.Handle", + `${emit("open-start")}\n $null = $process.Handle\n ${emit("handle-cached")}`, + ) + .replace(" if ($actual -eq", ` ${emit("identity=$actual")}\n if ($actual -eq`) + .replace(" & ", ` ${emit("taskkill-start")}\n & `) + .replace("$code = $LASTEXITCODE", `$code = $LASTEXITCODE\n ${emit("taskkill-exit=$code")}`) + .replace("} catch {", `} catch {\n ${emit("helper-error=$_")}`); + encode(command, `${emit("helper-start")}\n${script}`); +} + if (process.argv[2] === "--exit-caller") { + phase("caller-capture-start", { target: process.argv[3] }); const command = createWindowsProcessTreeSignal(Number(process.argv[3]), "SIGKILL"); + phase("caller-capture-complete"); + traceCommand(command, process.argv[6]!); encode(command, `${gate(process.argv[4]!)}\n${decode(command)}`); const helper = spawnHidden(command.command, command.args, { detached: true, stdio: "ignore" }); writeFileSync(process.argv[5]!, String(helper.pid)); + phase("caller-helper-spawned", { helperPid: helper.pid }); process.exit(0); } -async function until(condition: () => boolean): Promise { - const deadline = Date.now() + 15000; - while (!condition()) { - assert(Date.now() < deadline, "Native process condition timed out"); - await new Promise((resolve) => setTimeout(resolve, 25)); - } +async function until(condition: () => boolean, label = "native condition"): Promise { + phase(`${label}:waiting`); + await waitUntil(condition, 15000, label); + phase(`${label}:complete`); } const directory = mkdtempSync(join(tmpdir(), "prime-agent-identity-native-")); const stopMarker = join(directory, "stop-owned-tree"); -const leafScript = `const { existsSync } = require("node:fs"); setInterval(() => { if (existsSync(process.argv[1])) process.exit(0); }, 25);`; -const treeScript = `const { spawn } = require("node:child_process"); const { existsSync, writeFileSync } = require("node:fs"); const child = spawn(process.execPath, ["-e", ${JSON.stringify(leafScript)}, process.argv[2]], { stdio: "ignore" }); child.on("spawn", () => writeFileSync(process.argv[1], JSON.stringify([process.pid, child.pid]))); setInterval(() => { if (existsSync(process.argv[2])) process.exit(0); }, 25);`; +const treeScript = cooperativeTreeScript; const processes: Array<{ pid: number; identity?: string }> = []; const gates: string[] = []; const cleanupErrors: unknown[] = []; const owners: ChildProcess[] = []; +const traces: string[] = []; +function tracePath(name: string): string { + const path = join(directory, `${name}.log`); + traces.push(path); + return path; +} +function dumpTraces(stage: string): void { + for (const path of traces) { + try { + phase("helper-trace", { stage, path, text: readFileSync(path, "utf8").slice(0, 4096) }); + } catch (error) { + phase("helper-trace-unavailable", { stage, path, error: String(error) }); + } + } +} let execFinished = true; let testError: unknown; @@ -55,7 +97,9 @@ async function readTree(path: string): Promise<[number, number]> { processes.push(...records); for (const record of records) { assert(isProcessAlive(record.pid)); + phase("capture-start", record); record.identity = captureWindowsProcessCreationTime(record.pid); + phase("capture-complete", record); } return pids; } @@ -69,10 +113,24 @@ async function liveTree(name: string): Promise<{ root: ChildProcess; pids: [numb } async function runHelper(command: { command: string; args: string[] }): Promise { - return waitForChildProcess(spawnHidden(command.command, command.args, { stdio: "ignore", detached: true })); + const log = tracePath(`helper-${traces.length}`); + traceCommand(command, log); + const helper = spawnHidden(command.command, command.args, { stdio: "ignore", detached: true }); + owners.push(helper); + phase("helper-spawned", { helperPid: helper.pid, log }); + helper.once("error", (error) => phase("helper-error", String(error))); + helper.once("exit", (code, signal) => phase("helper-exit", { code, signal })); + return observe(waitForChildProcess(helper), 15000, "helper completion"); } try { + const mismatched = await liveTree("mismatched"); + const mismatchCommand = createWindowsProcessTreeSignal(mismatched.pids[0], "SIGKILL"); + encode(mismatchCommand, decode(mismatchCommand).replace(/\$actual -eq '\d+'/, "$actual -eq '1'")); + assert.equal(await runHelper(mismatchCommand), 3); + assert(mismatched.pids.every(isProcessAlive)); + console.log("PASS mismatched identity leaves the whole tree alive"); + // A real abort must remove both live processes; a root-only fallback would cancel escalation. const ready = join(directory, "exec-tree"); const controller = new AbortController(); @@ -81,41 +139,40 @@ try { signal: controller.signal, }); result.then( - () => { + (value) => { execFinished = true; + phase("owned-exec-result", { code: value.code, killed: value.killed, stderr: value.stderr.slice(0, 4096) }); }, - () => { + (error) => { execFinished = true; + phase("owned-exec-error", String(error)); }, ); const owned = await readTree(ready); + phase("owned-exec-abort", owned); controller.abort(); - assert.equal((await result).killed, true); + assert.equal((await observe(result, 15000, "owned exec result")).killed, true); await until(() => owned.every((pid) => !isProcessAlive(pid))); console.log("PASS owned exec removes a live whole tree"); - const mismatched = await liveTree("mismatched"); - const mismatchCommand = createWindowsProcessTreeSignal(mismatched.pids[0], "SIGKILL"); - encode(mismatchCommand, decode(mismatchCommand).replace(/\$actual -eq '\d+'/, "$actual -eq '1'")); - assert.equal(await runHelper(mismatchCommand), 3); - assert(mismatched.pids.every(isProcessAlive)); - console.log("PASS mismatched identity leaves the whole tree alive"); - const departing = await liveTree("departing"); const release = join(directory, "caller-exit-gate"); const helperPidFile = join(directory, "caller-exit-helper"); + const callerLog = tracePath("caller-exit-helper"); gates.push(release); const caller = spawnHidden( process.execPath, - [process.argv[1]!, "--exit-caller", String(departing.pids[0]), release, helperPidFile], + [process.argv[1]!, "--exit-caller", String(departing.pids[0]), release, helperPidFile, callerLog], { stdio: "ignore" }, ); owners.push(caller); - assert.equal(await waitForChildProcess(caller), 0); + assert.equal(await observe(waitForChildProcess(caller), 15000, "original caller exit"), 0); + phase("original-caller-exited"); assert(departing.pids.every(isProcessAlive)); const helperPid = Number(readFileSync(helperPidFile, "utf8")); assert(isProcessAlive(helperPid)); processes.push({ pid: helperPid, identity: captureWindowsProcessCreationTime(helperPid) }); + phase("caller-gate-release", { helperPid, alive: isProcessAlive(helperPid) }); writeFileSync(release, "release"); await until(() => departing.pids.every((pid) => !isProcessAlive(pid)) && !isProcessAlive(helperPid)); console.log("PASS verification and tree kill after the original caller exits"); @@ -136,23 +193,29 @@ try { .replace(" if ($actual -eq", ` [IO.File]::WriteAllText(${quote(verified)}, $actual)\n if ($actual -eq`) .replace("exit $code", "if (!$pin.IsClosed) { exit 97 }\nexit $code"); encode(heldCommand, heldScript); + traceCommand(heldCommand, tracePath("held-helper")); const heldHelper = spawnHidden(heldCommand.command, heldCommand.args, { stdio: "ignore", detached: true }); owners.push(heldHelper); const heldResult = waitForChildProcess(heldHelper); heldResult.catch((error) => cleanupErrors.push(error)); processes.push({ pid: heldHelper.pid!, identity: captureWindowsProcessCreationTime(heldHelper.pid!) }); - await until(() => existsSync(pinReady)); + await until(() => existsSync(pinReady), "helper pin ready"); + phase("helper-pin-held", { helperPid: heldHelper.pid, target: held.pids[0], identity }); held.root.kill("SIGKILL"); - await waitForChildProcess(held.root); + await observe(waitForChildProcess(held.root), 15000, "held target exit"); writeFileSync(pinRelease, "release"); - assert.notEqual(await heldResult, 97, "Process.Dispose must close the helper's own pin"); + assert.notEqual( + await observe(heldResult, 15000, "held helper completion"), + 97, + "Process.Dispose must close the helper's own pin", + ); assert.equal(readFileSync(verified, "utf8"), identity, "StartTime must query the pinned object after its exit"); console.log("PASS cached HANDLE lifetime and exact FILETIME after target exit"); const gone = await liveTree("gone"); const goneCommand = createWindowsProcessTreeSignal(gone.pids[0], "SIGKILL"); gone.root.kill("SIGKILL"); - await waitForChildProcess(gone.root); + await observe(waitForChildProcess(gone.root), 15000, "gone target exit"); assert.notEqual(await runHelper(goneCommand), 0); assert(isProcessAlive(gone.pids[1])); console.log("PASS target gone before verification fails closed"); @@ -160,7 +223,8 @@ try { testError = error; } finally { const recorded = JSON.stringify(processes.map(({ pid, identity }) => ({ pid, identity: identity ?? null }))); - console.error(`Native fixture records before cleanup (${directory}): ${recorded}`); + phase("before-cleanup", { directory, records: processes, alive: processes.map(({ pid }) => isProcessAlive(pid)) }); + dumpTraces("before-cleanup"); try { writeFileSync(join(directory, "process-records.json"), recorded); } catch (error) { @@ -184,6 +248,7 @@ try { } catch (error) { cleanupErrors.push(error); } + dumpTraces("after-cleanup"); // Preserve the marker and records: a late-starting descendant must still stop itself. for (const error of cleanupErrors) console.error("Native fixture cleanup error:", error); } From 3d4aeb1753b9a0a0d1fd49437d33391c4040fd68 Mon Sep 17 00:00:00 2001 From: Seth Date: Mon, 7 Sep 2026 21:18:12 -0400 Subject: [PATCH 42/45] Probe Windows PowerShell launch and visibility behavior Compare fixed sentinel launches without changing production termination. Preserve bounded functional, caller-exit, quoting, and visibility evidence. --- .github/workflows/ci.yml | 19 + .../test/fixtures/windows-launch-observer.ts | 107 +++++ .../windows-powershell-launch-native.ts | 432 ++++++++++++++++++ 3 files changed, 558 insertions(+) create mode 100644 packages/coding-agent/test/fixtures/windows-launch-observer.ts create mode 100644 packages/coding-agent/test/fixtures/windows-powershell-launch-native.ts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a28028b5db3..614a1492794 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -379,6 +379,25 @@ jobs: working-directory: packages/coding-agent run: node ./node_modules/.cache/windows-process-signal-native.mjs + - name: Build the PowerShell launch probe + id: windows_launch_probe + if: ${{ !cancelled() && steps.windows_bun.outcome == 'success' && steps.windows_bun_verified.outcome == 'success' && steps.windows_dependencies.outcome == 'success' }} + timeout-minutes: 3 + working-directory: packages/coding-agent + run: bun build ./test/fixtures/windows-powershell-launch-native.ts --target=node --external koffi --outfile=./node_modules/.cache/windows-powershell-launch-native.mjs + + - name: Probe PowerShell launch under Bun + if: ${{ !cancelled() && steps.windows_bun.outcome == 'success' && steps.windows_bun_verified.outcome == 'success' && steps.windows_dependencies.outcome == 'success' }} + timeout-minutes: 3 + working-directory: packages/coding-agent + run: bun ./test/fixtures/windows-powershell-launch-native.ts + + - name: Probe PowerShell launch under Node + if: ${{ !cancelled() && steps.windows_bun.outcome == 'success' && steps.windows_bun_verified.outcome == 'success' && steps.windows_dependencies.outcome == 'success' && steps.windows_launch_probe.outcome == 'success' }} + timeout-minutes: 3 + working-directory: packages/coding-agent + run: node ./node_modules/.cache/windows-powershell-launch-native.mjs + - name: Test terminal behavior working-directory: packages/tui run: >- diff --git a/packages/coding-agent/test/fixtures/windows-launch-observer.ts b/packages/coding-agent/test/fixtures/windows-launch-observer.ts new file mode 100644 index 00000000000..1ea19787f7d --- /dev/null +++ b/packages/coding-agent/test/fixtures/windows-launch-observer.ts @@ -0,0 +1,107 @@ +/** Test-only WinEvent observer. A missing control or unobservable console is inconclusive. */ +export function visibilityObserverScript(directory: string): string { + return ( + `$root = '${directory.replaceAll("'", "''")}'\n` + + String.raw` +$ErrorActionPreference = 'Stop' +Add-Type -ReferencedAssemblies @('System.dll', 'System.Core.dll', 'System.Windows.Forms.dll') -TypeDefinition @' +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Runtime.InteropServices; +using System.Text; +using System.Threading; +using System.Windows.Forms; +public static class LaunchVisibility { + delegate void WinEvent(IntPtr hook, uint ev, IntPtr hwnd, int obj, int child, uint thread, uint time); + delegate bool EnumProc(IntPtr hwnd, IntPtr param); + [DllImport("user32.dll")] static extern IntPtr SetWinEventHook(uint min, uint max, IntPtr mod, WinEvent cb, uint pid, uint tid, uint flags); + [DllImport("user32.dll")] static extern bool UnhookWinEvent(IntPtr hook); + [DllImport("user32.dll")] static extern bool EnumWindows(EnumProc cb, IntPtr param); + [DllImport("user32.dll")] static extern bool IsWindowVisible(IntPtr hwnd); + [DllImport("user32.dll")] static extern uint GetWindowThreadProcessId(IntPtr hwnd, out uint pid); + [DllImport("user32.dll", CharSet=CharSet.Unicode)] static extern int GetClassName(IntPtr hwnd, StringBuilder text, int count); + [DllImport("user32.dll", CharSet=CharSet.Unicode)] static extern IntPtr CreateWindowEx(uint ex, string cls, string title, uint style, int x, int y, int w, int h, IntPtr parent, IntPtr menu, IntPtr instance, IntPtr param); + [DllImport("user32.dll")] static extern bool DestroyWindow(IntPtr hwnd); + [DllImport("user32.dll")] static extern IntPtr GetProcessWindowStation(); + [StructLayout(LayoutKind.Sequential)] struct UserFlags { public int inherit; public int reserved; public uint flags; } + [DllImport("user32.dll")] static extern bool GetUserObjectInformation(IntPtr handle, int index, out UserFlags flags, uint size, out uint needed); + static StreamWriter output; + static string root, phase = "control|armed"; + static IntPtr control; + static bool controlSeen; + static HashSet baseline = new HashSet(); + static WinEvent callback = OnEvent; + static void Record(string kind, IntPtr hwnd, bool enumerated) { + uint pid; GetWindowThreadProcessId(hwnd, out pid); + var cls = new StringBuilder(256); GetClassName(hwnd, cls, 256); + output.WriteLine(String.Join("\t", phase, DateTime.UtcNow.ToString("o"), kind, pid, hwnd.ToInt64(), IsWindowVisible(hwnd), enumerated, cls.ToString().Replace("\t", " ").Replace("\r", " ").Replace("\n", " "))); + } + static void OnEvent(IntPtr hook, uint ev, IntPtr hwnd, int obj, int child, uint thread, uint time) { + if (hwnd == IntPtr.Zero || (ev == 0x8002 && (obj != 0 || child != 0))) return; + if (hwnd == control && ev == 0x8002) controlSeen = true; + Record(ev == 3 ? "foreground" : "show", hwnd, false); + } + static HashSet Windows() { + var result = new HashSet(); + EnumWindows((hwnd, param) => { result.Add(hwnd); return true; }, IntPtr.Zero); + return result; + } + public static int Run(string directory) { + root = directory; + using (output = new StreamWriter(Path.Combine(root, "windows.tsv"), false, new UTF8Encoding(false))) { + output.AutoFlush = true; + IntPtr show = SetWinEventHook(0x8002, 0x8002, IntPtr.Zero, callback, 0, 0, 0); + IntPtr foreground = SetWinEventHook(3, 3, IntPtr.Zero, callback, 0, 0, 0); + try { + // Off-screen and no-activate: real WS_VISIBLE/SHOW control without taking focus. + control = CreateWindowEx(0x08000080, "STATIC", "Prime Agent visibility control", 0x90000000, -32000, -32000, 8, 8, IntPtr.Zero, IntPtr.Zero, IntPtr.Zero, IntPtr.Zero); + var clock = Stopwatch.StartNew(); + while (!controlSeen && clock.ElapsedMilliseconds < 2000) { Application.DoEvents(); Thread.Sleep(10); } + if (control != IntPtr.Zero) DestroyWindow(control); + Application.DoEvents(); + foreach (var hwnd in Windows()) if (IsWindowVisible(hwnd)) baseline.Add(hwnd); + UserFlags flags; uint needed; + bool visibleStation = GetUserObjectInformation(GetProcessWindowStation(), 1, out flags, 12, out needed) && (flags.flags & 1) != 0; + string ready = Path.Combine(root, "observer-ready"); + File.WriteAllText(ready + ".tmp", (show != IntPtr.Zero && foreground != IntPtr.Zero && controlSeen) + "|" + visibleStation); + File.Move(ready + ".tmp", ready); + clock.Restart(); + while (!File.Exists(Path.Combine(root, "observer-stop")) && clock.ElapsedMilliseconds < 150000) { + Application.DoEvents(); + string request = ""; + try { request = File.ReadAllText(Path.Combine(root, "phase")); } catch (IOException) { } + string[] parts = request.Split('|'); + if (parts.Length == 3 && (parts[1] == "armed" || parts[1] == "audit")) { + phase = parts[0] + "|" + parts[1]; + if (File.Exists(Path.Combine(root, "phase-ack")) && File.ReadAllText(Path.Combine(root, "phase-ack")) == request) { Thread.Sleep(10); continue; } + Application.DoEvents(); + var windows = Windows(); + foreach (var hwnd in windows) if (IsWindowVisible(hwnd) && !baseline.Contains(hwnd)) Record("visible-snapshot", hwnd, true); + long console; + if (parts[1] == "audit" && Int64.TryParse(parts[2], out console)) Record("console", new IntPtr(console), windows.Contains(new IntPtr(console))); + File.WriteAllText(Path.Combine(root, "phase-ack"), request); + } + Thread.Sleep(10); + } + if (!File.Exists(Path.Combine(root, "observer-stop"))) return 4; + // The controller requests stop only after sentinel cleanup. Drain late SHOW/handoff events. + clock.Restart(); + while (clock.ElapsedMilliseconds < 200) { Application.DoEvents(); Thread.Sleep(10); } + foreach (var hwnd in Windows()) if (IsWindowVisible(hwnd) && !baseline.Contains(hwnd)) Record("visible-snapshot", hwnd, true); + Application.DoEvents(); + File.WriteAllText(Path.Combine(root, "observer-drained"), "drained"); + return 0; + } finally { + if (show != IntPtr.Zero) UnhookWinEvent(show); + if (foreground != IntPtr.Zero) UnhookWinEvent(foreground); + } + } + } +} +'@ +exit ([LaunchVisibility]::Run($root)) +` + ); +} diff --git a/packages/coding-agent/test/fixtures/windows-powershell-launch-native.ts b/packages/coding-agent/test/fixtures/windows-powershell-launch-native.ts new file mode 100644 index 00000000000..d9ed427affa --- /dev/null +++ b/packages/coding-agent/test/fixtures/windows-powershell-launch-native.ts @@ -0,0 +1,432 @@ +import assert from "node:assert/strict"; +import type { ChildProcess, SpawnOptions } from "node:child_process"; +import { createHash } from "node:crypto"; +import { existsSync, mkdtempSync, readFileSync, renameSync, rmdirSync, symlinkSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join, win32 } from "node:path"; +import { isProcessAlive, spawnHidden, waitForChildProcess } from "../../src/utils/child-process.js"; +import { createWindowsProcessTreeSignal } from "../../src/utils/windows-process-signal.js"; +import { visibilityObserverScript } from "./windows-launch-observer.js"; +import { observe, waitUntil } from "./windows-process-observation.js"; + +const quote = (text: string) => `'${text.replaceAll("'", "''")}'`; +const encode = (script: string) => Buffer.from(script, "utf16le").toString("base64"); +function safeImage(path: string): string { + assert(win32.isAbsolute(path) && !/["%!&|<>^\r\n]/.test(path), `Unsafe command image: ${path}`); + return path; +} +function cmdArguments(image: string, encoded: string): string[] { + assert(/^[A-Za-z0-9+/]+={0,2}$/.test(encoded), "Payload must be one base64 argument"); + return ["/d", "/s", "/c", `""${safeImage(image)}" -NoLogo -NoProfile -NonInteractive -EncodedCommand ${encoded}"`]; +} + +function sentinel(directory: string, name: string): string { + const path = (suffix: string) => quote(join(directory, `${name}-${suffix}`)); + return ` +$ErrorActionPreference = 'Stop' +[IO.File]::WriteAllText(${path("started")}, 'fixed launch sentinel v1 started') +try { + $metadata = @(([Diagnostics.Process]::GetCurrentProcess().MainModule.FileName), ([Environment]::CommandLine), ([string]$PSVersionTable.PSVersion)) -join [Environment]::NewLine + [IO.File]::WriteAllText(${path("metadata")}, $metadata) + Add-Type -TypeDefinition @' +using System; +using System.Runtime.InteropServices; +public static class SentinelConsole { + [DllImport("kernel32.dll")] public static extern IntPtr GetConsoleWindow(); + [DllImport("user32.dll")] public static extern bool IsWindowVisible(IntPtr hwnd); + [StructLayout(LayoutKind.Sequential)] public struct StartupInfo { + public uint cb; public IntPtr reserved, desktop, title; + public uint x, y, width, height, columns, rows, fill, flags; + public ushort show, reservedLength; + public IntPtr reservedData, stdin, stdout, stderr; + } + [DllImport("kernel32.dll")] public static extern void GetStartupInfo(out StartupInfo info); +} +'@ + $info = New-Object SentinelConsole+StartupInfo + [SentinelConsole]::GetStartupInfo([ref]$info) + $console = [SentinelConsole]::GetConsoleWindow() + [IO.File]::WriteAllText(${path("ready.tmp")}, "$PID|$($console.ToInt64())|$([SentinelConsole]::IsWindowVisible($console))|$($info.flags)|$($info.show)") + [IO.File]::Move(${path("ready.tmp")}, ${path("ready")}) + while (![IO.File]::Exists(${path("gate")})) { Start-Sleep -Milliseconds 25 } + [IO.File]::WriteAllText(${path("done.tmp")}, 'fixed launch sentinel v1 completed') + [IO.File]::Move(${path("done.tmp")}, ${path("done")}) + exit 37 +} catch { + [IO.File]::WriteAllText(${path("error")}, [string]$_) + exit 38 +} +`; +} + +// Local construction checks execute no native factory, subprocess or sentinel. +if (process.argv[2] === "--construction-check") { + const script = sentinel("C:\\owned probe's directory", "C"); + const encoded = encode(script); + assert.equal(Buffer.from(encoded, "base64").toString("utf16le"), script); + assert(!script.toLowerCase().includes("taskkill")); + assert(script.includes("exit 37")); + const image = "C:\\owned path\\PowerShell junction\\powershell.exe"; + assert.equal( + cmdArguments(image, encoded)[3], + `""${image}" -NoLogo -NoProfile -NonInteractive -EncodedCommand ${encoded}"`, + ); + for (const bad of ["C:\\%TEMP%\\x.exe", "C:\\x!y.exe", 'C:\\x"y.exe', "C:\\x&y.exe", "relative.exe"]) { + assert.throws(() => cmdArguments(bad, encoded)); + } + assert.throws(() => cmdArguments(image, "not-base64&exit")); + console.log("PASS fixed sentinel roundtrip, quoted space path and unsafe expansion rejection"); + process.exit(0); +} + +assert.equal(process.platform, "win32", "Native launch probe requires Windows"); +const version = process.versions.node.split(".").map(Number); +assert(version[0]! > 22 || (version[0] === 22 && version[1]! >= 8)); +const began = Date.now(); +const workDeadline = began + 135000; // Reserve owned cleanup and reporting inside the 3-minute step. +const budget = (milliseconds: number) => Math.max(0, Math.min(milliseconds, workDeadline - Date.now())); +const record = (phase: string, data: unknown = null) => + console.error(JSON.stringify({ ms: Date.now() - began, phase, data })); +const hash = (path: string) => createHash("sha256").update(readFileSync(path)).digest("hex"); +// Obtain canonical paths/flags, but NEVER launch the factory's original target-signal payload. +const template = createWindowsProcessTreeSignal(process.pid, "SIGKILL"); +const powershell = safeImage(template.command); +const cmd = safeImage(win32.join(win32.dirname(win32.dirname(win32.dirname(powershell))), "cmd.exe")); +assert.deepEqual(template.args.slice(0, -1), ["-NoLogo", "-NoProfile", "-NonInteractive", "-EncodedCommand"]); +function launchSpec(directory: string, name: string, trampoline: boolean, image = powershell) { + const script = sentinel(directory, name); + const encoded = encode(script); + assert.equal(Buffer.from(encoded, "base64").toString("utf16le"), script); + assert(!script.toLowerCase().includes("taskkill")); + const args = trampoline ? cmdArguments(image, encoded) : [...template.args.slice(0, -1), encoded]; + assert(trampoline ? args[3]!.endsWith(` ${encoded}"`) : args.at(-1) === encoded); + return { command: trampoline ? cmd : image, args, scriptHash: createHash("sha256").update(script).digest("hex") }; +} + +if (process.argv[2] === "--caller") { + const directory = process.argv[3]!; + const spec = launchSpec(directory, "D", true); + const child = spawnHidden(spec.command, spec.args, { + detached: true, + stdio: "ignore", + windowsVerbatimArguments: true, + argv0: `"${cmd}"`, + }); + writeFileSync(join(directory, "D-broker"), String(child.pid)); + child.unref(); + process.exit(0); +} + +const directory = mkdtempSync(join(tmpdir(), "prime-agent launch probe-")); +const junction = join(directory, "PowerShell path with spaces"); +let junctionCreated = false; +const owners: ChildProcess[] = []; +const exits: Array> = []; +const errors: unknown[] = []; +const auditErrors: unknown[] = []; +const departedPids: number[] = []; +const trampolineCases = new Set(); +let observerResult: Promise | undefined; +let observerReady = false; +const observerCode = visibilityObserverScript(directory); +record("provenance", { + directory, + runtime: process.execPath, + node: process.versions.node, + bun: process.versions.bun, + powershell, + powershellHash: hash(powershell), + cmd, + cmdHash: hash(cmd), +}); +function own(child: ChildProcess): Promise { + owners.push(child); + const result = waitForChildProcess(child); + result.catch((error) => record("owned-process-error", String(error))); + exits.push(result); + return result; +} +async function setPhase(name: string, kind: "armed" | "audit", console = "0"): Promise { + const request = `${name}|${kind}|${console}`; + try { + writeFileSync(join(directory, "phase"), request); + } catch (error) { + observerReady = false; + auditErrors.push(error); + } + if (!observerReady) return; + try { + await waitUntil( + () => + existsSync(join(directory, "phase-ack")) && readFileSync(join(directory, "phase-ack"), "utf8") === request, + budget(5000), + "visibility observer acknowledgement", + ); + } catch (error) { + observerReady = false; + auditErrors.push(error); + } +} +function audit(name: string): void { + try { + if (!observerReady) { + auditErrors.push(new Error(`${name}: visibility INCONCLUSIVE (observer/control/station unavailable)`)); + return; + } + const rows = readFileSync(join(directory, "windows.tsv"), "utf8") + .split(/\r?\n/) + .map((line) => line.split("\t")); + const relevant = rows.filter((row) => row[0]?.startsWith(`${name}|`)); + const visible = relevant.filter((row) => ["show", "foreground", "visible-snapshot"].includes(row[2]!)); + const console = relevant.filter((row) => row[2] === "console").at(-1); + record("visibility-audit", { name, rows: relevant }); + if (visible.length) + auditErrors.push( + new Error(`${name}: visible window/foreground event observed; hidden startup not established`), + ); + else if ( + !console || + console[4] === "0" || + console[5] !== "False" || + console[6] !== "True" || + console[7] !== "ConsoleWindowClass" + ) { + auditErrors.push( + new Error( + `${name}: visibility INCONCLUSIVE (console not enumerable/classic; terminal handoff not excluded)`, + ), + ); + } + } catch (error) { + auditErrors.push(new Error(`${name}: visibility INCONCLUSIVE: ${String(error)}`)); + } +} +async function run(name: string, detached: boolean, trampoline: boolean, image = powershell, payloadName = name) { + assert(Date.now() < workDeadline, "Launch probe work budget expired"); + await setPhase(name, "armed"); + const spec = launchSpec(directory, payloadName, trampoline, image); + const options: SpawnOptions = { + detached, + stdio: "ignore", + ...(trampoline ? { windowsVerbatimArguments: true, argv0: `"${cmd}"` } : {}), + }; + record("launch", { name, ...spec, options: { ...options, windowsHide: true } }); + const child = spawnHidden(spec.command, spec.args, options); + const result = own(child); + if (trampoline) trampolineCases.add(name); + let completed = false; + result.then( + () => { + completed = true; + }, + () => { + completed = true; + }, + ); + try { + await waitUntil( + () => completed || existsSync(join(directory, `${payloadName}-ready`)), + budget(15000), + `${name} sentinel start`, + ); + if (existsSync(join(directory, `${payloadName}-ready`))) { + const ready = readFileSync(join(directory, `${payloadName}-ready`), "utf8"); + record("sentinel-ready", { + name, + ready, + metadata: readFileSync(join(directory, `${payloadName}-metadata`), "utf8"), + }); + await setPhase(name, "audit", ready.split("|")[1]); + if (trampoline) { + if (ready.split("|")[2] === "True") auditErrors.push(new Error(`${name}: sentinel console is visible`)); + // Audit the complete event log after the observer exits, not at gate release. + } + } + } finally { + writeFileSync(join(directory, `${payloadName}-gate`), "release"); + } + const code = await observe(result, budget(15000), `${name} sentinel exit`); + const started = existsSync(join(directory, `${payloadName}-started`)); + const done = + existsSync(join(directory, `${payloadName}-done`)) && + readFileSync(join(directory, `${payloadName}-done`), "utf8") === "fixed launch sentinel v1 completed" && + !existsSync(join(directory, `${payloadName}-error`)); + record("result", { name, pid: child.pid, code, started, done }); + if (payloadName !== name) { + for (const suffix of ["started", "metadata", "ready", "ready.tmp", "gate", "done", "done.tmp", "error"]) { + const path = join(directory, `${payloadName}-${suffix}`); + if (existsSync(path)) renameSync(path, join(directory, `${name}-${suffix}`)); + } + } + return { code, started, done }; +} + +try { + try { + assert.equal(Buffer.from(encode(observerCode), "base64").toString("utf16le"), observerCode); + const observer = spawnHidden(powershell, [...template.args.slice(0, -1), encode(observerCode)], { + stdio: ["ignore", "ignore", "pipe"], + detached: false, + }); + let observerStderr = ""; + observer.stderr?.on("data", (data: Buffer) => { + observerStderr += data.toString().slice(0, Math.max(0, 4096 - observerStderr.length)); + }); + observer.once("exit", (code, signal) => record("observer-exit", { code, signal, stderr: observerStderr })); + const observerExit = own(observer); + observerResult = observerExit; + let observerExited = false; + observerExit.then( + () => { + observerExited = true; + }, + () => { + observerExited = true; + }, + ); + try { + await waitUntil( + () => observerExited || existsSync(join(directory, "observer-ready")), + budget(15000), + "visibility observer control", + ); + observerReady = + existsSync(join(directory, "observer-ready")) && + readFileSync(join(directory, "observer-ready"), "utf8") === "True|True"; + } catch (error) { + auditErrors.push(error); + } + record("observer-control", { + ready: observerReady, + exited: observerExited, + control: existsSync(join(directory, "observer-ready")) + ? readFileSync(join(directory, "observer-ready"), "utf8") + : null, + }); + } catch (error) { + observerReady = false; + auditErrors.push(error); + record("observer-unavailable", String(error)); + } + + // Identical image, argv, payload and stdio. Archive A's owned files before reusing AB paths. + const a = await run("A", true, false, powershell, "AB"); + const b = await run("B", false, false, powershell, "AB"); + if (a.code !== 0 || a.started || a.done) errors.push(new Error("A did not reproduce detached non-execution")); + if (b.code !== 37 || !b.started || !b.done) errors.push(new Error("B did not execute the identical sentinel")); + + const c = await run("C", true, true); + assert(c.code === 37 && c.started && c.done, "C trampoline failed; stop without another launcher"); + symlinkSync(win32.dirname(powershell), junction, "junction"); + junctionCreated = true; + record("owned-junction", { junction, target: win32.dirname(powershell) }); + const spaceImage = safeImage(join(junction, win32.basename(powershell))); + assert.equal(hash(spaceImage), hash(powershell)); + const space = await run("C-space", true, true, spaceImage); + assert(space.code === 37 && space.started && space.done, "C-space quoting failed; stop"); + + assert(Date.now() < workDeadline, "Launch probe work budget expired"); + await setPhase("D", "armed"); + record("D-launch-plan", launchSpec(directory, "D", true)); + const caller = spawnHidden(process.execPath, [process.argv[1]!, "--caller", directory], { stdio: "ignore" }); + trampolineCases.add("D"); + assert.equal(await observe(own(caller), budget(5000), "D original caller exit"), 0); + const broker = Number(readFileSync(join(directory, "D-broker"), "utf8")); + assert(Number.isInteger(broker) && broker > 0); + departedPids.push(broker); + record("D-caller-exited-before-gate", { caller: caller.pid, broker }); + assert(!existsSync(join(directory, "D-gate"))); + await waitUntil(() => existsSync(join(directory, "D-ready")), budget(15000), "D sentinel after caller exit"); + const dReady = readFileSync(join(directory, "D-ready"), "utf8"); + const sentinelPid = Number(dReady.split("|")[0]); + assert(Number.isInteger(sentinelPid) && sentinelPid > 0); + departedPids.push(sentinelPid); + record("sentinel-ready", { + name: "D", + ready: dReady, + metadata: readFileSync(join(directory, "D-metadata"), "utf8"), + }); + await setPhase("D", "audit", dReady.split("|")[1]); + if (dReady.split("|")[2] === "True") auditErrors.push(new Error("D: sentinel console is visible")); + writeFileSync(join(directory, "D-gate"), "release"); + await waitUntil( + () => + existsSync(join(directory, "D-done")) && + readFileSync(join(directory, "D-done"), "utf8") === "fixed launch sentinel v1 completed", + budget(15000), + "D sentinel completion", + ); + await waitUntil( + () => departedPids.every((pid) => !isProcessAlive(pid)), + budget(15000), + "D broker and sentinel exit", + ); + assert.equal(readFileSync(join(directory, "D-done"), "utf8"), "fixed launch sentinel v1 completed"); + assert(!existsSync(join(directory, "D-error")), "D sentinel reported an error before exit"); + record("D-completed-after-caller-exit", { departedPids }); +} catch (error) { + errors.push(error); +} finally { + record("before-cleanup", { + directory, + owners: owners.map((child) => ({ pid: child.pid, exit: child.exitCode, signal: child.signalCode })), + errors: [...errors, ...auditErrors].map(String), + }); + for (const name of ["AB", "A", "B", "C", "C-space", "D"]) { + try { + writeFileSync(join(directory, `${name}-gate`), "release"); + } catch (error) { + errors.push(error); + } + } + const cleanupDeadline = Date.now() + 15000; + const cleanupBudget = () => Math.max(0, cleanupDeadline - Date.now()); + try { + await observe( + Promise.all([ + Promise.all(exits.filter((result) => result !== observerResult)), + waitUntil( + () => departedPids.every((pid) => !isProcessAlive(pid)), + cleanupBudget(), + "departed probe cleanup", + ), + ]), + cleanupBudget(), + "owned probe cleanup", + ); + } catch (error) { + errors.push(error); + } + try { + // Keep observing through sentinel exits, then drain queued events before closing. + writeFileSync(join(directory, "observer-stop"), "stop"); + assert(observerResult, "Visibility observer was not started"); + assert.equal(await observe(observerResult, cleanupBudget(), "visibility observer exit"), 0); + assert.equal(readFileSync(join(directory, "observer-drained"), "utf8"), "drained"); + } catch (error) { + observerReady = false; + auditErrors.push(error); + } + for (const name of trampolineCases) audit(name); + for (const name of ["AB", "A", "B", "C", "C-space", "D"]) { + for (const suffix of ["started", "metadata", "ready", "ready.tmp", "done", "done.tmp", "error", "broker"]) { + const path = join(directory, `${name}-${suffix}`); + if (existsSync(path)) + record("preserved-sentinel-artifact", { name, suffix, text: readFileSync(path, "utf8") }); + } + } + if (junctionCreated) { + try { + rmdirSync(junction); + record("owned-junction-removed", junction); + } catch (error) { + errors.push(error); + } // Never recurse into the system directory. + } + const events = join(directory, "windows.tsv"); + if (existsSync(events)) record("preserved-window-events", readFileSync(events, "utf8")); + for (const error of [...errors, ...auditErrors]) record("probe-failure-or-inconclusive", String(error)); +} +if (errors.length || auditErrors.length) + throw new AggregateError([...errors, ...auditErrors], "Launch probe failed or visibility is inconclusive"); +record("PASS-launch-and-observed-hidden-console", { directory }); From acf899ab82661b89a0b8b5c20175622ec81dbddd Mon Sep 17 00:00:00 2001 From: Seth Date: Mon, 7 Sep 2026 22:30:36 -0400 Subject: [PATCH 43/45] Probe Windows runtime broker and compiled executable reentry Keep PowerShell attached to the test broker and preserve caller-exit evidence. Exercise the same fixture through the existing compiled Windows executable. --- .github/workflows/ci.yml | 13 + .../windows-powershell-launch-native.ts | 344 +++++++++++++----- 2 files changed, 265 insertions(+), 92 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 614a1492794..68feb75c4a6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -442,8 +442,21 @@ jobs: test.test_repl.ReplTest.test_interrupt_await_suspended - name: Build the native Windows executable + id: windows_native_binary + if: ${{ !cancelled() && steps.windows_bun.outcome == 'success' && steps.windows_bun_verified.outcome == 'success' && steps.windows_dependencies.outcome == 'success' }} run: bun run build:binary + - name: Probe the compiled Windows runtime broker + if: ${{ !cancelled() && steps.windows_native_binary.outcome == 'success' }} + timeout-minutes: 3 + working-directory: packages/coding-agent + env: + BUN_BE_BUN: "1" + run: | + $binary = (Resolve-Path "./dist/pi.exe").Path + $hash = (Get-FileHash -LiteralPath $binary -Algorithm SHA256).Hash.ToLowerInvariant() + & $binary ./test/fixtures/windows-powershell-launch-native.ts --expect-runtime $binary --expect-sha256 $hash --expect-kind bun + - name: Cross-compile and inspect the Windows Arm64 executable working-directory: packages/coding-agent shell: pwsh diff --git a/packages/coding-agent/test/fixtures/windows-powershell-launch-native.ts b/packages/coding-agent/test/fixtures/windows-powershell-launch-native.ts index d9ed427affa..f39a6d35a12 100644 --- a/packages/coding-agent/test/fixtures/windows-powershell-launch-native.ts +++ b/packages/coding-agent/test/fixtures/windows-powershell-launch-native.ts @@ -1,9 +1,22 @@ import assert from "node:assert/strict"; import type { ChildProcess, SpawnOptions } from "node:child_process"; import { createHash } from "node:crypto"; -import { existsSync, mkdtempSync, readFileSync, renameSync, rmdirSync, symlinkSync, writeFileSync } from "node:fs"; +import { + appendFileSync, + closeSync, + existsSync, + mkdirSync, + mkdtempSync, + openSync, + readFileSync, + realpathSync, + renameSync, + rmdirSync, + symlinkSync, + writeFileSync, +} from "node:fs"; import { tmpdir } from "node:os"; -import { join, win32 } from "node:path"; +import { join, resolve, win32 } from "node:path"; import { isProcessAlive, spawnHidden, waitForChildProcess } from "../../src/utils/child-process.js"; import { createWindowsProcessTreeSignal } from "../../src/utils/windows-process-signal.js"; import { visibilityObserverScript } from "./windows-launch-observer.js"; @@ -15,9 +28,45 @@ function safeImage(path: string): string { assert(win32.isAbsolute(path) && !/["%!&|<>^\r\n]/.test(path), `Unsafe command image: ${path}`); return path; } -function cmdArguments(image: string, encoded: string): string[] { - assert(/^[A-Za-z0-9+/]+={0,2}$/.test(encoded), "Payload must be one base64 argument"); - return ["/d", "/s", "/c", `""${safeImage(image)}" -NoLogo -NoProfile -NonInteractive -EncodedCommand ${encoded}"`]; +const brokerProgram = String.raw` +const { spawn } = require("node:child_process"); +const { appendFileSync } = require("node:fs"); +const spec = JSON.parse(process.env.PRIME_AGENT_LAUNCH_PROBE_SPEC); +const report = (event, data) => appendFileSync(spec.trace, JSON.stringify({ event, pid: process.pid, data }) + "\n"); +report("start", { runtime: process.execPath, kind: process.versions.bun ? "bun" : "node", revision: globalThis.Bun?.revision, cwd: process.cwd() }); +const child = spawn(spec.command, spec.args, { detached: false, windowsHide: true, stdio: "ignore" }); +report("spawned", { pid: child.pid }); +child.once("error", (error) => { report("error", String(error)); process.exitCode = 1; }); +child.once("exit", (code, signal) => { + report("exit", { code, signal }); + process.exitCode = signal || code === null ? 1 : code; +}); +// Do not unref the inner child or exit before its actual exit event. +`; +function runtimeEnvironment(source: NodeJS.ProcessEnv, bun: boolean): NodeJS.ProcessEnv { + const env = Object.fromEntries(Object.entries(source).filter(([key]) => !/^(BUN|NODE)_/i.test(key))); + if (bun) env.BUN_BE_BUN = "1"; + return env; +} +function runtimeArguments(bun: boolean, script: string): string[] { + return bun + ? ["--no-env-file", "--no-install", "--config=broker-empty.toml", "--eval", script] + : ["--input-type=commonjs", "--eval", script]; +} + +function isNoConsoleMode(ready: string[] | undefined, observed: string[] | undefined): boolean { + return Boolean( + ready && + ready[1] === "0" && + ready[2] === "False" && + (Number(ready[3]) & 1) !== 0 && + ready[4] === "0" && + ready[5] === "0" && + ready[6] === "6" && + observed?.[4] === "0" && + observed[5] === "False" && + observed[6] === "False", + ); } function sentinel(directory: string, name: string): string { @@ -41,12 +90,15 @@ public static class SentinelConsole { public IntPtr reservedData, stdin, stdout, stderr; } [DllImport("kernel32.dll")] public static extern void GetStartupInfo(out StartupInfo info); + [DllImport("kernel32.dll", SetLastError=true)] public static extern uint GetConsoleProcessList([Out] uint[] ids, uint count); } '@ $info = New-Object SentinelConsole+StartupInfo [SentinelConsole]::GetStartupInfo([ref]$info) $console = [SentinelConsole]::GetConsoleWindow() - [IO.File]::WriteAllText(${path("ready.tmp")}, "$PID|$($console.ToInt64())|$([SentinelConsole]::IsWindowVisible($console))|$($info.flags)|$($info.show)") + $consoleCount = [SentinelConsole]::GetConsoleProcessList([uint32[]]@(0), 1) + $consoleError = [Runtime.InteropServices.Marshal]::GetLastWin32Error() + [IO.File]::WriteAllText(${path("ready.tmp")}, "$PID|$($console.ToInt64())|$([SentinelConsole]::IsWindowVisible($console))|$($info.flags)|$($info.show)|$consoleCount|$consoleError") [IO.File]::Move(${path("ready.tmp")}, ${path("ready")}) while (![IO.File]::Exists(${path("gate")})) { Start-Sleep -Milliseconds 25 } [IO.File]::WriteAllText(${path("done.tmp")}, 'fixed launch sentinel v1 completed') @@ -66,16 +118,33 @@ if (process.argv[2] === "--construction-check") { assert.equal(Buffer.from(encoded, "base64").toString("utf16le"), script); assert(!script.toLowerCase().includes("taskkill")); assert(script.includes("exit 37")); - const image = "C:\\owned path\\PowerShell junction\\powershell.exe"; - assert.equal( - cmdArguments(image, encoded)[3], - `""${image}" -NoLogo -NoProfile -NonInteractive -EncodedCommand ${encoded}"`, - ); - for (const bad of ["C:\\%TEMP%\\x.exe", "C:\\x!y.exe", 'C:\\x"y.exe', "C:\\x&y.exe", "relative.exe"]) { - assert.throws(() => cmdArguments(bad, encoded)); + const inherited = { + NODE_OPTIONS: "bad", + nOdE_InSpEcT: "bad", + BUN_OPTIONS: "bad", + bun_inspect: "bad", + HOME: "owned-home", + }; + assert.deepEqual(runtimeEnvironment(inherited, false), { HOME: "owned-home" }); + assert.deepEqual(runtimeEnvironment(inherited, true), { HOME: "owned-home", BUN_BE_BUN: "1" }); + assert.equal(inherited.NODE_OPTIONS, "bad"); + assert.deepEqual(runtimeArguments(false, brokerProgram), ["--input-type=commonjs", "--eval", brokerProgram]); + assert.deepEqual(runtimeArguments(true, brokerProgram).slice(0, 3), [ + "--no-env-file", + "--no-install", + "--config=broker-empty.toml", + ]); + const data = { command: "C:\\owned space path\\powershell.exe", args: ["-EncodedCommand", encoded] }; + assert.deepEqual(JSON.parse(JSON.stringify(data)), data); + const observed = ["C|audit", "time", "console", "0", "0", "False", "False", ""]; + assert(isNoConsoleMode("1|0|False|257|0|0|6".split("|"), observed)); + for (const incomplete of ["1|0|False|0|1|0|6", "1|0|False|257|0|1|0", "1|0|False|257|0", "1|0|False|257|0|0|0"]) { + assert(!isNoConsoleMode(incomplete.split("|"), observed)); } - assert.throws(() => cmdArguments(image, "not-base64&exit")); - console.log("PASS fixed sentinel roundtrip, quoted space path and unsafe expansion rejection"); + assert(!isNoConsoleMode("1|0|False|257|0|0|6".split("|"), undefined)); + console.log( + "PASS sentinel/transport, runtime argv, startup-env scrub, parent-env preservation and no-console guards", + ); process.exit(0); } @@ -91,33 +160,66 @@ const hash = (path: string) => createHash("sha256").update(readFileSync(path)).d // Obtain canonical paths/flags, but NEVER launch the factory's original target-signal payload. const template = createWindowsProcessTreeSignal(process.pid, "SIGKILL"); const powershell = safeImage(template.command); -const cmd = safeImage(win32.join(win32.dirname(win32.dirname(win32.dirname(powershell))), "cmd.exe")); +const isBun = Boolean(process.versions.bun); +const bunRevision = (Reflect.get(globalThis, "Bun") as { revision?: string } | undefined)?.revision; +if (isBun) assert.equal(bunRevision, "34cbb9a40b4bd1bd767d134a7065e66c2432a676"); +if (process.argv[2] === "--expect-runtime") { + assert.equal(process.argv[4], "--expect-sha256"); + assert.equal(process.argv[6], "--expect-kind"); + assert.equal(process.argv.length, 8); + assert(win32.isAbsolute(process.argv[3]!)); + assert.equal(win32.normalize(process.execPath).toLowerCase(), win32.normalize(process.argv[3]!).toLowerCase()); + assert.equal(hash(process.execPath), process.argv[5]!.toLowerCase()); + assert.equal(isBun ? "bun" : "node", process.argv[7]); +} assert.deepEqual(template.args.slice(0, -1), ["-NoLogo", "-NoProfile", "-NonInteractive", "-EncodedCommand"]); -function launchSpec(directory: string, name: string, trampoline: boolean, image = powershell) { +function launchSpec(directory: string, name: string, broker: boolean, image = powershell) { const script = sentinel(directory, name); const encoded = encode(script); assert.equal(Buffer.from(encoded, "base64").toString("utf16le"), script); assert(!script.toLowerCase().includes("taskkill")); - const args = trampoline ? cmdArguments(image, encoded) : [...template.args.slice(0, -1), encoded]; - assert(trampoline ? args[3]!.endsWith(` ${encoded}"`) : args.at(-1) === encoded); - return { command: trampoline ? cmd : image, args, scriptHash: createHash("sha256").update(script).digest("hex") }; + const args = [...template.args.slice(0, -1), encoded]; + assert.equal(args.at(-1), encoded); + const options: SpawnOptions = {}; + if (broker) { + options.cwd = join(directory, "runtime-cwd"); + assert.equal(readFileSync(join(options.cwd, "broker-empty.toml"), "utf8"), ""); + options.env = runtimeEnvironment(process.env, isBun); + options.env.PRIME_AGENT_LAUNCH_PROBE_SPEC = JSON.stringify({ + command: image, + args, + trace: join(directory, `${name}-runtime.jsonl`), + }); + } + return { + command: broker ? process.execPath : image, + args: broker ? runtimeArguments(isBun, brokerProgram) : args, + options, + payload: { command: image, args }, + scriptHash: createHash("sha256").update(script).digest("hex"), + }; } if (process.argv[2] === "--caller") { const directory = process.argv[3]!; - const spec = launchSpec(directory, "D", true); - const child = spawnHidden(spec.command, spec.args, { - detached: true, - stdio: "ignore", - windowsVerbatimArguments: true, - argv0: `"${cmd}"`, - }); - writeFileSync(join(directory, "D-broker"), String(child.pid)); + const name = process.argv[4]!; + assert(["D", "D-stderr"].includes(name)); + const stage = (text: string) => { + if (name === "D-stderr") appendFileSync(join(directory, `${name}-caller-stages`), `${text}\n`); + }; + stage("after native factory"); + const spec = launchSpec(directory, name, true); + stage("before spawn"); + const child = spawnHidden(spec.command, spec.args, { ...spec.options, detached: true, stdio: "ignore" }); + writeFileSync(join(directory, `${name}-broker`), String(child.pid)); + stage("after spawn; before immediate unref + process.exit(0)"); child.unref(); process.exit(0); } const directory = mkdtempSync(join(tmpdir(), "prime-agent launch probe-")); +mkdirSync(join(directory, "runtime-cwd")); +writeFileSync(join(directory, "runtime-cwd", "broker-empty.toml"), ""); const junction = join(directory, "PowerShell path with spaces"); let junctionCreated = false; const owners: ChildProcess[] = []; @@ -126,6 +228,7 @@ const errors: unknown[] = []; const auditErrors: unknown[] = []; const departedPids: number[] = []; const trampolineCases = new Set(); +const readyByCase = new Map(); let observerResult: Promise | undefined; let observerReady = false; const observerCode = visibilityObserverScript(directory); @@ -134,10 +237,11 @@ record("provenance", { runtime: process.execPath, node: process.versions.node, bun: process.versions.bun, + bunRevision, powershell, powershellHash: hash(powershell), - cmd, - cmdHash: hash(cmd), + runtimeHash: hash(process.execPath), + startupCwd: join(directory, "runtime-cwd"), }); function own(child: ChildProcess): Promise { owners.push(child); @@ -146,6 +250,19 @@ function own(child: ChildProcess): Promise { exits.push(result); return result; } +function verifyBroker(name: string): void { + const events = readFileSync(join(directory, `${name}-runtime.jsonl`), "utf8") + .trim() + .split("\n") + .map((line) => JSON.parse(line)); + const started = events.find((event) => event.event === "start"); + assert.equal(win32.normalize(started?.data.runtime).toLowerCase(), win32.normalize(process.execPath).toLowerCase()); + assert.equal(started.data.kind, isBun ? "bun" : "node"); + if (isBun) assert.equal(started.data.revision, bunRevision); + assert.equal(realpathSync(started.data.cwd), realpathSync(join(directory, "runtime-cwd"))); + assert(!events.some((event) => event.event === "error"), `${name}: runtime broker reported an error`); + assert.deepEqual(events.filter((event) => event.event === "exit").at(-1)?.data, { code: 37, signal: null }); +} async function setPhase(name: string, kind: "armed" | "audit", console = "0"): Promise { const request = `${name}|${kind}|${console}`; try { @@ -184,18 +301,19 @@ function audit(name: string): void { auditErrors.push( new Error(`${name}: visible window/foreground event observed; hidden startup not established`), ); - else if ( - !console || - console[4] === "0" || - console[5] !== "False" || - console[6] !== "True" || - console[7] !== "ConsoleWindowClass" - ) { - auditErrors.push( - new Error( - `${name}: visibility INCONCLUSIVE (console not enumerable/classic; terminal handoff not excluded)`, - ), - ); + else { + const ready = readyByCase.get(name); + const noConsole = isNoConsoleMode(ready, console); + const hiddenClassic = + console?.[4] !== "0" && + console?.[5] === "False" && + console?.[6] === "True" && + console?.[7] === "ConsoleWindowClass"; + record("console-mode", { name, ready, noConsole, hiddenClassic }); + if (!noConsole && !hiddenClassic) + auditErrors.push( + new Error(`${name}: visibility INCONCLUSIVE (no validated no-console mode or hidden classic console)`), + ); } } catch (error) { auditErrors.push(new Error(`${name}: visibility INCONCLUSIVE: ${String(error)}`)); @@ -205,12 +323,15 @@ async function run(name: string, detached: boolean, trampoline: boolean, image = assert(Date.now() < workDeadline, "Launch probe work budget expired"); await setPhase(name, "armed"); const spec = launchSpec(directory, payloadName, trampoline, image); - const options: SpawnOptions = { - detached, - stdio: "ignore", - ...(trampoline ? { windowsVerbatimArguments: true, argv0: `"${cmd}"` } : {}), - }; - record("launch", { name, ...spec, options: { ...options, windowsHide: true } }); + const options: SpawnOptions = { ...spec.options, detached, stdio: "ignore" }; + record("launch", { + name, + command: spec.command, + args: spec.args, + payload: spec.payload, + scriptHash: spec.scriptHash, + options: { detached, stdio: "ignore", windowsHide: true, cwd: options.cwd }, + }); const child = spawnHidden(spec.command, spec.args, options); const result = own(child); if (trampoline) trampolineCases.add(name); @@ -231,6 +352,8 @@ async function run(name: string, detached: boolean, trampoline: boolean, image = ); if (existsSync(join(directory, `${payloadName}-ready`))) { const ready = readFileSync(join(directory, `${payloadName}-ready`), "utf8"); + assert(/^\d+\|-?\d+\|(True|False)\|\d+\|\d+\|\d+\|\d+$/.test(ready)); + readyByCase.set(name, ready.split("|")); record("sentinel-ready", { name, ready, @@ -252,6 +375,7 @@ async function run(name: string, detached: boolean, trampoline: boolean, image = readFileSync(join(directory, `${payloadName}-done`), "utf8") === "fixed launch sentinel v1 completed" && !existsSync(join(directory, `${payloadName}-error`)); record("result", { name, pid: child.pid, code, started, done }); + if (trampoline) verifyBroker(name); if (payloadName !== name) { for (const suffix of ["started", "metadata", "ready", "ready.tmp", "gate", "done", "done.tmp", "error"]) { const path = join(directory, `${payloadName}-${suffix}`); @@ -316,7 +440,7 @@ try { if (b.code !== 37 || !b.started || !b.done) errors.push(new Error("B did not execute the identical sentinel")); const c = await run("C", true, true); - assert(c.code === 37 && c.started && c.done, "C trampoline failed; stop without another launcher"); + assert(c.code === 37 && c.started && c.done, "C runtime broker failed; stop"); symlinkSync(win32.dirname(powershell), junction, "junction"); junctionCreated = true; record("owned-junction", { junction, target: win32.dirname(powershell) }); @@ -325,45 +449,69 @@ try { const space = await run("C-space", true, true, spaceImage); assert(space.code === 37 && space.started && space.done, "C-space quoting failed; stop"); - assert(Date.now() < workDeadline, "Launch probe work budget expired"); - await setPhase("D", "armed"); - record("D-launch-plan", launchSpec(directory, "D", true)); - const caller = spawnHidden(process.execPath, [process.argv[1]!, "--caller", directory], { stdio: "ignore" }); - trampolineCases.add("D"); - assert.equal(await observe(own(caller), budget(5000), "D original caller exit"), 0); - const broker = Number(readFileSync(join(directory, "D-broker"), "utf8")); - assert(Number.isInteger(broker) && broker > 0); - departedPids.push(broker); - record("D-caller-exited-before-gate", { caller: caller.pid, broker }); - assert(!existsSync(join(directory, "D-gate"))); - await waitUntil(() => existsSync(join(directory, "D-ready")), budget(15000), "D sentinel after caller exit"); - const dReady = readFileSync(join(directory, "D-ready"), "utf8"); - const sentinelPid = Number(dReady.split("|")[0]); - assert(Number.isInteger(sentinelPid) && sentinelPid > 0); - departedPids.push(sentinelPid); - record("sentinel-ready", { - name: "D", - ready: dReady, - metadata: readFileSync(join(directory, "D-metadata"), "utf8"), - }); - await setPhase("D", "audit", dReady.split("|")[1]); - if (dReady.split("|")[2] === "True") auditErrors.push(new Error("D: sentinel console is visible")); - writeFileSync(join(directory, "D-gate"), "release"); - await waitUntil( - () => - existsSync(join(directory, "D-done")) && - readFileSync(join(directory, "D-done"), "utf8") === "fixed launch sentinel v1 completed", - budget(15000), - "D sentinel completion", - ); - await waitUntil( - () => departedPids.every((pid) => !isProcessAlive(pid)), - budget(15000), - "D broker and sentinel exit", - ); - assert.equal(readFileSync(join(directory, "D-done"), "utf8"), "fixed launch sentinel v1 completed"); - assert(!existsSync(join(directory, "D-error")), "D sentinel reported an error before exit"); - record("D-completed-after-caller-exit", { departedPids }); + for (const name of ["D", "D-stderr"]) { + let stderrFd: number | undefined; + try { + assert(Date.now() < workDeadline, "Launch probe work budget expired"); + await setPhase(name, "armed"); + if (name === "D-stderr") stderrFd = openSync(join(directory, `${name}-caller-stderr`), "w"); + const args = isBun ? runtimeArguments(true, "").slice(0, 3) : []; + args.push(resolve(process.argv[1]!), "--caller", directory, name); + const caller = spawnHidden(process.execPath, args, { + cwd: join(directory, "runtime-cwd"), + env: runtimeEnvironment(process.env, isBun), + stdio: stderrFd === undefined ? "ignore" : ["ignore", "ignore", stderrFd], + }); + trampolineCases.add(name); + const code = await observe(own(caller), budget(5000), `${name} original caller exit`); + const callerSucceeded = code === 0; + record("original-caller-exit", { + name, + pid: caller.pid, + code, + stdio: name === "D" ? "ignore" : "stderr-file", + }); + if (!callerSucceeded) errors.push(new Error(`${name}: explicit original caller exit failed: ${code}`)); + const broker = Number(readFileSync(join(directory, `${name}-broker`), "utf8")); + assert(Number.isInteger(broker) && broker > 0); + departedPids.push(broker); + assert(!existsSync(join(directory, `${name}-gate`))); + await waitUntil( + () => existsSync(join(directory, `${name}-ready`)), + budget(15000), + `${name} sentinel after caller exit`, + ); + const ready = readFileSync(join(directory, `${name}-ready`), "utf8"); + assert(/^\d+\|-?\d+\|(True|False)\|\d+\|\d+\|\d+\|\d+$/.test(ready)); + const fields = ready.split("|"); + readyByCase.set(name, fields); + const sentinelPid = Number(fields[0]); + assert(Number.isInteger(sentinelPid) && sentinelPid > 0); + departedPids.push(sentinelPid); + record("sentinel-ready", { name, ready, metadata: readFileSync(join(directory, `${name}-metadata`), "utf8") }); + await setPhase(name, "audit", fields[1]); + if (fields[2] === "True") auditErrors.push(new Error(`${name}: sentinel console is visible`)); + writeFileSync(join(directory, `${name}-gate`), "release"); + await waitUntil( + () => !isProcessAlive(broker) && !isProcessAlive(sentinelPid), + budget(15000), + `${name} broker and sentinel exit`, + ); + assert.equal(readFileSync(join(directory, `${name}-done`), "utf8"), "fixed launch sentinel v1 completed"); + assert(!existsSync(join(directory, `${name}-error`)), `${name} sentinel reported an error before exit`); + verifyBroker(name); + record(callerSucceeded ? "D-completed-after-caller-exit" : "D-cleanup-completion-NOT-survival-proof", { + name, + broker, + sentinelPid, + }); + } catch (error) { + errors.push(error); + } finally { + writeFileSync(join(directory, `${name}-gate`), "release"); + if (stderrFd !== undefined) closeSync(stderrFd); + } + } } catch (error) { errors.push(error); } finally { @@ -372,7 +520,7 @@ try { owners: owners.map((child) => ({ pid: child.pid, exit: child.exitCode, signal: child.signalCode })), errors: [...errors, ...auditErrors].map(String), }); - for (const name of ["AB", "A", "B", "C", "C-space", "D"]) { + for (const name of ["AB", "A", "B", "C", "C-space", "D", "D-stderr"]) { try { writeFileSync(join(directory, `${name}-gate`), "release"); } catch (error) { @@ -408,8 +556,20 @@ try { auditErrors.push(error); } for (const name of trampolineCases) audit(name); - for (const name of ["AB", "A", "B", "C", "C-space", "D"]) { - for (const suffix of ["started", "metadata", "ready", "ready.tmp", "done", "done.tmp", "error", "broker"]) { + for (const name of ["AB", "A", "B", "C", "C-space", "D", "D-stderr"]) { + for (const suffix of [ + "started", + "metadata", + "ready", + "ready.tmp", + "done", + "done.tmp", + "error", + "broker", + "runtime.jsonl", + "caller-stages", + "caller-stderr", + ]) { const path = join(directory, `${name}-${suffix}`); if (existsSync(path)) record("preserved-sentinel-artifact", { name, suffix, text: readFileSync(path, "utf8") }); @@ -429,4 +589,4 @@ try { } if (errors.length || auditErrors.length) throw new AggregateError([...errors, ...auditErrors], "Launch probe failed or visibility is inconclusive"); -record("PASS-launch-and-observed-hidden-console", { directory }); +record("PASS-runtime-broker-hidden-startup", { directory }); From b0c9d868ae350924a61aeef4d6b6194175f371f3 Mon Sep 17 00:00:00 2001 From: Seth Date: Mon, 7 Sep 2026 23:15:26 -0400 Subject: [PATCH 44/45] Validate windowless console evidence and preserve Windows coverage Record the sentinel console PID and classify complete successful windowless queries. Run independent artifact and installer steps after successful native builds. --- .github/workflows/ci.yml | 3 ++ .../windows-powershell-launch-native.ts | 54 +++++++++++++------ 2 files changed, 41 insertions(+), 16 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 68feb75c4a6..1f64a0bae84 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -458,6 +458,7 @@ jobs: & $binary ./test/fixtures/windows-powershell-launch-native.ts --expect-runtime $binary --expect-sha256 $hash --expect-kind bun - name: Cross-compile and inspect the Windows Arm64 executable + if: ${{ !cancelled() && steps.windows_native_binary.outcome == 'success' }} working-directory: packages/coding-agent shell: pwsh run: | @@ -479,10 +480,12 @@ jobs: } - name: Test the PowerShell 5.1 installer end to end + if: ${{ !cancelled() && steps.windows_native_binary.outcome == 'success' }} shell: powershell run: ./scripts/test-windows-installer.ps1 - name: Smoke test the native Windows executable + if: ${{ !cancelled() && steps.windows_native_binary.outcome == 'success' }} shell: pwsh run: | & ./packages/coding-agent/dist/pi.exe --version diff --git a/packages/coding-agent/test/fixtures/windows-powershell-launch-native.ts b/packages/coding-agent/test/fixtures/windows-powershell-launch-native.ts index f39a6d35a12..c5d126c0d4c 100644 --- a/packages/coding-agent/test/fixtures/windows-powershell-launch-native.ts +++ b/packages/coding-agent/test/fixtures/windows-powershell-launch-native.ts @@ -54,15 +54,19 @@ function runtimeArguments(bun: boolean, script: string): string[] { : ["--input-type=commonjs", "--eval", script]; } -function isNoConsoleMode(ready: string[] | undefined, observed: string[] | undefined): boolean { +const sentinelReadyPattern = /^\d+\|-?\d+\|(True|False)\|\d+\|\d+\|\d+\|\d+\|\d+$/; + +function isWindowlessConsole(ready: string[] | undefined, observed: string[] | undefined): boolean { return Boolean( ready && + /^[1-9]\d*$/.test(ready[0] ?? "") && ready[1] === "0" && ready[2] === "False" && (Number(ready[3]) & 1) !== 0 && ready[4] === "0" && - ready[5] === "0" && - ready[6] === "6" && + // The query has one slot; success must identify this sentinel. + ready[5] === "1" && + ready[7] === ready[0] && observed?.[4] === "0" && observed[5] === "False" && observed[6] === "False", @@ -96,9 +100,10 @@ public static class SentinelConsole { $info = New-Object SentinelConsole+StartupInfo [SentinelConsole]::GetStartupInfo([ref]$info) $console = [SentinelConsole]::GetConsoleWindow() - $consoleCount = [SentinelConsole]::GetConsoleProcessList([uint32[]]@(0), 1) + $consoleIds = [uint32[]]@(0) + $consoleCount = [SentinelConsole]::GetConsoleProcessList($consoleIds, $consoleIds.Length) $consoleError = [Runtime.InteropServices.Marshal]::GetLastWin32Error() - [IO.File]::WriteAllText(${path("ready.tmp")}, "$PID|$($console.ToInt64())|$([SentinelConsole]::IsWindowVisible($console))|$($info.flags)|$($info.show)|$consoleCount|$consoleError") + [IO.File]::WriteAllText(${path("ready.tmp")}, "$PID|$($console.ToInt64())|$([SentinelConsole]::IsWindowVisible($console))|$($info.flags)|$($info.show)|$consoleCount|$consoleError|$($consoleIds[0])") [IO.File]::Move(${path("ready.tmp")}, ${path("ready")}) while (![IO.File]::Exists(${path("gate")})) { Start-Sleep -Milliseconds 25 } [IO.File]::WriteAllText(${path("done.tmp")}, 'fixed launch sentinel v1 completed') @@ -137,13 +142,28 @@ if (process.argv[2] === "--construction-check") { const data = { command: "C:\\owned space path\\powershell.exe", args: ["-EncodedCommand", encoded] }; assert.deepEqual(JSON.parse(JSON.stringify(data)), data); const observed = ["C|audit", "time", "console", "0", "0", "False", "False", ""]; - assert(isNoConsoleMode("1|0|False|257|0|0|6".split("|"), observed)); - for (const incomplete of ["1|0|False|0|1|0|6", "1|0|False|257|0|1|0", "1|0|False|257|0", "1|0|False|257|0|0|0"]) { - assert(!isNoConsoleMode(incomplete.split("|"), observed)); + for (const error of ["0", "6", "203"]) { + const ready = `1|0|False|257|0|1|${error}|1`; + assert(sentinelReadyPattern.test(ready)); + assert(isWindowlessConsole(ready.split("|"), observed)); + } + assert(!sentinelReadyPattern.test("1|0|False|257|0|1|203")); + assert(!sentinelReadyPattern.test("1|0|False|257|0|1|203|invalid")); + for (const invalid of [ + "1|0|False|0|1|1|203|1", + "1|0|False|257|0|1|203|2", + "0|0|False|257|0|1|203|0", + "1|0|False|257|0|2|203|1", + "1|0|False|257|0|0|6|0", + "1|0|False|257|0|0|0|0", + "1|0|False|257|0|1|203", + "1|0|False|257|0", + ]) { + assert(!isWindowlessConsole(invalid.split("|"), observed)); } - assert(!isNoConsoleMode("1|0|False|257|0|0|6".split("|"), undefined)); + assert(!isWindowlessConsole("1|0|False|257|0|1|203|1".split("|"), undefined)); console.log( - "PASS sentinel/transport, runtime argv, startup-env scrub, parent-env preservation and no-console guards", + "PASS sentinel/transport, runtime argv, startup-env scrub, parent-env preservation and windowless-console guards", ); process.exit(0); } @@ -303,16 +323,18 @@ function audit(name: string): void { ); else { const ready = readyByCase.get(name); - const noConsole = isNoConsoleMode(ready, console); + const windowlessConsole = isWindowlessConsole(ready, console); const hiddenClassic = console?.[4] !== "0" && console?.[5] === "False" && console?.[6] === "True" && console?.[7] === "ConsoleWindowClass"; - record("console-mode", { name, ready, noConsole, hiddenClassic }); - if (!noConsole && !hiddenClassic) + record("console-mode", { name, ready, windowlessConsole, hiddenClassic }); + if (!windowlessConsole && !hiddenClassic) auditErrors.push( - new Error(`${name}: visibility INCONCLUSIVE (no validated no-console mode or hidden classic console)`), + new Error( + `${name}: visibility INCONCLUSIVE (no validated windowless console or hidden classic console)`, + ), ); } } catch (error) { @@ -352,7 +374,7 @@ async function run(name: string, detached: boolean, trampoline: boolean, image = ); if (existsSync(join(directory, `${payloadName}-ready`))) { const ready = readFileSync(join(directory, `${payloadName}-ready`), "utf8"); - assert(/^\d+\|-?\d+\|(True|False)\|\d+\|\d+\|\d+\|\d+$/.test(ready)); + assert(sentinelReadyPattern.test(ready)); readyByCase.set(name, ready.split("|")); record("sentinel-ready", { name, @@ -482,7 +504,7 @@ try { `${name} sentinel after caller exit`, ); const ready = readFileSync(join(directory, `${name}-ready`), "utf8"); - assert(/^\d+\|-?\d+\|(True|False)\|\d+\|\d+\|\d+\|\d+$/.test(ready)); + assert(sentinelReadyPattern.test(ready)); const fields = ready.split("|"); readyByCase.set(name, fields); const sentinelPid = Number(fields[0]); From 9282f0e8fd59fed3575aaecde195bae9678b827d Mon Sep 17 00:00:00 2001 From: Seth Date: Tue, 8 Sep 2026 19:13:43 -0400 Subject: [PATCH 45/45] Fix Bun Windows process signaling and preserve the uninstall host --- .github/workflows/ci.yml | 13 ++++++ install.ps1 | 2 +- .../coding-agent/src/utils/child-process.ts | 29 ++++++++++-- .../fixtures/windows-process-signal-native.ts | 13 ++++-- .../test/windows-process-signal.test.ts | 45 ++++++++++++++++++- scripts/test-windows-installer.ps1 | 14 +++++- 6 files changed, 105 insertions(+), 11 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d3708bc5e93..89165a0f8f9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -420,6 +420,19 @@ jobs: if: ${{ !cancelled() && steps.windows_bun.outcome == 'success' && steps.windows_bun_verified.outcome == 'success' && steps.windows_dependencies.outcome == 'success' }} run: bun run build:binary + - name: Test identity-pinned tree signals under the compiled Windows runtime + if: ${{ !cancelled() && steps.windows_native_binary.outcome == 'success' }} + timeout-minutes: 3 + working-directory: packages/coding-agent + env: + BUN_BE_BUN: "1" + run: | + $binary = (Resolve-Path "./dist/pi.exe").Path + $hash = (Get-FileHash -LiteralPath $binary -Algorithm SHA256).Hash.ToLowerInvariant() + Write-Host "Identity fixture runtime: $binary SHA256=$hash" + & $binary ./test/fixtures/windows-process-signal-native.ts + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + - name: Probe the compiled Windows runtime broker if: ${{ !cancelled() && steps.windows_native_binary.outcome == 'success' }} timeout-minutes: 3 diff --git a/install.ps1 b/install.ps1 index 224baea7cf7..0100dd0fecf 100644 --- a/install.ps1 +++ b/install.ps1 @@ -250,7 +250,7 @@ function Uninstall-PrimeAgent { if ($Uninstall) { Uninstall-PrimeAgent - exit 0 + return } if ($DownloadBaseUrl -eq ("__PRIME_AGENT_DOWNLOAD_BASE" + "_URL__")) { throw "Installer download URL is not configured. Use the published installer or set PRIME_AGENT_DOWNLOAD_BASE_URL." diff --git a/packages/coding-agent/src/utils/child-process.ts b/packages/coding-agent/src/utils/child-process.ts index eb437c63a2c..0ff456f372d 100644 --- a/packages/coding-agent/src/utils/child-process.ts +++ b/packages/coding-agent/src/utils/child-process.ts @@ -18,7 +18,7 @@ import { } from "node:child_process"; import { readFileSync } from "node:fs"; import { constants } from "node:os"; -import { basename } from "node:path"; +import { basename, win32 } from "node:path"; import { createWindowsProcessTreeSignal } from "./windows-process-signal.js"; const EXIT_STDIO_GRACE_MS = 100; @@ -168,6 +168,30 @@ export function signalProcessGroupIfHeld(pgid: number, signal: NodeJS.Signals): return true; } +export function spawnWindowsProcessTreeSignal(command: { command: string; args: string[] }): ChildProcess { + const env: NodeJS.ProcessEnv = Object.fromEntries( + Object.entries(process.env).filter( + ([key]) => !/^(BUN|NODE)_/i.test(key) && key.toUpperCase() !== "PRIME_AGENT_WINDOWS_SIGNAL", + ), + ); + env.BUN_BE_BUN = "1"; + env.PRIME_AGENT_WINDOWS_SIGNAL = JSON.stringify(command); + const script = String.raw` +const { spawn } = require("node:child_process"); +const spec = JSON.parse(process.env.PRIME_AGENT_WINDOWS_SIGNAL); +delete process.env.PRIME_AGENT_WINDOWS_SIGNAL; +delete process.env.BUN_BE_BUN; +const child = spawn(spec.command, spec.args, { detached: false, windowsHide: true, stdio: "ignore" }); +child.once("error", () => { process.exitCode = 1; }); +child.once("exit", (code, signal) => { process.exitCode = signal || code === null ? 1 : code; }); +`; + return spawnHidden( + process.execPath, + ["--no-env-file", "--no-install", String.raw`--config=\\.\NUL`, "--eval", script], + { cwd: win32.dirname(command.command), env, detached: true, stdio: "ignore" }, + ); +} + export function signalProcessGroupOrProcess( pid: number, signal: NodeJS.Signals, @@ -183,8 +207,7 @@ export function signalProcessGroupOrProcess( else console.error(`Could not signal Windows process tree ${pid} (${signal}): ${error.message}`); }; try { - const { command, args } = createWindowsProcessTreeSignal(pid, signal); - const helper = spawnHidden(command, args, { stdio: "ignore", detached: true }); + const helper = spawnWindowsProcessTreeSignal(createWindowsProcessTreeSignal(pid, signal)); helper.once("error", reportFailure); helper.once("exit", (code) => { if (code !== 0 && isProcessAlive(pid)) reportFailure(`identity helper exited with code ${code}`); diff --git a/packages/coding-agent/test/fixtures/windows-process-signal-native.ts b/packages/coding-agent/test/fixtures/windows-process-signal-native.ts index 0b0e9fd9ef4..5d8bbec8d1d 100644 --- a/packages/coding-agent/test/fixtures/windows-process-signal-native.ts +++ b/packages/coding-agent/test/fixtures/windows-process-signal-native.ts @@ -4,7 +4,12 @@ import { existsSync, mkdtempSync, readFileSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { execCommand } from "../../src/core/exec.js"; -import { isProcessAlive, spawnHidden, waitForChildProcess } from "../../src/utils/child-process.js"; +import { + isProcessAlive, + spawnHidden, + spawnWindowsProcessTreeSignal, + waitForChildProcess, +} from "../../src/utils/child-process.js"; import { captureWindowsProcessCreationTime, createWindowsProcessTreeSignal, @@ -52,7 +57,7 @@ if (process.argv[2] === "--exit-caller") { phase("caller-capture-complete"); traceCommand(command, process.argv[6]!); encode(command, `${gate(process.argv[4]!)}\n${decode(command)}`); - const helper = spawnHidden(command.command, command.args, { detached: true, stdio: "ignore" }); + const helper = spawnWindowsProcessTreeSignal(command); writeFileSync(process.argv[5]!, String(helper.pid)); phase("caller-helper-spawned", { helperPid: helper.pid }); process.exit(0); @@ -114,7 +119,7 @@ async function liveTree(name: string): Promise<{ root: ChildProcess; pids: [numb async function runHelper(command: { command: string; args: string[] }): Promise { const log = tracePath(`helper-${traces.length}`); traceCommand(command, log); - const helper = spawnHidden(command.command, command.args, { stdio: "ignore", detached: true }); + const helper = spawnWindowsProcessTreeSignal(command); owners.push(helper); phase("helper-spawned", { helperPid: helper.pid, log }); helper.once("error", (error) => phase("helper-error", String(error))); @@ -193,7 +198,7 @@ try { .replace("exit $code", "if (!$pin.IsClosed) { exit 97 }\nexit $code"); encode(heldCommand, heldScript); traceCommand(heldCommand, tracePath("held-helper")); - const heldHelper = spawnHidden(heldCommand.command, heldCommand.args, { stdio: "ignore", detached: true }); + const heldHelper = spawnWindowsProcessTreeSignal(heldCommand); owners.push(heldHelper); const heldResult = waitForChildProcess(heldHelper); heldResult.catch((error) => cleanupErrors.push(error)); diff --git a/packages/coding-agent/test/windows-process-signal.test.ts b/packages/coding-agent/test/windows-process-signal.test.ts index 21df31cf1b5..1ce7a6cda47 100644 --- a/packages/coding-agent/test/windows-process-signal.test.ts +++ b/packages/coding-agent/test/windows-process-signal.test.ts @@ -1,6 +1,7 @@ import type * as ChildProcessModule from "node:child_process"; import { EventEmitter } from "node:events"; import { createRequire } from "node:module"; +import { win32 } from "node:path"; import { PassThrough } from "node:stream"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { execCommand } from "../src/core/exec.js"; @@ -102,6 +103,39 @@ describe("Windows process identity capture", () => { }); describe("Windows shared signal failures", () => { + it("re-enters the current Bun with isolated startup without changing the parent environment", () => { + const originalEnv = process.env; + const inherited = { + ...originalEnv, + bUn_PrElOaD: "untrusted", + NoDe_OpTiOnS: "untrusted", + prime_agent_windows_signal: "untrusted", + }; + process.env = inherited; + try { + signalProcessGroupOrProcess(42, "SIGKILL"); + const [executable, args, options] = calls.spawn.mock.calls[0]!; + expect(executable).toBe(process.execPath); + expect(args).toEqual([ + "--no-env-file", + "--no-install", + String.raw`--config=\\.\NUL`, + "--eval", + expect.any(String), + ]); + const command = JSON.parse(options.env.PRIME_AGENT_WINDOWS_SIGNAL); + expect(options.cwd).toBe(win32.dirname(command.command)); + expect(options.env.BUN_BE_BUN).toBe("1"); + for (const key of ["bUn_PrElOaD", "NoDe_OpTiOnS", "prime_agent_windows_signal"]) { + expect(options.env[key]).toBeUndefined(); + expect(process.env[key]).toBe("untrusted"); + } + expect(process.env).toBe(inherited); + } finally { + process.env = originalEnv; + } + }); + it("fails closed without throwing or spawning when the native adapter is unavailable", () => { vi.mocked(ffi.dlopen).mockImplementation(() => { throw new Error("adapter unavailable"); @@ -135,7 +169,13 @@ describe("Windows shared signal failures", () => { expect(failure).toHaveBeenCalledTimes(1); expect(kill.mock.calls.every(([, signal]) => signal === 0)).toBe(true); expect(calls.spawn).toHaveBeenCalledTimes(1); - expect(calls.spawn.mock.calls[0]![2]).toEqual({ stdio: "ignore", detached: true, windowsHide: true }); + expect(calls.spawn.mock.calls[0]![2]).toEqual({ + cwd: expect.stringMatching(/\\System32\\WindowsPowerShell\\v1\.0$/i), + env: expect.objectContaining({ BUN_BE_BUN: "1", PRIME_AGENT_WINDOWS_SIGNAL: expect.any(String) }), + stdio: "ignore", + detached: true, + windowsHide: true, + }); expect(helper.unref).toHaveBeenCalledTimes(1); }); }); @@ -228,7 +268,8 @@ describe("owned exec failure reporting", () => { controller.abort(); await vi.advanceTimersByTimeAsync(5000); expect(calls.spawn).toHaveBeenCalledTimes(3); - const script = Buffer.from(calls.spawn.mock.calls[2]![1].at(-1), "base64").toString("utf16le"); + const command = JSON.parse(calls.spawn.mock.calls[2]![2].env.PRIME_AGENT_WINDOWS_SIGNAL); + const script = Buffer.from(command.args.at(-1), "base64").toString("utf16le"); expect(script).toContain("/T /F"); expect(result).toBeUndefined(); helper.emit("exit", 0); diff --git a/scripts/test-windows-installer.ps1 b/scripts/test-windows-installer.ps1 index e72a0f82913..dab60622e12 100644 --- a/scripts/test-windows-installer.ps1 +++ b/scripts/test-windows-installer.ps1 @@ -103,7 +103,19 @@ try { Remove-Item -LiteralPath (Join-Path $serverRoot "stable") Set-Content -LiteralPath (Join-Path $serverRoot "beta") -Value $version -Encoding Ascii & (Join-Path $testRoot "install-beta.ps1") -Update - & (Join-Path $testRoot "install-beta.ps1") -Uninstall + $uninstallWrapper = Join-Path $testRoot "uninstall-scriptblock.ps1" + @' +param([string]$InstallerPath) +$ErrorActionPreference = "Stop" +& ([scriptblock]::Create((Get-Content -LiteralPath $InstallerPath -Raw))) -Uninstall +Write-Output "prime-agent-uninstall-host-continued" +'@ | Set-Content -LiteralPath $uninstallWrapper -Encoding UTF8 + $currentHost = (Get-Process -Id $PID).Path + $uninstallOutput = & $currentHost -NoProfile -NonInteractive -ExecutionPolicy Bypass -File $uninstallWrapper (Join-Path $testRoot "install-beta.ps1") + if ($LASTEXITCODE -ne 0) { throw "Uninstall scriptblock host failed with $LASTEXITCODE" } + if ($uninstallOutput -cnotcontains "prime-agent-uninstall-host-continued") { + throw "Uninstall exited its hosting PowerShell session" + } if (Test-Path -LiteralPath (Join-Path $localAppData "PrimeAgent")) { throw "Uninstall left the PrimeAgent install directory behind" }