@technicalshree/auto-fix 1.1.4 → 1.2.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.
- package/dist/core/detectEnvironment.js +10 -1
- package/dist/core/executor.js +10 -3
- package/dist/core/planBuilder.js +6 -2
- package/dist/core/run.js +4 -1
- package/dist/core/undo.js +7 -2
- package/dist/subsystems/engines.js +55 -0
- package/dist/subsystems/environment.js +71 -0
- package/dist/subsystems/node.js +1 -1
- package/dist/subsystems/python.js +21 -0
- package/package.json +1 -1
|
@@ -42,7 +42,7 @@ export async function detectEnvironment(cwd, config) {
|
|
|
42
42
|
try {
|
|
43
43
|
const parsed = JSON.parse(await readFile(packageJsonPath, "utf8"));
|
|
44
44
|
scripts = Object.keys(parsed.scripts ?? {});
|
|
45
|
-
const deps = { ...
|
|
45
|
+
const deps = { ...parsed.dependencies, ...parsed.devDependencies };
|
|
46
46
|
hasNext = Boolean(deps.next) || scripts.some((s) => s.toLowerCase().includes("next"));
|
|
47
47
|
hasVite = Boolean(deps.vite) || scripts.some((s) => s.toLowerCase().includes("vite"));
|
|
48
48
|
}
|
|
@@ -60,6 +60,11 @@ export async function detectEnvironment(cwd, config) {
|
|
|
60
60
|
const lockfileCandidates = ["package-lock.json", "pnpm-lock.yaml", "yarn.lock"];
|
|
61
61
|
const lockfiles = (await Promise.all(lockfileCandidates.map(async (name) => ((await fileExists(path.join(cwd, name))) ? name : null)))).filter((v) => Boolean(v));
|
|
62
62
|
const lockfileCorrupted = await detectLockfileCorruption(cwd);
|
|
63
|
+
const hasEnv = await fileExists(path.join(cwd, ".env"));
|
|
64
|
+
const hasEnvExample = await fileExists(path.join(cwd, ".env.example"));
|
|
65
|
+
const nodeVersionCandidates = [".nvmrc", ".node-version"];
|
|
66
|
+
const nodeVersionFile = (await Promise.all(nodeVersionCandidates.map(async (name) => ((await fileExists(path.join(cwd, name))) ? name : null)))).find((v) => Boolean(v)) ?? undefined;
|
|
67
|
+
const pythonVersionFile = (await fileExists(path.join(cwd, ".python-version"))) ? ".python-version" : undefined;
|
|
63
68
|
const issues = [];
|
|
64
69
|
if (hasPackage && !(await fileExists(path.join(cwd, "node_modules"))))
|
|
65
70
|
issues.push("node_modules directory missing");
|
|
@@ -69,6 +74,8 @@ export async function detectEnvironment(cwd, config) {
|
|
|
69
74
|
issues.push("docker compose project detected (state may require refresh)");
|
|
70
75
|
if (lockfileCorrupted)
|
|
71
76
|
issues.push("lockfile appears corrupted; frozen installs likely to fail");
|
|
77
|
+
if (hasEnvExample && !hasEnv)
|
|
78
|
+
issues.push(".env file missing but .env.example exists");
|
|
72
79
|
return {
|
|
73
80
|
node: {
|
|
74
81
|
detected: hasPackage,
|
|
@@ -88,6 +95,8 @@ export async function detectEnvironment(cwd, config) {
|
|
|
88
95
|
venvExists,
|
|
89
96
|
},
|
|
90
97
|
docker: { detected: Boolean(composeFile), composeFile },
|
|
98
|
+
environment: { hasEnv, hasEnvExample },
|
|
99
|
+
engines: { nodeVersionFile, pythonVersionFile },
|
|
91
100
|
issues,
|
|
92
101
|
};
|
|
93
102
|
}
|
package/dist/core/executor.js
CHANGED
|
@@ -75,8 +75,9 @@ export async function executeSteps(ctx, steps, snapshotDir, hooks) {
|
|
|
75
75
|
}
|
|
76
76
|
current.status = "running";
|
|
77
77
|
hooks?.onStepStart(current);
|
|
78
|
-
if (step.destructive) {
|
|
79
|
-
const
|
|
78
|
+
if (step.destructive || (step.snapshotPaths && step.snapshotPaths.length > 0)) {
|
|
79
|
+
const candidates = step.destructive ? snapshotCandidates(step) : step.snapshotPaths ?? [];
|
|
80
|
+
const snaps = await snapshotPathsForStep(ctx.cwd, path.join(snapshotDir, ctx.runId), step.id, candidates);
|
|
80
81
|
current.snapshotPaths = snaps;
|
|
81
82
|
}
|
|
82
83
|
let failed = false;
|
|
@@ -112,7 +113,13 @@ export async function executeSteps(ctx, steps, snapshotDir, hooks) {
|
|
|
112
113
|
current.output = commandOutputs.join("\n\n");
|
|
113
114
|
if (failed) {
|
|
114
115
|
current.status = "failed";
|
|
115
|
-
|
|
116
|
+
const combinedOutput = commandOutputs.join(" ");
|
|
117
|
+
if (/EPERM|EBUSY|EACCES/.test(combinedOutput) && (step.subsystem === "node" || step.phase === "node")) {
|
|
118
|
+
current.error = "Permission/lock error (EPERM/EBUSY). Close your IDE, dev servers, or file watchers and retry.";
|
|
119
|
+
}
|
|
120
|
+
else {
|
|
121
|
+
current.error = "One or more commands failed";
|
|
122
|
+
}
|
|
116
123
|
}
|
|
117
124
|
else {
|
|
118
125
|
current.status = "success";
|
package/dist/core/planBuilder.js
CHANGED
|
@@ -2,6 +2,8 @@ import { buildNodeSteps } from "../subsystems/node.js";
|
|
|
2
2
|
import { buildPythonSteps } from "../subsystems/python.js";
|
|
3
3
|
import { buildDockerSteps } from "../subsystems/docker.js";
|
|
4
4
|
import { buildCheckSteps } from "../subsystems/checks.js";
|
|
5
|
+
import { buildEnvSteps } from "../subsystems/environment.js";
|
|
6
|
+
import { buildEngineSteps } from "../subsystems/engines.js";
|
|
5
7
|
export function buildPortSteps(flags, config) {
|
|
6
8
|
if (!flags.killPorts)
|
|
7
9
|
return [];
|
|
@@ -23,9 +25,11 @@ export function buildPortSteps(flags, config) {
|
|
|
23
25
|
},
|
|
24
26
|
];
|
|
25
27
|
}
|
|
26
|
-
export function buildPlan(detection, config, flags) {
|
|
28
|
+
export async function buildPlan(cwd, detection, config, flags) {
|
|
27
29
|
const steps = [];
|
|
28
|
-
// strict order: ports -> docker -> node -> python -> checks(format, lint, test)
|
|
30
|
+
// strict order: env -> engines -> ports -> docker -> node -> python -> checks(format, lint, test)
|
|
31
|
+
steps.push(...await buildEnvSteps(cwd, detection, config, flags));
|
|
32
|
+
steps.push(...await buildEngineSteps(cwd, detection, config, flags));
|
|
29
33
|
steps.push(...buildPortSteps(flags, config));
|
|
30
34
|
if (flags.focus === "all" || flags.focus === "docker")
|
|
31
35
|
steps.push(...buildDockerSteps(detection, config, flags));
|
package/dist/core/run.js
CHANGED
|
@@ -80,7 +80,7 @@ export async function runAutoFix(ctx, config, reportDir, callbacks) {
|
|
|
80
80
|
if (!runAll) {
|
|
81
81
|
ctx.flags.focus = "node";
|
|
82
82
|
}
|
|
83
|
-
const plan = buildPlan(detection, config, ctx.flags);
|
|
83
|
+
const plan = await buildPlan(ctx.cwd, detection, config, ctx.flags);
|
|
84
84
|
const guarded = applyPolyglotGuard(ctx, detection, plan);
|
|
85
85
|
callbacks?.onPlanReady?.(guarded.steps);
|
|
86
86
|
const desiredSnapshotDir = path.resolve(ctx.cwd, config.output.snapshot_dir);
|
|
@@ -94,6 +94,9 @@ export async function runAutoFix(ctx, config, reportDir, callbacks) {
|
|
|
94
94
|
guarded.warnings.push("Added .autofix/ to .gitignore");
|
|
95
95
|
if (!writable)
|
|
96
96
|
guarded.warnings.push(`.autofix not writable; using temp snapshot dir: ${snapshotDir}`);
|
|
97
|
+
if (config.docker.safe_down && guarded.steps.some((s) => s.checkKind === "test")) {
|
|
98
|
+
guarded.warnings.push("Tests may require services; run `docker compose up -d` and re-run tests.");
|
|
99
|
+
}
|
|
97
100
|
const report = {
|
|
98
101
|
runId: ctx.runId,
|
|
99
102
|
command: ctx.command,
|
package/dist/core/undo.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
|
-
import { cp } from "node:fs/promises";
|
|
2
|
+
import { cp, mkdir } from "node:fs/promises";
|
|
3
3
|
import { fileExists, readJsonFile } from "../utils/fs.js";
|
|
4
4
|
export async function undoLatest(reportPath, cwd) {
|
|
5
5
|
const report = await readJsonFile(reportPath);
|
|
@@ -29,9 +29,14 @@ export async function undoLatest(reportPath, cwd) {
|
|
|
29
29
|
missingSnapshot.push(snap);
|
|
30
30
|
continue;
|
|
31
31
|
}
|
|
32
|
+
// The snapshot filename encodes the original relative path with separators replaced by `_`.
|
|
33
|
+
// Extract just the filename portion (after the stepId directory) and reverse the encoding.
|
|
32
34
|
const base = path.basename(snap);
|
|
33
|
-
const
|
|
35
|
+
const relativePath = base.replace(/_/g, path.sep);
|
|
36
|
+
const target = path.join(cwd, relativePath);
|
|
34
37
|
try {
|
|
38
|
+
const targetDir = path.dirname(target);
|
|
39
|
+
await mkdir(targetDir, { recursive: true });
|
|
35
40
|
await cp(snap, target, { recursive: true, force: true });
|
|
36
41
|
restored.push(target);
|
|
37
42
|
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { readFile } from "node:fs/promises";
|
|
3
|
+
async function readVersionFile(cwd, fileName) {
|
|
4
|
+
try {
|
|
5
|
+
const raw = await readFile(path.join(cwd, fileName), "utf8");
|
|
6
|
+
return raw.trim();
|
|
7
|
+
}
|
|
8
|
+
catch {
|
|
9
|
+
return null;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
export async function buildEngineSteps(cwd, detection, config, flags) {
|
|
13
|
+
const steps = [];
|
|
14
|
+
if (detection.engines.nodeVersionFile) {
|
|
15
|
+
const desired = await readVersionFile(cwd, detection.engines.nodeVersionFile);
|
|
16
|
+
if (desired) {
|
|
17
|
+
const matchMajor = desired.replace(/^v/, "").split(".")[0];
|
|
18
|
+
const actualMajor = process.version.replace(/^v/, "").split(".")[0];
|
|
19
|
+
if (matchMajor !== actualMajor && matchMajor !== "") {
|
|
20
|
+
steps.push({
|
|
21
|
+
id: "engines-node-version-mismatch",
|
|
22
|
+
title: `Node version drift detected: expected ~${matchMajor}, running ${process.version}`,
|
|
23
|
+
subsystem: "engines",
|
|
24
|
+
phase: "engines",
|
|
25
|
+
rationale: `Host Node.js version drifts from ${detection.engines.nodeVersionFile} definition.`,
|
|
26
|
+
commands: [],
|
|
27
|
+
destructive: false,
|
|
28
|
+
irreversible: false,
|
|
29
|
+
undoable: false,
|
|
30
|
+
status: "proposed",
|
|
31
|
+
proposedReason: `Run nvm use or switch Node versions to v${matchMajor}.x`,
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
if (detection.engines.pythonVersionFile) {
|
|
37
|
+
const desired = await readVersionFile(cwd, detection.engines.pythonVersionFile);
|
|
38
|
+
if (desired) {
|
|
39
|
+
steps.push({
|
|
40
|
+
id: "engines-python-version-mismatch",
|
|
41
|
+
title: `Python version drift: project expects ${desired}`,
|
|
42
|
+
subsystem: "engines",
|
|
43
|
+
phase: "engines",
|
|
44
|
+
rationale: `Host Python version should align with ${detection.engines.pythonVersionFile}.`,
|
|
45
|
+
commands: [],
|
|
46
|
+
destructive: false,
|
|
47
|
+
irreversible: false,
|
|
48
|
+
undoable: false,
|
|
49
|
+
status: "proposed",
|
|
50
|
+
proposedReason: `Verify your Python version matches ${desired}. Run: python3 --version`,
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return steps;
|
|
55
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { readFile } from "node:fs/promises";
|
|
3
|
+
async function parseEnvKeys(filePath) {
|
|
4
|
+
try {
|
|
5
|
+
const raw = await readFile(filePath, "utf8");
|
|
6
|
+
const keys = [];
|
|
7
|
+
for (const line of raw.split("\n")) {
|
|
8
|
+
const trimmed = line.trim();
|
|
9
|
+
if (!trimmed || trimmed.startsWith("#"))
|
|
10
|
+
continue;
|
|
11
|
+
const idx = trimmed.indexOf("=");
|
|
12
|
+
if (idx > 0) {
|
|
13
|
+
keys.push(trimmed.substring(0, idx).trim());
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
return keys;
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
return [];
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
async function findMissingKeys(envPath, envExamplePath) {
|
|
23
|
+
const envKeys = new Set(await parseEnvKeys(envPath));
|
|
24
|
+
const exampleKeys = await parseEnvKeys(envExamplePath);
|
|
25
|
+
return exampleKeys.filter((k) => !envKeys.has(k));
|
|
26
|
+
}
|
|
27
|
+
export async function buildEnvSteps(cwd, detection, config, flags) {
|
|
28
|
+
if (!detection.environment.hasEnvExample)
|
|
29
|
+
return [];
|
|
30
|
+
const steps = [];
|
|
31
|
+
const envPath = path.join(cwd, ".env");
|
|
32
|
+
const envExamplePath = path.join(cwd, ".env.example");
|
|
33
|
+
if (!detection.environment.hasEnv) {
|
|
34
|
+
steps.push({
|
|
35
|
+
id: "env-sync-copy-example",
|
|
36
|
+
title: "Copy .env.example to .env",
|
|
37
|
+
subsystem: "environment",
|
|
38
|
+
phase: "environment",
|
|
39
|
+
rationale: ".env is missing but .env.example exists.",
|
|
40
|
+
commands: [`cp ${envExamplePath} ${envPath}`],
|
|
41
|
+
destructive: false,
|
|
42
|
+
irreversible: false,
|
|
43
|
+
undoable: true,
|
|
44
|
+
snapshotPaths: [".env.example"], // Snapshot the example file as breadcrumb
|
|
45
|
+
undoHints: [{ action: "Delete .env", command: `rm -f ${envPath}` }],
|
|
46
|
+
status: "planned",
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
else {
|
|
50
|
+
const missingKeys = await findMissingKeys(envPath, envExamplePath);
|
|
51
|
+
if (missingKeys.length > 0) {
|
|
52
|
+
// Create a small shell script command to append missing keys safely
|
|
53
|
+
const appends = missingKeys.map((k) => `echo "${k}=" >> ${envPath}`).join(" && ");
|
|
54
|
+
steps.push({
|
|
55
|
+
id: "env-sync-append-missing",
|
|
56
|
+
title: `Append ${missingKeys.length} missing key(s) to .env`,
|
|
57
|
+
subsystem: "environment",
|
|
58
|
+
phase: "environment",
|
|
59
|
+
rationale: `.env is out of sync with .env.example (missing keys: ${missingKeys.join(", ")}).`,
|
|
60
|
+
commands: [appends],
|
|
61
|
+
destructive: false,
|
|
62
|
+
irreversible: false,
|
|
63
|
+
undoable: true,
|
|
64
|
+
snapshotPaths: [".env"],
|
|
65
|
+
undoHints: [{ action: "Restore original .env" }],
|
|
66
|
+
status: "planned",
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return steps;
|
|
71
|
+
}
|
package/dist/subsystems/node.js
CHANGED
|
@@ -110,7 +110,7 @@ export function buildNodeSteps(detection, config, flags) {
|
|
|
110
110
|
title: "IRREVERSIBLE: Remove node_modules for clean reinstall",
|
|
111
111
|
subsystem: "node",
|
|
112
112
|
phase: "node",
|
|
113
|
-
rationale: "Deep cleanup requested to
|
|
113
|
+
rationale: "Deep cleanup requested. If this fails with EPERM, close your IDE or dev servers to release file locks.",
|
|
114
114
|
commands: ["rm -rf node_modules", `${pm} install`],
|
|
115
115
|
destructive: true,
|
|
116
116
|
irreversible: true,
|
|
@@ -60,5 +60,26 @@ export function buildPythonSteps(detection, config, flags) {
|
|
|
60
60
|
status: "planned",
|
|
61
61
|
});
|
|
62
62
|
}
|
|
63
|
+
// PRD v1.2: IDE Integration Auto-Configuration for VS Code
|
|
64
|
+
// Only sync when .venv exists or is being created by a preceding step
|
|
65
|
+
const venvWillExist = detection.python.venvExists || steps.some((s) => s.id === "python-create-venv");
|
|
66
|
+
if (venvWillExist) {
|
|
67
|
+
steps.push({
|
|
68
|
+
id: "python-vscode-sync",
|
|
69
|
+
title: "Sync Python virtual environment with VS Code",
|
|
70
|
+
subsystem: "python",
|
|
71
|
+
phase: "python",
|
|
72
|
+
rationale: "VS Code needs to know the correct virtual environment path to prevent linting errors.",
|
|
73
|
+
commands: [
|
|
74
|
+
`mkdir -p .vscode && node -e "const fs=require('fs');const p='.vscode/settings.json';let s={};try{s=JSON.parse(fs.readFileSync(p,'utf8'))}catch(e){}s['python.defaultInterpreterPath']='${config.python.venv_path}';fs.writeFileSync(p,JSON.stringify(s,null,2))"`
|
|
75
|
+
],
|
|
76
|
+
destructive: false,
|
|
77
|
+
irreversible: false,
|
|
78
|
+
undoable: true,
|
|
79
|
+
snapshotPaths: [".vscode/settings.json"],
|
|
80
|
+
undoHints: [{ action: "Restore .vscode/settings.json" }],
|
|
81
|
+
status: "planned",
|
|
82
|
+
});
|
|
83
|
+
}
|
|
63
84
|
return steps;
|
|
64
85
|
}
|