diff --git a/.github/workflows/pre-release.yml b/.github/workflows/pre-release.yml new file mode 100644 index 0000000..ec40453 --- /dev/null +++ b/.github/workflows/pre-release.yml @@ -0,0 +1,304 @@ +name: Create GitHub Pre-Release + +on: + workflow_dispatch: + inputs: + version_type: + description: "Version bump type" + required: true + default: "patch" + type: choice + options: + - patch + - minor + - major + - prerelease + +concurrency: + group: release-${{ github.ref_name }} + cancel-in-progress: true + +permissions: + contents: write + packages: write + +env: + NODE_VERSION: 22 + RUST_TOOLCHAIN: stable + +jobs: + bump-version: + runs-on: ubuntu-latest + outputs: + new_tag: ${{ steps.version.outputs.new_tag }} + new_version: ${{ steps.version.outputs.new_version }} + branch_suffix: ${{ steps.branch.outputs.suffix }} + steps: + - uses: actions/checkout@v4 + with: + token: ${{ secrets.GITHUB_TOKEN }} + + - name: Setup Node + uses: actions/setup-node@v4 + with: + node-version: ${{ env.NODE_VERSION }} + + - name: Cache cargo-edit + uses: actions/cache@v3 + id: cache-cargo-edit + with: + path: ~/.cargo/bin/cargo-set-version + key: cargo-edit-${{ runner.os }}-${{ env.RUST_TOOLCHAIN }} + + - name: Install cargo-edit + if: steps.cache-cargo-edit.outputs.cache-hit != 'true' + run: cargo install cargo-edit + + - name: Generate branch suffix + id: branch + run: | + branch_name="${{ github.ref_name }}" + suffix=$(echo "$branch_name" | tail -c 7 | sed 's/[^a-zA-Z0-9]//g' | tr '[:upper:]' '[:lower:]') + echo "Branch: $branch_name" + echo "Suffix: $suffix" + echo "suffix=$suffix" >> $GITHUB_OUTPUT + + - name: Determine and update versions + id: version + run: | + latest_npm_version=$(npm view mcp-dev-manager version 2>/dev/null || echo "0.0.0") + echo "Latest npm version: $latest_npm_version" + + timestamp=$(date +%Y%m%d%H%M%S) + + cd npx-cli + if [[ "${{ github.event.inputs.version_type }}" == "prerelease" ]]; then + npm version prerelease --preid="${{ steps.branch.outputs.suffix }}" --no-git-tag-version + new_version=$(node -p "require('./package.json').version") + new_tag="v${new_version}.${timestamp}" + else + npm version $latest_npm_version --no-git-tag-version --allow-same-version + npm version ${{ github.event.inputs.version_type }} --no-git-tag-version + new_version=$(node -p "require('./package.json').version") + new_tag="v${new_version}-${timestamp}" + fi + cd .. + + cargo set-version "$new_version" + + echo "New version: $new_version" + echo "new_version=$new_version" >> $GITHUB_OUTPUT + echo "new_tag=$new_tag" >> $GITHUB_OUTPUT + + - name: Commit changes and create tag + run: | + git config --local user.email "action@github.com" + git config --local user.name "GitHub Action" + git add npx-cli/package.json Cargo.toml + git commit -m "chore: bump version to ${{ steps.version.outputs.new_version }}" + git tag -a ${{ steps.version.outputs.new_tag }} -m "Release ${{ steps.version.outputs.new_tag }}" + git push + git push --tags + + build-backend: + needs: bump-version + runs-on: ${{ matrix.os }} + container: ${{ matrix.container }} + strategy: + matrix: + include: + - target: x86_64-unknown-linux-musl + os: ubuntu-latest + container: ghcr.io/rust-cross/cargo-zigbuild@sha256:af1bc2b869c5d76c1300f7a4685c2f1793d068e6e895c9f5c399b517b31a731e + name: linux-x64 + - target: aarch64-unknown-linux-musl + os: ubuntu-latest + name: linux-arm64 + container: ghcr.io/rust-cross/cargo-zigbuild@sha256:af1bc2b869c5d76c1300f7a4685c2f1793d068e6e895c9f5c399b517b31a731e + - target: x86_64-pc-windows-msvc + os: windows-latest + name: windows-x64 + - target: x86_64-apple-darwin + os: macos-13 + name: macos-x64 + - target: aarch64-apple-darwin + os: macos-14 + name: macos-arm64 + - target: aarch64-pc-windows-msvc + os: windows-latest + name: windows-arm64 + steps: + - uses: actions/checkout@v4 + with: + ref: ${{ needs.bump-version.outputs.new_tag }} + + - name: Install Rust toolchain + uses: dtolnay/rust-toolchain@stable + with: + toolchain: ${{ env.RUST_TOOLCHAIN }} + targets: ${{ matrix.target }} + + - name: Install libclang (Linux) + if: runner.os == 'Linux' + run: | + apt-get update + DEBIAN_FRONTEND=noninteractive apt-get install -y clang libclang-dev + + - name: Cache Rust dependencies + uses: Swatinem/rust-cache@v2 + with: + workspaces: "." + prefix-key: "cache-v1.0" + key: ${{ matrix.target }}_${{ matrix.os }} + cache-on-failure: true + shared-key: "shared" + cache-all-crates: true + + - name: Build backend (Linux) + if: runner.os == 'Linux' + run: cargo zigbuild --release --target ${{ matrix.target }} + + - name: Build backend (non-Linux) + if: runner.os != 'Linux' + run: cargo build --release --target ${{ matrix.target }} + + - name: Prepare binaries (non-macOS) + if: runner.os != 'macOS' + shell: bash + run: | + mkdir -p dist + if [[ "${{ matrix.os }}" == "windows-latest" ]]; then + cp target/${{ matrix.target }}/release/mcp-dev-manager.exe dist/mcp-dev-manager-${{ matrix.name }}.exe + else + cp target/${{ matrix.target }}/release/mcp-dev-manager dist/mcp-dev-manager-${{ matrix.name }} + fi + + - name: Prepare Apple certificate (macOS) + if: runner.os == 'macOS' + run: | + echo "${{ secrets.APPLE_CERTIFICATE_P12_BASE64 }}" | base64 --decode > certificate.p12 + + - name: Sign binary (macOS) + if: runner.os == 'macOS' + uses: indygreg/apple-code-sign-action@v1 + with: + input_path: target/${{ matrix.target }}/release/mcp-dev-manager + output_path: mcp-dev-manager + p12_file: certificate.p12 + p12_password: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }} + sign: true + sign_args: "--code-signature-flags=runtime" + + - name: Package binary (macOS) + if: runner.os == 'macOS' + run: zip mcp-dev-manager.zip mcp-dev-manager + + - name: Prepare signed binaries (macOS) + if: runner.os == 'macOS' + run: | + mkdir -p dist + cp mcp-dev-manager.zip dist/mcp-dev-manager-${{ matrix.name }}.zip + + - name: Clean up certificates (macOS) + if: runner.os == 'macOS' + run: rm -f certificate.p12 + + - name: Upload binary artifact + uses: actions/upload-artifact@v4 + with: + name: backend-binary-${{ matrix.name }} + path: dist/ + retention-days: 1 + + package-npx-cli: + needs: [bump-version, build-backend] + runs-on: ubuntu-latest + strategy: + matrix: + include: + - target: x86_64-unknown-linux-musl + name: linux-x64 + binary: mcp-dev-manager + - target: x86_64-pc-windows-msvc + name: windows-x64 + binary: mcp-dev-manager.exe + - target: x86_64-apple-darwin + name: macos-x64 + binary: mcp-dev-manager + - target: aarch64-apple-darwin + name: macos-arm64 + binary: mcp-dev-manager + - target: aarch64-pc-windows-msvc + name: windows-arm64 + binary: mcp-dev-manager.exe + - target: aarch64-unknown-linux-musl + name: linux-arm64 + binary: mcp-dev-manager + steps: + - uses: actions/checkout@v4 + with: + ref: ${{ needs.bump-version.outputs.new_tag }} + + - name: Download backend binary artifact + uses: actions/download-artifact@v4 + with: + name: backend-binary-${{ matrix.name }} + path: dist/ + + - name: Create platform package + if: matrix.name != 'macos-arm64' && matrix.name != 'macos-x64' + run: | + mkdir -p npx-cli/dist/${{ matrix.name }} + mkdir mcp-dev-manager-${{ matrix.name }} + + cp dist/mcp-dev-manager-${{ matrix.name }}* mcp-dev-manager-${{ matrix.name }}/${{ matrix.binary }} + + zip -j npx-cli/dist/${{ matrix.name }}/mcp-dev-manager.zip mcp-dev-manager-${{ matrix.name }}/${{ matrix.binary }} + + - name: Create platform package (macOS) + if: matrix.name == 'macos-arm64' || matrix.name == 'macos-x64' + run: | + mkdir -p npx-cli/dist/${{ matrix.name }} + cp dist/mcp-dev-manager-${{ matrix.name }}* npx-cli/dist/${{ matrix.name }}/mcp-dev-manager.zip + + - name: Upload platform package artifact + uses: actions/upload-artifact@v4 + with: + name: npx-platform-${{ matrix.name }} + path: npx-cli/dist/ + retention-days: 1 + + create-prerelease: + needs: [bump-version, build-backend, package-npx-cli] + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + ref: ${{ needs.bump-version.outputs.new_tag }} + + - name: Download backend npx-cli zips + uses: actions/download-artifact@v4 + with: + pattern: npx-platform-* + path: npx-cli/dist/ + merge-multiple: true + + - name: Setup Node for npm pack + uses: actions/setup-node@v4 + with: + node-version: ${{ env.NODE_VERSION }} + + - name: Pack + run: | + cd npx-cli + npm pack + + - name: Create GitHub Pre-Release + uses: softprops/action-gh-release@v2 + with: + tag_name: ${{ needs.bump-version.outputs.new_tag }} + name: Pre-release ${{ needs.bump-version.outputs.new_tag }} + prerelease: true + generate_release_notes: true + files: | + npx-cli/mcp-dev-manager-*.tgz diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 0000000..820b37c --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,129 @@ +name: Publish to npm + +on: + release: + types: [released] + workflow_dispatch: + inputs: + tag_name: + description: "Release tag (e.g., v1.2.3)" + required: true + release_id: + description: "GitHub release ID" + required: true + +concurrency: + group: publish + cancel-in-progress: true + +permissions: + contents: write + packages: write + +env: + NODE_VERSION: 22 + +jobs: + publish: + runs-on: ubuntu-latest + if: github.event.release.prerelease == false + steps: + - uses: actions/checkout@v4 + with: + ref: ${{ github.event.release.tag_name || inputs.tag_name }} + + - name: Setup Node + uses: actions/setup-node@v4 + with: + node-version: ${{ env.NODE_VERSION }} + + - name: Configure npm authentication + run: | + echo "//registry.npmjs.org/:_authToken=${{ secrets.NPM_TOKEN }}" > ~/.npmrc + + - name: Download release assets + uses: actions/github-script@v7 + env: + RELEASE_ID: ${{ inputs.release_id }} + with: + script: | + const fs = require('fs'); + const path = require('path'); + + const releaseId = context.payload.release?.id || process.env.RELEASE_ID; + console.log("releaseId:", releaseId); + + if (!releaseId) { + core.setFailed('No release ID found.'); + return; + } + + const release = await github.rest.repos.getRelease({ + owner: context.repo.owner, + repo: context.repo.repo, + release_id: releaseId + }); + + const tgzAsset = release.data.assets.find(asset => asset.name.endsWith('.tgz')); + + if (!tgzAsset) { + core.setFailed('No .tgz file found in release assets'); + return; + } + + const response = await github.rest.repos.getReleaseAsset({ + owner: context.repo.owner, + repo: context.repo.repo, + asset_id: tgzAsset.id, + headers: { + Accept: 'application/octet-stream' + } + }); + + const filePath = path.join('npx-cli', tgzAsset.name); + fs.writeFileSync(filePath, Buffer.from(response.data)); + + console.log(`Downloaded ${tgzAsset.name} to ${filePath}`); + + core.setOutput('package-file', filePath); + core.setOutput('package-name', tgzAsset.name); + + - name: Verify package integrity + id: verify + run: | + cd npx-cli + ls -la *.tgz + PACKAGE_FILE=$(ls *.tgz | head -n1) + echo "package-file=$PACKAGE_FILE" >> $GITHUB_OUTPUT + + - name: Publish to npm + run: | + cd npx-cli + PACKAGE_FILE="${{ steps.verify.outputs.package-file }}" + echo "Publishing $PACKAGE_FILE to npm..." + npm publish "$PACKAGE_FILE" --provenance --access public + echo "✅ Successfully published to npm!" + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + + - name: Update release description + uses: actions/github-script@v7 + env: + RELEASE_ID: ${{ inputs.release_id }} + with: + script: | + const releaseId = context.payload.release?.id || process.env.RELEASE_ID; + + const release = await github.rest.repos.getRelease({ + owner: context.repo.owner, + repo: context.repo.repo, + release_id: releaseId + }); + + const currentBody = release.data.body || ''; + await github.rest.repos.updateRelease({ + owner: context.repo.owner, + repo: context.repo.repo, + release_id: releaseId, + body: currentBody + '\n\n✅ **Published to npm registry**' + }); diff --git a/Cargo.toml b/Cargo.toml index 6fb850e..b25004c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,3 +18,8 @@ async-trait = "0.1" schemars = "1.0.4" rand = "0.8" libc = "0.2" + +[profile.release] +lto = true +codegen-units = 1 +strip = "symbols" diff --git a/npx-cli/.gitignore b/npx-cli/.gitignore new file mode 100644 index 0000000..1ab415f --- /dev/null +++ b/npx-cli/.gitignore @@ -0,0 +1,3 @@ +node_modules/ +dist/ +*.tgz diff --git a/npx-cli/bin/cli.js b/npx-cli/bin/cli.js new file mode 100644 index 0000000..98818b5 --- /dev/null +++ b/npx-cli/bin/cli.js @@ -0,0 +1,119 @@ +#!/usr/bin/env node + +const { execSync, spawn } = require("child_process"); +const AdmZip = require("adm-zip"); +const path = require("path"); +const fs = require("fs"); + +function getEffectiveArch() { + const platform = process.platform; + const nodeArch = process.arch; + + if (platform === "darwin") { + if (nodeArch === "arm64") return "arm64"; + + try { + const translated = execSync("sysctl -in sysctl.proc_translated", { + encoding: "utf8", + }).trim(); + if (translated === "1") return "arm64"; + } catch { + } + return "x64"; + } + + if (/arm/i.test(nodeArch)) return "arm64"; + + if (platform === "win32") { + const pa = process.env.PROCESSOR_ARCHITECTURE || ""; + const paw = process.env.PROCESSOR_ARCHITEW6432 || ""; + if (/arm/i.test(pa) || /arm/i.test(paw)) return "arm64"; + } + + return "x64"; +} + +const platform = process.platform; +const arch = getEffectiveArch(); + +function getPlatformDir() { + if (platform === "linux" && arch === "x64") return "linux-x64"; + if (platform === "linux" && arch === "arm64") return "linux-arm64"; + if (platform === "win32" && arch === "x64") return "windows-x64"; + if (platform === "win32" && arch === "arm64") return "windows-arm64"; + if (platform === "darwin" && arch === "x64") return "macos-x64"; + if (platform === "darwin" && arch === "arm64") return "macos-arm64"; + + console.error(`❌ Unsupported platform: ${platform}-${arch}`); + console.error("Supported platforms:"); + console.error(" - Linux x64"); + console.error(" - Linux ARM64"); + console.error(" - Windows x64"); + console.error(" - Windows ARM64"); + console.error(" - macOS x64 (Intel)"); + console.error(" - macOS ARM64 (Apple Silicon)"); + process.exit(1); +} + +function getBinaryName() { + return platform === "win32" ? "mcp-dev-manager.exe" : "mcp-dev-manager"; +} + +const platformDir = getPlatformDir(); +const extractDir = path.join(__dirname, "..", "dist", platformDir); + +fs.mkdirSync(extractDir, { recursive: true }); + +function extractAndRun() { + const binName = getBinaryName(); + const binPath = path.join(extractDir, binName); + const zipName = "mcp-dev-manager.zip"; + const zipPath = path.join(extractDir, zipName); + + if (fs.existsSync(binPath)) fs.unlinkSync(binPath); + + if (!fs.existsSync(zipPath)) { + console.error(`❌ ${zipName} not found at: ${zipPath}`); + console.error(`Current platform: ${platform}-${arch} (${platformDir})`); + process.exit(1); + } + + try { + const zip = new AdmZip(zipPath); + zip.extractAllTo(extractDir, true); + } catch (err) { + console.error("❌ Failed to extract mcp-dev-manager archive:", err.message); + if (process.env.MCP_DEV_MANAGER_DEBUG) { + console.error(err.stack); + } + process.exit(1); + } + + if (!fs.existsSync(binPath)) { + console.error(`❌ Extracted binary not found at: ${binPath}`); + console.error("This usually indicates a corrupt download. Please reinstall the package."); + process.exit(1); + } + + if (platform !== "win32") { + try { + fs.chmodSync(binPath, 0o755); + } catch { } + } + + console.log(`🚀 Launching mcp-dev-manager...`); + + const args = process.argv.slice(2); + const proc = spawn(binPath, args, { stdio: "inherit" }); + + proc.on("exit", (code) => process.exit(code || 0)); + proc.on("error", (err) => { + console.error("❌ Failed to start mcp-dev-manager:", err.message); + process.exit(1); + }); + + process.on("SIGINT", () => proc.kill("SIGINT")); + process.on("SIGTERM", () => proc.kill("SIGTERM")); +} + +extractAndRun(); diff --git a/npx-cli/package.json b/npx-cli/package.json new file mode 100644 index 0000000..2281f11 --- /dev/null +++ b/npx-cli/package.json @@ -0,0 +1,27 @@ +{ + "name": "mcp-dev-manager", + "private": false, + "version": "0.1.0", + "main": "index.js", + "bin": { + "mcp-dev-manager": "bin/cli.js" + }, + "keywords": [ + "mcp", + "dev-server", + "daemon" + ], + "author": "", + "license": "MIT", + "description": "MCP Dev Server Manager - A daemon for managing development servers", + "dependencies": { + "adm-zip": "^0.5.16" + }, + "files": [ + "dist", + "bin" + ], + "engines": { + "node": ">=18" + } +}