feat: keep run artifacts under hidden .pygienium/ and out of git

All per-check artifacts, all-summary, and export move from pygienium/ to
.pygienium/; ensureRunStateIgnored appends .pygienium/ to the target
repo's .gitignore on first run (opt out with --no-gitignore), so a run
never stages its own output. Export now reads a single canonical root.
This commit is contained in:
2026-08-09 16:45:29 -04:00
parent 288506e84d
commit d0e8ad5571
17 changed files with 183 additions and 82 deletions

View File

@@ -60,12 +60,24 @@ function resolveCwd(args: string, ctxCwd: string): string {
return resolve(ctxCwd, tok);
}
/** Strip a leading flag token (`--fix`) from args, returning the remainder. */
function splitFlags(args: string): { fix: boolean; rest: string } {
/**
* Strip leading flag tokens (`--fix`, `--fresh`, `--no-gitignore`) from args,
* returning the remainder (the positional `[path]`).
*/
function splitFlags(args: string): {
fix: boolean;
fresh: boolean;
rest: string;
noGitignore: boolean;
} {
const tokens = args.trim().length > 0 ? args.trim().split(/\s+/) : [];
const fix = tokens.includes("--fix");
const rest = tokens.filter((t) => t !== "--fix").join(" ");
return { fix, rest };
const fresh = tokens.includes("--fresh");
const noGitignore = tokens.includes("--no-gitignore");
const rest = tokens
.filter((t) => t !== "--fix" && t !== "--fresh" && t !== "--no-gitignore")
.join(" ");
return { fix, fresh, rest, noGitignore };
}
/**
@@ -76,12 +88,13 @@ function splitFlags(args: string): { fix: boolean; rest: string } {
function parseResumeArgs(
args: string,
ctxCwd: string,
): { cwd: string; fresh: boolean } {
): { cwd: string; fresh: boolean; gitignore: boolean } {
const tokens = args.trim().length > 0 ? args.trim().split(/\s+/) : [];
const fresh = tokens.includes("--fresh");
const gitignore = !tokens.includes("--no-gitignore");
const positional = tokens.find((t) => !t.startsWith("--"));
const cwd = positional ? resolve(ctxCwd, positional) : ctxCwd;
return { cwd, fresh };
return { cwd, fresh, gitignore };
}
/** `/pygienium-help` */
@@ -116,9 +129,12 @@ export async function handleCheckCommand(
hasUI: ctx.hasUI,
});
const giNote = outcome.gitignoreAppended
? " · .pygienium/ added to .gitignore"
: "";
print(
ctx,
`pygienium ${check.label}: ${outcome.status}${outcome.error ? `${outcome.error}` : ""}`,
`pygienium ${check.label}: ${outcome.status}${outcome.error ? `${outcome.error}` : ""}${giNote}`,
);
}
@@ -142,13 +158,17 @@ export async function handleAllCommand(
target: parsed.target,
fix: parsed.fix,
fresh: parsed.fresh,
gitignore: parsed.gitignore,
only: parsed.only,
ui: ctx.ui,
hasUI: ctx.hasUI,
});
const giNote = outcome.gitignoreAppended
? " · .pygienium/ added to .gitignore"
: "";
print(
ctx,
`pygienium: all-run ${outcome.status}${outcome.ran.length} ran, ${outcome.skipped.length} skipped; summary → ${allSummaryPath(ctx.cwd)} (${runStatePath(ctx.cwd)})`,
`pygienium: all-run ${outcome.status}${outcome.ran.length} ran, ${outcome.skipped.length} skipped; summary → ${allSummaryPath(ctx.cwd)} (${runStatePath(ctx.cwd)})${giNote}`,
);
}
@@ -178,7 +198,7 @@ export async function handleResumeCommand(
args: string,
ctx: PygieniumCtx,
): Promise<void> {
const { cwd, fresh } = parseResumeArgs(args, ctx.cwd);
const { cwd, fresh, gitignore } = parseResumeArgs(args, ctx.cwd);
let state = await loadRunState(cwd);
if (!state) {
print(ctx, "pygienium: no run state to resume.");
@@ -202,6 +222,7 @@ export async function handleResumeCommand(
// unless --fresh.
let ran = 0;
let skipped = 0;
let giAppended = false;
for (const entry of Object.values(state.checks)) {
const def = getCheck(entry.name);
if (!def) {
@@ -258,7 +279,7 @@ export async function handleExportCommand(
if (result.entries.length === 0) {
print(
ctx,
`pygienium: nothing to export (no findings.md/changes.md under ${cwd}/pygienium/checks/).`,
`pygienium: nothing to export (no findings.md/changes.md under ${cwd}/.pygienium/checks/).`,
);
return;
}