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:
@@ -13,7 +13,7 @@
|
||||
* - "why" comments that explain intent, rationale, or gotchas → KEEP
|
||||
* - code self-explanatory with no comment → no comment needed (don't add one)
|
||||
*
|
||||
* Artifacts (under `<cwd>/pygienium/checks/comments/`):
|
||||
* Artifacts (under `<cwd>/.pygienium/checks/comments/`):
|
||||
* - `findings.md` — per-file line refs for each smell
|
||||
* - `changes.md` — summary of edits + human-review items
|
||||
*
|
||||
@@ -28,14 +28,14 @@ export const COMMENTS_PHASE_ID = "C1";
|
||||
|
||||
/**
|
||||
* Directory where this check writes its `findings.md` and `changes.md`
|
||||
* artifacts: `<cwd>/pygienium/checks/comments/`. Based on `scope.cwd` (the
|
||||
* artifacts: `<cwd>/.pygienium/checks/comments/`. Based on `scope.cwd` (the
|
||||
* project root, always a directory) so the path is valid whether the scan
|
||||
* target is a single file or a directory. Matches the spec's
|
||||
* `pygienium/checks/comments/findings.md` relative-path notation.
|
||||
* `.pygienium/checks/comments/findings.md` relative-path notation.
|
||||
*/
|
||||
export function commentsArtifactDir(scope: CheckScope): string {
|
||||
const base = scope.cwd.replace(/\/+$/, "");
|
||||
return `${base}/pygienium/checks/comments`;
|
||||
return `${base}/.pygienium/checks/comments`;
|
||||
}
|
||||
|
||||
/** Absolute path to the findings artifact for this check. */
|
||||
@@ -71,7 +71,7 @@ Short + high value is the goal. Evaluate every comment in the target:
|
||||
/**
|
||||
* Build the analysis sub-agent task. Instructs the agent to read candidate
|
||||
* source files, identify comment smells per the rubric, and write per-file line
|
||||
* references to `pygienium/comments/findings.md`.
|
||||
* references to `.pygienium/comments/findings.md`.
|
||||
*/
|
||||
export function buildCommentsScanTask(_cwd: string, scope: CheckScope): string {
|
||||
const outDir = commentsArtifactDir(scope);
|
||||
@@ -119,7 +119,7 @@ Write the report under \`${outDir}\` (create directories as needed).
|
||||
/**
|
||||
* Build the fix sub-agent task from the scan findings. Instructs the agent to
|
||||
* apply safe removals/tightenings, leave "why" comments, and write a summary
|
||||
* of edits plus anything needing human review to `pygienium/comments/changes.md`.
|
||||
* of edits plus anything needing human review to `.pygienium/comments/changes.md`.
|
||||
*/
|
||||
export function buildCommentsFixTask(
|
||||
_cwd: string,
|
||||
|
||||
@@ -28,11 +28,11 @@ import { scopeRulesMarkdown } from "./scope.js";
|
||||
export const COMPLEXITY_PHASE_ID = "C4";
|
||||
|
||||
/**
|
||||
* Artifact directory: `<cwd>/pygienium/checks/complexity/`.
|
||||
* Artifact directory: `<cwd>/.pygienium/checks/complexity/`.
|
||||
*/
|
||||
export function complexityArtifactDir(scope: CheckScope): string {
|
||||
const base = scope.cwd.replace(/\/+$/, "");
|
||||
return `${base}/pygienium/checks/complexity`;
|
||||
return `${base}/.pygienium/checks/complexity`;
|
||||
}
|
||||
|
||||
/** Absolute path to findings artifact. */
|
||||
@@ -84,13 +84,12 @@ count decision points (if/else if/for/while/case/&&/||/catch) per function.
|
||||
* Build the analysis sub-agent task. Instructs the agent to:
|
||||
* 1. Compute cyclomatic complexity per function
|
||||
* 2. Identify structural complexity smells
|
||||
* 3. Write findings to pygienium/checks/complexity/findings.md
|
||||
* 3. Write findings to .pygienium/checks/complexity/findings.md
|
||||
*/
|
||||
export function buildComplexityScanTask(
|
||||
_cwd: string,
|
||||
scope: CheckScope,
|
||||
): string {
|
||||
const outDir = complexityArtifactDir(scope);
|
||||
const findingsFile = findingsPath(scope);
|
||||
return `# Task: excessive complexity scan
|
||||
|
||||
@@ -173,7 +172,7 @@ Always create findings.md so the run has an artifact.
|
||||
* 1. Split 50+ complexity functions
|
||||
* 2. Refactor or justify 35–49 functions
|
||||
* 3. Apply safe refactors for structural smells
|
||||
* 4. Write changes summary to pygienium/checks/complexity/changes.md
|
||||
* 4. Write changes summary to .pygienium/checks/complexity/changes.md
|
||||
*/
|
||||
export function buildComplexityFixTask(
|
||||
_cwd: string,
|
||||
|
||||
@@ -950,14 +950,14 @@ export async function applyDeadCodeFixes(
|
||||
// Artifact paths
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const CHECK_DIRNAME = "pygienium/checks/dead-code";
|
||||
const CHECK_DIRNAME = ".pygienium/checks/dead-code";
|
||||
|
||||
/** `<target>/pygienium/checks/dead-code/findings.md` */
|
||||
/** `<target>/.pygienium/checks/dead-code/findings.md` */
|
||||
export function findingsPath(target: string): string {
|
||||
return join(target, CHECK_DIRNAME, "findings.md");
|
||||
}
|
||||
|
||||
/** `<target>/pygienium/checks/dead-code/changes.md` */
|
||||
/** `<target>/.pygienium/checks/dead-code/changes.md` */
|
||||
export function changesPath(target: string): string {
|
||||
return join(target, CHECK_DIRNAME, "changes.md");
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
*
|
||||
* Lifecycle:
|
||||
* gate (need source files) → recon (shared) → scan sub-agent writes
|
||||
* `<cwd>/pygienium/checks/deep-modules/findings.md` → [with --fix] fix
|
||||
* `<cwd>/.pygienium/checks/deep-modules/findings.md` → [with --fix] fix
|
||||
* sub-agent writes `changes.md`, inlines safe pass-throughs, and lists
|
||||
* risky consolidations (external importers / public API) for human review.
|
||||
*
|
||||
@@ -32,7 +32,7 @@ import { isScopeSource, scopeRulesMarkdown } from "./scope.js";
|
||||
|
||||
/** Output directory for this check's persistent reports. */
|
||||
export function deepModulesOutputDir(cwd: string): string {
|
||||
return join(cwd, "pygienium", "checks", "deep-modules");
|
||||
return join(cwd, ".pygienium", "checks", "deep-modules");
|
||||
}
|
||||
|
||||
/** `findings.md` path for this check. */
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
*
|
||||
* Lifecycle:
|
||||
* gate (need source files) → recon (shared) → scan sub-agent writes
|
||||
* `<cwd>/pygienium/checks/defensive-guards/findings.md` separating redundant
|
||||
* `<cwd>/.pygienium/checks/defensive-guards/findings.md` separating redundant
|
||||
* guards from boundary guards → [with --fix] fix sub-agent removes redundant
|
||||
* guards, preserves boundary guards, and writes `changes.md` distinguishing
|
||||
* removed vs kept-with-reason.
|
||||
@@ -52,7 +52,7 @@ import { isScopeSource, scopeRulesMarkdown } from "./scope.js";
|
||||
|
||||
/** Output directory for this check's persistent reports. */
|
||||
export function defensiveGuardsOutputDir(cwd: string): string {
|
||||
return join(cwd, "pygienium", "checks", "defensive-guards");
|
||||
return join(cwd, ".pygienium", "checks", "defensive-guards");
|
||||
}
|
||||
|
||||
/** `findings.md` path for this check. */
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -3,9 +3,9 @@
|
||||
*
|
||||
* `/pygienium-export` walks each check's artifact directory (where
|
||||
* `findings.md` and `changes.md` live), applies `--check=` / `--status=`
|
||||
* filters, and writes a single bundle to `pygienium/export.{md|json}`.
|
||||
* filters, and writes a single bundle to `.pygienium/export.{md|json}`.
|
||||
*
|
||||
* Artifact root: `<cwd>/pygienium/checks/<name>/` — the single canonical
|
||||
* Artifact root: `<cwd>/.pygienium/checks/<name>/` — the single canonical
|
||||
* location every shipped check writes to.
|
||||
*
|
||||
* Statuses for `--status=` filtering come from the run-state; a check dir
|
||||
@@ -21,23 +21,23 @@ import type { RunState } from "./run-state.js";
|
||||
export type ExportFormat = "md" | "json";
|
||||
|
||||
/** Directory name (relative to cwd) that holds `checks/` and `export.md`. */
|
||||
export const PYGIENIUM_ARTIFACT_DIR = "pygienium";
|
||||
export const PYGIENIUM_ARTIFACT_DIR = ".pygienium";
|
||||
/** Subdirectory holding per-check `findings.md`/`changes.md`. */
|
||||
export const CHECKS_SUBDIR = "checks";
|
||||
/** Base filename for the bundle (`export.md` / `export.json`). */
|
||||
export const EXPORT_FILENAME_BASE = "export";
|
||||
|
||||
/** Resolve `<cwd>/pygienium/` (the artifact root). */
|
||||
/** Resolve `<cwd>/.pygienium/` (the artifact root). */
|
||||
export function pygieniumArtifactDir(cwd: string): string {
|
||||
return join(cwd, PYGIENIUM_ARTIFACT_DIR);
|
||||
}
|
||||
|
||||
/** Resolve `<cwd>/pygienium/checks/`. */
|
||||
/** Resolve `<cwd>/.pygienium/checks/`. */
|
||||
export function canonicalChecksRoot(cwd: string): string {
|
||||
return join(pygieniumArtifactDir(cwd), CHECKS_SUBDIR);
|
||||
}
|
||||
|
||||
/** Resolve `<cwd>/pygienium/export.<format>`. */
|
||||
/** Resolve `<cwd>/.pygienium/export.<format>`. */
|
||||
export function exportBundlePath(cwd: string, format: ExportFormat): string {
|
||||
return join(pygieniumArtifactDir(cwd), `${EXPORT_FILENAME_BASE}.${format}`);
|
||||
}
|
||||
@@ -153,7 +153,7 @@ async function gatherFromRoot(
|
||||
}
|
||||
|
||||
/**
|
||||
* Gather artifact entries from the canonical `<cwd>/pygienium/checks/` root,
|
||||
* Gather artifact entries from the canonical `<cwd>/.pygienium/checks/` root,
|
||||
* one entry per check directory. Entries are sorted alphabetically. Marks an
|
||||
* entry `unknown` when its check is absent from `state`.
|
||||
*/
|
||||
|
||||
@@ -62,6 +62,12 @@ export const CLI_FLAGS: HelpFlag[] = [
|
||||
scope: "all",
|
||||
description: "Comma-separated check names to run (subset of the registry).",
|
||||
},
|
||||
{
|
||||
name: "--no-gitignore",
|
||||
scope: "<check>, all, resume",
|
||||
description:
|
||||
"Don't add `.pygienium/` to the target repo's .gitignore (added by default so runs never stage their own output).",
|
||||
},
|
||||
{
|
||||
name: "--check=",
|
||||
scope: "export",
|
||||
@@ -118,7 +124,7 @@ export const COMMANDS: HelpCommand[] = [
|
||||
{
|
||||
usage: "pygienium-export [path] [--check=] [--status=] [--out=md|json]",
|
||||
description:
|
||||
"Bundle every check's findings.md + changes.md into pygienium/export.{md|json}.",
|
||||
"Bundle every check's findings.md + changes.md into .pygienium/export.{md|json}.",
|
||||
example: "/pygienium-export --out=json",
|
||||
},
|
||||
];
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
* init single run (mode "all") → run shared recon once →
|
||||
* for each registered check (in registry order): call `runCheck` with the
|
||||
* SHARED run-state record (not a fresh one per check) → reconcile run
|
||||
* status → write `pygienium/all-summary.md`.
|
||||
* status → write `.pygienium/all-summary.md`.
|
||||
*
|
||||
* Resumability: terminal checks (`complete`/`skipped`) are skipped on resume;
|
||||
* `in_progress`/`failed`/`pending` checks re-run. `--fresh` resets every check
|
||||
@@ -29,6 +29,7 @@ import { createPhaseStrip } from "../phases.js";
|
||||
import { runRecon } from "../recon.js";
|
||||
import {
|
||||
applyPhaseStatus,
|
||||
ensureRunStateIgnored,
|
||||
initRunState,
|
||||
loadRunState,
|
||||
markRunStatus,
|
||||
@@ -42,11 +43,11 @@ import {
|
||||
} from "../run-state.js";
|
||||
|
||||
/** Artifact directory name (relative to cwd) that holds `all-summary.md`. */
|
||||
export const ALL_ARTIFACT_DIR = "pygienium";
|
||||
export const ALL_ARTIFACT_DIR = ".pygienium";
|
||||
/** Filename for the unified per-check summary report. */
|
||||
export const ALL_SUMMARY_FILENAME = "all-summary.md";
|
||||
|
||||
/** Resolve `<cwd>/pygienium/all-summary.md`. */
|
||||
/** Resolve `<cwd>/.pygienium/all-summary.md`. */
|
||||
export function allSummaryPath(cwd: string): string {
|
||||
return join(cwd, ALL_ARTIFACT_DIR, ALL_SUMMARY_FILENAME);
|
||||
}
|
||||
@@ -62,10 +63,17 @@ export interface AllRunOptions {
|
||||
only?: string[];
|
||||
/** Reset and re-run every check, ignoring prior terminal state (`--fresh`). */
|
||||
fresh?: boolean;
|
||||
/**
|
||||
* Ensure `<cwd>/.gitignore` excludes `.pygienium/` before this run writes
|
||||
* state/artifacts (default true). Set false with `--no-gitignore`.
|
||||
*/
|
||||
gitignore?: boolean;
|
||||
/** UI context (optional; null in print mode). */
|
||||
ui?: ExtensionUIContext;
|
||||
/** Whether dialog-capable UI is available. */
|
||||
hasUI?: boolean;
|
||||
/** Optional callback to post completion messages into the chat. */
|
||||
sendChatMessage?: SendChatMessage;
|
||||
}
|
||||
|
||||
/** Outcome of {@link runAllChecks}. */
|
||||
@@ -80,6 +88,8 @@ export interface AllRunOutcome {
|
||||
ran: string[];
|
||||
/** Checks skipped because they were already terminal. */
|
||||
skipped: string[];
|
||||
/** True when this run appended `.pygienium/` to the repo's .gitignore. */
|
||||
gitignoreAppended?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -104,11 +114,13 @@ export function parseAllArgs(
|
||||
target: string;
|
||||
fix: boolean;
|
||||
fresh: boolean;
|
||||
gitignore: boolean;
|
||||
only: string[];
|
||||
} {
|
||||
const tokens = args.trim().length > 0 ? args.trim().split(/\s+/) : [];
|
||||
let fix = false;
|
||||
let fresh = false;
|
||||
let gitignore = true;
|
||||
let target = cwd;
|
||||
const only: string[] = [];
|
||||
for (const tok of tokens) {
|
||||
@@ -116,6 +128,8 @@ export function parseAllArgs(
|
||||
fix = true;
|
||||
} else if (tok === "--fresh") {
|
||||
fresh = true;
|
||||
} else if (tok === "--no-gitignore") {
|
||||
gitignore = false;
|
||||
} else if (tok.startsWith("--only=")) {
|
||||
for (const name of tok.slice("--only=".length).split(",")) {
|
||||
const trimmed = name.trim();
|
||||
@@ -125,7 +139,7 @@ export function parseAllArgs(
|
||||
target = tok;
|
||||
}
|
||||
}
|
||||
return { target: resolve(cwd, target), fix, fresh, only };
|
||||
return { target: resolve(cwd, target), fix, fresh, gitignore, only };
|
||||
}
|
||||
|
||||
/** Count non-empty lines in captured findings/changes text. */
|
||||
@@ -219,6 +233,11 @@ export async function runAllChecks(
|
||||
const fresh = opts.fresh ?? false;
|
||||
const hasUI = opts.hasUI ?? false;
|
||||
|
||||
// Keep pygienium's own output out of the scanned repo's git index unless
|
||||
// the caller opted out with --no-gitignore.
|
||||
const gitignoreAppended =
|
||||
opts.gitignore === false ? false : await ensureRunStateIgnored(cwd);
|
||||
|
||||
const selected = selectChecks(opts.only);
|
||||
if (selected.length === 0) {
|
||||
// `--only` selected nothing (or no checks registered). Still produce a
|
||||
@@ -227,7 +246,14 @@ export async function runAllChecks(
|
||||
markRunStatus(state, reconcileRunStatus(state));
|
||||
await saveRunState(state);
|
||||
const summaryPath = await writeAllSummary(state, []);
|
||||
return { status: state.status, state, summaryPath, ran: [], skipped: [] };
|
||||
return {
|
||||
status: state.status,
|
||||
state,
|
||||
summaryPath,
|
||||
ran: [],
|
||||
skipped: [],
|
||||
gitignoreAppended,
|
||||
};
|
||||
}
|
||||
|
||||
// --- Init / resume the single shared run-state --------------------------
|
||||
@@ -312,6 +338,7 @@ export async function runAllChecks(
|
||||
ui: opts.ui,
|
||||
hasUI,
|
||||
existingState: state,
|
||||
gitignore: opts.gitignore,
|
||||
});
|
||||
state = outcome.state;
|
||||
ran.push(check.name);
|
||||
|
||||
@@ -82,12 +82,14 @@ export interface RunCheckOptions {
|
||||
hasUI?: boolean;
|
||||
/** Pre-existing run state to update (for `/pygienium-all` and resume). */
|
||||
existingState?: RunState;
|
||||
gitignore?: boolean;
|
||||
}
|
||||
|
||||
/** Outcome of a single check run. */
|
||||
export interface CheckRunOutcome {
|
||||
/** Final check status. */
|
||||
status: "complete" | "failed" | "skipped";
|
||||
gitignoreAppended?: boolean;
|
||||
/** Findings text from the analysis phase. */
|
||||
findings?: string;
|
||||
/** Changes text from the fix phase (when run with --fix). */
|
||||
|
||||
@@ -9,8 +9,8 @@
|
||||
* @module pygienium/run-state
|
||||
*/
|
||||
|
||||
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
||||
import { dirname, join } from "node:path";
|
||||
import { appendFile, mkdir, readFile, stat, writeFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
|
||||
export const RUN_STATE_DIRNAME = ".pygienium";
|
||||
export const RUN_STATE_FILENAME = "run-state.json";
|
||||
@@ -152,6 +152,46 @@ export async function saveRunState(state: RunState): Promise<void> {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Memo of cwds whose `.gitignore` was already ensured this process, so the
|
||||
* check runs at most once per target per session.
|
||||
*/
|
||||
const gitIgnoreMemo = new Set<string>();
|
||||
|
||||
/**
|
||||
* Make sure `<cwd>/.gitignore` excludes `.pygienium/` (run-state + artifacts)
|
||||
* so a run never stages its own output into the scanned repo's git index.
|
||||
* Best-effort and idempotent: no-op outside a git work tree or when the entry
|
||||
* already exists. Returns true when it appended the entry (or created the file).
|
||||
*/
|
||||
export async function ensureRunStateIgnored(cwd: string): Promise<boolean> {
|
||||
if (gitIgnoreMemo.has(cwd)) return false;
|
||||
gitIgnoreMemo.add(cwd);
|
||||
try {
|
||||
// Only act inside a git work tree (works for worktrees too: .git is a file).
|
||||
await stat(join(cwd, ".git"));
|
||||
const ignorePath = join(cwd, ".gitignore");
|
||||
const marker = ".pygienium/";
|
||||
let content: string;
|
||||
try {
|
||||
content = await readFile(ignorePath, "utf8");
|
||||
} catch {
|
||||
await writeFile(ignorePath, `${marker}\n`, "utf8");
|
||||
return true;
|
||||
}
|
||||
if (content.split(/\r?\n/).some((l) => l.trim() === marker)) return false;
|
||||
const prefix = content.endsWith("\n") ? "" : "\n";
|
||||
await appendFile(
|
||||
ignorePath,
|
||||
`${prefix}# pygienium run-state and check artifacts\n${marker}\n`,
|
||||
"utf8",
|
||||
);
|
||||
return true;
|
||||
} catch {
|
||||
return false; // not a git work tree, or a best-effort write failed
|
||||
}
|
||||
}
|
||||
|
||||
/** Mark a phase's status (and optionally an error message). */
|
||||
export function applyPhaseStatus(
|
||||
state: RunState,
|
||||
|
||||
Reference in New Issue
Block a user