@technicalshree/auto-fix 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,44 @@
1
+ export const defaultConfig = {
2
+ version: 1,
3
+ ports: {
4
+ default: [3000, 5173, 8000, 8080],
5
+ extra: [9229],
6
+ },
7
+ node: {
8
+ package_manager: "auto",
9
+ deep_cleanup: {
10
+ remove_node_modules: true,
11
+ remove_lockfile: false,
12
+ },
13
+ caches: {
14
+ next: true,
15
+ vite: true,
16
+ directories: [".turbo", ".cache"],
17
+ },
18
+ },
19
+ python: {
20
+ venv_path: ".venv",
21
+ install: {
22
+ prefer: "uv",
23
+ },
24
+ tools: {
25
+ format: ["ruff format", "black ."],
26
+ lint: ["ruff check ."],
27
+ test: ["pytest -q"],
28
+ },
29
+ },
30
+ docker: {
31
+ compose_file: "auto",
32
+ safe_down: true,
33
+ rebuild: true,
34
+ prune: false,
35
+ },
36
+ checks: {
37
+ default: ["lint", "format", "test"],
38
+ },
39
+ output: {
40
+ report_dir: ".autofix/reports",
41
+ snapshot_dir: ".autofix/snapshots",
42
+ verbosity: "normal",
43
+ },
44
+ };
@@ -0,0 +1,44 @@
1
+ import path from "node:path";
2
+ import { readFile } from "node:fs/promises";
3
+ import { parse } from "yaml";
4
+ import { defaultConfig } from "./defaults.js";
5
+ import { fileExists } from "../utils/fs.js";
6
+ function mergeDeep(target, source) {
7
+ const output = { ...target };
8
+ for (const [key, value] of Object.entries(source)) {
9
+ if (Array.isArray(value)) {
10
+ output[key] = value;
11
+ continue;
12
+ }
13
+ if (value && typeof value === "object") {
14
+ const current = output[key];
15
+ output[key] = mergeDeep((current ?? {}), value);
16
+ continue;
17
+ }
18
+ output[key] = value;
19
+ }
20
+ return output;
21
+ }
22
+ async function findConfigPath(cwd) {
23
+ let current = cwd;
24
+ while (true) {
25
+ const candidate = path.join(current, ".autofix.yml");
26
+ if (await fileExists(candidate))
27
+ return candidate;
28
+ const gitDir = path.join(current, ".git");
29
+ if (await fileExists(gitDir))
30
+ return null;
31
+ const parent = path.dirname(current);
32
+ if (parent === current)
33
+ return null;
34
+ current = parent;
35
+ }
36
+ }
37
+ export async function loadConfig(cwd) {
38
+ const cfgPath = await findConfigPath(cwd);
39
+ if (!cfgPath)
40
+ return { config: defaultConfig, path: null };
41
+ const raw = await readFile(cfgPath, "utf8");
42
+ const user = parse(raw);
43
+ return { config: mergeDeep(defaultConfig, user), path: cfgPath };
44
+ }
@@ -0,0 +1,93 @@
1
+ import path from "node:path";
2
+ import { readFile } from "node:fs/promises";
3
+ import { parse as parseYaml } from "yaml";
4
+ import { fileExists } from "../utils/fs.js";
5
+ async function detectPackageManager(cwd) {
6
+ if (await fileExists(path.join(cwd, "pnpm-lock.yaml")))
7
+ return "pnpm";
8
+ if (await fileExists(path.join(cwd, "yarn.lock")))
9
+ return "yarn";
10
+ if (await fileExists(path.join(cwd, "package-lock.json")))
11
+ return "npm";
12
+ return "unknown";
13
+ }
14
+ async function detectLockfileCorruption(cwd) {
15
+ const pkgLock = path.join(cwd, "package-lock.json");
16
+ if (await fileExists(pkgLock)) {
17
+ try {
18
+ JSON.parse(await readFile(pkgLock, "utf8"));
19
+ }
20
+ catch {
21
+ return true;
22
+ }
23
+ }
24
+ const pnpmLock = path.join(cwd, "pnpm-lock.yaml");
25
+ if (await fileExists(pnpmLock)) {
26
+ try {
27
+ parseYaml(await readFile(pnpmLock, "utf8"));
28
+ }
29
+ catch {
30
+ return true;
31
+ }
32
+ }
33
+ return false;
34
+ }
35
+ export async function detectEnvironment(cwd, config) {
36
+ const packageJsonPath = path.join(cwd, "package.json");
37
+ const hasPackage = await fileExists(packageJsonPath);
38
+ let scripts = [];
39
+ let hasNext = false;
40
+ let hasVite = false;
41
+ if (hasPackage) {
42
+ try {
43
+ const parsed = JSON.parse(await readFile(packageJsonPath, "utf8"));
44
+ scripts = Object.keys(parsed.scripts ?? {});
45
+ const deps = { ...(parsed.dependencies ?? {}), ...(parsed.devDependencies ?? {}) };
46
+ hasNext = Boolean(deps.next) || scripts.some((s) => s.toLowerCase().includes("next"));
47
+ hasVite = Boolean(deps.vite) || scripts.some((s) => s.toLowerCase().includes("vite"));
48
+ }
49
+ catch {
50
+ // Ignore malformed package file in detection phase.
51
+ }
52
+ }
53
+ const requirements = (await fileExists(path.join(cwd, "requirements.txt"))) ||
54
+ (await fileExists(path.join(cwd, "requirements-dev.txt")));
55
+ const hasPyproject = await fileExists(path.join(cwd, "pyproject.toml"));
56
+ const venvPath = path.join(cwd, config.python.venv_path);
57
+ const venvExists = await fileExists(venvPath);
58
+ const composeCandidates = ["docker-compose.yml", "compose.yml", "docker-compose.yaml", "compose.yaml"];
59
+ const composeFile = (await Promise.all(composeCandidates.map(async (name) => ((await fileExists(path.join(cwd, name))) ? name : null)))).find((v) => Boolean(v)) ?? undefined;
60
+ const lockfileCandidates = ["package-lock.json", "pnpm-lock.yaml", "yarn.lock"];
61
+ const lockfiles = (await Promise.all(lockfileCandidates.map(async (name) => ((await fileExists(path.join(cwd, name))) ? name : null)))).filter((v) => Boolean(v));
62
+ const lockfileCorrupted = await detectLockfileCorruption(cwd);
63
+ const issues = [];
64
+ if (hasPackage && !(await fileExists(path.join(cwd, "node_modules"))))
65
+ issues.push("node_modules directory missing");
66
+ if ((hasPyproject || requirements) && !venvExists)
67
+ issues.push("python virtual environment missing");
68
+ if (composeFile)
69
+ issues.push("docker compose project detected (state may require refresh)");
70
+ if (lockfileCorrupted)
71
+ issues.push("lockfile appears corrupted; frozen installs likely to fail");
72
+ return {
73
+ node: {
74
+ detected: hasPackage,
75
+ packageManager: await detectPackageManager(cwd),
76
+ hasNodeModules: await fileExists(path.join(cwd, "node_modules")),
77
+ hasNext,
78
+ hasVite,
79
+ lockfiles,
80
+ lockfileCorrupted,
81
+ packageScripts: scripts,
82
+ },
83
+ python: {
84
+ detected: hasPyproject || requirements,
85
+ hasPyproject,
86
+ hasRequirements: requirements,
87
+ venvPath: config.python.venv_path,
88
+ venvExists,
89
+ },
90
+ docker: { detected: Boolean(composeFile), composeFile },
91
+ issues,
92
+ };
93
+ }
@@ -0,0 +1,124 @@
1
+ import path from "node:path";
2
+ import readline from "node:readline/promises";
3
+ import { setTimeout as sleep } from "node:timers/promises";
4
+ import { runShellCommand } from "../utils/process.js";
5
+ import { canAutoRunDestructive, shouldPromptForDestructive } from "./safety.js";
6
+ import { snapshotPathsForStep } from "./snapshots.js";
7
+ async function askConfirmation(question) {
8
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
9
+ const answer = (await rl.question(`${question} [y/N]: `)).trim().toLowerCase();
10
+ rl.close();
11
+ return answer === "y" || answer === "yes";
12
+ }
13
+ function snapshotCandidates(step) {
14
+ if (step.id.includes("lockfiles"))
15
+ return ["package-lock.json", "pnpm-lock.yaml", "yarn.lock"];
16
+ return [];
17
+ }
18
+ async function verifyPortsReleased(cwd, commands) {
19
+ const ports = commands
20
+ .map((c) => c.match(/:(\d+)/)?.[1])
21
+ .filter((v) => Boolean(v));
22
+ const maxMs = 2000;
23
+ const intervalMs = 100;
24
+ let elapsed = 0;
25
+ while (elapsed <= maxMs) {
26
+ const checks = await Promise.all(ports.map((p) => runShellCommand(`lsof -ti :${p}`, cwd)));
27
+ const busy = checks
28
+ .map((result, idx) => ({ port: ports[idx], pids: result.stdout.trim().split(/\s+/).filter(Boolean) }))
29
+ .filter((x) => x.pids.length > 0);
30
+ if (busy.length === 0) {
31
+ await sleep(150);
32
+ return { ok: true, details: "ports confirmed free" };
33
+ }
34
+ await sleep(intervalMs);
35
+ elapsed += intervalMs;
36
+ }
37
+ const remaining = await Promise.all(ports.map((p) => runShellCommand(`lsof -ti :${p}`, cwd)));
38
+ const detail = remaining
39
+ .map((r, i) => `${ports[i]}: ${r.stdout.trim() || "none"}`)
40
+ .join(", ");
41
+ return { ok: false, details: `remaining pid(s) -> ${detail}. Try: lsof -ti :<port> | xargs kill -9` };
42
+ }
43
+ export async function executeSteps(ctx, steps, snapshotDir, hooks) {
44
+ const output = [];
45
+ const confirmFn = hooks?.onConfirm ?? askConfirmation;
46
+ for (const step of steps) {
47
+ const current = { ...step };
48
+ if (ctx.flags.verbose)
49
+ console.log(`[phase:${step.phase}] ${step.title}`);
50
+ if (ctx.command === "doctor" || ctx.command === "plan" || ctx.flags.dryRun) {
51
+ current.status = step.status === "proposed" ? "proposed" : "planned";
52
+ output.push(current);
53
+ continue;
54
+ }
55
+ if (step.destructive && !canAutoRunDestructive(ctx)) {
56
+ if (shouldPromptForDestructive(ctx, step)) {
57
+ const ok = await confirmFn(`Run destructive step: ${step.title}?`);
58
+ if (!ok) {
59
+ current.status = "proposed";
60
+ current.proposedReason = "Needs explicit approval";
61
+ hooks?.onStepEnd(current);
62
+ output.push(current);
63
+ continue;
64
+ }
65
+ }
66
+ else {
67
+ current.status = "proposed";
68
+ current.proposedReason = ctx.interactive
69
+ ? "Needs --deep or --approve"
70
+ : "Non-interactive mode: destructive step skipped";
71
+ hooks?.onStepEnd(current);
72
+ output.push(current);
73
+ continue;
74
+ }
75
+ }
76
+ current.status = "running";
77
+ hooks?.onStepStart(current);
78
+ if (step.destructive) {
79
+ const snaps = await snapshotPathsForStep(ctx.cwd, path.join(snapshotDir, ctx.runId), step.id, snapshotCandidates(step));
80
+ current.snapshotPaths = snaps;
81
+ }
82
+ let failed = false;
83
+ const commandOutputs = [];
84
+ for (const command of step.commands) {
85
+ if (ctx.flags.verbose)
86
+ console.log(` \u25B8 ${command}`);
87
+ const result = await runShellCommand(command, ctx.cwd);
88
+ commandOutputs.push(`$ ${command}\n${result.stdout}${result.stderr}`.trim());
89
+ if (ctx.flags.verbose) {
90
+ if (result.stdout)
91
+ console.log(result.stdout.trimEnd());
92
+ if (result.stderr)
93
+ console.error(result.stderr.trimEnd());
94
+ }
95
+ if (!result.success) {
96
+ failed = true;
97
+ break;
98
+ }
99
+ }
100
+ if (!failed && step.id === "ports-cleanup") {
101
+ const portCheck = await verifyPortsReleased(ctx.cwd, step.commands);
102
+ if (!portCheck.ok) {
103
+ current.status = "failed";
104
+ current.error = portCheck.details;
105
+ current.output = commandOutputs.join("\n\n");
106
+ hooks?.onStepEnd(current);
107
+ output.push(current);
108
+ continue;
109
+ }
110
+ commandOutputs.push(portCheck.details);
111
+ }
112
+ current.output = commandOutputs.join("\n\n");
113
+ if (failed) {
114
+ current.status = "failed";
115
+ current.error = "One or more commands failed";
116
+ }
117
+ else {
118
+ current.status = "success";
119
+ }
120
+ hooks?.onStepEnd(current);
121
+ output.push(current);
122
+ }
123
+ return output;
124
+ }
@@ -0,0 +1,45 @@
1
+ import { buildNodeSteps } from "../subsystems/node.js";
2
+ import { buildPythonSteps } from "../subsystems/python.js";
3
+ import { buildDockerSteps } from "../subsystems/docker.js";
4
+ import { buildCheckSteps } from "../subsystems/checks.js";
5
+ export function buildPortSteps(flags, config) {
6
+ if (!flags.killPorts)
7
+ return [];
8
+ const ports = flags.killPorts.length > 0 ? flags.killPorts : [...config.ports.default, ...config.ports.extra];
9
+ if (ports.length === 0)
10
+ return [];
11
+ return [
12
+ {
13
+ id: "ports-cleanup",
14
+ title: "Kill processes using configured ports",
15
+ subsystem: "meta",
16
+ phase: "ports",
17
+ rationale: "Port conflict cleanup before subsystem repair.",
18
+ commands: ports.map((port) => `lsof -ti :${port} | xargs kill -9`),
19
+ destructive: false,
20
+ irreversible: false,
21
+ undoable: false,
22
+ status: "planned",
23
+ },
24
+ ];
25
+ }
26
+ export function buildPlan(detection, config, flags) {
27
+ const steps = [];
28
+ // strict order: ports -> docker -> node -> python -> checks(format, lint, test)
29
+ steps.push(...buildPortSteps(flags, config));
30
+ if (flags.focus === "all" || flags.focus === "docker")
31
+ steps.push(...buildDockerSteps(detection, config, flags));
32
+ if (flags.focus === "all" || flags.focus === "node")
33
+ steps.push(...buildNodeSteps(detection, config, flags));
34
+ if (flags.focus === "all" || flags.focus === "python")
35
+ steps.push(...buildPythonSteps(detection, config, flags));
36
+ const checks = [];
37
+ if (flags.focus === "all" || flags.focus === "node")
38
+ checks.push(...buildCheckSteps(detection, config, flags, "node"));
39
+ if (flags.focus === "all" || flags.focus === "python")
40
+ checks.push(...buildCheckSteps(detection, config, flags, "python"));
41
+ const order = { format: 1, lint: 2, test: 3 };
42
+ checks.sort((a, b) => (order[a.checkKind ?? "test"] - order[b.checkKind ?? "test"]));
43
+ steps.push(...checks);
44
+ return steps;
45
+ }
@@ -0,0 +1,125 @@
1
+ import path from "node:path";
2
+ import readline from "node:readline/promises";
3
+ import { tmpdir } from "node:os";
4
+ import { detectEnvironment } from "./detectEnvironment.js";
5
+ import { buildPlan } from "./planBuilder.js";
6
+ import { executeSteps } from "./executor.js";
7
+ import { ensureAutofixInGitignore, ensureWritableDir } from "../utils/fs.js";
8
+ function suggestNextAction(detection) {
9
+ if (detection.node.detected)
10
+ return "npm run dev";
11
+ if (detection.python.detected)
12
+ return "python -m pytest -q";
13
+ if (detection.docker.detected)
14
+ return "docker compose ps";
15
+ return "Review project setup and rerun auto-fix doctor";
16
+ }
17
+ function summarizeDetection(detection) {
18
+ const out = [];
19
+ if (detection.node.detected)
20
+ out.push("Node");
21
+ if (detection.python.detected)
22
+ out.push("Python");
23
+ if (detection.docker.detected)
24
+ out.push("Docker Compose");
25
+ if (out.length === 0)
26
+ out.push("No supported project type detected");
27
+ return out;
28
+ }
29
+ async function maybeConfirmPolyglot(ctx, detection) {
30
+ const count = [detection.node.detected, detection.python.detected, detection.docker.detected].filter(Boolean).length;
31
+ if (ctx.flags.focus !== "all" || count < 2)
32
+ return true;
33
+ if (ctx.flags.approve)
34
+ return true;
35
+ if (!ctx.interactive)
36
+ return false;
37
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
38
+ const answer = (await rl.question("Detected Node + Python + Docker scope. Run all subsystems? (Y/n): ")).trim().toLowerCase();
39
+ rl.close();
40
+ return !(answer === "n" || answer === "no");
41
+ }
42
+ function applyPolyglotGuard(ctx, detection, steps) {
43
+ const warnings = [];
44
+ const count = [detection.node.detected, detection.python.detected, detection.docker.detected].filter(Boolean).length;
45
+ if (count < 2 || ctx.flags.focus !== "all")
46
+ return { steps, warnings };
47
+ if (!ctx.interactive && !ctx.flags.approve) {
48
+ warnings.push("Non-interactive polyglot run defaulted to safe minimal set (ports + caches only)");
49
+ const filtered = steps.filter((s) => s.phase === "ports" || s.id.includes("clean-cache") || s.id.includes("next-cache") || s.id.includes("vite-cache"));
50
+ return { steps: filtered, warnings };
51
+ }
52
+ if (!ctx.flags.approve) {
53
+ const heavy = steps.filter((s) => s.phase === "docker" || s.id.includes("install-deps") || s.id.includes("reset-venv") || s.id.includes("remove-node-modules"));
54
+ if (heavy.length > 2)
55
+ warnings.push("Heavy polyglot plan detected. Use --approve for full deep multi-ecosystem cleanup.");
56
+ }
57
+ return { steps, warnings };
58
+ }
59
+ function computeSummary(steps, detection, warnings) {
60
+ const succeeded = steps.filter((s) => s.status === "success").length;
61
+ const failed = steps.filter((s) => s.status === "failed" || s.status === "partial").length;
62
+ const skipped = steps.filter((s) => s.status === "skipped" || s.status === "proposed" || s.status === "planned").length;
63
+ const irreversibleStepIds = steps.filter((s) => s.irreversible && (s.status === "success" || s.status === "proposed" || s.status === "planned")).map((s) => s.id);
64
+ return {
65
+ detectedEnvironment: summarizeDetection(detection),
66
+ actions: steps.map((s) => `${s.status}: ${s.title}`),
67
+ succeeded,
68
+ failed,
69
+ skipped,
70
+ nextBestAction: suggestNextAction(detection),
71
+ undoCoverage: irreversibleStepIds.length > 0 ? "partial" : "full",
72
+ irreversibleStepIds,
73
+ warnings,
74
+ };
75
+ }
76
+ export async function runAutoFix(ctx, config, reportDir, callbacks) {
77
+ const detection = await detectEnvironment(ctx.cwd, config);
78
+ callbacks?.onDetection?.(detection);
79
+ const runAll = await maybeConfirmPolyglot(ctx, detection);
80
+ if (!runAll) {
81
+ ctx.flags.focus = "node";
82
+ }
83
+ const plan = buildPlan(detection, config, ctx.flags);
84
+ const guarded = applyPolyglotGuard(ctx, detection, plan);
85
+ callbacks?.onPlanReady?.(guarded.steps);
86
+ const desiredSnapshotDir = path.resolve(ctx.cwd, config.output.snapshot_dir);
87
+ const writable = await ensureWritableDir(desiredSnapshotDir);
88
+ const snapshotDir = writable ? desiredSnapshotDir : path.join(tmpdir(), "autofix", ctx.runId);
89
+ if (!writable)
90
+ await ensureWritableDir(snapshotDir);
91
+ const executed = await executeSteps(ctx, guarded.steps, snapshotDir, callbacks?.stepHooks);
92
+ const gitignoreUpdated = await ensureAutofixInGitignore(ctx.cwd);
93
+ if (gitignoreUpdated)
94
+ guarded.warnings.push("Added .autofix/ to .gitignore");
95
+ if (!writable)
96
+ guarded.warnings.push(`.autofix not writable; using temp snapshot dir: ${snapshotDir}`);
97
+ const report = {
98
+ runId: ctx.runId,
99
+ command: ctx.command,
100
+ cwd: ctx.cwd,
101
+ startedAt: new Date().toISOString(),
102
+ finishedAt: new Date().toISOString(),
103
+ flags: ctx.flags,
104
+ detection,
105
+ steps: executed,
106
+ summary: computeSummary(executed, detection, guarded.warnings),
107
+ undo: executed
108
+ .filter((s) => s.snapshotPaths && s.snapshotPaths.length > 0)
109
+ .map((s) => ({
110
+ stepId: s.id,
111
+ snapshotPaths: s.snapshotPaths ?? [],
112
+ restored: [],
113
+ skipped: [],
114
+ missingSnapshot: [],
115
+ failed: [],
116
+ nextBestAction: s.undoHints?.[0]?.command,
117
+ })),
118
+ storage: {
119
+ reportDir,
120
+ snapshotDir,
121
+ fallbackToTemp: !writable,
122
+ },
123
+ };
124
+ return { report, gitignoreUpdated };
125
+ }
@@ -0,0 +1,9 @@
1
+ export function needsApproval(step) {
2
+ return step.destructive;
3
+ }
4
+ export function canAutoRunDestructive(ctx) {
5
+ return ctx.flags.deep || ctx.flags.approve;
6
+ }
7
+ export function shouldPromptForDestructive(ctx, step) {
8
+ return needsApproval(step) && !canAutoRunDestructive(ctx) && ctx.interactive;
9
+ }
@@ -0,0 +1,18 @@
1
+ import path from "node:path";
2
+ import { cp } from "node:fs/promises";
3
+ import { ensureDir, fileExists } from "../utils/fs.js";
4
+ export async function snapshotPathsForStep(cwd, snapshotRoot, stepId, candidates) {
5
+ const created = [];
6
+ for (const candidate of candidates) {
7
+ if (candidate === "node_modules")
8
+ continue;
9
+ const source = path.join(cwd, candidate);
10
+ if (!(await fileExists(source)))
11
+ continue;
12
+ const dest = path.join(snapshotRoot, stepId, candidate.replace(/[\\/:]/g, "_"));
13
+ await ensureDir(path.dirname(dest));
14
+ await cp(source, dest, { recursive: true, force: true });
15
+ created.push(dest);
16
+ }
17
+ return created;
18
+ }
@@ -0,0 +1,53 @@
1
+ import path from "node:path";
2
+ import { cp } from "node:fs/promises";
3
+ import { fileExists, readJsonFile } from "../utils/fs.js";
4
+ export async function undoLatest(reportPath, cwd) {
5
+ const report = await readJsonFile(reportPath);
6
+ if (!report)
7
+ return { report: null, entries: [] };
8
+ const entries = [];
9
+ for (const step of report.steps) {
10
+ const restored = [];
11
+ const failed = [];
12
+ const skipped = [];
13
+ const missingSnapshot = [];
14
+ if (!step.undoable || !step.snapshotPaths || step.snapshotPaths.length === 0) {
15
+ skipped.push("not undoable or no snapshot");
16
+ entries.push({
17
+ stepId: step.id,
18
+ snapshotPaths: step.snapshotPaths ?? [],
19
+ restored,
20
+ skipped,
21
+ missingSnapshot,
22
+ failed,
23
+ nextBestAction: step.undoHints?.[0]?.command,
24
+ });
25
+ continue;
26
+ }
27
+ for (const snap of step.snapshotPaths) {
28
+ if (!(await fileExists(snap))) {
29
+ missingSnapshot.push(snap);
30
+ continue;
31
+ }
32
+ const base = path.basename(snap);
33
+ const target = path.join(cwd, base.replace(/_/g, "/"));
34
+ try {
35
+ await cp(snap, target, { recursive: true, force: true });
36
+ restored.push(target);
37
+ }
38
+ catch {
39
+ failed.push(target);
40
+ }
41
+ }
42
+ entries.push({
43
+ stepId: step.id,
44
+ snapshotPaths: step.snapshotPaths,
45
+ restored,
46
+ skipped,
47
+ missingSnapshot,
48
+ failed,
49
+ nextBestAction: step.undoHints?.[0]?.command,
50
+ });
51
+ }
52
+ return { report, entries };
53
+ }
@@ -0,0 +1,73 @@
1
+ import { style } from "../utils/colors.js";
2
+ function modeBanner(command, flags, useColor) {
3
+ const c = style(useColor);
4
+ const tags = [];
5
+ if (flags.dryRun && command !== "plan")
6
+ tags.push("dry-run");
7
+ if (flags.deep)
8
+ tags.push("deep");
9
+ if (flags.approve)
10
+ tags.push("approve");
11
+ if (flags.forceFresh)
12
+ tags.push("force-fresh");
13
+ if (flags.focus !== "all")
14
+ tags.push(`focus:${flags.focus}`);
15
+ if (flags.killPorts)
16
+ tags.push("kill-ports");
17
+ if (flags.verbose)
18
+ tags.push("verbose");
19
+ if (flags.quiet)
20
+ tags.push("quiet");
21
+ const tagStr = tags.length > 0 ? ` ${c.dim(`[${tags.join(", ")}]`)}` : "";
22
+ return c.strong(`auto-fix · ${command}`) + tagStr;
23
+ }
24
+ export function renderSummary(report, useColor) {
25
+ const c = style(useColor);
26
+ const lines = [];
27
+ lines.push(modeBanner(report.command, report.flags, useColor));
28
+ lines.push("");
29
+ lines.push(c.title("Detected environment"));
30
+ lines.push(`- ${report.summary.detectedEnvironment.join(", ") || "None"}`);
31
+ lines.push(c.title("Plan/Actions"));
32
+ if (report.steps.length === 0) {
33
+ lines.push("- No actions were planned.");
34
+ }
35
+ else {
36
+ for (const step of report.steps) {
37
+ const tags = [];
38
+ if (step.irreversible)
39
+ tags.push("IRREVERSIBLE");
40
+ const extra = step.proposedReason ? ` (${step.proposedReason})` : "";
41
+ lines.push(`- [${step.status}] ${step.id} ${step.title}${tags.length ? ` [${tags.join(",")}]` : ""}${extra}`);
42
+ if (step.irreversible && step.irreversibleReason) {
43
+ lines.push(` reason: ${step.irreversibleReason} | This will NOT be covered by undo.`);
44
+ }
45
+ }
46
+ }
47
+ lines.push(c.title("Results"));
48
+ lines.push(`- Success: ${report.summary.succeeded}, Failed: ${report.summary.failed}, Skipped/Proposed: ${report.summary.skipped}`);
49
+ if (report.summary.irreversibleStepIds.length > 0) {
50
+ lines.push(`- Undo coverage: partial (some actions irreversible)`);
51
+ lines.push(`- Irreversible steps: ${report.summary.irreversibleStepIds.join(", ")}`);
52
+ }
53
+ for (const warning of report.summary.warnings) {
54
+ lines.push(`- Warning: ${warning}`);
55
+ }
56
+ lines.push(c.title("Next best action"));
57
+ lines.push(`- ${report.summary.nextBestAction}`);
58
+ return lines.join("\n");
59
+ }
60
+ export function renderQuietSummary(report, useColor) {
61
+ const c = style(useColor);
62
+ const lines = [];
63
+ lines.push(modeBanner(report.command, report.flags, useColor));
64
+ lines.push("");
65
+ lines.push(c.title("Results"));
66
+ lines.push(`- Success: ${report.summary.succeeded}, Failed: ${report.summary.failed}, Skipped/Proposed: ${report.summary.skipped}`);
67
+ if (report.summary.irreversibleStepIds.length > 0) {
68
+ lines.push(`- Undo coverage: partial (some actions irreversible)`);
69
+ }
70
+ lines.push(c.title("Next best action"));
71
+ lines.push(`- ${report.summary.nextBestAction}`);
72
+ return lines.join("\n");
73
+ }
@@ -0,0 +1,9 @@
1
+ import path from "node:path";
2
+ import { writeJsonFile } from "../utils/fs.js";
3
+ export async function writeRunReport(report, reportDir) {
4
+ const runReportPath = path.join(reportDir, `${report.runId}.json`);
5
+ const latestPath = path.join(reportDir, "latest.json");
6
+ await writeJsonFile(runReportPath, report);
7
+ await writeJsonFile(latestPath, report);
8
+ return { runReportPath, latestPath };
9
+ }