+122
| # Changelog | ||
| All notable changes to mendapi. This file follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) | ||
| and the project uses [semantic versioning](https://semver.org/spec/v2.0.0.html). | ||
| ## [Unreleased] | ||
| ## [0.5.6] - 2026-08-05 | ||
| Changes landed in the repository but not yet published to npm. The set of files | ||
| that differ from the published tarball is derived mechanically — see | ||
| `loop/release-drift.mjs` and the release-drift gate. | ||
| ### Fixed | ||
| - **`scan`, `fix`, `deps`, `llmfix` and `pr` no longer scan the wrong directory.** | ||
| A positional path (`mendapi deps ./my-repo` — the most natural thing to type) | ||
| was silently discarded, and `--repo` fell back to the current working | ||
| directory. The command then produced a complete, well-formed, confidently | ||
| worded report about a tree the user never asked about, and exited 0. Stray | ||
| positional arguments are now a usage error: the offending argument is named, | ||
| the message points at `--help`, and the process exits 2. | ||
| Affects `deps.js`, `fixer.js`, `llmfix.js`, `pr.js`, `scanner.js`. | ||
| - **`pr` no longer commits or deletes files you own.** The commit was staged | ||
| with `git add -A`, which swept in every artifact the clean-worktree check had | ||
| just exempted as yours — your `impact.json` and the entire `.mendapi/` | ||
| directory, change database included — and then removed `impact.json` from the | ||
| working tree when it checked back out to the base branch. Staging is now | ||
| limited to the files the fixer actually rewrote. | ||
| - **`pr` works when `node` is not on `PATH`.** The child fixer was spawned as a | ||
| bare `node`, which crashes under an agent, a CI runner, or an npx shim with a | ||
| different `PATH`. It now re-enters through `process.execPath`. | ||
| - **`pr` no longer collides with other users through a shared temp path.** Its | ||
| probe run wrote to a hardcoded `/tmp/mendapi-probe` and left the directory | ||
| behind; each run now gets a private temp directory and cleans up after itself. | ||
| - **`pr --from-report` resolves the report path against your shell's working | ||
| directory**, not against wherever git checkouts happen to have moved the | ||
| process mid-run. Argument errors are also reported before worktree state, so a | ||
| mistyped filename no longer surfaces as a confusing "dirty worktree" complaint | ||
| naming the report you asked for. | ||
| - **`fix --from-report --json` emits run-level `verification`.** The aggregate | ||
| output omitted the block that the per-migration output carries, so anything | ||
| consuming the documented JSON pipeline — including the CI recipe in the docs — | ||
| hit a `TypeError` on its first run. | ||
| - **`scan`'s closing hint names a real file.** It always printed the placeholder | ||
| `<report>`, leaving the user to guess. It now reflects actual state: with | ||
| `--out`, it prints the real filename; without it, it tells you to add `--out` | ||
| first. | ||
| ### Changed | ||
| - **README quickstart starts with `sync`.** The first block told a new user to | ||
| run `scan` against a change database that does not exist yet on a cold | ||
| install, so the documented first command exited 2. The three-step path | ||
| (`sync` → `scan --out` → `fix --from-report`) is now explicit, and the one | ||
| command that touches the network is named as such. | ||
| ## [0.5.5] - 2026-08-04 | ||
| ### Fixed | ||
| - `mendapi <subcommand> -h` was not normalized to `--help` before dispatch, so | ||
| the short flag failed on every subcommand. | ||
| ## [0.5.4] - 2026-08-03 | ||
| ### Added | ||
| - `--version` flag, read from `package.json` as the single source of truth. | ||
| ### Fixed | ||
| - `sync --help` is offline-safe: it prints usage instead of triggering a network | ||
| fetch. | ||
| - The CLI fails loudly on Node older than 22.13 (the `node:sqlite` floor) | ||
| instead of failing obscurely later. | ||
| ## [0.5.3] - 2026-08-02 | ||
| ### Fixed | ||
| - Default `--out-dir` is `<cwd>/.mendapi` rather than a path inside the | ||
| development tree. | ||
| - Database path resolution is single-sourced through `dbpath.js`, so npm | ||
| consumers persist `sentinel.db` under `<cwd>/.mendapi` instead of inside | ||
| `node_modules`. | ||
| ## [0.5.2] - 2026-08-02 | ||
| ### Fixed | ||
| - `astlite.js` is included in the published package. Without it, `npx mendapi | ||
| fix` was broken in 0.5.2's initial upload. | ||
| ## [0.5.1] - 2026-08-01 | ||
| ### Added | ||
| - `mcpName` and MCP-related keywords, for the Model Context Protocol registry. | ||
| ## [0.5.0] - 2026-07-31 | ||
| First public release on npm. | ||
| ### Added | ||
| - `sync` — fetch the upstream API change feed into a local SQLite database. The | ||
| only command that touches the network. | ||
| - `scan` — find upstream breaking changes that hit your code, with file, line, | ||
| symbol and a confidence score. | ||
| - `fix` — draft the migration as a reviewable diff; nothing is written until you | ||
| pass `--apply`. | ||
| - `review`, `deps`, `revalidate`, `llmfix`, `pr` subcommands. | ||
| - `mendapi mcp` — a zero-dependency stdio MCP server exposing the toolset to any | ||
| MCP client. | ||
| - Agent skill for Claude Code, Cursor and other agent runtimes. |
+11
-1
@@ -44,2 +44,9 @@ #!/usr/bin/env node | ||
| // Flag-only parser. Positional arguments are a usage error, NOT something to | ||
| // silently drop: `mendapi deps ./some/repo` is the most natural thing a user | ||
| // types, and dropping the path made `--repo` fall back to process.cwd() — | ||
| // scanning the WRONG tree while reporting success (Loop 665: a 1-file fixture | ||
| // path silently became a 1016-file scan of the cwd, 8s of CPU, wrong answer, | ||
| // exit 0). Every path/value on these subcommands is passed via an explicit | ||
| // flag, so anything not starting with `--` can only be a mistake. Fail loud. | ||
| function parseArgs(argv) { | ||
@@ -49,3 +56,6 @@ const args = {}; | ||
| const a = argv[i]; | ||
| if (a.startsWith('--')) args[a.slice(2)] = argv[i + 1] && !argv[i + 1].startsWith('--') ? argv[++i] : true; | ||
| if (a.startsWith('--')) { args[a.slice(2)] = argv[i + 1] && !argv[i + 1].startsWith('--') ? argv[++i] : true; continue; } | ||
| console.error(`Unexpected argument: ${a}`); | ||
| console.error('This command takes flags only (for example: --repo <path>). Run with --help for usage.'); | ||
| process.exit(2); | ||
| } | ||
@@ -52,0 +62,0 @@ return args; |
+3
-2
| { | ||
| "name": "mendapi", | ||
| "version": "0.5.5", | ||
| "version": "0.5.6", | ||
| "license": "AGPL-3.0-only", | ||
@@ -41,3 +41,4 @@ "type": "module", | ||
| "reclassify.js", | ||
| "README.md" | ||
| "README.md", | ||
| "CHANGELOG.md" | ||
| ], | ||
@@ -44,0 +45,0 @@ "engines": { |
+86
-11
@@ -19,5 +19,6 @@ #!/usr/bin/env node | ||
| import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'node:fs'; | ||
| import { readFileSync, writeFileSync, mkdirSync, mkdtempSync, rmSync, existsSync } from 'node:fs'; | ||
| import { execFileSync } from 'node:child_process'; | ||
| import { join, dirname } from 'node:path'; | ||
| import { join, dirname, basename, resolve } from 'node:path'; | ||
| import { tmpdir } from 'node:os'; | ||
| import { fileURLToPath } from 'node:url'; | ||
@@ -27,2 +28,9 @@ | ||
| // Flag-only parser. Positional arguments are a usage error, NOT something to | ||
| // silently drop: `mendapi deps ./some/repo` is the most natural thing a user | ||
| // types, and dropping the path made `--repo` fall back to process.cwd() — | ||
| // scanning the WRONG tree while reporting success (Loop 665: a 1-file fixture | ||
| // path silently became a 1016-file scan of the cwd, 8s of CPU, wrong answer, | ||
| // exit 0). Every path/value on these subcommands is passed via an explicit | ||
| // flag, so anything not starting with `--` can only be a mistake. Fail loud. | ||
| function parseArgs(argv) { | ||
@@ -32,3 +40,6 @@ const args = {}; | ||
| const a = argv[i]; | ||
| if (a.startsWith('--')) args[a.slice(2)] = argv[i + 1] && !argv[i + 1].startsWith('--') ? argv[++i] : true; | ||
| if (a.startsWith('--')) { args[a.slice(2)] = argv[i + 1] && !argv[i + 1].startsWith('--') ? argv[++i] : true; continue; } | ||
| console.error(`Unexpected argument: ${a}`); | ||
| console.error('This command takes flags only (for example: --repo <path>). Run with --help for usage.'); | ||
| process.exit(2); | ||
| } | ||
@@ -42,2 +53,16 @@ return args; | ||
| // Always re-enter Node through process.execPath, never a bare 'node' from PATH: | ||
| // the CLI may be launched by an agent, a CI runner, or an npx shim whose PATH | ||
| // does not contain the interpreter currently executing us. --disable-warning | ||
| // matches what cli.js does for single-process subcommands; pr is the only | ||
| // subcommand that spawns a grandchild, so it has to repeat the suppression or | ||
| // node:sqlite's ExperimentalWarning leaks onto stderr from the child. | ||
| function node(argv, opts = {}) { | ||
| return execFileSync( | ||
| process.execPath, | ||
| ['--disable-warning=ExperimentalWarning', ...argv], | ||
| { encoding: 'utf8', ...opts }, | ||
| ); | ||
| } | ||
| function fail(msg, code = 2) { | ||
@@ -54,12 +79,44 @@ console.error(msg); | ||
| // Validate the caller's own arguments BEFORE inspecting the worktree. | ||
| // Argument errors belong to the caller; worktree state belongs to the repo. | ||
| // Reporting the latter first told a user who simply mistyped a filename that | ||
| // their worktree was dirty — and the file it named as the offender was the | ||
| // report they had asked for, which reads as though the tool contradicts | ||
| // itself. Resolve against the caller's cwd here too: the fixer runs as a | ||
| // child and git checkouts move the process around mid-run, so a relative | ||
| // path kept unresolved can be re-resolved against the wrong directory later. | ||
| let reportRef = null; | ||
| if (!args.migration && args['from-report']) { | ||
| reportRef = resolve(String(args['from-report'])); | ||
| if (!existsSync(reportRef)) fail(`Impact report not found: ${reportRef}`); | ||
| try { | ||
| JSON.parse(readFileSync(reportRef, 'utf8')); | ||
| } catch (e) { | ||
| fail(`Impact report is not valid JSON: ${reportRef}\n${e.message}`); | ||
| } | ||
| } | ||
| // 1. Clean worktree required — a fix PR must not mix in unrelated edits. | ||
| // mendapi's OWN untracked artifacts do not count as user edits: the | ||
| // documented flow is `scan --out impact.json` -> `fix` (writes .mendapi/) | ||
| // -> `pr`, so counting them made the documented order refuse itself. | ||
| // Anything else — real source edits, other untracked files — still blocks. | ||
| const status = git(repo, 'status', '--porcelain'); | ||
| if (status) fail(`Worktree is dirty; commit or stash first:\n${status}`); | ||
| const ours = new Set(['.mendapi/', '.mendapi']); | ||
| if (args['from-report']) ours.add(basename(args['from-report'])); | ||
| if (args['out-dir']) ours.add(basename(args['out-dir']) + '/'); | ||
| const foreign = status | ||
| .split('\n') | ||
| .filter(Boolean) | ||
| .filter((line) => { | ||
| // Only untracked entries ("?? path") can be ours; modified tracked files | ||
| // are always foreign, even under .mendapi/. | ||
| if (!line.startsWith('?? ')) return true; | ||
| return !ours.has(line.slice(3).trim()); | ||
| }); | ||
| if (foreign.length) fail(`Worktree is dirty; commit or stash first:\n${foreign.join('\n')}`); | ||
| // Resolve migration name (direct or via impact report). | ||
| let migration = args.migration; | ||
| let reportRef = null; | ||
| if (!migration && args['from-report']) { | ||
| const impact = JSON.parse(readFileSync(args['from-report'], 'utf8')); | ||
| reportRef = args['from-report']; | ||
| if (!migration && reportRef) { | ||
| // Delegate provider->migration matching to the fixer by running it in | ||
@@ -69,3 +126,9 @@ // from-report dry-run first would duplicate work; instead reuse its map | ||
| // determinism we re-derive from the report using the fixer's own CLI. | ||
| const probe = execFileSync('node', [join(ROOT, 'fixer.js'), '--from-report', reportRef, '--repo', repo, '--out-dir', '/tmp/mendapi-probe'], { encoding: 'utf8' }); | ||
| const probeDir = mkdtempSync(join(tmpdir(), 'mendapi-probe-')); | ||
| let probe; | ||
| try { | ||
| probe = node([join(ROOT, 'fixer.js'), '--from-report', reportRef, '--repo', repo, '--out-dir', probeDir]); | ||
| } finally { | ||
| rmSync(probeDir, { recursive: true, force: true }); | ||
| } | ||
| const m = probe.match(/^Applicable migrations: (.+)$/m); | ||
@@ -95,3 +158,3 @@ if (!m || m[1].trim() === '(none)') fail('No applicable migrations for this impact report.', 1); | ||
| try { | ||
| fixOut = execFileSync('node', fixerArgs, { encoding: 'utf8' }); | ||
| fixOut = node(fixerArgs); | ||
| } catch (e) { | ||
@@ -110,3 +173,15 @@ git(repo, 'checkout', baseBranch); | ||
| const title = `fix: migrate to new ${report.provider} API (${migration})`; | ||
| git(repo, 'add', '-A'); | ||
| // Stage ONLY the files the fixer rewrote. `git add -A` would sweep in every | ||
| // artifact the clean-worktree check just exempted above — the impact report, | ||
| // .mendapi/ (including the multi-hundred-MB change database) — burying a | ||
| // two-line codemod in an unreviewable commit and, worse, deleting the user's | ||
| // impact.json from the working tree on checkout back to base. The exemption | ||
| // list says those files are not the user's edits; it must not then claim | ||
| // they are part of the fix. | ||
| const staged = report.files.map((f) => f.file); | ||
| if (!staged.length) { | ||
| git(repo, 'checkout', baseBranch); | ||
| fail('Fix report lists no changed files; nothing to commit.', 1); | ||
| } | ||
| git(repo, 'add', '--', ...staged); | ||
| git(repo, 'commit', '-m', title, '-m', `Automated by mendapi.\n\nMigration: ${report.title}\nReference: ${report.reference}`); | ||
@@ -113,0 +188,0 @@ const sha = git(repo, 'rev-parse', '--short', 'HEAD'); |
+8
-4
@@ -9,8 +9,12 @@ # mendapi | ||
| ```bash | ||
| # In any repo — zero config, zero npm dependencies, nothing leaves your machine | ||
| npx mendapi scan | ||
| # In any repo — zero config, zero npm dependencies | ||
| npx mendapi sync # one network call: fetch the change feed | ||
| npx mendapi scan --out impact.json # local only, from here on | ||
| npx mendapi fix --from-report impact.json | ||
| ``` | ||
| You get every upstream breaking change that actually hits your code — file, line, and symbol — scored for confidence. Then `npx mendapi fix` drafts the migration as a reviewable diff. | ||
| `sync` pulls the upstream API change feed into a local SQLite database. It is the only command that touches the network, and you run it once (then whenever you want fresher data). | ||
| `scan` reports every upstream breaking change that actually hits your code — file, line, and symbol — scored for confidence. `fix` drafts the migration as a reviewable diff, without touching a single file until you pass `--apply`. Neither reads or writes anything outside your machine. | ||
| Using an AI coding agent? One line plugs mendapi into Claude Code as an MCP server (Cursor and every other MCP client work too — [details below](#use-it-from-your-ai-coding-agent-mcp)): | ||
@@ -164,3 +168,3 @@ | ||
| Published on npm as [`mendapi`](https://www.npmjs.com/package/mendapi) (v0.5.5). Early release — the change database and migration pack registry grow daily; interfaces may still shift before 1.0. | ||
| Published on npm as [`mendapi`](https://www.npmjs.com/package/mendapi) (v0.5.6). Early release — the change database and migration pack registry grow daily; interfaces may still shift before 1.0. | ||
@@ -167,0 +171,0 @@ ## License |
+21
-5
@@ -81,2 +81,9 @@ #!/usr/bin/env node | ||
| // Flag-only parser. Positional arguments are a usage error, NOT something to | ||
| // silently drop: `mendapi deps ./some/repo` is the most natural thing a user | ||
| // types, and dropping the path made `--repo` fall back to process.cwd() — | ||
| // scanning the WRONG tree while reporting success (Loop 665: a 1-file fixture | ||
| // path silently became a 1016-file scan of the cwd, 8s of CPU, wrong answer, | ||
| // exit 0). Every path/value on these subcommands is passed via an explicit | ||
| // flag, so anything not starting with `--` can only be a mistake. Fail loud. | ||
| function parseArgs(argv) { | ||
@@ -86,3 +93,6 @@ const args = {}; | ||
| const a = argv[i]; | ||
| if (a.startsWith('--')) args[a.slice(2)] = argv[i + 1] && !argv[i + 1].startsWith('--') ? argv[++i] : true; | ||
| if (a.startsWith('--')) { args[a.slice(2)] = argv[i + 1] && !argv[i + 1].startsWith('--') ? argv[++i] : true; continue; } | ||
| console.error(`Unexpected argument: ${a}`); | ||
| console.error('This command takes flags only (for example: --repo <path>). Run with --help for usage.'); | ||
| process.exit(2); | ||
| } | ||
@@ -683,3 +693,3 @@ return args; | ||
| function printTerminalReport(report, elapsedMs) { | ||
| function printTerminalReport(report, elapsedMs, savedReport) { | ||
| const out = []; | ||
@@ -750,4 +760,10 @@ out.push(''); | ||
| } | ||
| out.push(dim('Next: mendapi review <report> --pending (semantic review of medium hits)')); | ||
| out.push(dim(' mendapi fix --from-report <report> (preview the fix as a local diff)')); | ||
| if (savedReport) { | ||
| out.push(dim('Next: mendapi review ' + savedReport + ' --pending')); | ||
| out.push(dim(' mendapi fix --from-report ' + savedReport)); | ||
| } else { | ||
| out.push(dim('Next: re-run with --out impact.json to save the report, then:')); | ||
| out.push(dim(' mendapi review impact.json --pending (semantic review of medium hits)')); | ||
| out.push(dim(' mendapi fix --from-report impact.json (preview the fix as a local diff)')); | ||
| } | ||
| out.push(''); | ||
@@ -896,3 +912,3 @@ console.log(out.join('\n')); | ||
| writeFileSync(args.out, json); | ||
| printTerminalReport(report, elapsed); | ||
| printTerminalReport(report, elapsed, args.out); | ||
| console.log(`report written: ${args.out}`); | ||
@@ -899,0 +915,0 @@ } else if (args.json) { |
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
Filesystem access
Supply chain riskAccesses the file system, and could potentially read sensitive data.
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
945012
1.55%20
5.26%15233
1.05%172
2.38%