feat(M1): Baseline verification, quality tooling foundation, and current-state report
This commit is contained in:
@@ -0,0 +1,161 @@
|
||||
#!/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", [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);
|
||||
}
|
||||
Reference in New Issue
Block a user