diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index cf29435..a0641b0 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -12,27 +12,60 @@ jobs: include: - runner: macos-latest target: aarch64-apple-darwin + node_target: darwin-arm64 + - runner: macos-15-intel + target: x86_64-apple-darwin + node_target: darwin-x64 - runner: ubuntu-latest target: x86_64-unknown-linux-gnu + node_target: linux-x64 - runner: ubuntu-24.04-arm target: aarch64-unknown-linux-gnu + node_target: linux-arm64 + - runner: windows-2022 + target: x86_64-pc-windows-gnu + node_target: win32-x64 runs-on: ${{ matrix.runner }} + defaults: + run: + shell: bash steps: - uses: actions/checkout@v4 - uses: actions/setup-go@v5 with: go-version-file: go.mod + # Windows cgo build needs a GCC toolchain (pg_query_go uses -std=gnu99) + - name: Set up MinGW (Windows) + if: runner.os == 'Windows' + uses: msys2/setup-msys2@v2 + with: + msystem: MINGW64 + install: mingw-w64-x86_64-gcc + path-type: inherit - name: Build run: | + ext="" + [ "${{ runner.os }}" = "Windows" ] && ext=".exe" CGO_ENABLED=1 go build \ -ldflags "-X main.version=${GITHUB_REF_NAME}" \ - -o dryrun \ + -o "dryrun${ext}" \ ./cmd/dryrun - tar -cJf dry_run_cli-${{ matrix.target }}.tar.xz dryrun + - name: Archive (tar.xz) + if: runner.os != 'Windows' + run: tar -cJf dry_run_cli-${{ matrix.target }}.tar.xz dryrun + - name: Archive (zip) + if: runner.os == 'Windows' + run: 7z a dry_run_cli-${{ matrix.target }}.zip dryrun.exe + - uses: actions/upload-artifact@v4 + with: + name: tar-${{ matrix.target }} + path: dry_run_cli-${{ matrix.target }}.* + retention-days: 1 + # raw binary for the npm platform packages - uses: actions/upload-artifact@v4 with: - name: ${{ matrix.target }} - path: dry_run_cli-${{ matrix.target }}.tar.xz + name: bin-${{ matrix.node_target }} + path: dryrun${{ runner.os == 'Windows' && '.exe' || '' }} retention-days: 1 release: @@ -44,15 +77,52 @@ jobs: - uses: actions/checkout@v4 - uses: actions/download-artifact@v4 with: + pattern: tar-* merge-multiple: true path: dist/ - name: Checksums - run: shasum -a 256 dist/*.tar.xz > dist/checksums.txt + run: cd dist && shasum -a 256 *.tar.xz *.zip > checksums.txt - name: Publish release run: | gh release create "$GITHUB_REF_NAME" \ --title "$GITHUB_REF_NAME" \ --generate-notes \ - dist/*.tar.xz dist/checksums.txt + dist/*.tar.xz dist/*.zip dist/checksums.txt env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + npm-publish: + needs: build + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 22 + registry-url: https://registry.npmjs.org + - uses: actions/download-artifact@v4 + with: + pattern: bin-* + path: bins/ + - name: Stage binaries + run: | + for t in darwin-arm64 darwin-x64 linux-x64 linux-arm64; do + mkdir -p npm/packages/$t/bin + cp bins/bin-$t/dryrun npm/packages/$t/bin/dryrun + chmod +x npm/packages/$t/bin/dryrun + done + mkdir -p npm/packages/win32-x64/bin + cp bins/bin-win32-x64/dryrun.exe npm/packages/win32-x64/bin/dryrun.exe + - name: Set versions + run: node npm/scripts/set-version.mjs "${GITHUB_REF_NAME#v}" + - name: Publish platform packages + run: | + for t in darwin-arm64 darwin-x64 linux-x64 linux-arm64 win32-x64; do + npm publish --access public npm/packages/$t + done + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + - name: Publish launcher package + run: npm publish --access public npm/dryrun + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} diff --git a/README.md b/README.md index 8260690..af66387 100644 --- a/README.md +++ b/README.md @@ -46,15 +46,55 @@ No database connection needed. The assistant never sees credentials. **The server should do analysis, not pass-through.** Returning raw `\d+` output is marginally better than pasting it into the chat yourself. The value is in *interpreting* that data: checking whether a migration is safe for your PostgreSQL version, flagging missing FK indexes, and validating column references against the actual schema. +## Install + +**Homebrew:** + +```sh +brew install boringsql/boringsql/dryrun +``` + +**npm / npx:** + +If you already have Node, you can run `dryrun` without installing anything: + +```sh +npx @boringsql/dryrun --version +``` + +That fetches the prebuilt binary for your platform (darwin-arm64, linux-x64, linux-arm64), caches it, and prints the version. To put `dryrun` permanently on your PATH: + +```sh +npm install -g @boringsql/dryrun +dryrun --version +``` + +The npm package wraps the same Go binary; every CLI command works identically. Commands like `lint` need a schema snapshot first — see [Quickstart](#quickstart). Prebuilt binaries cover macOS (Apple Silicon + Intel), Linux (x64 + arm64), and Windows x64. On other platforms (Alpine/musl, Windows arm64), use Homebrew or build from source. + +**From source:** + +Requires Go 1.26+. If you don't have it, install via [go.dev/dl](https://go.dev/dl/). + +```sh +git clone https://github.com/boringsql/dryrun.git +cd dryrun +go build -o bin/dryrun ./cmd/dryrun +``` + +The binary is at `bin/dryrun`. + ## 30-second demo -Point **`dryrun`** at any schema JSON file (see [examples/demo](examples/demo/) for a ready-made one): +With `dryrun` installed, lint a ready-made schema snapshot from a clone of this repo, no database and no setup: ```sh -cd examples/demo +git clone https://github.com/boringsql/dryrun.git +cd dryrun/examples/demo dryrun lint ``` +(Installed via npm or Homebrew but didn't clone the repo? You won't have `examples/demo` — jump to [Quickstart](#quickstart) to point `dryrun` at your own schema. The sample output below is what `lint` produces.) + ``` [ERROR] public.audit_log: table has no primary key fix: add a primary key (bigint GENERATED ALWAYS AS IDENTITY recommended) @@ -74,27 +114,7 @@ dryrun lint 22 violation(s): 6 error, 16 warning, 0 info (13 tables checked) ``` -No database needed. Works entirely from the JSON file. - -## Install - -**Homebrew:** - -```sh -brew install boringsql/boringsql/dryrun -``` - -**From source:** - -Requires Go 1.26+. If you don't have it, install via [go.dev/dl](https://go.dev/dl/). - -```sh -git clone https://github.com/boringsql/dryrun.git -cd dryrun -go build -o bin/dryrun ./cmd/dryrun -``` - -The binary is at `bin/dryrun`. +No database needed. Works entirely from the offline snapshot. ## Quickstart @@ -236,6 +256,25 @@ If you built from source, use the full path to the binary: claude mcp add dryrun -- /path/to/dryrun mcp-serve ``` +Or, with no install at all, point the client at `npx`: + +```sh +claude mcp add dryrun -- npx -y @boringsql/dryrun mcp-serve +``` + +The raw client config for this form is: + +```json +{ + "mcpServers": { + "dryrun": { + "command": "npx", + "args": ["-y", "@boringsql/dryrun", "mcp-serve"] + } + } +} +``` + That's it. The server auto-discovers `.dryrun/schema.json` in the current project. No database credentials needed, your AI assistant gets full schema intelligence from the offline snapshot. For projects with multiple databases, run one `dryrun mcp-serve` per database and add an entry per server in your client config. Native multi-database serving inside one MCP process is tracked in [#4](https://github.com/boringSQL/dryrun/issues/4). diff --git a/npm/.gitignore b/npm/.gitignore new file mode 100644 index 0000000..179e909 --- /dev/null +++ b/npm/.gitignore @@ -0,0 +1 @@ +packages/*/bin/ diff --git a/npm/dryrun/README.md b/npm/dryrun/README.md new file mode 100644 index 0000000..2883434 --- /dev/null +++ b/npm/dryrun/README.md @@ -0,0 +1,36 @@ +# dryrun + +PostgreSQL schema/query advisor with a built-in MCP server. This npm package is +a thin launcher — the real binary is written in Go and ships as a +platform-specific dependency that npm selects automatically. + +## Use as an MCP server + +No install needed. Add to your MCP client config: + +```json +{ + "mcpServers": { + "dryrun": { + "command": "npx", + "args": ["-y", "@boringsql/dryrun", "mcp-serve"] + } + } +} +``` + +`npx` fetches the package on first run and caches it; subsequent launches are +local. Pin a version for reproducibility: `"@boringsql/dryrun@0.8.2"`. + +## CLI + +```sh +npx @boringsql/dryrun --help +``` + +Or install globally: `npm i -g @boringsql/dryrun` then `dryrun --help`. + +## Supported platforms + +`darwin-arm64`, `linux-x64`, `linux-arm64`. On other platforms, grab a prebuilt +binary from the [GitHub Releases](https://github.com/boringSQL/dryrun/releases). diff --git a/npm/dryrun/bin/dryrun.js b/npm/dryrun/bin/dryrun.js new file mode 100644 index 0000000..7c27d5b --- /dev/null +++ b/npm/dryrun/bin/dryrun.js @@ -0,0 +1,55 @@ +#!/usr/bin/env node +"use strict"; + +// Launcher for the dryrun Go binary. The actual binary ships inside a +// platform-specific optionalDependency (@boringsql/dryrun--); npm +// installs only the one matching the host. We resolve it and exec, forwarding +// argv + stdio verbatim so MCP stdio transport passes straight through. + +const { spawnSync } = require("node:child_process"); + +// node arch/platform -> our published platform packages +const SUPPORTED = { + "darwin arm64": "@boringsql/dryrun-darwin-arm64", + "darwin x64": "@boringsql/dryrun-darwin-x64", + "linux x64": "@boringsql/dryrun-linux-x64", + "linux arm64": "@boringsql/dryrun-linux-arm64", + "win32 x64": "@boringsql/dryrun-win32-x64", +}; + +function resolveBinary() { + const key = `${process.platform} ${process.arch}`; + const pkg = SUPPORTED[key]; + if (!pkg) { + fail( + `unsupported platform ${key}.\n` + + `Supported: ${Object.keys(SUPPORTED).join(", ")}.\n` + + `Install a prebuilt binary from https://github.com/boringSQL/dryrun/releases instead.` + ); + } + try { + const binName = process.platform === "win32" ? "dryrun.exe" : "dryrun"; + return require.resolve(`${pkg}/bin/${binName}`); + } catch (_e) { + fail( + `the platform package ${pkg} is not installed.\n` + + `This usually means npm skipped optional dependencies. Reinstall without\n` + + `--no-optional (e.g. \`npm i @boringsql/dryrun\`), or grab a binary from\n` + + `https://github.com/boringSQL/dryrun/releases.` + ); + } +} + +function fail(msg) { + process.stderr.write(`dryrun: ${msg}\n`); + process.exit(1); +} + +const result = spawnSync(resolveBinary(), process.argv.slice(2), { + stdio: "inherit", +}); + +if (result.error) fail(result.error.message); +// propagate signal-kills as the conventional 128+signal exit code +if (result.signal) process.exit(1); +process.exit(result.status === null ? 1 : result.status); diff --git a/npm/dryrun/package.json b/npm/dryrun/package.json new file mode 100644 index 0000000..117642a --- /dev/null +++ b/npm/dryrun/package.json @@ -0,0 +1,39 @@ +{ + "name": "@boringsql/dryrun", + "version": "0.0.0", + "description": "dryrun — PostgreSQL schema/query advisor and MCP server. npx launcher.", + "bin": { + "dryrun": "bin/dryrun.js" + }, + "files": [ + "bin/dryrun.js" + ], + "optionalDependencies": { + "@boringsql/dryrun-darwin-arm64": "0.0.0", + "@boringsql/dryrun-darwin-x64": "0.0.0", + "@boringsql/dryrun-linux-x64": "0.0.0", + "@boringsql/dryrun-linux-arm64": "0.0.0", + "@boringsql/dryrun-win32-x64": "0.0.0" + }, + "engines": { + "node": ">=16" + }, + "license": "BSD-2-Clause", + "repository": { + "type": "git", + "url": "git+https://github.com/boringSQL/dryrun.git" + }, + "homepage": "https://github.com/boringSQL/dryrun#readme", + "bugs": { + "url": "https://github.com/boringSQL/dryrun/issues" + }, + "keywords": [ + "postgres", + "postgresql", + "mcp", + "model-context-protocol", + "sql", + "schema", + "dryrun" + ] +} diff --git a/npm/packages/darwin-arm64/package.json b/npm/packages/darwin-arm64/package.json new file mode 100644 index 0000000..f101baf --- /dev/null +++ b/npm/packages/darwin-arm64/package.json @@ -0,0 +1,19 @@ +{ + "name": "@boringsql/dryrun-darwin-arm64", + "version": "0.0.0", + "description": "darwin-arm64 binary for dryrun", + "os": [ + "darwin" + ], + "cpu": [ + "arm64" + ], + "files": [ + "bin/dryrun" + ], + "license": "BSD-2-Clause", + "repository": { + "type": "git", + "url": "git+https://github.com/boringSQL/dryrun.git" + } +} diff --git a/npm/packages/darwin-x64/package.json b/npm/packages/darwin-x64/package.json new file mode 100644 index 0000000..8232239 --- /dev/null +++ b/npm/packages/darwin-x64/package.json @@ -0,0 +1,19 @@ +{ + "name": "@boringsql/dryrun-darwin-x64", + "version": "0.0.0", + "description": "darwin-x64 binary for dryrun", + "os": [ + "darwin" + ], + "cpu": [ + "x64" + ], + "files": [ + "bin/dryrun" + ], + "license": "BSD-2-Clause", + "repository": { + "type": "git", + "url": "git+https://github.com/boringSQL/dryrun.git" + } +} diff --git a/npm/packages/linux-arm64/package.json b/npm/packages/linux-arm64/package.json new file mode 100644 index 0000000..498019f --- /dev/null +++ b/npm/packages/linux-arm64/package.json @@ -0,0 +1,19 @@ +{ + "name": "@boringsql/dryrun-linux-arm64", + "version": "0.0.0", + "description": "linux-arm64 binary for dryrun", + "os": [ + "linux" + ], + "cpu": [ + "arm64" + ], + "files": [ + "bin/dryrun" + ], + "license": "BSD-2-Clause", + "repository": { + "type": "git", + "url": "git+https://github.com/boringSQL/dryrun.git" + } +} diff --git a/npm/packages/linux-x64/package.json b/npm/packages/linux-x64/package.json new file mode 100644 index 0000000..49088b3 --- /dev/null +++ b/npm/packages/linux-x64/package.json @@ -0,0 +1,19 @@ +{ + "name": "@boringsql/dryrun-linux-x64", + "version": "0.0.0", + "description": "linux-x64 binary for dryrun", + "os": [ + "linux" + ], + "cpu": [ + "x64" + ], + "files": [ + "bin/dryrun" + ], + "license": "BSD-2-Clause", + "repository": { + "type": "git", + "url": "git+https://github.com/boringSQL/dryrun.git" + } +} diff --git a/npm/packages/win32-x64/package.json b/npm/packages/win32-x64/package.json new file mode 100644 index 0000000..19c9078 --- /dev/null +++ b/npm/packages/win32-x64/package.json @@ -0,0 +1,19 @@ +{ + "name": "@boringsql/dryrun-win32-x64", + "version": "0.0.0", + "description": "win32-x64 binary for dryrun", + "os": [ + "win32" + ], + "cpu": [ + "x64" + ], + "files": [ + "bin/dryrun.exe" + ], + "license": "BSD-2-Clause", + "repository": { + "type": "git", + "url": "git+https://github.com/boringSQL/dryrun.git" + } +} diff --git a/npm/scripts/set-version.mjs b/npm/scripts/set-version.mjs new file mode 100644 index 0000000..e08a9b0 --- /dev/null +++ b/npm/scripts/set-version.mjs @@ -0,0 +1,36 @@ +// Stamp one version across the main package, its platform packages, and the +// optionalDependencies pins (which must match exactly). Run in CI from the tag: +// node npm/scripts/set-version.mjs 0.8.2 +import { readFileSync, writeFileSync } from "node:fs"; +import { fileURLToPath } from "node:url"; +import { dirname, join } from "node:path"; + +const version = process.argv[2]; +if (!version) { + console.error("usage: set-version.mjs "); + process.exit(1); +} + +const root = join(dirname(fileURLToPath(import.meta.url)), ".."); +const platforms = ["darwin-arm64", "darwin-x64", "linux-x64", "linux-arm64", "win32-x64"]; + +function patch(path, fn) { + const pkg = JSON.parse(readFileSync(path, "utf8")); + fn(pkg); + writeFileSync(path, JSON.stringify(pkg, null, 2) + "\n"); +} + +for (const p of platforms) { + patch(join(root, "packages", p, "package.json"), (pkg) => { + pkg.version = version; + }); +} + +patch(join(root, "dryrun", "package.json"), (pkg) => { + pkg.version = version; + for (const p of platforms) { + pkg.optionalDependencies[`@boringsql/dryrun-${p}`] = version; + } +}); + +console.log(`set npm packages to ${version}`);