#!/usr/bin/env node /** * run-shellcheck.mjs — shell script quality wrapper (M1, S-105) * * Discovers *.sh files under scripts/ and skills/ (excluding node_modules * and generated agent-variant directories), then runs shellcheck on each. * * shellcheck is a REQUIRED prerequisite. The script fails immediately when * shellcheck is not found on PATH. Install it with: * macOS: brew install shellcheck * Debian: apt-get install shellcheck * * Exit codes: * 0 — all files passed shellcheck * 1 — one or more files have shellcheck findings * 2 — shellcheck is missing from PATH */ import { spawnSync } from "node:child_process"; import { readdirSync } from "node:fs"; import path from "node:path"; import { fileURLToPath } from "node:url"; const __dirname = path.dirname(fileURLToPath(import.meta.url)); const REPO_ROOT = path.resolve(__dirname, "../.."); // ── Prerequisites check ──────────────────────────────────────────────────── function checkShellcheck() { const result = spawnSync("shellcheck", ["--version"], { encoding: "utf8" }); if (result.error || result.status === null || result.status > 1) { console.error( [ "ERROR: shellcheck is not available on PATH.", "", "shellcheck is a required prerequisite for this repository.", "Install it before running lint:", "", " macOS: brew install shellcheck", " Debian/Ubuntu: sudo apt-get install shellcheck", " Other: https://github.com/koalaman/shellcheck#installing", ].join("\n") ); process.exit(2); } } // ── File discovery ───────────────────────────────────────────────────────── /** * Directories to scan for *.sh files. * Relative paths from REPO_ROOT. */ const SCAN_DIRS = ["scripts", "skills"]; /** * Path segments that indicate a directory should be skipped entirely. * Matches generated agent-variant bundles and npm/pnpm install artifacts. */ const SKIP_SEGMENTS = new Set([ "node_modules", // Generated agent-variant directories (excluded per M1 workspace policy) // skills//codex — except web-automation/codex which is canonical // We skip all codex variants to be safe; shellcheck only cares about .sh files // and all variants share the same scripts anyway. ]); /** * Exact relative paths (from REPO_ROOT) to skip, matched after normalisation. * These are the generated variants that duplicate canonical source. */ const SKIP_PATHS = new Set([ "skills/atlassian/codex", "skills/atlassian/claude-code", "skills/atlassian/cursor", "skills/atlassian/opencode", "skills/atlassian/pi", "skills/web-automation/claude-code", "skills/web-automation/cursor", "skills/web-automation/opencode", "skills/web-automation/pi", "pi-package", ]); function shouldSkip(absPath) { const rel = path.relative(REPO_ROOT, absPath); // Check exact-prefix matches (directory and its children) for (const skip of SKIP_PATHS) { if (rel === skip || rel.startsWith(skip + path.sep)) return true; } // Check path-segment matches (e.g. node_modules anywhere in path) for (const seg of rel.split(path.sep)) { if (SKIP_SEGMENTS.has(seg)) return true; } return false; } function collectShellFiles(dir) { const found = []; let entries; try { entries = readdirSync(dir, { withFileTypes: true }); } catch { return found; // directory does not exist — skip silently } for (const entry of entries) { const full = path.join(dir, entry.name); if (shouldSkip(full)) continue; if (entry.isDirectory()) { found.push(...collectShellFiles(full)); } else if (entry.isFile() && entry.name.endsWith(".sh")) { found.push(full); } } return found; } // ── Main ─────────────────────────────────────────────────────────────────── checkShellcheck(); const files = SCAN_DIRS.flatMap((d) => collectShellFiles(path.join(REPO_ROOT, d)) ); if (files.length === 0) { console.log("shellcheck: no .sh files found — nothing to check."); process.exit(0); } console.log(`shellcheck: scanning ${files.length} file(s)…`); let failures = 0; for (const file of files.sort()) { const result = spawnSync("shellcheck", ["-x", "--source-path=SCRIPTDIR", file], { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], }); const output = (result.stdout + result.stderr).trim(); const rel = path.relative(REPO_ROOT, file); if (result.status !== 0) { failures += 1; // Print findings prefixed with the relative path so output is reproducible // regardless of cwd. console.error(`\n--- ${rel} ---`); if (output) console.error(output); } else { // Quiet on success — only show problems } } if (failures > 0) { console.error( `\nshellcheck: ${failures} file(s) have findings. ` + `See docs/CLEANUP-BASELINE.md for the as-is baseline.` ); process.exit(1); } else { console.log("shellcheck: all files passed."); process.exit(0); }