@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.
@@ -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 = { ...(parsed.dependencies ?? {}), ...(parsed.devDependencies ?? {}) };
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
  }
@@ -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 snaps = await snapshotPathsForStep(ctx.cwd, path.join(snapshotDir, ctx.runId), step.id, snapshotCandidates(step));
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
- current.error = "One or more commands failed";
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";
@@ -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 target = path.join(cwd, base.replace(/_/g, "/"));
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
+ }
@@ -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 resolve dependency drift.",
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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@technicalshree/auto-fix",
3
- "version": "1.1.4",
3
+ "version": "1.2.0",
4
4
  "description": "Detect, diagnose, and safely fix common local dev environment issues",
5
5
  "main": "dist/cli.js",
6
6
  "repository": {