162 lines
5.2 KiB
JavaScript
162 lines
5.2 KiB
JavaScript
#!/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/<skill>/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);
|
|
}
|