Download scripts/lint.js from SaylorTwift/gemini-cli: direct link, hf CLI and curl.
- Browser
- Download file 15.3 kB
-
https://huggingface.co/SaylorTwift/gemini-cli/resolve/main/scripts/lint.js
- Command line
-
hf download hf://SaylorTwift/gemini-cli/scripts/lint.js
-
curl -L -o lint.js https://huggingface.co/SaylorTwift/gemini-cli/resolve/main/scripts/lint.js
15.3 kB
| /** | |
| * @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 { | |
| 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.1', | |
| '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 { | |
| 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 { | |
| 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); | |
| } | |
| } | |
| export function runGithubActionsPinningLinter() { | |
| console.log('\nRunning GitHub Actions pinning linter...'); | |
| let files = []; | |
| try { | |
| files = execSync( | |
| "git ls-files '.github/workflows/*.yml' '.github/workflows/*.yaml' '.github/actions/**/*.yml' '.github/actions/**/*.yaml'", | |
| ) | |
| .toString() | |
| .trim() | |
| .split('\n') | |
| .filter(Boolean); | |
| } catch (e) { | |
| console.error('Error finding GitHub Actions workflow files:', e.message); | |
| process.exit(1); | |
| } | |
| let violationsFound = false; | |
| // Improved regex to capture action name and ref, handling optional quotes and comments. | |
| const USES_PATTERN = /uses:\s*['"]?([^@\s'"]+)@([^#\s'"]+)['"]?/; | |
| const SHA_PATTERN = /^[0-9a-f]{40}$/i; | |
| for (const file of files) { | |
| if (!existsSync(file) || lstatSync(file).isDirectory()) { | |
| continue; | |
| } | |
| const content = readFileSync(file, 'utf-8'); | |
| const lines = content.split('\n'); | |
| for (let i = 0; i < lines.length; i++) { | |
| const line = lines[i]; | |
| const match = line.match(USES_PATTERN); | |
| if (match) { | |
| const action = match[1]; | |
| let ref = match[2]; | |
| // Clean up any trailing quotes that might have been captured | |
| ref = ref.replace(/['"]$/, ''); | |
| // Skip local actions (starting with ./), docker actions, and explicit exclusions | |
| if ( | |
| action.startsWith('./') || | |
| action.startsWith('docker://') || | |
| line.includes('# github-actions-pinning:ignore') | |
| ) { | |
| continue; | |
| } | |
| if (!SHA_PATTERN.test(ref)) { | |
| violationsFound = true; | |
| const lineNum = i + 1; | |
| console.error( | |
| `::error file=${file},line=${lineNum}::Action "${action}" uses "${ref}" instead of a 40-character SHA.`, | |
| ); | |
| } | |
| } | |
| } | |
| } | |
| if (violationsFound) { | |
| console.error(` | |
| GitHub Actions pinning violations found. Please use exact commit hashes. | |
| To automatically fix these, you can use the "ratchet" tool (https://github.com/sethvargo/ratchet): | |
| - Mac/Linux (Homebrew): brew install ratchet && ratchet pin .github/workflows/*.yml .github/actions/**/*.yml | |
| - Other platforms: Download from GitHub releases and run "ratchet pin .github/workflows/*.yml .github/actions/**/*.yml" | |
| If you must use a tag, you can ignore this check by adding a comment (discouraged): | |
| uses: some-action@v1 # github-actions-pinning:ignore | |
| `); | |
| process.exit(1); | |
| } else { | |
| console.log('No GitHub Actions pinning violations found.'); | |
| } | |
| } | |
| 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.includes('--check-github-actions-pinning')) { | |
| runGithubActionsPinningLinter(); | |
| } | |
| if (args.length === 0) { | |
| setupLinters(); | |
| runESLint(); | |
| runActionlint(); | |
| runShellcheck(); | |
| runYamllint(); | |
| runPrettier(); | |
| runSensitiveKeywordLinter(); | |
| runTSConfigLinter(); | |
| runGithubActionsPinningLinter(); | |
| console.log('\nAll linting checks passed!'); | |
| } | |
| } | |
| main(); | |