mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-03-10 14:10:37 -07:00
438 lines
12 KiB
JavaScript
438 lines
12 KiB
JavaScript
#!/usr/bin/env node
|
|
|
|
/**
|
|
* @license
|
|
* Copyright 2025 Google LLC
|
|
* SPDX-License-Identifier: Apache-2.0
|
|
*/
|
|
|
|
import { execSync } from 'node:child_process';
|
|
import {
|
|
mkdirSync,
|
|
rmSync,
|
|
readFileSync,
|
|
existsSync,
|
|
lstatSync,
|
|
} from 'node:fs';
|
|
import { tmpdir } from 'node:os';
|
|
import { join } from 'node:path';
|
|
|
|
const ACTIONLINT_VERSION = '1.7.7';
|
|
const SHELLCHECK_VERSION = '0.11.0';
|
|
const YAMLLINT_VERSION = '1.35.1';
|
|
|
|
const TEMP_DIR =
|
|
process.env.GEMINI_LINT_TEMP_DIR || join(tmpdir(), 'gemini-cli-linters');
|
|
|
|
function getPlatformArch() {
|
|
const platform = process.platform;
|
|
const arch = process.arch;
|
|
if (platform === 'linux' && arch === 'x64') {
|
|
return {
|
|
actionlint: 'linux_amd64',
|
|
shellcheck: 'linux.x86_64',
|
|
};
|
|
}
|
|
if (platform === 'darwin' && arch === 'x64') {
|
|
return {
|
|
actionlint: 'darwin_amd64',
|
|
shellcheck: 'darwin.x86_64',
|
|
};
|
|
}
|
|
if (platform === 'darwin' && arch === 'arm64') {
|
|
return {
|
|
actionlint: 'darwin_arm64',
|
|
shellcheck: 'darwin.aarch64',
|
|
};
|
|
}
|
|
if (platform === 'win32' && arch === 'x64') {
|
|
return {
|
|
actionlint: 'windows_amd64',
|
|
// shellcheck is not used for Windows since it uses the .zip release
|
|
// which has a consistent name across architectures
|
|
};
|
|
}
|
|
throw new Error(`Unsupported platform/architecture: ${platform}/${arch}`);
|
|
}
|
|
|
|
const platformArch = getPlatformArch();
|
|
|
|
const PYTHON_VENV_PATH = join(TEMP_DIR, 'python_venv');
|
|
|
|
const pythonVenvPythonPath = join(
|
|
PYTHON_VENV_PATH,
|
|
process.platform === 'win32' ? 'Scripts' : 'bin',
|
|
process.platform === 'win32' ? 'python.exe' : 'python',
|
|
);
|
|
|
|
const isWindows = process.platform === 'win32';
|
|
|
|
const actionlintCheck = isWindows
|
|
? `where actionlint 2>nul`
|
|
: 'command -v actionlint';
|
|
|
|
const actionlintInstaller = isWindows
|
|
? `powershell -Command "` +
|
|
`New-Item -ItemType Directory -Force -Path '${TEMP_DIR}/actionlint' | Out-Null; ` +
|
|
`Invoke-WebRequest -Uri 'https://github.com/rhysd/actionlint/releases/download/v${ACTIONLINT_VERSION}/actionlint_${ACTIONLINT_VERSION}_${platformArch.actionlint}.zip' -OutFile '${TEMP_DIR}/.actionlint.zip'; ` +
|
|
`Add-Type -AssemblyName System.IO.Compression.FileSystem; ` +
|
|
`[System.IO.Compression.ZipFile]::ExtractToDirectory('${TEMP_DIR}/.actionlint.zip', '${TEMP_DIR}/actionlint')"`
|
|
: `
|
|
mkdir -p "${TEMP_DIR}/actionlint"
|
|
curl -sSLo "${TEMP_DIR}/.actionlint.tgz" "https://github.com/rhysd/actionlint/releases/download/v${ACTIONLINT_VERSION}/actionlint_${ACTIONLINT_VERSION}_${platformArch.actionlint}.tar.gz"
|
|
tar -xzf "${TEMP_DIR}/.actionlint.tgz" -C "${TEMP_DIR}/actionlint"
|
|
`;
|
|
|
|
const shellcheckCheck = isWindows
|
|
? `where shellcheck 2>nul`
|
|
: 'command -v shellcheck';
|
|
|
|
const shellcheckInstaller = isWindows
|
|
? `powershell -Command "` +
|
|
`Invoke-WebRequest -Uri 'https://github.com/koalaman/shellcheck/releases/download/v${SHELLCHECK_VERSION}/shellcheck-v${SHELLCHECK_VERSION}.zip' -OutFile '${TEMP_DIR}/.shellcheck.zip'; ` +
|
|
`Add-Type -AssemblyName System.IO.Compression.FileSystem; ` +
|
|
`[System.IO.Compression.ZipFile]::ExtractToDirectory('${TEMP_DIR}/.shellcheck.zip', '${TEMP_DIR}/shellcheck')"`
|
|
: `
|
|
mkdir -p "${TEMP_DIR}/shellcheck"
|
|
curl -sSLo "${TEMP_DIR}/.shellcheck.txz" "https://github.com/koalaman/shellcheck/releases/download/v${SHELLCHECK_VERSION}/shellcheck-v${SHELLCHECK_VERSION}.${platformArch.shellcheck}.tar.xz"
|
|
tar -xf "${TEMP_DIR}/.shellcheck.txz" -C "${TEMP_DIR}/shellcheck" --strip-components=1
|
|
`;
|
|
|
|
const yamllintCheck = isWindows
|
|
? `if exist "${PYTHON_VENV_PATH}\\Scripts\\yamllint.exe" (exit 0) else (exit 1)`
|
|
: `test -x "${PYTHON_VENV_PATH}/bin/yamllint"`;
|
|
|
|
const yamllintInstaller = isWindows
|
|
? `python -m venv "${PYTHON_VENV_PATH}" && ` +
|
|
`"${pythonVenvPythonPath}" -m pip install --upgrade pip && ` +
|
|
`"${pythonVenvPythonPath}" -m pip install "yamllint==${YAMLLINT_VERSION}" --index-url https://pypi.org/simple`
|
|
: `
|
|
python3 -m venv "${PYTHON_VENV_PATH}" && \
|
|
"${pythonVenvPythonPath}" -m pip install --upgrade pip && \
|
|
"${pythonVenvPythonPath}" -m pip install "yamllint==${YAMLLINT_VERSION}" --index-url https://pypi.org/simple
|
|
`;
|
|
|
|
/**
|
|
* @typedef {{
|
|
* check: string;
|
|
* installer: string;
|
|
* run: string;
|
|
* }}
|
|
*/
|
|
|
|
/**
|
|
* @type {{[linterName: string]: Linter}}
|
|
*/
|
|
const LINTERS = {
|
|
actionlint: {
|
|
check: actionlintCheck,
|
|
installer: actionlintInstaller,
|
|
run: `
|
|
actionlint \
|
|
-color \
|
|
-ignore 'SC2002:' \
|
|
-ignore 'SC2016:' \
|
|
-ignore 'SC2129:' \
|
|
-ignore 'label ".+" is unknown'
|
|
`,
|
|
},
|
|
shellcheck: {
|
|
check: shellcheckCheck,
|
|
installer: shellcheckInstaller,
|
|
run: `
|
|
git ls-files | grep -E '^([^.]+|.*\\.(sh|zsh|bash))' | xargs file --mime-type \
|
|
| grep "text/x-shellscript" | awk '{ print substr($1, 1, length($1)-1) }' \
|
|
| xargs shellcheck \
|
|
--check-sourced \
|
|
--enable=all \
|
|
--exclude=SC2002,SC2129,SC2310 \
|
|
--severity=style \
|
|
--format=gcc \
|
|
--color=never | sed -e 's/note:/warning:/g' -e 's/style:/warning:/g'
|
|
`,
|
|
},
|
|
yamllint: {
|
|
check: yamllintCheck,
|
|
installer: yamllintInstaller,
|
|
run: "git ls-files | grep -E '\\.(yaml|yml)' | xargs yamllint --format github",
|
|
},
|
|
};
|
|
|
|
function runCommand(command, stdio = 'inherit') {
|
|
try {
|
|
const env = { ...process.env };
|
|
const nodeBin = join(process.cwd(), 'node_modules', '.bin');
|
|
const sep = isWindows ? ';' : ':';
|
|
const pythonBin = isWindows
|
|
? join(PYTHON_VENV_PATH, 'Scripts')
|
|
: join(PYTHON_VENV_PATH, 'bin');
|
|
// Windows sometimes uses 'Path' instead of 'PATH'
|
|
const pathKey = 'Path' in env ? 'Path' : 'PATH';
|
|
env[pathKey] = [
|
|
nodeBin,
|
|
join(TEMP_DIR, 'actionlint'),
|
|
join(TEMP_DIR, 'shellcheck'),
|
|
pythonBin,
|
|
env[pathKey],
|
|
].join(sep);
|
|
execSync(command, { stdio, env, shell: true });
|
|
return true;
|
|
} catch (_e) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
export function setupLinters() {
|
|
console.log('Setting up linters...');
|
|
if (!process.env.GEMINI_LINT_TEMP_DIR) {
|
|
rmSync(TEMP_DIR, { recursive: true, force: true });
|
|
}
|
|
mkdirSync(TEMP_DIR, { recursive: true });
|
|
|
|
for (const linter in LINTERS) {
|
|
const { check, installer } = LINTERS[linter];
|
|
if (!runCommand(check, 'ignore')) {
|
|
console.log(`Installing ${linter}...`);
|
|
if (!runCommand(installer)) {
|
|
console.error(
|
|
`Failed to install ${linter}. Please install it manually.`,
|
|
);
|
|
process.exit(1);
|
|
}
|
|
}
|
|
}
|
|
console.log('All required linters are available.');
|
|
}
|
|
|
|
export function runESLint() {
|
|
console.log('\nRunning ESLint...');
|
|
if (!runCommand('npm run lint')) {
|
|
process.exit(1);
|
|
}
|
|
}
|
|
|
|
export function runActionlint() {
|
|
console.log('\nRunning actionlint...');
|
|
if (!runCommand(LINTERS.actionlint.run)) {
|
|
process.exit(1);
|
|
}
|
|
}
|
|
|
|
export function runShellcheck() {
|
|
console.log('\nRunning shellcheck...');
|
|
if (!runCommand(LINTERS.shellcheck.run)) {
|
|
process.exit(1);
|
|
}
|
|
}
|
|
|
|
export function runYamllint() {
|
|
console.log('\nRunning yamllint...');
|
|
if (!runCommand(LINTERS.yamllint.run)) {
|
|
process.exit(1);
|
|
}
|
|
}
|
|
|
|
export function runPrettier() {
|
|
console.log('\nRunning Prettier...');
|
|
if (!runCommand('prettier --check .')) {
|
|
console.log(
|
|
'Prettier check failed. Please run "npm run format" to fix formatting issues.',
|
|
);
|
|
process.exit(1);
|
|
}
|
|
}
|
|
|
|
export function runSensitiveKeywordLinter() {
|
|
console.log('\nRunning sensitive keyword linter...');
|
|
const SENSITIVE_PATTERN = /gemini-\d+(\.\d+)?/g;
|
|
const ALLOWED_KEYWORDS = new Set([
|
|
'gemini-3',
|
|
'gemini-3.0',
|
|
'gemini-2.5',
|
|
'gemini-2.0',
|
|
'gemini-1.5',
|
|
'gemini-1.0',
|
|
]);
|
|
|
|
function getChangedFiles() {
|
|
const baseRef = process.env.GITHUB_BASE_REF || 'main';
|
|
try {
|
|
execSync(`git fetch origin ${baseRef}`);
|
|
const mergeBase = execSync(`git merge-base HEAD origin/${baseRef}`)
|
|
.toString()
|
|
.trim();
|
|
return execSync(`git diff --name-only ${mergeBase}..HEAD`)
|
|
.toString()
|
|
.trim()
|
|
.split('\n')
|
|
.filter(Boolean);
|
|
} catch (_error) {
|
|
console.error(`Could not get changed files against origin/${baseRef}.`);
|
|
try {
|
|
console.log('Falling back to diff against HEAD~1');
|
|
return execSync(`git diff --name-only HEAD~1..HEAD`)
|
|
.toString()
|
|
.trim()
|
|
.split('\n')
|
|
.filter(Boolean);
|
|
} catch (_fallbackError) {
|
|
console.error('Could not get changed files against HEAD~1 either.');
|
|
process.exit(1);
|
|
}
|
|
}
|
|
}
|
|
|
|
const changedFiles = getChangedFiles();
|
|
let violationsFound = false;
|
|
|
|
for (const file of changedFiles) {
|
|
if (!existsSync(file) || lstatSync(file).isDirectory()) {
|
|
continue;
|
|
}
|
|
const content = readFileSync(file, 'utf-8');
|
|
const lines = content.split('\n');
|
|
let match;
|
|
while ((match = SENSITIVE_PATTERN.exec(content)) !== null) {
|
|
const keyword = match[0];
|
|
if (!ALLOWED_KEYWORDS.has(keyword)) {
|
|
violationsFound = true;
|
|
const matchIndex = match.index;
|
|
let lineNum = 0;
|
|
let charCount = 0;
|
|
for (let i = 0; i < lines.length; i++) {
|
|
const line = lines[i];
|
|
if (charCount + line.length + 1 > matchIndex) {
|
|
lineNum = i + 1;
|
|
const colNum = matchIndex - charCount + 1;
|
|
console.log(
|
|
`::warning file=${file},line=${lineNum},col=${colNum}::Found sensitive keyword "${keyword}". Please make sure this change is appropriate to submit.`,
|
|
);
|
|
break;
|
|
}
|
|
charCount += line.length + 1; // +1 for the newline
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!violationsFound) {
|
|
console.log('No sensitive keyword violations found.');
|
|
}
|
|
}
|
|
|
|
function stripJSONComments(json) {
|
|
return json.replace(
|
|
/\\"|"(?:\\"|[^"])*"|(\/\/.*|\/\*[\s\S]*?\*\/)/g,
|
|
(m, g) => (g ? '' : m),
|
|
);
|
|
}
|
|
|
|
export function runTSConfigLinter() {
|
|
console.log('\nRunning tsconfig linter...');
|
|
|
|
let files = [];
|
|
try {
|
|
// Find all tsconfig.json files under packages/ using a git pathspec
|
|
files = execSync("git ls-files 'packages/**/tsconfig.json'")
|
|
.toString()
|
|
.trim()
|
|
.split('\n')
|
|
.filter(Boolean);
|
|
} catch (e) {
|
|
console.error('Error finding tsconfig.json files:', e.message);
|
|
process.exit(1);
|
|
}
|
|
|
|
let hasError = false;
|
|
|
|
for (const file of files) {
|
|
const tsconfigPath = join(process.cwd(), file);
|
|
if (!existsSync(tsconfigPath)) {
|
|
console.error(`Error: ${tsconfigPath} does not exist.`);
|
|
hasError = true;
|
|
continue;
|
|
}
|
|
|
|
try {
|
|
const content = readFileSync(tsconfigPath, 'utf-8');
|
|
const config = JSON.parse(stripJSONComments(content));
|
|
|
|
// Check if exclude exists and matches exactly
|
|
if (config.exclude) {
|
|
if (!Array.isArray(config.exclude)) {
|
|
console.error(
|
|
`Error: ${file} "exclude" must be an array. Found: ${JSON.stringify(
|
|
config.exclude,
|
|
)}`,
|
|
);
|
|
hasError = true;
|
|
} else {
|
|
const allowedExclude = new Set(['node_modules', 'dist']);
|
|
const invalidExcludes = config.exclude.filter(
|
|
(item) => !allowedExclude.has(item),
|
|
);
|
|
|
|
if (invalidExcludes.length > 0) {
|
|
console.error(
|
|
`Error: ${file} "exclude" contains invalid items: ${JSON.stringify(
|
|
invalidExcludes,
|
|
)}. Only "node_modules" and "dist" are allowed.`,
|
|
);
|
|
hasError = true;
|
|
}
|
|
}
|
|
}
|
|
} catch (error) {
|
|
console.error(`Error parsing ${tsconfigPath}: ${error.message}`);
|
|
hasError = true;
|
|
}
|
|
}
|
|
|
|
if (hasError) {
|
|
process.exit(1);
|
|
}
|
|
}
|
|
|
|
function main() {
|
|
const args = process.argv.slice(2);
|
|
|
|
if (args.includes('--setup')) {
|
|
setupLinters();
|
|
}
|
|
if (args.includes('--eslint')) {
|
|
runESLint();
|
|
}
|
|
if (args.includes('--actionlint')) {
|
|
runActionlint();
|
|
}
|
|
if (args.includes('--shellcheck')) {
|
|
runShellcheck();
|
|
}
|
|
if (args.includes('--yamllint')) {
|
|
runYamllint();
|
|
}
|
|
if (args.includes('--prettier')) {
|
|
runPrettier();
|
|
}
|
|
if (args.includes('--sensitive-keywords')) {
|
|
runSensitiveKeywordLinter();
|
|
}
|
|
if (args.includes('--tsconfig')) {
|
|
runTSConfigLinter();
|
|
}
|
|
|
|
if (args.length === 0) {
|
|
setupLinters();
|
|
runESLint();
|
|
runActionlint();
|
|
runShellcheck();
|
|
runYamllint();
|
|
runPrettier();
|
|
runSensitiveKeywordLinter();
|
|
runTSConfigLinter();
|
|
console.log('\nAll linting checks passed!');
|
|
}
|
|
}
|
|
|
|
main();
|