diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 7bb8b9b..acf0000 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -11,11 +11,45 @@ on: - "v*" jobs: - publish: + verify-tag: runs-on: ubuntu-latest - environment: npm-publish - permissions: - contents: read + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Verify tag matches package version + if: startsWith(github.ref, 'refs/tags/') + run: | + PKG_VERSION=$(jq -r '.version' package.json) + TAG_VERSION=${GITHUB_REF#refs/tags/} + if [ "v${PKG_VERSION}" != "${TAG_VERSION}" ]; then + echo "Tag (${TAG_VERSION}) does not match package.json version (v${PKG_VERSION})." + exit 1 + fi + + build: + needs: verify-tag + name: ${{ matrix.name }} + runs-on: ${{ matrix.runner }} + strategy: + fail-fast: false + matrix: + include: + - name: macOS arm64 + runner: macos-14 + binary: letta-macos-arm64 + - name: macOS x64 + runner: macos-13 + binary: letta-macos-x64 + - name: Linux x64 + runner: ubuntu-24.04 + binary: letta-linux-x64 + - name: Linux arm64 + runner: ubuntu-24.04-arm + binary: letta-linux-arm64 + - name: Windows x64 + runner: windows-latest + binary: letta-windows-x64.exe steps: - name: Checkout uses: actions/checkout@v4 @@ -28,23 +62,72 @@ jobs: - name: Install dependencies run: bun install - - name: Verify tag matches package version - if: startsWith(github.ref, 'refs/tags/') - run: | - PKG_VERSION=$(jq -r '.version' package.json) - TAG_VERSION=${GITHUB_REF#refs/tags/} - if [ "v${PKG_VERSION}" != "${TAG_VERSION}" ]; then - echo "Tag (${TAG_VERSION}) does not match package.json version (v${PKG_VERSION})." - exit 1 - fi - - name: Build binary run: bun run build + - name: Rename binary + shell: bash + run: | + if [[ "${{ matrix.runner }}" == windows* ]]; then + mv bin/letta.exe bin/${{ matrix.binary }} + else + mv bin/letta bin/${{ matrix.binary }} + fi + + - name: Upload binary artifact + uses: actions/upload-artifact@v4 + with: + name: ${{ matrix.binary }} + path: bin/${{ matrix.binary }} + if-no-files-found: error + + publish: + needs: build + runs-on: ubuntu-latest + environment: npm-publish + permissions: + contents: write + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Bun + uses: oven-sh/setup-bun@v1 + with: + bun-version: 1.3.0 + + - name: Install dependencies + run: bun install + + - name: Download all binary artifacts + uses: actions/download-artifact@v4 + with: + path: bin-artifacts + + - name: Move binaries to bin directory + run: | + mkdir -p bin + mv bin-artifacts/*/* bin/ + ls -lah bin/ + chmod +x bin/letta-* + + - name: Smoke test - help + run: bun bin/letta.js --help + + - name: Smoke test - version + run: bun bin/letta.js --version || echo "Version flag not implemented yet" + - name: Integration smoke test (real API) env: LETTA_API_KEY: ${{ secrets.LETTA_API_KEY }} - run: ./bin/letta --prompt "ping" --tools "" --permission-mode plan + run: bun bin/letta.js --prompt "ping" --tools "" --permission-mode plan + + - name: Create GitHub Release + uses: softprops/action-gh-release@v2 + with: + tag_name: ${{ github.ref_name }} + files: bin/letta-* + fail_on_unmatched_files: true - name: Publish to npm env: diff --git a/.gitignore b/.gitignore index 675d805..3527d9e 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ node_modules bun.lockb dist bin/ +!bin/letta.js .DS_Store # Logs diff --git a/bin/letta.js b/bin/letta.js new file mode 100755 index 0000000..538b2e1 --- /dev/null +++ b/bin/letta.js @@ -0,0 +1,86 @@ +#!/usr/bin/env node + +/** + * Unified entry point for the Letta CLI. + * Detects the platform and spawns the appropriate compiled binary. + * + * Note: Uses #!/usr/bin/env node (not bun) for maximum compatibility + * when users install via npm/npx. Bun can still run this file. + */ + +import path from "path"; +import { fileURLToPath } from "url"; +import { spawn } from "child_process"; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +const { platform, arch } = process; + +// Map platform/arch to binary name +let binaryName = null; +switch (platform) { + case "linux": + switch (arch) { + case "x64": + binaryName = "letta-linux-x64"; + break; + case "arm64": + binaryName = "letta-linux-arm64"; + break; + } + break; + case "darwin": + switch (arch) { + case "x64": + binaryName = "letta-macos-x64"; + break; + case "arm64": + binaryName = "letta-macos-arm64"; + break; + } + break; + case "win32": + switch (arch) { + case "x64": + binaryName = "letta-windows-x64.exe"; + break; + } + break; +} + +if (!binaryName) { + console.error(`Error: Unsupported platform: ${platform} ${arch}`); + console.error("Supported platforms:"); + console.error(" - macOS: arm64, x64"); + console.error(" - Linux: arm64, x64"); + console.error(" - Windows: x64"); + process.exit(1); +} + +const binaryPath = path.join(__dirname, binaryName); + +// Spawn the binary with all arguments +const child = spawn(binaryPath, process.argv.slice(2), { + stdio: "inherit", + env: process.env, +}); + +// Forward signals to child process +function forwardSignal(signal) { + process.on(signal, () => { + child.kill(signal); + }); +} + +forwardSignal("SIGINT"); +forwardSignal("SIGTERM"); + +// Exit with the same code as the child process +child.on("exit", (code, signal) => { + if (signal) { + process.kill(process.pid, signal); + } else { + process.exit(code || 0); + } +}); diff --git a/package.json b/package.json index c82793f..7c3b37a 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "description": "Letta Code is a CLI tool for interacting with stateful Letta agents from the terminal.", "type": "module", "bin": { - "letta": "bin/letta" + "letta": "bin/letta.js" }, "files": [ "LICENSE", @@ -42,7 +42,7 @@ "fix": "bunx --bun @biomejs/biome@2.2.5 check --write src", "dev:ui": "bun --loader:.md=text --loader:.mdx=text --loader:.txt=text run src/index.ts", "build": "bun build src/index.ts --compile --loader:.md=text --loader:.mdx=text --loader:.txt=text --outfile bin/letta", - "prepublishOnly": "bun run build", + "prepublishOnly": "echo 'Binaries should be built in CI. Run bun run build for local development.'", "postinstall": "bun scripts/postinstall-patches.js || true" }, "lint-staged": {