From 3b943c1582026bdf65feb13a73259ce0e034ab2f Mon Sep 17 00:00:00 2001 From: matt korwel Date: Mon, 9 Jun 2025 12:19:42 -0700 Subject: [PATCH] Windows: Refactor Shell Scripts to Node.js for Cross-Platform Compatibility (#784) --- docs/tools/shell.md | 9 +- package-lock.json | 713 +++++++++++++++--- package.json | 22 +- packages/cli/package.json | 6 +- .../components/messages/ToolGroupMessage.tsx | 2 +- .../ui/hooks/shellCommandProcessor.test.ts | 2 + .../cli/src/ui/hooks/shellCommandProcessor.ts | 78 +- .../src/ui/hooks/useLoadingIndicator.test.ts | 58 +- packages/cli/src/utils/sandbox.ts | 135 ++-- packages/core/package.json | 2 +- packages/core/src/core/prompts.ts | 12 +- packages/core/src/tools/edit.ts | 4 + packages/core/src/tools/shell.ts | 108 ++- packages/core/src/utils/editor.test.ts | 12 +- packages/core/src/utils/editor.ts | 14 +- scripts/build.js | 54 ++ scripts/build.sh | 30 - scripts/build_package.js | 37 + scripts/build_package.sh | 33 - scripts/build_sandbox.js | 125 +++ scripts/build_sandbox.sh | 102 --- scripts/clean.js | 32 + scripts/clean.sh | 21 - scripts/copy_bundle_assets.js | 48 ++ scripts/copy_bundle_assets.sh | 13 - scripts/generate-git-commit-info.js | 61 ++ scripts/generate-git-commit-info.sh | 44 -- scripts/publish-sandbox.js | 55 ++ scripts/publish-sandbox.sh | 41 - scripts/sandbox.js | 123 +++ scripts/sandbox.sh | 103 --- scripts/sandbox_command.js | 126 ++++ scripts/sandbox_command.sh | 122 --- scripts/setup-dev.js | 42 ++ scripts/setup-dev.sh | 34 - scripts/start.js | 61 ++ scripts/start.sh | 37 - tmperrors.txt | 55 ++ 38 files changed, 1723 insertions(+), 853 deletions(-) create mode 100644 scripts/build.js delete mode 100755 scripts/build.sh create mode 100644 scripts/build_package.js delete mode 100755 scripts/build_package.sh create mode 100644 scripts/build_sandbox.js delete mode 100755 scripts/build_sandbox.sh create mode 100644 scripts/clean.js delete mode 100755 scripts/clean.sh create mode 100644 scripts/copy_bundle_assets.js delete mode 100755 scripts/copy_bundle_assets.sh create mode 100644 scripts/generate-git-commit-info.js delete mode 100755 scripts/generate-git-commit-info.sh create mode 100644 scripts/publish-sandbox.js delete mode 100755 scripts/publish-sandbox.sh create mode 100644 scripts/sandbox.js delete mode 100755 scripts/sandbox.sh create mode 100644 scripts/sandbox_command.js delete mode 100755 scripts/sandbox_command.sh create mode 100644 scripts/setup-dev.js delete mode 100755 scripts/setup-dev.sh create mode 100644 scripts/start.js delete mode 100755 scripts/start.sh create mode 100644 tmperrors.txt diff --git a/docs/tools/shell.md b/docs/tools/shell.md index d8055deb..e60fb9a4 100644 --- a/docs/tools/shell.md +++ b/docs/tools/shell.md @@ -4,9 +4,9 @@ This document provides details on the shell tool. ## `run_shell_command` -- **Purpose:** Executes a given shell command using `bash -c `. This tool is essential for interacting with the underlying operating system, running scripts, or performing command-line operations. +- **Purpose:** Executes a given shell command. On Windows, this will be executed with `cmd.exe /c`. On other platforms, it will be executed with `bash -c`. This tool is essential for interacting with the underlying operating system, running scripts, or performing command-line operations. - **Arguments:** - - `command` (string, required): The exact bash command to execute. + - `command` (string, required): The exact shell command to execute. - `description` (string, optional): A brief description of the command's purpose, which will be shown to the user. - `directory` (string, optional): The directory (relative to the project root) in which to execute the command. If not provided, the command runs in the project root. - **Behavior:** @@ -22,10 +22,14 @@ This document provides details on the shell tool. - `Signal`: The signal number if the command was terminated by a signal. - `Background PIDs`: A list of PIDs for any background processes started. - **Examples:** + - Listing files in the current directory: + ``` + run_shell_command(command="ls -la") ``` + - Running a script in a specific directory: ``` run_shell_command(command="./my_script.sh", directory="scripts", description="Run my custom script") @@ -34,6 +38,7 @@ This document provides details on the shell tool. ``` run_shell_command(command="npm run dev &", description="Start development server in background") ``` + - **Important Notes:** - **Security:** Be cautious when executing commands, especially those constructed from user input, to prevent security vulnerabilities. - **Interactive Commands:** Avoid commands that require interactive user input, as this can cause the tool to hang. Use non-interactive flags if available (e.g., `npm init -y`). diff --git a/package-lock.json b/package-lock.json index dbc8cfdd..becc665a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,13 +18,15 @@ "@types/mime-types": "^2.1.4", "@types/minimatch": "^5.1.2", "@vitest/coverage-v8": "^3.1.1", - "esbuild": "^0.25.4", + "cross-env": "^7.0.3", + "esbuild": "^0.23.0", "eslint": "^9.24.0", "eslint-config-prettier": "^10.1.2", "eslint-plugin-import": "^2.31.0", "eslint-plugin-license-header": "^0.8.0", "eslint-plugin-react": "^7.37.5", "eslint-plugin-react-hooks": "^5.2.0", + "glob": "^10.4.2", "globals": "^16.0.0", "json": "^11.0.0", "lodash": "^4.17.21", @@ -293,9 +295,9 @@ } }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.5.tgz", - "integrity": "sha512-9o3TMmpmftaCMepOdA5k/yDw8SfInyzWWTjYTFCX3kPSDJMROQTb8jg+h9Cnwnmm1vOzvxN7gIfB5V2ewpjtGA==", + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.23.1.tgz", + "integrity": "sha512-6VhYk1diRqrhBAqpJEdjASR/+WVRtfjpqKuNw11cLiaWpAT/Uu+nokB+UJnevzy/P9C/ty6AOe0dwueMrGh/iQ==", "cpu": [ "ppc64" ], @@ -310,9 +312,9 @@ } }, "node_modules/@esbuild/android-arm": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.5.tgz", - "integrity": "sha512-AdJKSPeEHgi7/ZhuIPtcQKr5RQdo6OO2IL87JkianiMYMPbCtot9fxPbrMiBADOWWm3T2si9stAiVsGbTQFkbA==", + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.23.1.tgz", + "integrity": "sha512-uz6/tEy2IFm9RYOyvKl88zdzZfwEfKZmnX9Cj1BHjeSGNuGLuMD1kR8y5bteYmwqKm1tj8m4cb/aKEorr6fHWQ==", "cpu": [ "arm" ], @@ -327,9 +329,9 @@ } }, "node_modules/@esbuild/android-arm64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.5.tgz", - "integrity": "sha512-VGzGhj4lJO+TVGV1v8ntCZWJktV7SGCs3Pn1GRWI1SBFtRALoomm8k5E9Pmwg3HOAal2VDc2F9+PM/rEY6oIDg==", + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.23.1.tgz", + "integrity": "sha512-xw50ipykXcLstLeWH7WRdQuysJqejuAGPd30vd1i5zSyKK3WE+ijzHmLKxdiCMtH1pHz78rOg0BKSYOSB/2Khw==", "cpu": [ "arm64" ], @@ -344,9 +346,9 @@ } }, "node_modules/@esbuild/android-x64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.5.tgz", - "integrity": "sha512-D2GyJT1kjvO//drbRT3Hib9XPwQeWd9vZoBJn+bu/lVsOZ13cqNdDeqIF/xQ5/VmWvMduP6AmXvylO/PIc2isw==", + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.23.1.tgz", + "integrity": "sha512-nlN9B69St9BwUoB+jkyU090bru8L0NA3yFvAd7k8dNsVH8bi9a8cUAUSEcEEgTp2z3dbEDGJGfP6VUnkQnlReg==", "cpu": [ "x64" ], @@ -361,9 +363,9 @@ } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.5.tgz", - "integrity": "sha512-GtaBgammVvdF7aPIgH2jxMDdivezgFu6iKpmT+48+F8Hhg5J/sfnDieg0aeG/jfSvkYQU2/pceFPDKlqZzwnfQ==", + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.23.1.tgz", + "integrity": "sha512-YsS2e3Wtgnw7Wq53XXBLcV6JhRsEq8hkfg91ESVadIrzr9wO6jJDMZnCQbHm1Guc5t/CdDiFSSfWP58FNuvT3Q==", "cpu": [ "arm64" ], @@ -378,9 +380,9 @@ } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.5.tgz", - "integrity": "sha512-1iT4FVL0dJ76/q1wd7XDsXrSW+oLoquptvh4CLR4kITDtqi2e/xwXwdCVH8hVHU43wgJdsq7Gxuzcs6Iq/7bxQ==", + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.23.1.tgz", + "integrity": "sha512-aClqdgTDVPSEGgoCS8QDG37Gu8yc9lTHNAQlsztQ6ENetKEO//b8y31MMu2ZaPbn4kVsIABzVLXYLhCGekGDqw==", "cpu": [ "x64" ], @@ -395,9 +397,9 @@ } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.5.tgz", - "integrity": "sha512-nk4tGP3JThz4La38Uy/gzyXtpkPW8zSAmoUhK9xKKXdBCzKODMc2adkB2+8om9BDYugz+uGV7sLmpTYzvmz6Sw==", + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.23.1.tgz", + "integrity": "sha512-h1k6yS8/pN/NHlMl5+v4XPfikhJulk4G+tKGFIOwURBSFzE8bixw1ebjluLOjfwtLqY0kewfjLSrO6tN2MgIhA==", "cpu": [ "arm64" ], @@ -412,9 +414,9 @@ } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.5.tgz", - "integrity": "sha512-PrikaNjiXdR2laW6OIjlbeuCPrPaAl0IwPIaRv+SMV8CiM8i2LqVUHFC1+8eORgWyY7yhQY+2U2fA55mBzReaw==", + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.23.1.tgz", + "integrity": "sha512-lK1eJeyk1ZX8UklqFd/3A60UuZ/6UVfGT2LuGo3Wp4/z7eRTRYY+0xOu2kpClP+vMTi9wKOfXi2vjUpO1Ro76g==", "cpu": [ "x64" ], @@ -429,9 +431,9 @@ } }, "node_modules/@esbuild/linux-arm": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.5.tgz", - "integrity": "sha512-cPzojwW2okgh7ZlRpcBEtsX7WBuqbLrNXqLU89GxWbNt6uIg78ET82qifUy3W6OVww6ZWobWub5oqZOVtwolfw==", + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.23.1.tgz", + "integrity": "sha512-CXXkzgn+dXAPs3WBwE+Kvnrf4WECwBdfjfeYHpMeVxWE0EceB6vhWGShs6wi0IYEqMSIzdOF1XjQ/Mkm5d7ZdQ==", "cpu": [ "arm" ], @@ -446,9 +448,9 @@ } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.5.tgz", - "integrity": "sha512-Z9kfb1v6ZlGbWj8EJk9T6czVEjjq2ntSYLY2cw6pAZl4oKtfgQuS4HOq41M/BcoLPzrUbNd+R4BXFyH//nHxVg==", + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.23.1.tgz", + "integrity": "sha512-/93bf2yxencYDnItMYV/v116zff6UyTjo4EtEQjUBeGiVpMmffDNUyD9UN2zV+V3LRV3/on4xdZ26NKzn6754g==", "cpu": [ "arm64" ], @@ -463,9 +465,9 @@ } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.5.tgz", - "integrity": "sha512-sQ7l00M8bSv36GLV95BVAdhJ2QsIbCuCjh/uYrWiMQSUuV+LpXwIqhgJDcvMTj+VsQmqAHL2yYaasENvJ7CDKA==", + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.23.1.tgz", + "integrity": "sha512-VTN4EuOHwXEkXzX5nTvVY4s7E/Krz7COC8xkftbbKRYAl96vPiUssGkeMELQMOnLOJ8k3BY1+ZY52tttZnHcXQ==", "cpu": [ "ia32" ], @@ -480,9 +482,9 @@ } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.5.tgz", - "integrity": "sha512-0ur7ae16hDUC4OL5iEnDb0tZHDxYmuQyhKhsPBV8f99f6Z9KQM02g33f93rNH5A30agMS46u2HP6qTdEt6Q1kg==", + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.23.1.tgz", + "integrity": "sha512-Vx09LzEoBa5zDnieH8LSMRToj7ir/Jeq0Gu6qJ/1GcBq9GkfoEAoXvLiW1U9J1qE/Y/Oyaq33w5p2ZWrNNHNEw==", "cpu": [ "loong64" ], @@ -497,9 +499,9 @@ } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.5.tgz", - "integrity": "sha512-kB/66P1OsHO5zLz0i6X0RxlQ+3cu0mkxS3TKFvkb5lin6uwZ/ttOkP3Z8lfR9mJOBk14ZwZ9182SIIWFGNmqmg==", + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.23.1.tgz", + "integrity": "sha512-nrFzzMQ7W4WRLNUOU5dlWAqa6yVeI0P78WKGUo7lg2HShq/yx+UYkeNSE0SSfSure0SqgnsxPvmAUu/vu0E+3Q==", "cpu": [ "mips64el" ], @@ -514,9 +516,9 @@ } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.5.tgz", - "integrity": "sha512-UZCmJ7r9X2fe2D6jBmkLBMQetXPXIsZjQJCjgwpVDz+YMcS6oFR27alkgGv3Oqkv07bxdvw7fyB71/olceJhkQ==", + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.23.1.tgz", + "integrity": "sha512-dKN8fgVqd0vUIjxuJI6P/9SSSe/mB9rvA98CSH2sJnlZ/OCZWO1DJvxj8jvKTfYUdGfcq2dDxoKaC6bHuTlgcw==", "cpu": [ "ppc64" ], @@ -531,9 +533,9 @@ } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.5.tgz", - "integrity": "sha512-kTxwu4mLyeOlsVIFPfQo+fQJAV9mh24xL+y+Bm6ej067sYANjyEw1dNHmvoqxJUCMnkBdKpvOn0Ahql6+4VyeA==", + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.23.1.tgz", + "integrity": "sha512-5AV4Pzp80fhHL83JM6LoA6pTQVWgB1HovMBsLQ9OZWLDqVY8MVobBXNSmAJi//Csh6tcY7e7Lny2Hg1tElMjIA==", "cpu": [ "riscv64" ], @@ -548,9 +550,9 @@ } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.5.tgz", - "integrity": "sha512-K2dSKTKfmdh78uJ3NcWFiqyRrimfdinS5ErLSn3vluHNeHVnBAFWC8a4X5N+7FgVE1EjXS1QDZbpqZBjfrqMTQ==", + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.23.1.tgz", + "integrity": "sha512-9ygs73tuFCe6f6m/Tb+9LtYxWR4c9yg7zjt2cYkjDbDpV/xVn+68cQxMXCjUpYwEkze2RcU/rMnfIXNRFmSoDw==", "cpu": [ "s390x" ], @@ -565,9 +567,9 @@ } }, "node_modules/@esbuild/linux-x64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.5.tgz", - "integrity": "sha512-uhj8N2obKTE6pSZ+aMUbqq+1nXxNjZIIjCjGLfsWvVpy7gKCOL6rsY1MhRh9zLtUtAI7vpgLMK6DxjO8Qm9lJw==", + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.23.1.tgz", + "integrity": "sha512-EV6+ovTsEXCPAp58g2dD68LxoP/wK5pRvgy0J/HxPGB009omFPv3Yet0HiaqvrIrgPTBuC6wCH1LTOY91EO5hQ==", "cpu": [ "x64" ], @@ -594,14 +596,15 @@ "os": [ "netbsd" ], + "peer": true, "engines": { "node": ">=18" } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.5.tgz", - "integrity": "sha512-WOb5fKrvVTRMfWFNCroYWWklbnXH0Q5rZppjq0vQIdlsQKuw6mdSihwSo4RV/YdQ5UCKKvBy7/0ZZYLBZKIbwQ==", + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.23.1.tgz", + "integrity": "sha512-aevEkCNu7KlPRpYLjwmdcuNz6bDFiE7Z8XC4CPqExjTvrHugh28QzUXVOZtiYghciKUacNktqxdpymplil1beA==", "cpu": [ "x64" ], @@ -616,9 +619,9 @@ } }, "node_modules/@esbuild/openbsd-arm64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.5.tgz", - "integrity": "sha512-7A208+uQKgTxHd0G0uqZO8UjK2R0DDb4fDmERtARjSHWxqMTye4Erz4zZafx7Di9Cv+lNHYuncAkiGFySoD+Mw==", + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.23.1.tgz", + "integrity": "sha512-3x37szhLexNA4bXhLrCC/LImN/YtWis6WXr1VESlfVtVeoFJBRINPJ3f0a/6LV8zpikqoUg4hyXw0sFBt5Cr+Q==", "cpu": [ "arm64" ], @@ -633,9 +636,9 @@ } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.5.tgz", - "integrity": "sha512-G4hE405ErTWraiZ8UiSoesH8DaCsMm0Cay4fsFWOOUcz8b8rC6uCvnagr+gnioEjWn0wC+o1/TAHt+It+MpIMg==", + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.23.1.tgz", + "integrity": "sha512-aY2gMmKmPhxfU+0EdnN+XNtGbjfQgwZj43k8G3fyrDM/UdZww6xrWxmDkuz2eCZchqVeABjV5BpildOrUbBTqA==", "cpu": [ "x64" ], @@ -650,9 +653,9 @@ } }, "node_modules/@esbuild/sunos-x64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.5.tgz", - "integrity": "sha512-l+azKShMy7FxzY0Rj4RCt5VD/q8mG/e+mDivgspo+yL8zW7qEwctQ6YqKX34DTEleFAvCIUviCFX1SDZRSyMQA==", + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.23.1.tgz", + "integrity": "sha512-RBRT2gqEl0IKQABT4XTj78tpk9v7ehp+mazn2HbUeZl1YMdaGAQqhapjGTCe7uw7y0frDi4gS0uHzhvpFuI1sA==", "cpu": [ "x64" ], @@ -667,9 +670,9 @@ } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.5.tgz", - "integrity": "sha512-O2S7SNZzdcFG7eFKgvwUEZ2VG9D/sn/eIiz8XRZ1Q/DO5a3s76Xv0mdBzVM5j5R639lXQmPmSo0iRpHqUUrsxw==", + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.23.1.tgz", + "integrity": "sha512-4O+gPR5rEBe2FpKOVyiJ7wNDPA8nGzDuJ6gN4okSA1gEOYZ67N8JPk58tkWtdtPeLz7lBnY6I5L3jdsr3S+A6A==", "cpu": [ "arm64" ], @@ -684,9 +687,9 @@ } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.5.tgz", - "integrity": "sha512-onOJ02pqs9h1iMJ1PQphR+VZv8qBMQ77Klcsqv9CNW2w6yLqoURLcgERAIurY6QE63bbLuqgP9ATqajFLK5AMQ==", + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.23.1.tgz", + "integrity": "sha512-BcaL0Vn6QwCwre3Y717nVHZbAa4UBEigzFm6VdsVdT/MbZ38xoj1X9HPkZhbmaBGUD1W8vxAfffbDe8bA6AKnQ==", "cpu": [ "ia32" ], @@ -701,9 +704,9 @@ } }, "node_modules/@esbuild/win32-x64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.5.tgz", - "integrity": "sha512-TXv6YnJ8ZMVdX+SXWVBo/0p8LTcrUYngpWjvm91TMjjBQii7Oz11Lw5lbDV5Y0TzuhSJHwiH4hEtC1I42mMS0g==", + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.23.1.tgz", + "integrity": "sha512-BHpFFeslkWrXWyUPnbKm+xYYVYruCinGcftSBaa8zoF9hZO4BcSCFUvHVTtzpIY6YzUnYtuEhZ+C9iEXjxnasg==", "cpu": [ "x64" ], @@ -2131,6 +2134,13 @@ "@types/deep-eql": "*" } }, + "node_modules/@types/command-exists": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@types/command-exists/-/command-exists-1.2.3.tgz", + "integrity": "sha512-PpbaE2XWLaWYboXD6k70TcXO/OdOyyRFq5TVpmlUELNxdkkmXU9fkImNosmXU1DtsNrqdUgWd/nJQYXgwmtdXQ==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/deep-eql": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", @@ -3552,6 +3562,12 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "license": "MIT" }, + "node_modules/command-exists": { + "version": "1.2.9", + "resolved": "https://registry.npmjs.org/command-exists/-/command-exists-1.2.9.tgz", + "integrity": "sha512-LTQ/SGc+s0Xc0Fu5WaKnR0YiygZkm9eKFvyS+fRsU7/ZWFF8ykFM6Pc9aCVf1+xasOOZpO3BAVgVrKvsqKHV7w==", + "license": "MIT" + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -3620,6 +3636,25 @@ "node": ">= 0.10" } }, + "node_modules/cross-env": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-7.0.3.tgz", + "integrity": "sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.1" + }, + "bin": { + "cross-env": "src/bin/cross-env.js", + "cross-env-shell": "src/bin/cross-env-shell.js" + }, + "engines": { + "node": ">=10.14", + "npm": ">=6", + "yarn": ">=1" + } + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -4247,9 +4282,9 @@ ] }, "node_modules/esbuild": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.5.tgz", - "integrity": "sha512-P8OtKZRv/5J5hhz0cUAdu/cLuPIKXpQl1R9pZtvmHWQvrAUVd0UNIPT4IB4W3rNOqVO0rlqHmCIbSwxh/c9yUQ==", + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.23.1.tgz", + "integrity": "sha512-VVNz/9Sa0bs5SELtn3f7qhJCDPCF5oMEl5cO9/SSinpE9hbPVvxbd572HH5AKiP7WD8INO53GgfDDhRjkylHEg==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -4260,31 +4295,30 @@ "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.25.5", - "@esbuild/android-arm": "0.25.5", - "@esbuild/android-arm64": "0.25.5", - "@esbuild/android-x64": "0.25.5", - "@esbuild/darwin-arm64": "0.25.5", - "@esbuild/darwin-x64": "0.25.5", - "@esbuild/freebsd-arm64": "0.25.5", - "@esbuild/freebsd-x64": "0.25.5", - "@esbuild/linux-arm": "0.25.5", - "@esbuild/linux-arm64": "0.25.5", - "@esbuild/linux-ia32": "0.25.5", - "@esbuild/linux-loong64": "0.25.5", - "@esbuild/linux-mips64el": "0.25.5", - "@esbuild/linux-ppc64": "0.25.5", - "@esbuild/linux-riscv64": "0.25.5", - "@esbuild/linux-s390x": "0.25.5", - "@esbuild/linux-x64": "0.25.5", - "@esbuild/netbsd-arm64": "0.25.5", - "@esbuild/netbsd-x64": "0.25.5", - "@esbuild/openbsd-arm64": "0.25.5", - "@esbuild/openbsd-x64": "0.25.5", - "@esbuild/sunos-x64": "0.25.5", - "@esbuild/win32-arm64": "0.25.5", - "@esbuild/win32-ia32": "0.25.5", - "@esbuild/win32-x64": "0.25.5" + "@esbuild/aix-ppc64": "0.23.1", + "@esbuild/android-arm": "0.23.1", + "@esbuild/android-arm64": "0.23.1", + "@esbuild/android-x64": "0.23.1", + "@esbuild/darwin-arm64": "0.23.1", + "@esbuild/darwin-x64": "0.23.1", + "@esbuild/freebsd-arm64": "0.23.1", + "@esbuild/freebsd-x64": "0.23.1", + "@esbuild/linux-arm": "0.23.1", + "@esbuild/linux-arm64": "0.23.1", + "@esbuild/linux-ia32": "0.23.1", + "@esbuild/linux-loong64": "0.23.1", + "@esbuild/linux-mips64el": "0.23.1", + "@esbuild/linux-ppc64": "0.23.1", + "@esbuild/linux-riscv64": "0.23.1", + "@esbuild/linux-s390x": "0.23.1", + "@esbuild/linux-x64": "0.23.1", + "@esbuild/netbsd-x64": "0.23.1", + "@esbuild/openbsd-arm64": "0.23.1", + "@esbuild/openbsd-x64": "0.23.1", + "@esbuild/sunos-x64": "0.23.1", + "@esbuild/win32-arm64": "0.23.1", + "@esbuild/win32-ia32": "0.23.1", + "@esbuild/win32-x64": "0.23.1" } }, "node_modules/escalade": { @@ -9535,6 +9569,479 @@ "url": "https://opencollective.com/vitest" } }, + "node_modules/vite/node_modules/@esbuild/aix-ppc64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.5.tgz", + "integrity": "sha512-9o3TMmpmftaCMepOdA5k/yDw8SfInyzWWTjYTFCX3kPSDJMROQTb8jg+h9Cnwnmm1vOzvxN7gIfB5V2ewpjtGA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/android-arm": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.5.tgz", + "integrity": "sha512-AdJKSPeEHgi7/ZhuIPtcQKr5RQdo6OO2IL87JkianiMYMPbCtot9fxPbrMiBADOWWm3T2si9stAiVsGbTQFkbA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/android-arm64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.5.tgz", + "integrity": "sha512-VGzGhj4lJO+TVGV1v8ntCZWJktV7SGCs3Pn1GRWI1SBFtRALoomm8k5E9Pmwg3HOAal2VDc2F9+PM/rEY6oIDg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/android-x64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.5.tgz", + "integrity": "sha512-D2GyJT1kjvO//drbRT3Hib9XPwQeWd9vZoBJn+bu/lVsOZ13cqNdDeqIF/xQ5/VmWvMduP6AmXvylO/PIc2isw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/darwin-arm64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.5.tgz", + "integrity": "sha512-GtaBgammVvdF7aPIgH2jxMDdivezgFu6iKpmT+48+F8Hhg5J/sfnDieg0aeG/jfSvkYQU2/pceFPDKlqZzwnfQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/darwin-x64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.5.tgz", + "integrity": "sha512-1iT4FVL0dJ76/q1wd7XDsXrSW+oLoquptvh4CLR4kITDtqi2e/xwXwdCVH8hVHU43wgJdsq7Gxuzcs6Iq/7bxQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.5.tgz", + "integrity": "sha512-nk4tGP3JThz4La38Uy/gzyXtpkPW8zSAmoUhK9xKKXdBCzKODMc2adkB2+8om9BDYugz+uGV7sLmpTYzvmz6Sw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/freebsd-x64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.5.tgz", + "integrity": "sha512-PrikaNjiXdR2laW6OIjlbeuCPrPaAl0IwPIaRv+SMV8CiM8i2LqVUHFC1+8eORgWyY7yhQY+2U2fA55mBzReaw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-arm": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.5.tgz", + "integrity": "sha512-cPzojwW2okgh7ZlRpcBEtsX7WBuqbLrNXqLU89GxWbNt6uIg78ET82qifUy3W6OVww6ZWobWub5oqZOVtwolfw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-arm64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.5.tgz", + "integrity": "sha512-Z9kfb1v6ZlGbWj8EJk9T6czVEjjq2ntSYLY2cw6pAZl4oKtfgQuS4HOq41M/BcoLPzrUbNd+R4BXFyH//nHxVg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-ia32": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.5.tgz", + "integrity": "sha512-sQ7l00M8bSv36GLV95BVAdhJ2QsIbCuCjh/uYrWiMQSUuV+LpXwIqhgJDcvMTj+VsQmqAHL2yYaasENvJ7CDKA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-loong64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.5.tgz", + "integrity": "sha512-0ur7ae16hDUC4OL5iEnDb0tZHDxYmuQyhKhsPBV8f99f6Z9KQM02g33f93rNH5A30agMS46u2HP6qTdEt6Q1kg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-mips64el": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.5.tgz", + "integrity": "sha512-kB/66P1OsHO5zLz0i6X0RxlQ+3cu0mkxS3TKFvkb5lin6uwZ/ttOkP3Z8lfR9mJOBk14ZwZ9182SIIWFGNmqmg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-ppc64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.5.tgz", + "integrity": "sha512-UZCmJ7r9X2fe2D6jBmkLBMQetXPXIsZjQJCjgwpVDz+YMcS6oFR27alkgGv3Oqkv07bxdvw7fyB71/olceJhkQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-riscv64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.5.tgz", + "integrity": "sha512-kTxwu4mLyeOlsVIFPfQo+fQJAV9mh24xL+y+Bm6ej067sYANjyEw1dNHmvoqxJUCMnkBdKpvOn0Ahql6+4VyeA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-s390x": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.5.tgz", + "integrity": "sha512-K2dSKTKfmdh78uJ3NcWFiqyRrimfdinS5ErLSn3vluHNeHVnBAFWC8a4X5N+7FgVE1EjXS1QDZbpqZBjfrqMTQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-x64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.5.tgz", + "integrity": "sha512-uhj8N2obKTE6pSZ+aMUbqq+1nXxNjZIIjCjGLfsWvVpy7gKCOL6rsY1MhRh9zLtUtAI7vpgLMK6DxjO8Qm9lJw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/netbsd-x64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.5.tgz", + "integrity": "sha512-WOb5fKrvVTRMfWFNCroYWWklbnXH0Q5rZppjq0vQIdlsQKuw6mdSihwSo4RV/YdQ5UCKKvBy7/0ZZYLBZKIbwQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.5.tgz", + "integrity": "sha512-7A208+uQKgTxHd0G0uqZO8UjK2R0DDb4fDmERtARjSHWxqMTye4Erz4zZafx7Di9Cv+lNHYuncAkiGFySoD+Mw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/openbsd-x64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.5.tgz", + "integrity": "sha512-G4hE405ErTWraiZ8UiSoesH8DaCsMm0Cay4fsFWOOUcz8b8rC6uCvnagr+gnioEjWn0wC+o1/TAHt+It+MpIMg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/sunos-x64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.5.tgz", + "integrity": "sha512-l+azKShMy7FxzY0Rj4RCt5VD/q8mG/e+mDivgspo+yL8zW7qEwctQ6YqKX34DTEleFAvCIUviCFX1SDZRSyMQA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-arm64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.5.tgz", + "integrity": "sha512-O2S7SNZzdcFG7eFKgvwUEZ2VG9D/sn/eIiz8XRZ1Q/DO5a3s76Xv0mdBzVM5j5R639lXQmPmSo0iRpHqUUrsxw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-ia32": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.5.tgz", + "integrity": "sha512-onOJ02pqs9h1iMJ1PQphR+VZv8qBMQ77Klcsqv9CNW2w6yLqoURLcgERAIurY6QE63bbLuqgP9ATqajFLK5AMQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-x64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.5.tgz", + "integrity": "sha512-TXv6YnJ8ZMVdX+SXWVBo/0p8LTcrUYngpWjvm91TMjjBQii7Oz11Lw5lbDV5Y0TzuhSJHwiH4hEtC1I42mMS0g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/esbuild": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.5.tgz", + "integrity": "sha512-P8OtKZRv/5J5hhz0cUAdu/cLuPIKXpQl1R9pZtvmHWQvrAUVd0UNIPT4IB4W3rNOqVO0rlqHmCIbSwxh/c9yUQ==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.5", + "@esbuild/android-arm": "0.25.5", + "@esbuild/android-arm64": "0.25.5", + "@esbuild/android-x64": "0.25.5", + "@esbuild/darwin-arm64": "0.25.5", + "@esbuild/darwin-x64": "0.25.5", + "@esbuild/freebsd-arm64": "0.25.5", + "@esbuild/freebsd-x64": "0.25.5", + "@esbuild/linux-arm": "0.25.5", + "@esbuild/linux-arm64": "0.25.5", + "@esbuild/linux-ia32": "0.25.5", + "@esbuild/linux-loong64": "0.25.5", + "@esbuild/linux-mips64el": "0.25.5", + "@esbuild/linux-ppc64": "0.25.5", + "@esbuild/linux-riscv64": "0.25.5", + "@esbuild/linux-s390x": "0.25.5", + "@esbuild/linux-x64": "0.25.5", + "@esbuild/netbsd-arm64": "0.25.5", + "@esbuild/netbsd-x64": "0.25.5", + "@esbuild/openbsd-arm64": "0.25.5", + "@esbuild/openbsd-x64": "0.25.5", + "@esbuild/sunos-x64": "0.25.5", + "@esbuild/win32-arm64": "0.25.5", + "@esbuild/win32-ia32": "0.25.5", + "@esbuild/win32-x64": "0.25.5" + } + }, "node_modules/vite/node_modules/fdir": { "version": "6.4.5", "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.5.tgz", @@ -10164,7 +10671,8 @@ "name": "@gemini-cli/cli", "version": "0.1.0", "dependencies": { - "@gemini-cli/core": "0.1.0", + "@gemini-cli/core": "file:../core", + "command-exists": "^1.2.9", "diff": "^7.0.0", "dotenv": "^16.4.7", "highlight.js": "^11.11.1", @@ -10190,6 +10698,7 @@ }, "devDependencies": { "@testing-library/react": "^14.0.0", + "@types/command-exists": "^1.2.3", "@types/diff": "^7.0.2", "@types/dotenv": "^6.1.1", "@types/node": "^20.11.24", diff --git a/package.json b/package.json index 1babc5d4..2bc3d71f 100644 --- a/package.json +++ b/package.json @@ -6,16 +6,16 @@ "packages/*" ], "scripts": { - "generate": "scripts/generate-git-commit-info.sh", - "build": "scripts/build.sh", - "build:sandbox": "scripts/build_sandbox.sh -s", + "generate": "node scripts/generate-git-commit-info.js", + "build": "node scripts/build.js", + "build:sandbox": "node scripts/build_sandbox.js", "build:all": "npm run build && npm run build:sandbox", - "clean": "scripts/clean.sh", + "clean": "node scripts/clean.js", "prepare": "npm run bundle", "test": "npm run test --workspaces", "test:ci": "npm run test:ci --workspaces --if-present", - "start": "NODE_ENV=development scripts/start.sh", - "debug": "NODE_ENV=development DEBUG=1 scripts/start.sh", + "start": "node scripts/start.js", + "debug": "cross-env DEBUG=1 node scripts/start.js", "lint:fix": "eslint . --fix", "lint": "eslint . --ext .ts,.tsx", "typecheck": "npm run typecheck --workspaces --if-present", @@ -25,14 +25,14 @@ "auth:docker": "gcloud auth configure-docker us-west1-docker.pkg.dev", "auth": "npm run auth:npm && npm run auth:docker", "prerelease:dev": "npm run prerelease:version --workspaces && npm run prerelease:deps --workspaces", - "bundle": "npm run generate && node esbuild.config.js && bash scripts/copy_bundle_assets.sh", + "bundle": "npm run generate && node esbuild.config.js && node scripts/copy_bundle_assets.js", "build:cli": "npm run build --workspace packages/cli", "build:core": "npm run build --workspace packages/core", "build:packages": "npm run build:core && npm run build:cli", - "build:docker": "scripts/build_sandbox.sh -s", + "build:docker": "node scripts/build_sandbox.js -s", "tag:docker": "docker tag gemini-cli-sandbox ${SANDBOX_IMAGE_REGISTRY:?SANDBOX_IMAGE_REGISTRY not set}/${SANDBOX_IMAGE_NAME:?SANDBOX_IMAGE_NAME not set}:$npm_package_version", "prepare:cli-packagejson": "node scripts/prepare-cli-packagejson.js", - "publish:sandbox": "scripts/publish-sandbox.sh", + "publish:sandbox": "node scripts/publish-sandbox.js", "publish:npm": "npm publish --workspaces ${NPM_PUBLISH_TAG:+--tag=$NPM_PUBLISH_TAG} ${NPM_DRY_RUN:+--dry-run}", "publish:release": "npm run build:packages && npm run prepare:cli-packagejson && npm run build:docker && npm run tag:docker && npm run publish:sandbox && npm run publish:npm" }, @@ -49,13 +49,15 @@ "@types/mime-types": "^2.1.4", "@types/minimatch": "^5.1.2", "@vitest/coverage-v8": "^3.1.1", - "esbuild": "^0.25.4", + "cross-env": "^7.0.3", + "esbuild": "^0.23.0", "eslint": "^9.24.0", "eslint-config-prettier": "^10.1.2", "eslint-plugin-import": "^2.31.0", "eslint-plugin-license-header": "^0.8.0", "eslint-plugin-react": "^7.37.5", "eslint-plugin-react-hooks": "^5.2.0", + "glob": "^10.4.2", "globals": "^16.0.0", "json": "^11.0.0", "lodash": "^4.17.21", diff --git a/packages/cli/package.json b/packages/cli/package.json index 0e50e7d5..03eca1c7 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -8,7 +8,7 @@ "gemini": "dist/index.js" }, "scripts": { - "build": "../../scripts/build_package.sh", + "build": "node ../../scripts/build_package.js", "clean": "rm -rf dist", "start": "node dist/index.js", "debug": "node --inspect-brk dist/index.js", @@ -29,7 +29,7 @@ "sandboxImageUri": "gemini-cli-sandbox" }, "dependencies": { - "@gemini-cli/core": "0.1.0", + "@gemini-cli/core": "file:../core", "diff": "^7.0.0", "dotenv": "^16.4.7", "highlight.js": "^11.11.1", @@ -48,6 +48,7 @@ "string-width": "^7.1.0", "strip-ansi": "^7.1.0", "strip-json-comments": "^3.1.1", + "command-exists": "^1.2.9", "yargs": "^17.7.2" }, "devDependencies": { @@ -58,6 +59,7 @@ "@types/react": "^18.3.1", "@types/shell-quote": "^1.7.5", "@types/yargs": "^17.0.32", + "@types/command-exists": "^1.2.3", "ink-testing-library": "^4.0.0", "jsdom": "^26.1.0", "typescript": "^5.3.3", diff --git a/packages/cli/src/ui/components/messages/ToolGroupMessage.tsx b/packages/cli/src/ui/components/messages/ToolGroupMessage.tsx index 46fcecff..b01e5f9b 100644 --- a/packages/cli/src/ui/components/messages/ToolGroupMessage.tsx +++ b/packages/cli/src/ui/components/messages/ToolGroupMessage.tsx @@ -58,7 +58,7 @@ export const ToolGroupMessage: React.FC = ({ {toolCalls.map((tool) => { const isConfirming = toolAwaitingApproval?.callId === tool.callId; return ( - + ({ vi.mock('os', () => ({ default: { tmpdir: vi.fn(() => '/tmp'), + platform: vi.fn(() => 'linux'), }, tmpdir: vi.fn(() => '/tmp'), + platform: vi.fn(() => 'linux'), })); // Configure the fs mock to use new vi.fn() instances created within the factory diff --git a/packages/cli/src/ui/hooks/shellCommandProcessor.ts b/packages/cli/src/ui/hooks/shellCommandProcessor.ts index 9d6ff03a..f7502f3f 100644 --- a/packages/cli/src/ui/hooks/shellCommandProcessor.ts +++ b/packages/cli/src/ui/hooks/shellCommandProcessor.ts @@ -43,13 +43,23 @@ export const useShellCommandProcessor = ( return false; } - // wrap command to write pwd to temporary file - let commandToExecute = rawQuery.trim(); - const pwdFileName = `shell_pwd_${crypto.randomBytes(6).toString('hex')}.tmp`; - const pwdFilePath = path.join(os.tmpdir(), pwdFileName); - if (!commandToExecute.endsWith('&')) commandToExecute += ';'; - // note here we could also restore a previous pwd with `cd {cwd}; { ... }` - commandToExecute = `{ ${commandToExecute} }; __code=$?; pwd >${pwdFilePath}; exit $__code`; + const isWindows = os.platform() === 'win32'; + let commandToExecute: string; + let pwdFilePath: string | undefined; + + if (isWindows) { + commandToExecute = rawQuery; + } else { + // wrap command to write pwd to temporary file + let command = rawQuery.trim(); + const pwdFileName = `shell_pwd_${crypto + .randomBytes(6) + .toString('hex')}.tmp`; + pwdFilePath = path.join(os.tmpdir(), pwdFileName); + if (!command.endsWith('&')) command += ';'; + // note here we could also restore a previous pwd with `cd {cwd}; { ... }` + commandToExecute = `{ ${command} }; __code=$?; pwd >${pwdFilePath}; exit $__code`; + } const userMessageTimestamp = Date.now(); addItemToHistory( @@ -101,7 +111,7 @@ export const useShellCommandProcessor = ( userMessageTimestamp, ); } - if (fs.existsSync(pwdFilePath)) { + if (pwdFilePath && fs.existsSync(pwdFilePath)) { const pwd = fs.readFileSync(pwdFilePath, 'utf8').trim(); if (pwd !== targetDir) { addItemToHistory( @@ -118,11 +128,16 @@ export const useShellCommandProcessor = ( }, ); } else { - const child = spawn('bash', ['-c', commandToExecute], { - cwd: targetDir, - stdio: ['ignore', 'pipe', 'pipe'], - detached: true, // Important for process group killing - }); + const child = isWindows + ? spawn('cmd.exe', ['/c', commandToExecute], { + cwd: targetDir, + stdio: ['ignore', 'pipe', 'pipe'], + }) + : spawn('bash', ['-c', commandToExecute], { + cwd: targetDir, + stdio: ['ignore', 'pipe', 'pipe'], + detached: true, // Important for process group killing + }); let exited = false; let output = ''; @@ -155,24 +170,29 @@ export const useShellCommandProcessor = ( onDebugMessage( `Aborting shell command (PID: ${child.pid}) due to signal.`, ); - try { - // attempt to SIGTERM process group (negative PID) - // fall back to SIGKILL (to group) after 200ms - process.kill(-child.pid, 'SIGTERM'); - await new Promise((resolve) => setTimeout(resolve, 200)); - if (child.pid && !exited) { - process.kill(-child.pid, 'SIGKILL'); - } - } catch (_e) { - // if group kill fails, fall back to killing just the main process + if (os.platform() === 'win32') { + // For Windows, use taskkill to kill the process tree + spawn('taskkill', ['/pid', child.pid.toString(), '/f', '/t']); + } else { try { - if (child.pid) { - child.kill('SIGKILL'); + // attempt to SIGTERM process group (negative PID) + // fall back to SIGKILL (to group) after 200ms + process.kill(-child.pid, 'SIGTERM'); + await new Promise((resolve) => setTimeout(resolve, 200)); + if (child.pid && !exited) { + process.kill(-child.pid, 'SIGKILL'); } } catch (_e) { - console.error( - `failed to kill shell process ${child.pid}: ${_e}`, - ); + // if group kill fails, fall back to killing just the main process + try { + if (child.pid) { + child.kill('SIGKILL'); + } + } catch (_e) { + console.error( + `failed to kill shell process ${child.pid}: ${_e}`, + ); + } } } } @@ -208,7 +228,7 @@ export const useShellCommandProcessor = ( userMessageTimestamp, ); } - if (fs.existsSync(pwdFilePath)) { + if (pwdFilePath && fs.existsSync(pwdFilePath)) { const pwd = fs.readFileSync(pwdFilePath, 'utf8').trim(); if (pwd !== targetDir) { addItemToHistory( diff --git a/packages/cli/src/ui/hooks/useLoadingIndicator.test.ts b/packages/cli/src/ui/hooks/useLoadingIndicator.test.ts index 92ae81a2..ec6732c7 100644 --- a/packages/cli/src/ui/hooks/useLoadingIndicator.test.ts +++ b/packages/cli/src/ui/hooks/useLoadingIndicator.test.ts @@ -5,7 +5,7 @@ */ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; -import { act, renderHook } from '@testing-library/react'; +import { renderHook, act } from '@testing-library/react'; import { useLoadingIndicator } from './useLoadingIndicator.js'; import { StreamingState } from '../types.js'; import { @@ -32,7 +32,7 @@ describe('useLoadingIndicator', () => { expect(result.current.currentLoadingPhrase).toBe(WITTY_LOADING_PHRASES[0]); }); - it('should reflect values when Responding', () => { + it('should reflect values when Responding', async () => { const { result } = renderHook(() => useLoadingIndicator(StreamingState.Responding), ); @@ -42,29 +42,33 @@ describe('useLoadingIndicator', () => { expect(WITTY_LOADING_PHRASES).toContain( result.current.currentLoadingPhrase, ); - const _initialPhrase = result.current.currentLoadingPhrase; + const initialPhrase = result.current.currentLoadingPhrase; - act(() => { - vi.advanceTimersByTime(PHRASE_CHANGE_INTERVAL_MS); + await act(async () => { + await vi.advanceTimersByTimeAsync(PHRASE_CHANGE_INTERVAL_MS); }); + // Phrase should cycle if PHRASE_CHANGE_INTERVAL_MS has passed + expect(result.current.currentLoadingPhrase).not.toBe(initialPhrase); expect(WITTY_LOADING_PHRASES).toContain( result.current.currentLoadingPhrase, ); }); - it('should show waiting phrase and retain elapsedTime when WaitingForConfirmation', () => { + it('should show waiting phrase and retain elapsedTime when WaitingForConfirmation', async () => { const { result, rerender } = renderHook( ({ streamingState }) => useLoadingIndicator(streamingState), { initialProps: { streamingState: StreamingState.Responding } }, ); - act(() => { - vi.advanceTimersByTime(60000); + await act(async () => { + await vi.advanceTimersByTimeAsync(60000); }); expect(result.current.elapsedTime).toBe(60); - rerender({ streamingState: StreamingState.WaitingForConfirmation }); + act(() => { + rerender({ streamingState: StreamingState.WaitingForConfirmation }); + }); expect(result.current.currentLoadingPhrase).toBe( 'Waiting for user confirmation...', @@ -72,60 +76,66 @@ describe('useLoadingIndicator', () => { expect(result.current.elapsedTime).toBe(60); // Elapsed time should be retained // Timer should not advance further - act(() => { - vi.advanceTimersByTime(2000); + await act(async () => { + await vi.advanceTimersByTimeAsync(2000); }); expect(result.current.elapsedTime).toBe(60); }); - it('should reset elapsedTime and use a witty phrase when transitioning from WaitingForConfirmation to Responding', () => { + it('should reset elapsedTime and use a witty phrase when transitioning from WaitingForConfirmation to Responding', async () => { const { result, rerender } = renderHook( ({ streamingState }) => useLoadingIndicator(streamingState), { initialProps: { streamingState: StreamingState.Responding } }, ); - act(() => { - vi.advanceTimersByTime(5000); // 5s + await act(async () => { + await vi.advanceTimersByTimeAsync(5000); // 5s }); expect(result.current.elapsedTime).toBe(5); - rerender({ streamingState: StreamingState.WaitingForConfirmation }); + act(() => { + rerender({ streamingState: StreamingState.WaitingForConfirmation }); + }); expect(result.current.elapsedTime).toBe(5); expect(result.current.currentLoadingPhrase).toBe( 'Waiting for user confirmation...', ); - rerender({ streamingState: StreamingState.Responding }); + act(() => { + rerender({ streamingState: StreamingState.Responding }); + }); expect(result.current.elapsedTime).toBe(0); // Should reset expect(WITTY_LOADING_PHRASES).toContain( result.current.currentLoadingPhrase, ); - act(() => { - vi.advanceTimersByTime(1000); + await act(async () => { + await vi.advanceTimersByTimeAsync(1000); }); expect(result.current.elapsedTime).toBe(1); }); - it('should reset timer and phrase when streamingState changes from Responding to Idle', () => { + it('should reset timer and phrase when streamingState changes from Responding to Idle', async () => { const { result, rerender } = renderHook( ({ streamingState }) => useLoadingIndicator(streamingState), { initialProps: { streamingState: StreamingState.Responding } }, ); - act(() => { - vi.advanceTimersByTime(10000); // 10s + await act(async () => { + await vi.advanceTimersByTimeAsync(10000); // 10s }); expect(result.current.elapsedTime).toBe(10); - rerender({ streamingState: StreamingState.Idle }); + act(() => { + rerender({ streamingState: StreamingState.Idle }); + }); expect(result.current.elapsedTime).toBe(0); expect(result.current.currentLoadingPhrase).toBe(WITTY_LOADING_PHRASES[0]); // Timer should not advance - act(() => { - vi.advanceTimersByTime(2000); + await act(async () => { + await vi.advanceTimersByTimeAsync(2000); }); expect(result.current.elapsedTime).toBe(0); }); diff --git a/packages/cli/src/utils/sandbox.ts b/packages/cli/src/utils/sandbox.ts index cc51a327..c6cee188 100644 --- a/packages/cli/src/utils/sandbox.ts +++ b/packages/cli/src/utils/sandbox.ts @@ -11,11 +11,24 @@ import fs from 'node:fs'; import { readFile } from 'node:fs/promises'; import { quote } from 'shell-quote'; import { readPackageUp } from 'read-package-up'; +import commandExists from 'command-exists'; import { USER_SETTINGS_DIR, SETTINGS_DIRECTORY_NAME, } from '../config/settings.js'; +function getContainerPath(hostPath: string): string { + if (os.platform() !== 'win32') { + return hostPath; + } + const withForwardSlashes = hostPath.replace(/\\/g, '/'); + const match = withForwardSlashes.match(/^([A-Z]):\/(.*)/i); + if (match) { + return `/${match[1].toLowerCase()}/${match[2]}`; + } + return hostPath; +} + const LOCAL_DEV_SANDBOX_IMAGE_NAME = 'gemini-cli-sandbox'; /** @@ -98,9 +111,9 @@ export function sandbox_command(sandbox?: string | boolean): string { if (sandbox === true) { // look for docker or podman, in that order - if (execSync('command -v docker || true').toString().trim()) { + if (commandExists.sync('docker')) { return 'docker'; // Set sandbox to 'docker' if found - } else if (execSync('command -v podman || true').toString().trim()) { + } else if (commandExists.sync('podman')) { return 'podman'; // Set sandbox to 'podman' if found } else { console.error( @@ -111,7 +124,7 @@ export function sandbox_command(sandbox?: string | boolean): string { } } else if (sandbox) { // confirm that specfied command exists - if (execSync(`command -v ${sandbox} || true`).toString().trim()) { + if (commandExists.sync(sandbox)) { return sandbox; } else { console.error( @@ -124,7 +137,7 @@ export function sandbox_command(sandbox?: string | boolean): string { // unless SEATBELT_PROFILE is set to 'none', which we allow as an escape hatch if ( os.platform() === 'darwin' && - execSync('command -v sandbox-exec || true').toString().trim() && + commandExists.sync('sandbox-exec') && process.env.SEATBELT_PROFILE !== 'none' ) { return 'sandbox-exec'; @@ -150,71 +163,68 @@ function ports(): string[] { } function entrypoint(workdir: string): string[] { - // set up bash command to be run inside container - // start with setting up PATH and PYTHONPATH with optional suffixes from host - const bashCmds = []; + const isWindows = os.platform() === 'win32'; + const containerWorkdir = getContainerPath(workdir); + const shellCmds = []; + const pathSeparator = isWindows ? ';' : ':'; - // copy any paths in PATH that are under working directory in sandbox - // note we can't just pass these as --env since that would override base PATH - // instead we construct a suffix and append as part of bashCmd below let pathSuffix = ''; if (process.env.PATH) { - const paths = process.env.PATH.split(':'); - for (const path of paths) { - if (path.startsWith(workdir)) { - pathSuffix += `:${path}`; + const paths = process.env.PATH.split(pathSeparator); + for (const p of paths) { + const containerPath = getContainerPath(p); + if ( + containerPath.toLowerCase().startsWith(containerWorkdir.toLowerCase()) + ) { + pathSuffix += `:${containerPath}`; } } } if (pathSuffix) { - bashCmds.push(`export PATH="$PATH${pathSuffix}";`); // suffix includes leading ':' + shellCmds.push(`export PATH="$PATH${pathSuffix}";`); } - // copy any paths in PYTHONPATH that are under working directory in sandbox - // note we can't just pass these as --env since that would override base PYTHONPATH - // instead we construct a suffix and append as part of bashCmd below let pythonPathSuffix = ''; if (process.env.PYTHONPATH) { - const paths = process.env.PYTHONPATH.split(':'); - for (const path of paths) { - if (path.startsWith(workdir)) { - pythonPathSuffix += `:${path}`; + const paths = process.env.PYTHONPATH.split(pathSeparator); + for (const p of paths) { + const containerPath = getContainerPath(p); + if ( + containerPath.toLowerCase().startsWith(containerWorkdir.toLowerCase()) + ) { + pythonPathSuffix += `:${containerPath}`; } } } if (pythonPathSuffix) { - bashCmds.push(`export PYTHONPATH="$PYTHONPATH${pythonPathSuffix}";`); // suffix includes leading ':' + shellCmds.push(`export PYTHONPATH="$PYTHONPATH${pythonPathSuffix}";`); } - // source sandbox.bashrc if exists under project settings directory const projectSandboxBashrc = path.join( SETTINGS_DIRECTORY_NAME, 'sandbox.bashrc', ); if (fs.existsSync(projectSandboxBashrc)) { - bashCmds.push(`source ${projectSandboxBashrc};`); + shellCmds.push(`source ${getContainerPath(projectSandboxBashrc)};`); } - // also set up redirects (via socat) so servers can listen on localhost instead of 0.0.0.0 ports().forEach((p) => - bashCmds.push( + shellCmds.push( `socat TCP4-LISTEN:${p},bind=$(hostname -i),fork,reuseaddr TCP4:127.0.0.1:${p} 2> /dev/null &`, ), ); - // append remaining args (bash -c "gemini cli_args...") - // cli_args need to be quoted before being inserted into bash_cmd const cliArgs = process.argv.slice(2).map((arg) => quote([arg])); const cliCmd = process.env.NODE_ENV === 'development' ? process.env.DEBUG ? 'npm run debug --' : 'npm rebuild && npm run start --' - : process.env.DEBUG // for production binary debugging + : process.env.DEBUG ? `node --inspect-brk=0.0.0.0:${process.env.DEBUG_PORT || '9229'} $(which gemini)` : 'gemini'; - const args = [...bashCmds, cliCmd, ...cliArgs]; + const args = [...shellCmds, cliCmd, ...cliArgs]; return ['bash', '-c', args.join(' ')]; } @@ -259,7 +269,7 @@ export async function start_sandbox(sandbox: string) { `CACHE_DIR=${fs.realpathSync(execSync(`getconf DARWIN_USER_CACHE_DIR`).toString().trim())}`, '-f', profileFile, - 'bash', + 'sh', '-c', [ `SANDBOX=sandbox-exec`, @@ -274,7 +284,7 @@ export async function start_sandbox(sandbox: string) { console.error(`hopping into sandbox (command: ${sandbox}) ...`); // determine full path for gemini-cli to distinguish linked vs installed setting - const gcPath = execSync(`realpath $(which gemini)`).toString().trim(); + const gcPath = fs.realpathSync(process.argv[1]); const projectSandboxDockerfile = path.join( SETTINGS_DIRECTORY_NAME, @@ -283,7 +293,8 @@ export async function start_sandbox(sandbox: string) { const isCustomProjectSandbox = fs.existsSync(projectSandboxDockerfile); const image = await getSandboxImageName(isCustomProjectSandbox); - const workdir = process.cwd(); + const workdir = path.resolve(process.cwd()); + const containerWorkdir = getContainerPath(workdir); // if BUILD_SANDBOX is set, then call scripts/build_sandbox.sh under gemini-cli repo // @@ -332,7 +343,7 @@ export async function start_sandbox(sandbox: string) { // use interactive mode and auto-remove container on exit // run init binary inside container to forward signals & reap zombies - const args = ['run', '-i', '--rm', '--init', '--workdir', workdir]; + const args = ['run', '-i', '--rm', '--init', '--workdir', containerWorkdir]; // add TTY only if stdin is TTY as well, i.e. for piped input don't init TTY in container if (process.stdin.isTTY) { @@ -340,25 +351,25 @@ export async function start_sandbox(sandbox: string) { } // mount current directory as working directory in sandbox (set via --workdir) - args.push('--volume', `${process.cwd()}:${workdir}`); + args.push('--volume', `${workdir}:${containerWorkdir}`); // mount user settings directory inside container, after creating if missing // note user/home changes inside sandbox and we mount at BOTH paths for consistency const userSettingsDirOnHost = USER_SETTINGS_DIR; - const userSettingsDirInSandbox = `/home/node/${SETTINGS_DIRECTORY_NAME}`; + const userSettingsDirInSandbox = getContainerPath(userSettingsDirOnHost); if (!fs.existsSync(userSettingsDirOnHost)) { fs.mkdirSync(userSettingsDirOnHost); } - args.push('--volume', `${userSettingsDirOnHost}:${userSettingsDirOnHost}`); + args.push('--volume', `${userSettingsDirOnHost}:${userSettingsDirInSandbox}`); if (userSettingsDirInSandbox !== userSettingsDirOnHost) { args.push( '--volume', - `${userSettingsDirOnHost}:${userSettingsDirInSandbox}`, + `${userSettingsDirOnHost}:${getContainerPath(userSettingsDirOnHost)}`, ); } - // mount os.tmpdir() as /tmp inside container - args.push('--volume', `${os.tmpdir()}:/tmp`); + // mount os.tmpdir() as os.tmpdir() inside container + args.push('--volume', `${os.tmpdir()}:${getContainerPath(os.tmpdir())}`); // mount paths listed in SANDBOX_MOUNTS if (process.env.SANDBOX_MOUNTS) { @@ -401,13 +412,10 @@ export async function start_sandbox(sandbox: string) { // name container after image, plus numeric suffix to avoid conflicts const imageName = parseImageName(image); let index = 0; - while ( - execSync( - `${sandbox} ps -a --format "{{.Names}}" | grep "${imageName}-${index}" || true`, - ) - .toString() - .trim() - ) { + const containerNameCheck = execSync(`${sandbox} ps -a --format "{{.Names}}"`) + .toString() + .trim(); + while (containerNameCheck.includes(`${imageName}-${index}`)) { index++; } const containerName = `${imageName}-${index}`; @@ -435,7 +443,9 @@ export async function start_sandbox(sandbox: string) { // also mount-replace VIRTUAL_ENV directory with /sandbox.venv // sandbox can then set up this new VIRTUAL_ENV directory using sandbox.bashrc (see below) // directory will be empty if not set up, which is still preferable to having host binaries - if (process.env.VIRTUAL_ENV?.startsWith(workdir)) { + if ( + process.env.VIRTUAL_ENV?.toLowerCase().startsWith(workdir.toLowerCase()) + ) { const sandboxVenvPath = path.resolve( SETTINGS_DIRECTORY_NAME, 'sandbox.venv', @@ -443,8 +453,14 @@ export async function start_sandbox(sandbox: string) { if (!fs.existsSync(sandboxVenvPath)) { fs.mkdirSync(sandboxVenvPath, { recursive: true }); } - args.push('--volume', `${sandboxVenvPath}:${process.env.VIRTUAL_ENV}`); - args.push('--env', `VIRTUAL_ENV=${process.env.VIRTUAL_ENV}`); + args.push( + '--volume', + `${sandboxVenvPath}:${getContainerPath(process.env.VIRTUAL_ENV)}`, + ); + args.push( + '--env', + `VIRTUAL_ENV=${getContainerPath(process.env.VIRTUAL_ENV)}`, + ); } // copy additional environment variables from SANDBOX_ENV @@ -498,13 +514,24 @@ export async function start_sandbox(sandbox: string) { // spawn child and let it inherit stdio const child = spawn(sandbox, args, { stdio: 'inherit', - detached: true, + detached: os.platform() !== 'win32', + }); + + child.on('error', (err) => { + console.error('Sandbox process error:', err); }); // uncomment this line (and comment the await on following line) to let parent exit // child.unref(); - await new Promise((resolve) => { - child.on('close', resolve); + await new Promise((resolve) => { + child.on('close', (code, signal) => { + if (code !== 0) { + console.log( + `Sandbox process exited with code: ${code}, signal: ${signal}`, + ); + } + resolve(); + }); }); } diff --git a/packages/core/package.json b/packages/core/package.json index a750b1ab..344f3a5a 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -6,7 +6,7 @@ "main": "dist/index.js", "scripts": { "start": "node dist/src/index.js", - "build": "../../scripts/build_package.sh", + "build": "node ../../scripts/build_package.js", "clean": "rm -rf dist", "lint": "eslint . --ext .ts,.tsx", "format": "prettier --write .", diff --git a/packages/core/src/core/prompts.ts b/packages/core/src/core/prompts.ts index ca04fd38..084ab1f3 100644 --- a/packages/core/src/core/prompts.ts +++ b/packages/core/src/core/prompts.ts @@ -4,6 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 */ +import os from 'node:os'; import path from 'node:path'; import fs from 'node:fs'; import { LSTool } from '../tools/ls.js'; @@ -131,8 +132,15 @@ You are running outside of a sandbox container, directly on the user's system. F ${(function () { // note git repo can change so we need to check every time system prompt is generated - const gitRootCmd = 'git rev-parse --show-toplevel 2>/dev/null || true'; - const gitRoot = execSync(gitRootCmd)?.toString()?.trim(); + const isWindows = os.platform() === 'win32'; + const devNull = isWindows ? 'NUL' : '/dev/null'; + const gitRootCmd = `git rev-parse --show-toplevel 2>${devNull}`; + let gitRoot = ''; + try { + gitRoot = execSync(gitRootCmd)?.toString()?.trim(); + } catch (_e) { + // ignore + } if (gitRoot) { return ` # Git Repository diff --git a/packages/core/src/tools/edit.ts b/packages/core/src/tools/edit.ts index 3240fa30..fdabc5b6 100644 --- a/packages/core/src/tools/edit.ts +++ b/packages/core/src/tools/edit.ts @@ -206,6 +206,8 @@ Expectation for required parameters: try { currentContent = fs.readFileSync(params.file_path, 'utf8'); + // Normalize line endings to LF for consistent processing. + currentContent = currentContent.replace(/\r\n/g, '\n'); fileExists = true; } catch (err: unknown) { if (!isNodeError(err) || err.code !== 'ENOENT') { @@ -303,6 +305,8 @@ Expectation for required parameters: try { currentContent = fs.readFileSync(params.file_path, 'utf8'); + // Normalize line endings to LF for consistent processing. + currentContent = currentContent.replace(/\r\n/g, '\n'); fileExists = true; } catch (err: unknown) { if (isNodeError(err) && err.code === 'ENOENT') { diff --git a/packages/core/src/tools/shell.ts b/packages/core/src/tools/shell.ts index e1cde43b..4bae498b 100644 --- a/packages/core/src/tools/shell.ts +++ b/packages/core/src/tools/shell.ts @@ -169,20 +169,35 @@ export class ShellTool extends BaseTool { }; } - // wrap command to append subprocess pids (via pgrep) to temporary file - const tempFileName = `shell_pgrep_${crypto.randomBytes(6).toString('hex')}.tmp`; - const tempFilePath = path.join(os.tmpdir(), tempFileName); + const isWindows = os.platform() === 'win32'; - let command = params.command.trim(); - if (!command.endsWith('&')) command += ';'; - command = `{ ${command} }; __code=$?; pgrep -g 0 >${tempFilePath} 2>&1; exit $__code;`; + // pgrep is not available on Windows, so we can't get background PIDs + const command = isWindows + ? params.command + : (() => { + // wrap command to append subprocess pids (via pgrep) to temporary file + const tempFileName = `shell_pgrep_${crypto + .randomBytes(6) + .toString('hex')}.tmp`; + const tempFilePath = path.join(os.tmpdir(), tempFileName); + + let command = params.command.trim(); + if (!command.endsWith('&')) command += ';'; + return `{ ${command} }; __code=$?; pgrep -g 0 >${tempFilePath} 2>&1; exit $__code;`; + })(); // spawn command in specified directory (or project root if not specified) - const shell = spawn('bash', ['-c', command], { - stdio: ['ignore', 'pipe', 'pipe'], - detached: true, // ensure subprocess starts its own process group (esp. in Linux) - cwd: path.resolve(this.config.getTargetDir(), params.directory || ''), - }); + const shell = isWindows + ? spawn('cmd.exe', ['/c', command], { + stdio: ['ignore', 'pipe', 'pipe'], + // detached: true, // ensure subprocess starts its own process group (esp. in Linux) + cwd: path.resolve(this.config.getTargetDir(), params.directory || ''), + }) + : spawn('bash', ['-c', command], { + stdio: ['ignore', 'pipe', 'pipe'], + detached: true, // ensure subprocess starts its own process group (esp. in Linux) + cwd: path.resolve(this.config.getTargetDir(), params.directory || ''), + }); let exited = false; let stdout = ''; @@ -241,22 +256,27 @@ export class ShellTool extends BaseTool { const abortHandler = async () => { if (shell.pid && !exited) { - try { - // attempt to SIGTERM process group (negative PID) - // fall back to SIGKILL (to group) after 200ms - process.kill(-shell.pid, 'SIGTERM'); - await new Promise((resolve) => setTimeout(resolve, 200)); - if (shell.pid && !exited) { - process.kill(-shell.pid, 'SIGKILL'); - } - } catch (_e) { - // if group kill fails, fall back to killing just the main process + if (os.platform() === 'win32') { + // For Windows, use taskkill to kill the process tree + spawn('taskkill', ['/pid', shell.pid.toString(), '/f', '/t']); + } else { try { - if (shell.pid) { - shell.kill('SIGKILL'); + // attempt to SIGTERM process group (negative PID) + // fall back to SIGKILL (to group) after 200ms + process.kill(-shell.pid, 'SIGTERM'); + await new Promise((resolve) => setTimeout(resolve, 200)); + if (shell.pid && !exited) { + process.kill(-shell.pid, 'SIGKILL'); } } catch (_e) { - console.error(`failed to kill shell process ${shell.pid}: ${_e}`); + // if group kill fails, fall back to killing just the main process + try { + if (shell.pid) { + shell.kill('SIGKILL'); + } + } catch (_e) { + console.error(`failed to kill shell process ${shell.pid}: ${_e}`); + } } } } @@ -272,26 +292,32 @@ export class ShellTool extends BaseTool { // parse pids (pgrep output) from temporary file and remove it const backgroundPIDs: number[] = []; - if (fs.existsSync(tempFilePath)) { - const pgrepLines = fs - .readFileSync(tempFilePath, 'utf8') - .split('\n') - .filter(Boolean); - for (const line of pgrepLines) { - if (!/^\d+$/.test(line)) { - console.error(`pgrep: ${line}`); + if (os.platform() !== 'win32') { + const tempFileName = `shell_pgrep_${crypto + .randomBytes(6) + .toString('hex')}.tmp`; + const tempFilePath = path.join(os.tmpdir(), tempFileName); + if (fs.existsSync(tempFilePath)) { + const pgrepLines = fs + .readFileSync(tempFilePath, 'utf8') + .split('\n') + .filter(Boolean); + for (const line of pgrepLines) { + if (!/^\d+$/.test(line)) { + console.error(`pgrep: ${line}`); + } + const pid = Number(line); + // exclude the shell subprocess pid + if (pid !== shell.pid) { + backgroundPIDs.push(pid); + } } - const pid = Number(line); - // exclude the shell subprocess pid - if (pid !== shell.pid) { - backgroundPIDs.push(pid); + fs.unlinkSync(tempFilePath); + } else { + if (!abortSignal.aborted) { + console.error('missing pgrep output'); } } - fs.unlinkSync(tempFilePath); - } else { - if (!abortSignal.aborted) { - console.error('missing pgrep output'); - } } let llmContent = ''; diff --git a/packages/core/src/utils/editor.test.ts b/packages/core/src/utils/editor.test.ts index 74237c74..20917c0f 100644 --- a/packages/core/src/utils/editor.test.ts +++ b/packages/core/src/utils/editor.test.ts @@ -21,7 +21,11 @@ describe('checkHasEditor', () => { it('should return true for vscode if "code" command exists', () => { (execSync as Mock).mockReturnValue(Buffer.from('/usr/bin/code')); expect(checkHasEditor('vscode')).toBe(true); - expect(execSync).toHaveBeenCalledWith('which code', { stdio: 'ignore' }); + const expectedCommand = + process.platform === 'win32' ? 'where.exe code.cmd' : 'command -v code'; + expect(execSync).toHaveBeenCalledWith(expectedCommand, { + stdio: 'ignore', + }); }); it('should return false for vscode if "code" command does not exist', () => { @@ -34,7 +38,11 @@ describe('checkHasEditor', () => { it('should return true for vim if "vim" command exists', () => { (execSync as Mock).mockReturnValue(Buffer.from('/usr/bin/vim')); expect(checkHasEditor('vim')).toBe(true); - expect(execSync).toHaveBeenCalledWith('which vim', { stdio: 'ignore' }); + const expectedCommand = + process.platform === 'win32' ? 'where.exe vim' : 'command -v vim'; + expect(execSync).toHaveBeenCalledWith(expectedCommand, { + stdio: 'ignore', + }); }); it('should return false for vim if "vim" command does not exist', () => { diff --git a/packages/core/src/utils/editor.ts b/packages/core/src/utils/editor.ts index 447aa0d2..6be5cffb 100644 --- a/packages/core/src/utils/editor.ts +++ b/packages/core/src/utils/editor.ts @@ -15,7 +15,10 @@ interface DiffCommand { function commandExists(cmd: string): boolean { try { - execSync(`which ${cmd}`, { stdio: 'ignore' }); + execSync( + process.platform === 'win32' ? `where.exe ${cmd}` : `command -v ${cmd}`, + { stdio: 'ignore' }, + ); return true; } catch { return false; @@ -24,7 +27,9 @@ function commandExists(cmd: string): boolean { export function checkHasEditor(editor: EditorType): boolean { if (editor === 'vscode') { - return commandExists('code'); + return process.platform === 'win32' + ? commandExists('code.cmd') + : commandExists('code'); } else if (editor === 'vim') { return commandExists('vim'); } @@ -116,7 +121,10 @@ export async function openDiff( }); } else { // Use execSync for terminal-based editors like vim - const command = `${diffCommand.command} ${diffCommand.args.map((arg) => `"${arg}"`).join(' ')}`; + const command = + process.platform === 'win32' + ? `${diffCommand.command} ${diffCommand.args.join(' ')}` + : `${diffCommand.command} ${diffCommand.args.map((arg) => `"${arg}"`).join(' ')}`; execSync(command, { stdio: 'inherit', encoding: 'utf8', diff --git a/scripts/build.js b/scripts/build.js new file mode 100644 index 00000000..373e268e --- /dev/null +++ b/scripts/build.js @@ -0,0 +1,54 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { execSync } from 'child_process'; +import { existsSync } from 'fs'; +import { join } from 'path'; + +const root = join(import.meta.dirname, '..'); + +// npm install if node_modules was removed (e.g. via npm run clean or scripts/clean.js) +if (!existsSync(join(root, 'node_modules'))) { + execSync('npm install', { stdio: 'inherit', cwd: root }); +} + +// build all workspaces/packages +execSync('npm run generate', { stdio: 'inherit', cwd: root }); +execSync('npm run build --workspaces', { stdio: 'inherit', cwd: root }); + +// also build container image if sandboxing is enabled +// skip (-s) npm install + build since we did that above +try { + execSync('node scripts/sandbox_command.js -q', { + stdio: 'inherit', + cwd: root, + }); + if ( + process.env.BUILD_SANDBOX === '1' || + process.env.BUILD_SANDBOX === 'true' + ) { + execSync('node scripts/build_sandbox.js -s', { + stdio: 'inherit', + cwd: root, + }); + } +} catch { + // ignore +} diff --git a/scripts/build.sh b/scripts/build.sh deleted file mode 100755 index 0eba264f..00000000 --- a/scripts/build.sh +++ /dev/null @@ -1,30 +0,0 @@ -#!/bin/bash -# Copyright 2025 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -set -euo pipefail - -# npm install if node_modules was removed (e.g. via npm run clean or scripts/clean.sh) -if [ ! -d "node_modules" ]; then - npm install -fi - -# build all workspaces/packages -npm run build --workspaces - -# also build container image if sandboxing is enabled -# skip (-s) npm install + build since we did that above -if scripts/sandbox_command.sh -q && [[ "${BUILD_SANDBOX:-}" =~ ^(1|true)$ ]]; then - scripts/build_sandbox.sh -s -fi diff --git a/scripts/build_package.js b/scripts/build_package.js new file mode 100644 index 00000000..9e1b05bf --- /dev/null +++ b/scripts/build_package.js @@ -0,0 +1,37 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { execSync } from 'child_process'; +import { writeFileSync } from 'fs'; +import { join } from 'path'; + +if (!process.cwd().includes('packages')) { + console.error('must be invoked from a package directory'); + process.exit(1); +} + +// build typescript files +execSync('tsc --build', { stdio: 'inherit' }); + +// copy .{md,json} files +execSync('node ../../scripts/copy_files.js', { stdio: 'inherit' }); + +// touch dist/.last_build +writeFileSync(join(process.cwd(), 'dist', '.last_build'), ''); +process.exit(0); diff --git a/scripts/build_package.sh b/scripts/build_package.sh deleted file mode 100755 index e64d98cc..00000000 --- a/scripts/build_package.sh +++ /dev/null @@ -1,33 +0,0 @@ -#!/bin/bash -# Copyright 2025 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -set -euo pipefail - -if [[ $(pwd) != *"/packages/"* ]]; then - echo "must be invoked from a package directory" - exit 1 -fi - -# clean dist directory -# rm -rf dist/* - -# build typescript files -tsc --build - -# copy .{md,json} files -node ../../scripts/copy_files.js - -# touch dist/.last_build -touch dist/.last_build diff --git a/scripts/build_sandbox.js b/scripts/build_sandbox.js new file mode 100644 index 00000000..bfcf1bf9 --- /dev/null +++ b/scripts/build_sandbox.js @@ -0,0 +1,125 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { execSync } from 'child_process'; +import { chmodSync, readFileSync, rmSync } from 'fs'; +import { join } from 'path'; +import yargs from 'yargs'; +import { hideBin } from 'yargs/helpers'; + +const argv = yargs(hideBin(process.argv)) + .option('s', { + alias: 'skip-npm-install-build', + type: 'boolean', + default: false, + description: 'skip npm install + npm run build', + }) + .option('f', { + alias: 'dockerfile', + type: 'string', + description: 'use for custom image', + }) + .option('i', { + alias: 'image', + type: 'string', + description: 'use name for custom image', + }).argv; + +let sandboxCommand; +try { + sandboxCommand = execSync('node scripts/sandbox_command.js') + .toString() + .trim(); +} catch { + console.warn( + 'WARNING: container-based sandboxing is disabled (see README.md#sandboxing)', + ); + process.exit(0); +} + +if (sandboxCommand === 'sandbox-exec') { + console.warn( + 'WARNING: container-based sandboxing is disabled (see README.md#sandboxing)', + ); + process.exit(0); +} + +console.log(`using ${sandboxCommand} for sandboxing`); + +const baseImage = 'gemini-cli-sandbox'; +const customImage = argv.i; +const baseDockerfile = 'Dockerfile'; +const customDockerfile = argv.f; + +if (!argv.s) { + execSync('npm install', { stdio: 'inherit' }); + execSync('npm run build --workspaces', { stdio: 'inherit' }); +} + +console.log('packing @gemini-cli/cli ...'); +const cliPackageDir = join('packages', 'cli'); +rmSync(join(cliPackageDir, 'dist', 'gemini-cli-cli-*.tgz'), { force: true }); +execSync(`npm pack -w @gemini-cli/cli --pack-destination ./packages/cli/dist`, { + stdio: 'ignore', +}); + +console.log('packing @gemini-cli/core ...'); +const corePackageDir = join('packages', 'core'); +rmSync(join(corePackageDir, 'dist', 'gemini-cli-core-*.tgz'), { force: true }); +execSync( + `npm pack -w @gemini-cli/core --pack-destination ./packages/core/dist`, + { stdio: 'ignore' }, +); + +const packageVersion = JSON.parse( + readFileSync(join(process.cwd(), 'package.json'), 'utf-8'), +).version; + +chmodSync( + join(cliPackageDir, 'dist', `gemini-cli-cli-${packageVersion}.tgz`), + 0o755, +); +chmodSync( + join(corePackageDir, 'dist', `gemini-cli-core-${packageVersion}.tgz`), + 0o755, +); + +const buildStdout = process.env.VERBOSE ? 'inherit' : 'ignore'; + +function buildImage(imageName, dockerfile) { + console.log(`building ${imageName} ... (can be slow first time)`); + const buildCommand = + sandboxCommand === 'podman' + ? `${sandboxCommand} build --authfile=<(echo '{}')` + : `${sandboxCommand} --config=".docker" buildx build`; + + execSync( + `${buildCommand} ${process.env.BUILD_SANDBOX_FLAGS || ''} -f "${dockerfile}" -t "${imageName}" .`, + { stdio: buildStdout }, + ); + console.log(`built ${imageName}`); +} + +buildImage(baseImage, baseDockerfile); + +if (customDockerfile && customImage) { + buildImage(customImage, customDockerfile); +} + +execSync(`${sandboxCommand} image prune -f`, { stdio: 'ignore' }); diff --git a/scripts/build_sandbox.sh b/scripts/build_sandbox.sh deleted file mode 100755 index dabcffcf..00000000 --- a/scripts/build_sandbox.sh +++ /dev/null @@ -1,102 +0,0 @@ -#!/bin/bash -# Copyright 2025 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -set -euo pipefail - -# exit with warning if container-based sandboxing is disabled -# note this includes the case where sandbox-exec (seatbelt) is used -# this happens most commonly when user runs `npm run build:all` without enabling sandboxing -if ! scripts/sandbox_command.sh -q || [ "$(scripts/sandbox_command.sh)" == "sandbox-exec" ]; then - echo "WARNING: container-based sandboxing is disabled (see CONTRIBUTING.md#enabling-sandboxing)" - exit 0 -fi - -CMD=$(scripts/sandbox_command.sh) -echo "using $CMD for sandboxing" - -BASE_IMAGE=gemini-cli-sandbox -CUSTOM_IMAGE='' -BASE_DOCKERFILE=Dockerfile -CUSTOM_DOCKERFILE='' - -SKIP_NPM_INSTALL_BUILD=false -while getopts "sf:i:" opt; do - case ${opt} in - s) SKIP_NPM_INSTALL_BUILD=true ;; - f) - CUSTOM_DOCKERFILE=$OPTARG - ;; - i) - CUSTOM_IMAGE=$OPTARG - ;; - \?) - echo "usage: $(basename "$0") [-s] [-f ]" - echo " -s: skip npm install + npm run build" - echo " -f : use for custom image" - echo " -i : use name for custom image" - exit 1 - ;; - esac -done -shift $((OPTIND - 1)) - -# npm install + npm run build unless skipping via -s option -if [ "$SKIP_NPM_INSTALL_BUILD" = false ]; then - npm install - npm run build --workspaces -fi - -# prepare global installation files for prod builds -# pack cli -echo "packing @gemini-cli/cli ..." -rm -f packages/cli/dist/gemini-cli-cli-*.tgz -npm pack -w @gemini-cli/cli --pack-destination ./packages/cli/dist &>/dev/null -# pack core -echo "packing @gemini-cli/core ..." -rm -f packages/core/dist/gemini-cli-core-*.tgz -npm pack -w @gemini-cli/core --pack-destination ./packages/core/dist &>/dev/null -# give node user (used during installation, see Dockerfile) access to these files -chmod 755 packages/*/dist/gemini-cli-*.tgz - -# redirect build output to /dev/null unless VERBOSE is set -BUILD_STDOUT="/dev/null" -if [ -n "${VERBOSE:-}" ]; then - BUILD_STDOUT="/dev/stdout" -fi - -build_image() { - if [[ "$CMD" == "podman" ]]; then - # use empty --authfile to skip unnecessary auth refresh overhead - $CMD build --authfile=<(echo '{}') "$@" >$BUILD_STDOUT - elif [[ "$CMD" == "docker" ]]; then - $CMD --config=".docker" buildx build "$@" >$BUILD_STDOUT - else - $CMD build "$@" >$BUILD_STDOUT - fi -} - -echo "building $BASE_IMAGE ... (can be slow first time)" -# shellcheck disable=SC2086 # allow globbing and word splitting for BUILD_SANDBOX_FLAGS -build_image ${BUILD_SANDBOX_FLAGS:-} -f "$BASE_DOCKERFILE" -t "$BASE_IMAGE" . -echo "built $BASE_IMAGE" - -if [[ -n "$CUSTOM_DOCKERFILE" && -n "$CUSTOM_IMAGE" ]]; then - echo "building $CUSTOM_IMAGE ... (can be slow first time)" - # shellcheck disable=SC2086 # allow globbing and word splitting for BUILD_SANDBOX_FLAGS - build_image ${BUILD_SANDBOX_FLAGS:-} -f "$CUSTOM_DOCKERFILE" -t "$CUSTOM_IMAGE" . - echo "built $CUSTOM_IMAGE" -fi - -$CMD image prune -f >/dev/null diff --git a/scripts/clean.js b/scripts/clean.js new file mode 100644 index 00000000..dd2911e9 --- /dev/null +++ b/scripts/clean.js @@ -0,0 +1,32 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { execSync } from 'child_process'; +import { rmSync } from 'fs'; +import { join } from 'path'; + +const root = join(import.meta.dirname, '..'); + +// remove npm install/build artifacts +rmSync(join(root, 'node_modules'), { recursive: true, force: true }); +rmSync(join(root, 'packages/cli/src/generated/'), { + recursive: true, + force: true, +}); +execSync('npm run clean --workspaces', { stdio: 'inherit', cwd: root }); diff --git a/scripts/clean.sh b/scripts/clean.sh deleted file mode 100755 index 2f6bc3c0..00000000 --- a/scripts/clean.sh +++ /dev/null @@ -1,21 +0,0 @@ -#!/bin/bash -# Copyright 2025 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -set -euo pipefail - -# remove npm install/build artifacts -rm -rf node_modules -rm -rf packages/cli/src/generated/ -npm run clean --workspaces diff --git a/scripts/copy_bundle_assets.js b/scripts/copy_bundle_assets.js new file mode 100644 index 00000000..cb1ac197 --- /dev/null +++ b/scripts/copy_bundle_assets.js @@ -0,0 +1,48 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { copyFileSync, existsSync, mkdirSync } from 'fs'; +import { join, basename } from 'path'; +import { glob } from 'glob'; + +const root = join(import.meta.dirname, '..'); +const bundleDir = join(root, 'bundle'); + +// Create the bundle directory if it doesn't exist +if (!existsSync(bundleDir)) { + mkdirSync(bundleDir); +} + +// Copy specific shell files to the root of the bundle directory +copyFileSync( + join(root, 'packages/core/src/tools/shell.md'), + join(bundleDir, 'shell.md'), +); +copyFileSync( + join(root, 'packages/core/src/tools/shell.json'), + join(bundleDir, 'shell.json'), +); + +// Find and copy all .sb files from packages to the root of the bundle directory +const sbFiles = glob.sync('packages/**/*.sb', { cwd: root }); +for (const file of sbFiles) { + copyFileSync(join(root, file), join(bundleDir, basename(file))); +} + +console.log('Assets copied to bundle/'); diff --git a/scripts/copy_bundle_assets.sh b/scripts/copy_bundle_assets.sh deleted file mode 100755 index 93d46364..00000000 --- a/scripts/copy_bundle_assets.sh +++ /dev/null @@ -1,13 +0,0 @@ -#!/bin/bash - -# Create the bundle directory if it doesn't exist -mkdir -p bundle - -# Copy specific shell files to the root of the bundle directory -cp "packages/core/src/tools/shell.md" "bundle/shell.md" -cp "packages/core/src/tools/shell.json" "bundle/shell.json" - -# Find and copy all .sb files from packages to the root of the bundle directory -find packages -name '*.sb' -exec cp -f {} bundle/ \; - -echo "Assets copied to bundle/" \ No newline at end of file diff --git a/scripts/generate-git-commit-info.js b/scripts/generate-git-commit-info.js new file mode 100644 index 00000000..f046a7fb --- /dev/null +++ b/scripts/generate-git-commit-info.js @@ -0,0 +1,61 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { execSync } from 'child_process'; +import { existsSync, mkdirSync, writeFileSync } from 'fs'; +import { join } from 'path'; + +const root = join(import.meta.dirname, '..'); +const generatedDir = join(root, 'packages/cli/src/generated'); +const gitCommitFile = join(generatedDir, 'git-commit.ts'); +let gitCommitInfo = 'N/A'; + +if (!existsSync(generatedDir)) { + mkdirSync(generatedDir, { recursive: true }); +} + +try { + const gitHash = execSync('git rev-parse --short HEAD', { + encoding: 'utf-8', + }).trim(); + if (gitHash) { + gitCommitInfo = gitHash; + const gitStatus = execSync('git status --porcelain', { + encoding: 'utf-8', + }).trim(); + if (gitStatus) { + gitCommitInfo = `${gitHash} (local modifications)`; + } + } +} catch { + // ignore +} + +const fileContent = `/** + * @license + * Copyright ${new Date().getFullYear()} Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +// This file is auto-generated by the build script (scripts/build.js) +// Do not edit this file manually. +export const GIT_COMMIT_INFO = '${gitCommitInfo}'; +`; + +writeFileSync(gitCommitFile, fileContent); diff --git a/scripts/generate-git-commit-info.sh b/scripts/generate-git-commit-info.sh deleted file mode 100755 index 2a64830f..00000000 --- a/scripts/generate-git-commit-info.sh +++ /dev/null @@ -1,44 +0,0 @@ -#!/bin/bash -# Copyright 2025 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -set -euo pipefail - -GENERATED_DIR="packages/cli/src/generated" -GIT_COMMIT_FILE="$GENERATED_DIR/git-commit.ts" -GIT_COMMIT_INFO="N/A" - -mkdir -p "$GENERATED_DIR" - -if command -v git &> /dev/null && git rev-parse --is-inside-work-tree &> /dev/null; then - GIT_HASH=$(git rev-parse --short HEAD 2>/dev/null || echo "") - if [ -n "$GIT_HASH" ]; then - GIT_COMMIT_INFO="$GIT_HASH" - if [ -n "$(git status --porcelain 2>/dev/null)" ]; then - GIT_COMMIT_INFO="$GIT_HASH (local modifications)" - fi - fi -fi - -cat < "$GIT_COMMIT_FILE" -/** - * @license - * Copyright $(date +%Y) Google LLC - * SPDX-License-Identifier: Apache-2.0 - */ - -// This file is auto-generated by the build script (scripts/build.sh) -// Do not edit this file manually. -export const GIT_COMMIT_INFO = '$GIT_COMMIT_INFO'; -EOL diff --git a/scripts/publish-sandbox.js b/scripts/publish-sandbox.js new file mode 100644 index 00000000..916089be --- /dev/null +++ b/scripts/publish-sandbox.js @@ -0,0 +1,55 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { execSync } from 'child_process'; + +const { + SANDBOX_IMAGE_REGISTRY, + SANDBOX_IMAGE_NAME, + npm_package_version, + DOCKER_DRY_RUN, +} = process.env; + +if (!SANDBOX_IMAGE_REGISTRY) { + console.error( + 'Error: SANDBOX_IMAGE_REGISTRY environment variable is not set.', + ); + process.exit(1); +} + +if (!SANDBOX_IMAGE_NAME) { + console.error('Error: SANDBOX_IMAGE_NAME environment variable is not set.'); + process.exit(1); +} + +if (!npm_package_version) { + console.error( + 'Error: npm_package_version environment variable is not set (should be run via npm).', + ); + process.exit(1); +} + +const imageUri = `${SANDBOX_IMAGE_REGISTRY}/${SANDBOX_IMAGE_NAME}:${npm_package_version}`; + +if (DOCKER_DRY_RUN) { + console.log(`DRY RUN: Would execute: docker push "${imageUri}"`); +} else { + console.log(`Executing: docker push "${imageUri}"`); + execSync(`docker push "${imageUri}"`, { stdio: 'inherit' }); +} diff --git a/scripts/publish-sandbox.sh b/scripts/publish-sandbox.sh deleted file mode 100755 index dfc16353..00000000 --- a/scripts/publish-sandbox.sh +++ /dev/null @@ -1,41 +0,0 @@ -#!/bin/bash -# Copyright 2025 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -set -euo pipefail - -# Ensure required environment variables are set -if [ -z "${SANDBOX_IMAGE_REGISTRY}" ]; then - echo "Error: SANDBOX_IMAGE_REGISTRY environment variable is not set." >&2 - exit 1 -fi - -if [ -z "${SANDBOX_IMAGE_NAME}" ]; then - echo "Error: SANDBOX_IMAGE_NAME environment variable is not set." >&2 - exit 1 -fi - -if [ -z "${npm_package_version}" ]; then - echo "Error: npm_package_version environment variable is not set (should be run via npm)." >&2 - exit 1 -fi - -IMAGE_URI="${SANDBOX_IMAGE_REGISTRY}/${SANDBOX_IMAGE_NAME}:${npm_package_version}" - -if [ -n "${DOCKER_DRY_RUN:-}" ]; then - echo "DRY RUN: Would execute: docker push \"${IMAGE_URI}\"" -else - echo "Executing: docker push \"${IMAGE_URI}\"" - docker push "${IMAGE_URI}" -fi diff --git a/scripts/sandbox.js b/scripts/sandbox.js new file mode 100644 index 00000000..58223180 --- /dev/null +++ b/scripts/sandbox.js @@ -0,0 +1,123 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { execSync, spawn } from 'child_process'; +import yargs from 'yargs'; +import { hideBin } from 'yargs/helpers'; + +try { + execSync('node scripts/sandbox_command.js -q'); +} catch { + console.error('ERROR: sandboxing disabled. See docs to enable sandboxing.'); + process.exit(1); +} + +const argv = yargs(hideBin(process.argv)).option('i', { + alias: 'interactive', + type: 'boolean', + default: false, +}).argv; + +if (argv.i && !process.stdin.isTTY) { + console.error( + 'ERROR: interactive mode (-i) requested without a terminal attached', + ); + process.exit(1); +} + +const image = 'gemini-cli-sandbox'; +const sandboxCommand = execSync('node scripts/sandbox_command.js') + .toString() + .trim(); + +const sandboxes = execSync( + `${sandboxCommand} ps --filter "ancestor=${image}" --format "{{.Names}}"`, +) + .toString() + .trim() + .split('\n') + .filter(Boolean); + +let sandboxName; +const firstArg = argv._[0]; + +if (firstArg) { + if (firstArg.startsWith(image) || /^\d+$/.test(firstArg)) { + sandboxName = firstArg.startsWith(image) + ? firstArg + : `${image}-${firstArg}`; + argv._.shift(); + } +} + +if (!sandboxName) { + if (sandboxes.length === 0) { + console.error( + 'No sandboxes found. Are you running gemini-cli with sandboxing enabled?', + ); + process.exit(1); + } + if (sandboxes.length > 1) { + console.error('Multiple sandboxes found:'); + sandboxes.forEach((s) => console.error(` ${s}`)); + console.error( + 'Sandbox name or index (0,1,...) must be specified as first argument', + ); + process.exit(1); + } + sandboxName = sandboxes[0]; +} + +if (!sandboxes.includes(sandboxName)) { + console.error(`unknown sandbox ${sandboxName}`); + console.error('known sandboxes:'); + sandboxes.forEach((s) => console.error(` ${s}`)); + process.exit(1); +} + +const execArgs = []; +let commandToRun = []; + +// Determine interactive flags. +// If a command is provided, only be interactive if -i is passed. +// If no command is provided, always be interactive. +if (argv._.length > 0) { + if (argv.i) { + execArgs.push('-it'); + } +} else { + execArgs.push('-it'); +} + +// Determine the command to run inside the container. +if (argv._.length > 0) { + // Join all positional arguments into a single command string. + const userCommand = argv._.join(' '); + // The container is Linux, so we use bash -l -c to execute the command string. + // This is cross-platform because it's what the container runs, not the host. + commandToRun = ['bash', '-l', '-c', userCommand]; +} else { + // No command provided, so we start an interactive bash login shell. + commandToRun = ['bash', '-l']; +} + +const spawnArgs = ['exec', ...execArgs, sandboxName, ...commandToRun]; + +// Use spawn to avoid shell injection issues and handle arguments correctly. +spawn(sandboxCommand, spawnArgs, { stdio: 'inherit' }); diff --git a/scripts/sandbox.sh b/scripts/sandbox.sh deleted file mode 100755 index b6a52c14..00000000 --- a/scripts/sandbox.sh +++ /dev/null @@ -1,103 +0,0 @@ -#!/bin/bash -# Copyright 2025 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -set -euo pipefail - -if ! scripts/sandbox_command.sh -q; then - echo "ERROR: sandboxing disabled. See docs to enable sandboxing." - exit 1 -fi - -# parse flags -interactive=false -while getopts "i" opt; do - case "$opt" in - \?) - echo "usage: sandbox.sh [-i] [sandbox-name-or-index = AUTO] [command... = bash -l]" - echo " -i: enable interactive mode for custom command (enabled by default for login shell)" - echo " (WARNING: interactive mode causes stderr to be redirected to stdout)" - exit 1 - ;; - i) - interactive=true - if [ ! -t 0 ]; then - echo "ERROR: interactive mode (-i) requested without a terminal attached" - exit 1 - fi - ;; - esac -done -shift $((OPTIND - 1)) - -IMAGE=gemini-cli-sandbox -CMD=$(scripts/sandbox_command.sh) - -# list all containers running on sandbox image -sandboxes=() -while IFS= read -r line; do - sandboxes+=("$line") -done < <($CMD ps --filter "ancestor=$IMAGE" --format "{{.Names}}") - -# take first argument as sandbox name if it starts with image name or is an integer -# otherwise require a unique sandbox to be running and take its name -if [[ "${1:-}" =~ ^$IMAGE(-[0-9]+)?$ ]]; then - SANDBOX=$1 - shift -elif [[ "${1:-}" =~ ^[0-9]+$ ]]; then - SANDBOX=$IMAGE-$1 - shift -else - # exit if no sandbox is running - if [ ${#sandboxes[@]} -eq 0 ]; then - echo "No sandboxes found. Are you running gemini-cli with sandboxing enabled?" - exit 1 - fi - # exit if multiple sandboxes are running - if [ ${#sandboxes[@]} -gt 1 ]; then - echo "Multiple sandboxes found:" - for sandbox in "${sandboxes[@]}"; do - echo " $sandbox" - done - echo "Sandbox name or index (0,1,...) must be specified as first argument" - exit 1 - fi - SANDBOX=${sandboxes[0]} -fi - -# check that sandbox exists -if ! [[ " ${sandboxes[*]} " == *" $SANDBOX "* ]]; then - echo "unknown sandbox $SANDBOX" - echo "known sandboxes:" - for sandbox in "${sandboxes[@]}"; do - echo " $sandbox" - done - exit 1 -fi - -# determine command and args for exec -if [ $# -gt 0 ]; then - cmd=(bash -l -c "$(printf '%q ' "$@")") # fixes quoting, e.g. bash -c 'echo $SANDBOX' - exec_args=() - if [ "$interactive" = true ]; then - exec_args=(-it) - fi -else - cmd=(bash -l) - exec_args=(-it) -fi - -# run command in sandbox -exec_args+=("$SANDBOX" "${cmd[@]}") -$CMD exec "${exec_args[@]}" diff --git a/scripts/sandbox_command.js b/scripts/sandbox_command.js new file mode 100644 index 00000000..7f8e8381 --- /dev/null +++ b/scripts/sandbox_command.js @@ -0,0 +1,126 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { execSync } from 'child_process'; +import { existsSync, readFileSync } from 'fs'; +import { join, dirname } from 'path'; +import os from 'os'; +import yargs from 'yargs'; +import { hideBin } from 'yargs/helpers'; +import dotenv from 'dotenv'; + +const argv = yargs(hideBin(process.argv)).option('q', { + alias: 'quiet', + type: 'boolean', + default: false, +}).argv; + +let geminiSandbox = process.env.GEMINI_SANDBOX; + +if (!geminiSandbox) { + const userSettingsFile = join(os.homedir(), '.gemini', 'settings.json'); + if (existsSync(userSettingsFile)) { + const settings = JSON.parse(readFileSync(userSettingsFile, 'utf-8')); + if (settings.sandbox) { + geminiSandbox = settings.sandbox; + } + } +} + +if (!geminiSandbox) { + let currentDir = process.cwd(); + while (currentDir !== '/') { + const geminiEnv = join(currentDir, '.gemini', '.env'); + const regularEnv = join(currentDir, '.env'); + if (existsSync(geminiEnv)) { + dotenv.config({ path: geminiEnv }); + break; + } else if (existsSync(regularEnv)) { + dotenv.config({ path: regularEnv }); + break; + } + currentDir = dirname(currentDir); + } + geminiSandbox = process.env.GEMINI_SANDBOX; +} + +if (process.env.GEMINI_CODE_SANDBOX) { + console.warn( + 'WARNING: GEMINI_CODE_SANDBOX is deprecated. Use GEMINI_SANDBOX instead.', + ); + geminiSandbox = process.env.GEMINI_CODE_SANDBOX; +} + +geminiSandbox = (geminiSandbox || '').toLowerCase(); + +const commandExists = (cmd) => { + const checkCommand = os.platform() === 'win32' ? 'where' : 'command -v'; + try { + execSync(`${checkCommand} ${cmd}`, { stdio: 'ignore' }); + return true; + } catch { + if (os.platform() === 'win32') { + try { + execSync(`${checkCommand} ${cmd}.exe`, { stdio: 'ignore' }); + return true; + } catch { + return false; + } + } + return false; + } +}; + +let command = ''; +if (['1', 'true'].includes(geminiSandbox)) { + if (commandExists('docker')) { + command = 'docker'; + } else if (commandExists('podman')) { + command = 'podman'; + } else { + console.error( + 'ERROR: install docker or podman or specify command in GEMINI_SANDBOX', + ); + process.exit(1); + } +} else if (geminiSandbox && !['0', 'false'].includes(geminiSandbox)) { + if (commandExists(geminiSandbox)) { + command = geminiSandbox; + } else { + console.error( + `ERROR: missing sandbox command '${geminiSandbox}' (from GEMINI_SANDBOX)`, + ); + process.exit(1); + } +} else { + if (os.platform() === 'darwin' && process.env.SEATBELT_PROFILE !== 'none') { + if (commandExists('sandbox-exec')) { + command = 'sandbox-exec'; + } else { + process.exit(1); + } + } else { + process.exit(1); + } +} + +if (!argv.q) { + console.log(command); +} +process.exit(0); diff --git a/scripts/sandbox_command.sh b/scripts/sandbox_command.sh deleted file mode 100755 index 468a4834..00000000 --- a/scripts/sandbox_command.sh +++ /dev/null @@ -1,122 +0,0 @@ -#!/bin/bash -# Copyright 2025 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# usage: scripts/sandbox_command.sh [-q] -# -q: quiet mode (do not print command, just exit w/ code 0 or 1) - -set -euo pipefail - -# parse flags -QUIET=false -while getopts ":q" opt; do - case ${opt} in - q) QUIET=true ;; - \?) - echo "Usage: $0 [-q]" - exit 1 - ;; - esac -done -shift $((OPTIND - 1)) - -# if GEMINI_SANDBOX is not set, see if it is set in user settings -# note it can be string or boolean, and if missing `npx json` will return empty string -USER_SETTINGS_FILE="$HOME/.gemini/settings.json" -if [ -z "${GEMINI_SANDBOX:-}" ] && [ -f "$USER_SETTINGS_FILE" ]; then - # Check if jq is available (more reliable than npx json) - if command -v jq &>/dev/null; then - USER_SANDBOX_SETTING=$(jq -r '.sandbox // empty' "$USER_SETTINGS_FILE" 2>/dev/null || echo "") - else - # Fallback to npx json with error handling - USER_SANDBOX_SETTING=$(sed -e 's/\/\/.*//' -e 's/\/\*.*\*\///g' -e '/^[[:space:]]*\/\//d' "$USER_SETTINGS_FILE" | npx json 'sandbox' 2>/dev/null || echo "") - fi - - # Avoid setting GEMINI_SANDBOX to complex objects - if [ -n "$USER_SANDBOX_SETTING" ] && [[ ! "$USER_SANDBOX_SETTING" =~ ^\{.*\}$ ]]; then - GEMINI_SANDBOX=$USER_SANDBOX_SETTING - fi -fi - -# if GEMINI_SANDBOX is not set, try to source .env in case set there -# allow .env to be in any ancestor directory (same as findEnvFile in config.ts) -# prefer gemini-specific .env under .gemini folder (also same as in findEnvFile) -if [ -z "${GEMINI_SANDBOX:-}" ]; then - current_dir=$(pwd) - dot_env_sourced=false - while [ "$current_dir" != "/" ]; do - if [ -f "$current_dir/.gemini/.env" ]; then - source "$current_dir/.gemini/.env" - dot_env_sourced=true - break - elif [ -f "$current_dir/.env" ]; then - source "$current_dir/.env" - dot_env_sourced=true - break - fi - current_dir=$(dirname "$current_dir") - done - # if .env is not found in any ancestor directory, try home as fallback - if [ "$dot_env_sourced" = false ]; then - if [ -f "$HOME/.gemini/.env" ]; then - source "$HOME/.gemini/.env" - dot_env_sourced=true - elif [ -f "$HOME/.env" ]; then - source "$HOME/.env" - dot_env_sourced=true - fi - fi -fi - -# copy and warn about deprecated GEMINI_CODE_SANDBOX -if [ -n "${GEMINI_CODE_SANDBOX:-}" ]; then - echo "WARNING: GEMINI_CODE_SANDBOX is deprecated. Use GEMINI_SANDBOX instead." >&2 - GEMINI_SANDBOX=$GEMINI_CODE_SANDBOX - export GEMINI_SANDBOX -fi - -# lowercase GEMINI_SANDBOX -GEMINI_SANDBOX=$(echo "${GEMINI_SANDBOX:-}" | tr '[:upper:]' '[:lower:]') - -# if GEMINI_SANDBOX is set to 1|true, then try to use docker or podman -# if non-empty and not 0|false, treat as custom command and check that it exists -# if empty or 0|false, then fail silently (after checking for possible fallbacks) -command="" -if [[ "${GEMINI_SANDBOX:-}" =~ ^(1|true)$ ]]; then - if command -v docker &>/dev/null; then - command="docker" - elif command -v podman &>/dev/null; then - command="podman" - else - echo "ERROR: install docker or podman or specify command in GEMINI_SANDBOX" >&2 - exit 1 - fi -elif [ -n "${GEMINI_SANDBOX:-}" ] && [[ ! "${GEMINI_SANDBOX:-}" =~ ^(0|false)$ ]]; then - if ! command -v "$GEMINI_SANDBOX" &>/dev/null; then - echo "ERROR: missing sandbox command '$GEMINI_SANDBOX' (from GEMINI_SANDBOX)" >&2 - exit 1 - fi - command="$GEMINI_SANDBOX" -else - # if we are on macOS and sandbox-exec is available, use that for minimal sandboxing - # unless SEATBELT_PROFILE is set to 'none', which we allow as an escape hatch - if [ "$(uname)" = "Darwin" ] && command -v sandbox-exec &>/dev/null && [ "${SEATBELT_PROFILE:-}" != "none" ]; then - command="sandbox-exec" - else # GEMINI_SANDBOX is empty or 0|false, so we fail w/o error msg - exit 1 - fi -fi - -if [ "$QUIET" = false ]; then echo "$command"; fi -exit 0 diff --git a/scripts/setup-dev.js b/scripts/setup-dev.js new file mode 100644 index 00000000..c4e2b22c --- /dev/null +++ b/scripts/setup-dev.js @@ -0,0 +1,42 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { execSync } from 'child_process'; + +try { + execSync('command -v npm', { stdio: 'ignore' }); +} catch { + console.log('npm not found. Installing npm via nvm...'); + try { + execSync( + 'curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.3/install.sh | bash', + { stdio: 'inherit' }, + ); + const nvmsh = `\\. "$HOME/.nvm/nvm.sh"`; + execSync(`${nvmsh} && nvm install 22`, { stdio: 'inherit' }); + execSync(`${nvmsh} && node -v`, { stdio: 'inherit' }); + execSync(`${nvmsh} && nvm current`, { stdio: 'inherit' }); + execSync(`${nvmsh} && npm -v`, { stdio: 'inherit' }); + } catch { + console.error('Failed to install nvm or node.'); + process.exit(1); + } +} + +console.log('Development environment setup complete.'); diff --git a/scripts/setup-dev.sh b/scripts/setup-dev.sh deleted file mode 100755 index de2ae336..00000000 --- a/scripts/setup-dev.sh +++ /dev/null @@ -1,34 +0,0 @@ -#!/bin/bash -# Copyright 2025 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -set -euo pipefail - -# Check if npm is installed -if ! command -v npm &>/dev/null; then - echo "npm not found. Installing npm via nvm..." - # Download and install nvm: - curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.3/install.sh | bash - # in lieu of restarting the shell - \. "$HOME/.nvm/nvm.sh" - # Download and install Node.js: - nvm install 22 - # Verify the Node.js version: - node -v # Should print "v22.15.0". - nvm current # Should print "v22.15.0". - # Verify npm version: - npm -v # Should print "10.9.2". -fi - -echo "Development environment setup complete." diff --git a/scripts/start.js b/scripts/start.js new file mode 100644 index 00000000..f9f85c8e --- /dev/null +++ b/scripts/start.js @@ -0,0 +1,61 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law_or_agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { spawn, execSync } from 'child_process'; +import { join } from 'path'; + +const root = join(import.meta.dirname, '..'); + +// check build status, write warnings to file for app to display if needed +execSync('node ./scripts/check-build-status.js', { + stdio: 'inherit', + cwd: root, +}); + +// if debugging is enabled and sandboxing is disabled, use --inspect-brk flag +// note with sandboxing this flag is passed to the binary inside the sandbox +// inside sandbox SANDBOX should be set and sandbox_command.js should fail +const nodeArgs = []; +try { + execSync('node scripts/sandbox_command.js -q', { + stdio: 'inherit', + cwd: root, + }); + if (process.env.DEBUG) { + if (process.env.SANDBOX) { + const port = process.env.DEBUG_PORT || '9229'; + nodeArgs.push(`--inspect-brk=0.0.0.0:${port}`); + } else { + nodeArgs.push('--inspect-brk'); + } + } +} catch { + // ignore +} + +nodeArgs.push('./packages/cli'); +nodeArgs.push(...process.argv.slice(2)); + +const env = { + ...process.env, + CLI_VERSION: 'development', + DEV: 'true', +}; + +spawn('node', nodeArgs, { stdio: 'inherit', env }); diff --git a/scripts/start.sh b/scripts/start.sh deleted file mode 100755 index 2caf540d..00000000 --- a/scripts/start.sh +++ /dev/null @@ -1,37 +0,0 @@ -#!/bin/bash -# Copyright 2025 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -set -euo pipefail - -# check build status, write warnings to file for app to display if needed -node ./scripts/check-build-status.js - -# if debugging is enabled and sandboxing is disabled, use --inspect-brk flag -# note with sandboxing this flag is passed to the binary inside the sandbox -# inside sandbox SANDBOX should be set and sandbox_command.sh should fail -node_args=() -if [ -n "${DEBUG:-}" ] && ! scripts/sandbox_command.sh -q; then - if [ -n "${SANDBOX:-}" ]; then - port="${DEBUG_PORT:-9229}" - node_args=("--inspect-brk=0.0.0.0:$port") - else - node_args=(--inspect-brk) - fi -fi -node_args+=("./packages/cli" "$@") - -# DEV=true to enable React Dev Tools (https://github.com/vadimdemedes/ink?tab=readme-ov-file#using-react-devtools) -# CLI_VERSION to display in the app ui footer -CLI_VERSION='development' DEV=true node "${node_args[@]}" diff --git a/tmperrors.txt b/tmperrors.txt new file mode 100644 index 00000000..f0e3b068 --- /dev/null +++ b/tmperrors.txt @@ -0,0 +1,55 @@ +PS C:\Users\mattk> npx -ddd https://github.com/google-gemini/gemini-cli#windows +npm verbose cli C:\nvm4w\nodejs\node.exe C:\Users\mattk\AppData\Roaming\npm\node_modules\npm\bin\npm-cli.js +npm info using npm@11.4.1 +npm info using node@v20.19.2 +npm silly config load:file:C:\Users\mattk\AppData\Roaming\npm\node_modules\npm\npmrc +npm silly config load:file:C:\Users\mattk\.npmrc +npm silly config load:file:C:\Users\mattk\AppData\Roaming\npm\etc\npmrc +npm verbose title npm exec https://github.com/google-gemini/gemini-cli#windows +npm verbose argv "exec" "--loglevel" "silly" "--" "https://github.com/google-gemini/gemini-cli#windows" +npm verbose logfile logs-max:10 dir:C:\Users\mattk\AppData\Local\npm-cache\_logs\2025-06-06T04_53_50_750Z- +npm verbose logfile C:\Users\mattk\AppData\Local\npm-cache\_logs\2025-06-06T04_53_50_750Z-debug-0.log +npm silly logfile start cleaning logs, removing 1 files +npm silly logfile done cleaning log files +npm silly packumentCache heap:4345298944 maxSize:1086324736 maxEntrySize:543162368 +npm silly packumentCache heap:4345298944 maxSize:1086324736 maxEntrySize:543162368 +npm silly packumentCache heap:4345298944 maxSize:1086324736 maxEntrySize:543162368 +Need to install the following packages: +github:google-gemini/gemini-cli#windows +Ok to proceed? (y) y + +npm silly idealTree buildDeps +npm silly fetch manifest gemini-cli@github:google-gemini/gemini-cli#windows +npm silly placeDep ROOT gemini-cli@0.1.0 OK for: want: github:google-gemini/gemini-cli#windows +npm silly fetch manifest dotenv@^16.5.0 +npm silly packumentCache full:https://registry.npmjs.org/dotenv cache-miss +npm http cache https://registry.npmjs.org/dotenv 25ms (cache hit) +npm silly packumentCache full:https://registry.npmjs.org/dotenv set size:238345 disposed:false +npm silly placeDep ROOT dotenv@16.5.0 OK for: gemini-cli@0.1.0 want: ^16.5.0 +npm silly reify moves {} +npm silly audit bulk request { 'gemini-cli': [ '0.1.0' ], dotenv: [ '16.5.0' ] } +npm http cache dotenv@https://registry.npmjs.org/dotenv/-/dotenv-16.5.0.tgz 0ms (cache hit) +npm http fetch POST 200 https://registry.npmjs.org/-/npm/v1/security/advisories/bulk 226ms +npm silly audit report {} +npm http fetch GET 404 https://codeload.github.com/google-gemini/gemini-cli/tar.gz/76909ce82dc4a0eab0cd79daecda01e3b0726368 202ms (cache skip) +npm warn cleanup Failed to remove some directories [ +npm warn cleanup [ +npm warn cleanup 'C:\\Users\\mattk\\AppData\\Local\\npm-cache\\_npx\\43478f5c7b4786be\\node_modules\\dotenv', +npm warn cleanup [Error: EPERM: operation not permitted, rmdir 'C:\Users\mattk\AppData\Local\npm-cache\_npx\43478f5c7b4786be\node_modules\dotenv\lib'] { +npm warn cleanup errno: -4048, +npm warn cleanup code: 'EPERM', +npm warn cleanup syscall: 'rmdir', +npm warn cleanup path: 'C:\\Users\\mattk\\AppData\\Local\\npm-cache\\_npx\\43478f5c7b4786be\\node_modules\\dotenv\\lib' +npm warn cleanup } +npm warn cleanup ] +npm warn cleanup ] +npm silly unfinished npm timer reify 1749185639289 +npm silly unfinished npm timer reify:unpack 1749185646392 +npm silly unfinished npm timer reifyNode:node_modules/gemini-cli 1749185646392 +npm verbose cwd C:\Users\mattk +npm verbose os Windows_NT 10.0.26100 +npm verbose node v20.19.2 +npm verbose npm v11.4.1 +npm verbose exit 1 +npm verbose code 1 +PS C:\Users\mattk>