Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 10 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,12 +70,16 @@ If an older FixMap is installed globally, some npm/npx combinations on Windows r
npx -y @aryam/fixmap@latest doctor
```

Remove the stale copy with `npm uninstall -g @aryam/fixmap`, or use the unambiguous form:
Remove the stale copy with `npm uninstall -g @aryam/fixmap`, or test an exact version in an isolated prefix and invoke that prefix's shim directly. This PowerShell sequence cannot be redirected to an older package in the current directory or one of its parents:

```bash
npm exec --yes --package=@aryam/fixmap@0.8.3 -- fixmap --version
```powershell
$fixmapPrefix = Join-Path $env:TEMP "fixmap-cli-0.8.3"
npm install --global --prefix $fixmapPrefix @aryam/fixmap@0.8.3
& "$fixmapPrefix\fixmap.cmd" --version
```

On macOS or Linux, use `fixmapPrefix="$(mktemp -d)"`, install with the same `--prefix`, and run `"$fixmapPrefix/bin/fixmap" --version`.

| Command | Answers |
| --- | --- |
| [`fixmap plan`](#cli) | Which files, tests, and risks should I look at first? |
Expand Down Expand Up @@ -277,14 +281,14 @@ npx -y @aryam/fixmap@latest doctor
- ok Running version: 0.8.3
- PROBLEM Global install: 0.3.1 (this process is 0.8.3)
A globally installed fixmap shadows the version npx was asked for. Run
`npm uninstall -g @aryam/fixmap`, or invoke the exact version with
`npm exec --package=@aryam/fixmap@<version> -- fixmap <command>`.
`npm uninstall -g @aryam/fixmap` or update the global installation. For a
clean pinned run, use the isolated-prefix command above.
- ok Node version: 24.13.0
```

It exits non-zero when it finds a shadow, so a CI step fails rather than reading on.

Doctor can compare the running package, the first `fixmap` shim on `PATH`, and npm's global package. It cannot infer a version you intended in some other shell command or inspect every historical npm-exec cache entry; when reproducibility matters, use `npm exec --yes --package=@aryam/fixmap@0.8.3 -- fixmap --version` and confirm the printed version before continuing.
Doctor compares the running package, the first `fixmap` shim on `PATH`, npm's global package, and an exact version requested through npm exec. It exits non-zero if npm requested one version but an older local or ancestor install ran instead. It cannot infer a version intended in some unrelated shell command or inspect every historical npm-exec cache entry; when reproducibility matters, use the isolated-prefix command above and invoke its shim directly.

### MCP server

Expand Down
2 changes: 1 addition & 1 deletion packages/cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ npx @aryam/fixmap plan --base main --head HEAD --format json --output fixmap-rep

Public GitHub issue, pull request, and repository URL modes are available in the CLI and MCP server for issue-only analysis. FixMap fetches task context anonymously, shallow-clones the default branch into an isolated temporary directory, disables credentials and repository execution surfaces, and removes the checkout before returning. Clone locally to use `--diff`, `--base`, `--head`, or working-tree inputs.

For long task text, use `--issue-file task.md`, `--issue @task.md`, or pipe text to `--issue -`. If a stale global installation shadows a pinned npx package on Windows, run `fixmap doctor`, remove the stale install with `npm uninstall -g @aryam/fixmap`, or use `npm exec --yes --package=@aryam/fixmap@0.8.3 -- fixmap ...`.
For long task text, use `--issue-file task.md`, `--issue @task.md`, or pipe text to `--issue -`. If a stale global, local, or ancestor installation shadows a pinned package on Windows, run `fixmap doctor` and update or remove the stale install. For a reproducible clean test, install the exact version into an isolated npm prefix and invoke that prefix's `fixmap` shim directly; the repository README includes complete PowerShell and POSIX commands.

## MCP server

Expand Down
28 changes: 26 additions & 2 deletions packages/cli/src/doctor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ export type DoctorDependencies = {
globalVersion?: () => Promise<string | undefined>;
nodeVersion?: () => string;
modulePath?: () => string;
requestedPackage?: () => string | undefined;
};

export async function runDoctorChecks(dependencies: DoctorDependencies = {}): Promise<DoctorReport> {
Expand All @@ -46,6 +47,22 @@ export async function runDoctorChecks(dependencies: DoctorDependencies = {}): Pr
findings.push({ label: "Running version", value: runningVersion, ok: true });
findings.push({ label: "Resolved from", value: modulePath(), ok: true });

const requestedPackage = (dependencies.requestedPackage ?? (() => process.env.npm_config_package))();
const requestedVersion = exactRequestedVersion(requestedPackage);
if (requestedVersion && requestedVersion !== runningVersion) {
findings.push({
label: "Requested package",
value: `${requestedVersion} (this process is ${runningVersion})`,
ok: false,
advice:
`npm requested @aryam/fixmap@${requestedVersion} but ran ${runningVersion}, usually because a local ` +
"or ancestor node_modules install shadowed it. Update or remove that install, or use the " +
"isolated-prefix command in the README."
});
} else if (requestedVersion) {
findings.push({ label: "Requested package", value: `${requestedVersion} (matches)`, ok: true });
}

const binary = await (dependencies.resolveBinary ?? resolveBinary)("fixmap");
const globalVersion = await (dependencies.globalVersion ?? readGlobalVersion)();

Expand All @@ -63,8 +80,8 @@ export async function runDoctorChecks(dependencies: DoctorDependencies = {}): Pr
ok: false,
advice:
"A globally installed fixmap shadows the version npx was asked for. " +
"Run `npm uninstall -g @aryam/fixmap`, or invoke the exact version with " +
"`npm exec --package=@aryam/fixmap@<version> -- fixmap <command>`."
"Run `npm uninstall -g @aryam/fixmap` or update the global installation. " +
"For a clean pinned run, use the isolated-prefix command in the README."
});
} else if (globalVersion) {
findings.push({ label: "Global install", value: `${globalVersion} (matches)`, ok: true });
Expand All @@ -84,6 +101,13 @@ export async function runDoctorChecks(dependencies: DoctorDependencies = {}): Pr
return { findings, healthy: findings.every((finding) => finding.ok) };
}

function exactRequestedVersion(packageSpec: string | undefined): string | undefined {
const match = packageSpec?.match(
/^@aryam\/fixmap@(\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?)$/
);
return match?.[1];
}

export function renderDoctorReport(report: DoctorReport): string {
const lines = ["# FixMap Doctor", ""];

Expand Down
47 changes: 47 additions & 0 deletions packages/cli/test/doctor.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { describe, expect, it } from "vitest";

import { runDoctorChecks } from "../src/doctor.js";

function dependencies(runningVersion: string, requestedPackage: string | undefined) {
return {
readVersion: () => runningVersion,
requestedPackage: () => requestedPackage,
resolveBinary: async () => undefined,
globalVersion: async () => undefined,
nodeVersion: () => "24.13.0",
modulePath: () => "C:/clean-prefix/node_modules/@aryam/fixmap/dist/doctor.js"
};
}

describe("runDoctorChecks", () => {
it("reports an exact npm exec request that resolved to an older ancestor install", async () => {
const report = await runDoctorChecks(dependencies("0.8.1", "@aryam/fixmap@0.8.3"));

expect(report.healthy).toBe(false);
expect(report.findings).toContainEqual(
expect.objectContaining({
label: "Requested package",
value: "0.8.3 (this process is 0.8.1)",
ok: false
})
);
});

it("reports a matching exact npm exec request as healthy", async () => {
const report = await runDoctorChecks(dependencies("0.8.3", "@aryam/fixmap@0.8.3"));

expect(report.healthy).toBe(true);
expect(report.findings).toContainEqual({
label: "Requested package",
value: "0.8.3 (matches)",
ok: true
});
});

it("does not claim an intended version for non-exact package requests", async () => {
const report = await runDoctorChecks(dependencies("0.8.3", "@aryam/fixmap@latest"));

expect(report.healthy).toBe(true);
expect(report.findings.some((finding) => finding.label === "Requested package")).toBe(false);
});
});