diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 279609db..a2daa4a8 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -60,7 +60,7 @@ jobs: run: bun run typecheck publish: - runs-on: ubuntu-latest + runs-on: macos-latest needs: [test, typecheck] if: github.repository == 'code-yeongyu/oh-my-opencode' steps: @@ -112,6 +112,9 @@ jobs: tsc --emitDeclarationOnly echo "=== Running build:schema ===" bun run build:schema + + - name: Build platform binaries + run: bun run build:binaries - name: Verify build output run: | @@ -121,6 +124,13 @@ jobs: ls -la dist/cli/ test -f dist/index.js || (echo "ERROR: dist/index.js not found!" && exit 1) test -f dist/cli/index.js || (echo "ERROR: dist/cli/index.js not found!" && exit 1) + echo "=== Platform binaries ===" + for platform in darwin-arm64 darwin-x64 linux-x64 linux-arm64 linux-x64-musl linux-arm64-musl; do + test -f "packages/${platform}/bin/oh-my-opencode" || (echo "ERROR: packages/${platform}/bin/oh-my-opencode not found!" && exit 1) + echo "✓ packages/${platform}/bin/oh-my-opencode" + done + test -f "packages/windows-x64/bin/oh-my-opencode.exe" || (echo "ERROR: packages/windows-x64/bin/oh-my-opencode.exe not found!" && exit 1) + echo "✓ packages/windows-x64/bin/oh-my-opencode.exe" - name: Publish run: bun run script/publish.ts diff --git a/.gitignore b/.gitignore index b43656d7..e913cc4b 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,10 @@ node_modules/ # Build output dist/ +# Platform binaries (built, not committed) +packages/*/bin/oh-my-opencode +packages/*/bin/oh-my-opencode.exe + # IDE .idea/ .vscode/ diff --git a/README.md b/README.md index a23e3fdc..a3dfa066 100644 --- a/README.md +++ b/README.md @@ -258,20 +258,17 @@ If you don't want all this, as mentioned, you can just pick and choose specific ### For Humans -> **⚠️ Prerequisite: Bun is required** -> -> This tool **requires [Bun](https://bun.sh/) to be installed** on your system. -> Even if you use `npx` to run the installer, the underlying runtime depends on Bun. - Run the interactive installer: ```bash -bunx oh-my-opencode install -# or use npx if bunx doesn't work npx oh-my-opencode install +# or with bun +bunx oh-my-opencode install ``` -> **Note for Ubuntu/Debian users**: If you installed Bun via Snap (`/snap/bin/bun`), `bunx` will fail with "script not found" due to Snap's sandboxing. Either use `npx` instead, or reinstall Bun via the official installer: `curl -fsSL https://bun.sh/install | bash` +> **Note**: The CLI ships with standalone binaries for all major platforms. No runtime (Bun/Node.js) is required for CLI execution after installation. +> +> **Supported platforms**: macOS (ARM64, x64), Linux (x64, ARM64, Alpine/musl), Windows (x64) Follow the prompts to configure your Claude, ChatGPT, and Gemini subscriptions. After installation, authenticate your providers as instructed. diff --git a/bin/oh-my-opencode.js b/bin/oh-my-opencode.js new file mode 100644 index 00000000..4ad39550 --- /dev/null +++ b/bin/oh-my-opencode.js @@ -0,0 +1,80 @@ +#!/usr/bin/env node +// bin/oh-my-opencode.js +// Wrapper script that detects platform and spawns the correct binary + +import { spawnSync } from "node:child_process"; +import { createRequire } from "node:module"; +import { getPlatformPackage, getBinaryPath } from "./platform.js"; + +const require = createRequire(import.meta.url); + +/** + * Detect libc family on Linux + * @returns {string | null} 'glibc', 'musl', or null if detection fails + */ +function getLibcFamily() { + if (process.platform !== "linux") { + return undefined; // Not needed on non-Linux + } + + try { + const detectLibc = require("detect-libc"); + return detectLibc.familySync(); + } catch { + // detect-libc not available + return null; + } +} + +function main() { + const { platform, arch } = process; + const libcFamily = getLibcFamily(); + + // Get platform package name + let pkg; + try { + pkg = getPlatformPackage({ platform, arch, libcFamily }); + } catch (error) { + console.error(`\noh-my-opencode: ${error.message}\n`); + process.exit(1); + } + + // Resolve binary path + const binRelPath = getBinaryPath(pkg, platform); + + let binPath; + try { + binPath = require.resolve(binRelPath); + } catch { + console.error(`\noh-my-opencode: Platform binary not installed.`); + console.error(`\nYour platform: ${platform}-${arch}${libcFamily === "musl" ? "-musl" : ""}`); + console.error(`Expected package: ${pkg}`); + console.error(`\nTo fix, run:`); + console.error(` npm install ${pkg}\n`); + process.exit(1); + } + + // Spawn the binary + const result = spawnSync(binPath, process.argv.slice(2), { + stdio: "inherit", + }); + + // Handle spawn errors + if (result.error) { + console.error(`\noh-my-opencode: Failed to execute binary.`); + console.error(`Error: ${result.error.message}\n`); + process.exit(2); + } + + // Handle signals + if (result.signal) { + const signalNum = result.signal === "SIGTERM" ? 15 : + result.signal === "SIGKILL" ? 9 : + result.signal === "SIGINT" ? 2 : 1; + process.exit(128 + signalNum); + } + + process.exit(result.status ?? 1); +} + +main(); diff --git a/bin/platform.js b/bin/platform.js new file mode 100644 index 00000000..ac728d3c --- /dev/null +++ b/bin/platform.js @@ -0,0 +1,38 @@ +// bin/platform.js +// Shared platform detection module - used by wrapper and postinstall + +/** + * Get the platform-specific package name + * @param {{ platform: string, arch: string, libcFamily?: string | null }} options + * @returns {string} Package name like "oh-my-opencode-darwin-arm64" + * @throws {Error} If libc cannot be detected on Linux + */ +export function getPlatformPackage({ platform, arch, libcFamily }) { + let suffix = ""; + if (platform === "linux") { + if (libcFamily === null || libcFamily === undefined) { + throw new Error( + "Could not detect libc on Linux. " + + "Please ensure detect-libc is installed or report this issue." + ); + } + if (libcFamily === "musl") { + suffix = "-musl"; + } + } + + // Map platform names: win32 -> windows (for package name) + const os = platform === "win32" ? "windows" : platform; + return `oh-my-opencode-${os}-${arch}${suffix}`; +} + +/** + * Get the path to the binary within a platform package + * @param {string} pkg Package name + * @param {string} platform Process platform + * @returns {string} Relative path like "oh-my-opencode-darwin-arm64/bin/oh-my-opencode" + */ +export function getBinaryPath(pkg, platform) { + const ext = platform === "win32" ? ".exe" : ""; + return `${pkg}/bin/oh-my-opencode${ext}`; +} diff --git a/bin/platform.test.ts b/bin/platform.test.ts new file mode 100644 index 00000000..77550992 --- /dev/null +++ b/bin/platform.test.ts @@ -0,0 +1,148 @@ +// bin/platform.test.ts +import { describe, expect, test } from "bun:test"; +import { getPlatformPackage, getBinaryPath } from "./platform.js"; + +describe("getPlatformPackage", () => { + // #region Darwin platforms + test("returns darwin-arm64 for macOS ARM64", () => { + // #given macOS ARM64 platform + const input = { platform: "darwin", arch: "arm64" }; + + // #when getting platform package + const result = getPlatformPackage(input); + + // #then returns correct package name + expect(result).toBe("oh-my-opencode-darwin-arm64"); + }); + + test("returns darwin-x64 for macOS Intel", () => { + // #given macOS x64 platform + const input = { platform: "darwin", arch: "x64" }; + + // #when getting platform package + const result = getPlatformPackage(input); + + // #then returns correct package name + expect(result).toBe("oh-my-opencode-darwin-x64"); + }); + // #endregion + + // #region Linux glibc platforms + test("returns linux-x64 for Linux x64 with glibc", () => { + // #given Linux x64 with glibc + const input = { platform: "linux", arch: "x64", libcFamily: "glibc" }; + + // #when getting platform package + const result = getPlatformPackage(input); + + // #then returns correct package name + expect(result).toBe("oh-my-opencode-linux-x64"); + }); + + test("returns linux-arm64 for Linux ARM64 with glibc", () => { + // #given Linux ARM64 with glibc + const input = { platform: "linux", arch: "arm64", libcFamily: "glibc" }; + + // #when getting platform package + const result = getPlatformPackage(input); + + // #then returns correct package name + expect(result).toBe("oh-my-opencode-linux-arm64"); + }); + // #endregion + + // #region Linux musl platforms + test("returns linux-x64-musl for Alpine x64", () => { + // #given Linux x64 with musl (Alpine) + const input = { platform: "linux", arch: "x64", libcFamily: "musl" }; + + // #when getting platform package + const result = getPlatformPackage(input); + + // #then returns correct package name with musl suffix + expect(result).toBe("oh-my-opencode-linux-x64-musl"); + }); + + test("returns linux-arm64-musl for Alpine ARM64", () => { + // #given Linux ARM64 with musl (Alpine) + const input = { platform: "linux", arch: "arm64", libcFamily: "musl" }; + + // #when getting platform package + const result = getPlatformPackage(input); + + // #then returns correct package name with musl suffix + expect(result).toBe("oh-my-opencode-linux-arm64-musl"); + }); + // #endregion + + // #region Windows platform + test("returns windows-x64 for Windows", () => { + // #given Windows x64 platform (win32 is Node's platform name) + const input = { platform: "win32", arch: "x64" }; + + // #when getting platform package + const result = getPlatformPackage(input); + + // #then returns correct package name with 'windows' not 'win32' + expect(result).toBe("oh-my-opencode-windows-x64"); + }); + // #endregion + + // #region Error cases + test("throws error for Linux with null libcFamily", () => { + // #given Linux platform with null libc detection + const input = { platform: "linux", arch: "x64", libcFamily: null }; + + // #when getting platform package + // #then throws descriptive error + expect(() => getPlatformPackage(input)).toThrow("Could not detect libc"); + }); + + test("throws error for Linux with undefined libcFamily", () => { + // #given Linux platform with undefined libc + const input = { platform: "linux", arch: "x64", libcFamily: undefined }; + + // #when getting platform package + // #then throws descriptive error + expect(() => getPlatformPackage(input)).toThrow("Could not detect libc"); + }); + // #endregion +}); + +describe("getBinaryPath", () => { + test("returns path without .exe for Unix platforms", () => { + // #given Unix platform package + const pkg = "oh-my-opencode-darwin-arm64"; + const platform = "darwin"; + + // #when getting binary path + const result = getBinaryPath(pkg, platform); + + // #then returns path without extension + expect(result).toBe("oh-my-opencode-darwin-arm64/bin/oh-my-opencode"); + }); + + test("returns path with .exe for Windows", () => { + // #given Windows platform package + const pkg = "oh-my-opencode-windows-x64"; + const platform = "win32"; + + // #when getting binary path + const result = getBinaryPath(pkg, platform); + + // #then returns path with .exe extension + expect(result).toBe("oh-my-opencode-windows-x64/bin/oh-my-opencode.exe"); + }); + + test("returns path without .exe for Linux", () => { + // #given Linux platform package + const pkg = "oh-my-opencode-linux-x64"; + const platform = "linux"; + + // #when getting binary path + const result = getBinaryPath(pkg, platform); + + // #then returns path without extension + expect(result).toBe("oh-my-opencode-linux-x64/bin/oh-my-opencode"); + }); +}); diff --git a/package.json b/package.json index 63e83ec6..253e0114 100644 --- a/package.json +++ b/package.json @@ -6,10 +6,12 @@ "types": "dist/index.d.ts", "type": "module", "bin": { - "oh-my-opencode": "./dist/cli/index.js" + "oh-my-opencode": "./bin/oh-my-opencode.js" }, "files": [ - "dist" + "dist", + "bin", + "postinstall.mjs" ], "exports": { ".": { @@ -20,8 +22,11 @@ }, "scripts": { "build": "bun build src/index.ts --outdir dist --target bun --format esm --external @ast-grep/napi && tsc --emitDeclarationOnly && bun build src/cli/index.ts --outdir dist/cli --target bun --format esm --external @ast-grep/napi && bun run build:schema", + "build:all": "bun run build && bun run build:binaries", + "build:binaries": "bun run script/build-binaries.ts", "build:schema": "bun run script/build-schema.ts", "clean": "rm -rf dist", + "postinstall": "node postinstall.mjs", "prepublishOnly": "bun run clean && bun run build", "typecheck": "tsc --noEmit", "test": "bun test" @@ -55,6 +60,7 @@ "@opencode-ai/plugin": "^1.1.19", "@opencode-ai/sdk": "^1.1.19", "commander": "^14.0.2", + "detect-libc": "^2.0.0", "hono": "^4.10.4", "js-yaml": "^4.1.1", "jsonc-parser": "^3.3.1", @@ -70,6 +76,15 @@ "bun-types": "latest", "typescript": "^5.7.3" }, + "optionalDependencies": { + "oh-my-opencode-darwin-arm64": "0.0.0", + "oh-my-opencode-darwin-x64": "0.0.0", + "oh-my-opencode-linux-arm64": "0.0.0", + "oh-my-opencode-linux-arm64-musl": "0.0.0", + "oh-my-opencode-linux-x64": "0.0.0", + "oh-my-opencode-linux-x64-musl": "0.0.0", + "oh-my-opencode-windows-x64": "0.0.0" + }, "trustedDependencies": [ "@ast-grep/cli", "@ast-grep/napi", diff --git a/packages/darwin-arm64/bin/.gitkeep b/packages/darwin-arm64/bin/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/packages/darwin-arm64/package.json b/packages/darwin-arm64/package.json new file mode 100644 index 00000000..bf06f9ac --- /dev/null +++ b/packages/darwin-arm64/package.json @@ -0,0 +1,16 @@ +{ + "name": "oh-my-opencode-darwin-arm64", + "version": "0.0.0", + "description": "Platform-specific binary for oh-my-opencode (darwin-arm64)", + "license": "MIT", + "repository": { + "type": "git", + "url": "https://github.com/code-yeongyu/oh-my-opencode" + }, + "os": ["darwin"], + "cpu": ["arm64"], + "files": ["bin"], + "bin": { + "oh-my-opencode": "./bin/oh-my-opencode" + } +} diff --git a/packages/darwin-x64/bin/.gitkeep b/packages/darwin-x64/bin/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/packages/darwin-x64/package.json b/packages/darwin-x64/package.json new file mode 100644 index 00000000..672a7cfa --- /dev/null +++ b/packages/darwin-x64/package.json @@ -0,0 +1,16 @@ +{ + "name": "oh-my-opencode-darwin-x64", + "version": "0.0.0", + "description": "Platform-specific binary for oh-my-opencode (darwin-x64)", + "license": "MIT", + "repository": { + "type": "git", + "url": "https://github.com/code-yeongyu/oh-my-opencode" + }, + "os": ["darwin"], + "cpu": ["x64"], + "files": ["bin"], + "bin": { + "oh-my-opencode": "./bin/oh-my-opencode" + } +} diff --git a/packages/linux-arm64-musl/bin/.gitkeep b/packages/linux-arm64-musl/bin/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/packages/linux-arm64-musl/package.json b/packages/linux-arm64-musl/package.json new file mode 100644 index 00000000..12c84287 --- /dev/null +++ b/packages/linux-arm64-musl/package.json @@ -0,0 +1,17 @@ +{ + "name": "oh-my-opencode-linux-arm64-musl", + "version": "0.0.0", + "description": "Platform-specific binary for oh-my-opencode (linux-arm64-musl)", + "license": "MIT", + "repository": { + "type": "git", + "url": "https://github.com/code-yeongyu/oh-my-opencode" + }, + "os": ["linux"], + "cpu": ["arm64"], + "libc": ["musl"], + "files": ["bin"], + "bin": { + "oh-my-opencode": "./bin/oh-my-opencode" + } +} diff --git a/packages/linux-arm64/bin/.gitkeep b/packages/linux-arm64/bin/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/packages/linux-arm64/package.json b/packages/linux-arm64/package.json new file mode 100644 index 00000000..0333020a --- /dev/null +++ b/packages/linux-arm64/package.json @@ -0,0 +1,17 @@ +{ + "name": "oh-my-opencode-linux-arm64", + "version": "0.0.0", + "description": "Platform-specific binary for oh-my-opencode (linux-arm64)", + "license": "MIT", + "repository": { + "type": "git", + "url": "https://github.com/code-yeongyu/oh-my-opencode" + }, + "os": ["linux"], + "cpu": ["arm64"], + "libc": ["glibc"], + "files": ["bin"], + "bin": { + "oh-my-opencode": "./bin/oh-my-opencode" + } +} diff --git a/packages/linux-x64-musl/bin/.gitkeep b/packages/linux-x64-musl/bin/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/packages/linux-x64-musl/package.json b/packages/linux-x64-musl/package.json new file mode 100644 index 00000000..feccce17 --- /dev/null +++ b/packages/linux-x64-musl/package.json @@ -0,0 +1,17 @@ +{ + "name": "oh-my-opencode-linux-x64-musl", + "version": "0.0.0", + "description": "Platform-specific binary for oh-my-opencode (linux-x64-musl)", + "license": "MIT", + "repository": { + "type": "git", + "url": "https://github.com/code-yeongyu/oh-my-opencode" + }, + "os": ["linux"], + "cpu": ["x64"], + "libc": ["musl"], + "files": ["bin"], + "bin": { + "oh-my-opencode": "./bin/oh-my-opencode" + } +} diff --git a/packages/linux-x64/bin/.gitkeep b/packages/linux-x64/bin/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/packages/linux-x64/package.json b/packages/linux-x64/package.json new file mode 100644 index 00000000..e447acaf --- /dev/null +++ b/packages/linux-x64/package.json @@ -0,0 +1,17 @@ +{ + "name": "oh-my-opencode-linux-x64", + "version": "0.0.0", + "description": "Platform-specific binary for oh-my-opencode (linux-x64)", + "license": "MIT", + "repository": { + "type": "git", + "url": "https://github.com/code-yeongyu/oh-my-opencode" + }, + "os": ["linux"], + "cpu": ["x64"], + "libc": ["glibc"], + "files": ["bin"], + "bin": { + "oh-my-opencode": "./bin/oh-my-opencode" + } +} diff --git a/packages/windows-x64/bin/.gitkeep b/packages/windows-x64/bin/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/packages/windows-x64/package.json b/packages/windows-x64/package.json new file mode 100644 index 00000000..a6c64e80 --- /dev/null +++ b/packages/windows-x64/package.json @@ -0,0 +1,16 @@ +{ + "name": "oh-my-opencode-windows-x64", + "version": "0.0.0", + "description": "Platform-specific binary for oh-my-opencode (windows-x64)", + "license": "MIT", + "repository": { + "type": "git", + "url": "https://github.com/code-yeongyu/oh-my-opencode" + }, + "os": ["win32"], + "cpu": ["x64"], + "files": ["bin"], + "bin": { + "oh-my-opencode": "./bin/oh-my-opencode.exe" + } +} diff --git a/postinstall.mjs b/postinstall.mjs new file mode 100644 index 00000000..8243a562 --- /dev/null +++ b/postinstall.mjs @@ -0,0 +1,43 @@ +// postinstall.mjs +// Runs after npm install to verify platform binary is available + +import { createRequire } from "node:module"; +import { getPlatformPackage, getBinaryPath } from "./bin/platform.js"; + +const require = createRequire(import.meta.url); + +/** + * Detect libc family on Linux + */ +function getLibcFamily() { + if (process.platform !== "linux") { + return undefined; + } + + try { + const detectLibc = require("detect-libc"); + return detectLibc.familySync(); + } catch { + return null; + } +} + +function main() { + const { platform, arch } = process; + const libcFamily = getLibcFamily(); + + try { + const pkg = getPlatformPackage({ platform, arch, libcFamily }); + const binPath = getBinaryPath(pkg, platform); + + // Try to resolve the binary + require.resolve(binPath); + console.log(`✓ oh-my-opencode binary installed for ${platform}-${arch}`); + } catch (error) { + console.warn(`⚠ oh-my-opencode: ${error.message}`); + console.warn(` The CLI may not work on this platform.`); + // Don't fail installation - let user try anyway + } +} + +main(); diff --git a/script/build-binaries.ts b/script/build-binaries.ts new file mode 100644 index 00000000..a0389942 --- /dev/null +++ b/script/build-binaries.ts @@ -0,0 +1,103 @@ +#!/usr/bin/env bun +// script/build-binaries.ts +// Build platform-specific binaries for CLI distribution + +import { $ } from "bun"; +import { existsSync } from "node:fs"; +import { join } from "node:path"; + +interface PlatformTarget { + dir: string; + target: string; + binary: string; + description: string; +} + +const PLATFORMS: PlatformTarget[] = [ + { dir: "darwin-arm64", target: "bun-darwin-arm64", binary: "oh-my-opencode", description: "macOS ARM64" }, + { dir: "darwin-x64", target: "bun-darwin-x64", binary: "oh-my-opencode", description: "macOS x64" }, + { dir: "linux-x64", target: "bun-linux-x64", binary: "oh-my-opencode", description: "Linux x64 (glibc)" }, + { dir: "linux-arm64", target: "bun-linux-arm64", binary: "oh-my-opencode", description: "Linux ARM64 (glibc)" }, + { dir: "linux-x64-musl", target: "bun-linux-x64-musl", binary: "oh-my-opencode", description: "Linux x64 (musl)" }, + { dir: "linux-arm64-musl", target: "bun-linux-arm64-musl", binary: "oh-my-opencode", description: "Linux ARM64 (musl)" }, + { dir: "windows-x64", target: "bun-windows-x64", binary: "oh-my-opencode.exe", description: "Windows x64" }, +]; + +const ENTRY_POINT = "src/cli/index.ts"; + +async function buildPlatform(platform: PlatformTarget): Promise { + const outfile = join("packages", platform.dir, "bin", platform.binary); + + console.log(`\n📦 Building ${platform.description}...`); + console.log(` Target: ${platform.target}`); + console.log(` Output: ${outfile}`); + + try { + await $`bun build --compile --minify --sourcemap --bytecode --target=${platform.target} ${ENTRY_POINT} --outfile=${outfile}`; + + // Verify binary exists + if (!existsSync(outfile)) { + console.error(` ❌ Binary not found after build: ${outfile}`); + return false; + } + + // Verify binary with file command (skip on Windows host for non-Windows targets) + if (process.platform !== "win32") { + const fileInfo = await $`file ${outfile}`.text(); + console.log(` ✓ ${fileInfo.trim()}`); + } else { + console.log(` ✓ Binary created successfully`); + } + + return true; + } catch (error) { + console.error(` ❌ Build failed: ${error}`); + return false; + } +} + +async function main() { + console.log("🔨 Building oh-my-opencode platform binaries"); + console.log(` Entry point: ${ENTRY_POINT}`); + console.log(` Platforms: ${PLATFORMS.length}`); + + // Verify entry point exists + if (!existsSync(ENTRY_POINT)) { + console.error(`\n❌ Entry point not found: ${ENTRY_POINT}`); + process.exit(1); + } + + const results: { platform: string; success: boolean }[] = []; + + for (const platform of PLATFORMS) { + const success = await buildPlatform(platform); + results.push({ platform: platform.description, success }); + } + + // Summary + console.log("\n" + "=".repeat(50)); + console.log("Build Summary:"); + console.log("=".repeat(50)); + + const succeeded = results.filter(r => r.success).length; + const failed = results.filter(r => !r.success).length; + + for (const result of results) { + const icon = result.success ? "✓" : "✗"; + console.log(` ${icon} ${result.platform}`); + } + + console.log("=".repeat(50)); + console.log(`Total: ${succeeded} succeeded, ${failed} failed`); + + if (failed > 0) { + process.exit(1); + } + + console.log("\n✅ All platform binaries built successfully!\n"); +} + +main().catch((error) => { + console.error("Fatal error:", error); + process.exit(1); +}); diff --git a/script/publish.ts b/script/publish.ts index 3a687331..8ca25461 100644 --- a/script/publish.ts +++ b/script/publish.ts @@ -1,12 +1,24 @@ #!/usr/bin/env bun import { $ } from "bun" +import { existsSync } from "node:fs" +import { join } from "node:path" const PACKAGE_NAME = "oh-my-opencode" const bump = process.env.BUMP as "major" | "minor" | "patch" | undefined const versionOverride = process.env.VERSION -console.log("=== Publishing oh-my-opencode ===\n") +const PLATFORM_PACKAGES = [ + "darwin-arm64", + "darwin-x64", + "linux-x64", + "linux-arm64", + "linux-x64-musl", + "linux-arm64-musl", + "windows-x64", +] + +console.log("=== Publishing oh-my-opencode (multi-package) ===\n") async function fetchPreviousVersion(): Promise { try { @@ -22,7 +34,9 @@ async function fetchPreviousVersion(): Promise { } function bumpVersion(version: string, type: "major" | "minor" | "patch"): string { - const [major, minor, patch] = version.split(".").map(Number) + // Handle prerelease versions (e.g., 3.0.0-beta.7) + const baseVersion = version.split("-")[0] + const [major, minor, patch] = baseVersion.split(".").map(Number) switch (type) { case "major": return `${major + 1}.0.0` @@ -33,14 +47,42 @@ function bumpVersion(version: string, type: "major" | "minor" | "patch"): string } } -async function updatePackageVersion(newVersion: string): Promise { - const pkgPath = new URL("../package.json", import.meta.url).pathname +async function updatePackageVersion(pkgPath: string, newVersion: string): Promise { let pkg = await Bun.file(pkgPath).text() pkg = pkg.replace(/"version": "[^"]+"/, `"version": "${newVersion}"`) - await Bun.file(pkgPath).write(pkg) + await Bun.write(pkgPath, pkg) console.log(`Updated: ${pkgPath}`) } +async function updateAllPackageVersions(newVersion: string): Promise { + console.log("\nSyncing version across all packages...") + + // Update main package.json + const mainPkgPath = new URL("../package.json", import.meta.url).pathname + await updatePackageVersion(mainPkgPath, newVersion) + + // Update optionalDependencies versions in main package.json + let mainPkg = await Bun.file(mainPkgPath).text() + for (const platform of PLATFORM_PACKAGES) { + const pkgName = `oh-my-opencode-${platform}` + mainPkg = mainPkg.replace( + new RegExp(`"${pkgName}": "[^"]+"`), + `"${pkgName}": "${newVersion}"` + ) + } + await Bun.write(mainPkgPath, mainPkg) + + // Update each platform package.json + for (const platform of PLATFORM_PACKAGES) { + const pkgPath = new URL(`../packages/${platform}/package.json`, import.meta.url).pathname + if (existsSync(pkgPath)) { + await updatePackageVersion(pkgPath, newVersion) + } else { + console.warn(`Warning: ${pkgPath} not found`) + } + } +} + async function generateChangelog(previous: string): Promise { const notes: string[] = [] @@ -113,28 +155,96 @@ function getDistTag(version: string): string | null { return tag || "next" } -async function buildAndPublish(version: string): Promise { - console.log("\nBuilding before publish...") - await $`bun run clean && bun run build` +interface PublishResult { + success: boolean + alreadyPublished?: boolean + error?: string +} - console.log("\nPublishing to npm...") - const distTag = getDistTag(version) +async function publishPackage(cwd: string, distTag: string | null): Promise { const tagArgs = distTag ? ["--tag", distTag] : [] + const provenanceArgs = process.env.CI ? ["--provenance"] : [] - if (process.env.CI) { - await $`npm publish --access public --provenance --ignore-scripts ${tagArgs}` - } else { - await $`npm publish --access public --ignore-scripts ${tagArgs}` + try { + await $`npm publish --access public --ignore-scripts ${provenanceArgs} ${tagArgs}`.cwd(cwd) + return { success: true } + } catch (error: any) { + const stderr = error?.stderr?.toString() || error?.message || "" + + // E409 = version already exists (idempotent success) + if ( + stderr.includes("EPUBLISHCONFLICT") || + stderr.includes("E409") || + stderr.includes("cannot publish over") || + stderr.includes("already exists") + ) { + return { success: true, alreadyPublished: true } + } + + return { success: false, error: stderr } } } +async function publishAllPackages(version: string): Promise { + const distTag = getDistTag(version) + + console.log("\n📦 Publishing platform packages...") + + // Publish platform packages first + for (const platform of PLATFORM_PACKAGES) { + const pkgDir = join(process.cwd(), "packages", platform) + const pkgName = `oh-my-opencode-${platform}` + + console.log(`\n Publishing ${pkgName}...`) + const result = await publishPackage(pkgDir, distTag) + + if (result.success) { + if (result.alreadyPublished) { + console.log(` ✓ ${pkgName}@${version} (already published)`) + } else { + console.log(` ✓ ${pkgName}@${version}`) + } + } else { + console.error(` ✗ ${pkgName} failed: ${result.error}`) + throw new Error(`Failed to publish ${pkgName}`) + } + } + + // Publish main package last + console.log(`\n📦 Publishing main package...`) + const mainResult = await publishPackage(process.cwd(), distTag) + + if (mainResult.success) { + if (mainResult.alreadyPublished) { + console.log(` ✓ ${PACKAGE_NAME}@${version} (already published)`) + } else { + console.log(` ✓ ${PACKAGE_NAME}@${version}`) + } + } else { + console.error(` ✗ ${PACKAGE_NAME} failed: ${mainResult.error}`) + throw new Error(`Failed to publish ${PACKAGE_NAME}`) + } +} + +async function buildPackages(): Promise { + console.log("\nBuilding packages...") + await $`bun run clean && bun run build` + console.log("Building platform binaries...") + await $`bun run build:binaries` +} + async function gitTagAndRelease(newVersion: string, notes: string[]): Promise { if (!process.env.CI) return console.log("\nCommitting and tagging...") await $`git config user.email "github-actions[bot]@users.noreply.github.com"` await $`git config user.name "github-actions[bot]"` + + // Add all package.json files await $`git add package.json assets/oh-my-opencode.schema.json` + for (const platform of PLATFORM_PACKAGES) { + await $`git add packages/${platform}/package.json`.nothrow() + } const hasStagedChanges = await $`git diff --cached --quiet`.nothrow() if (hasStagedChanges.exitCode !== 0) { @@ -181,15 +291,16 @@ async function main() { process.exit(0) } - await updatePackageVersion(newVersion) + await updateAllPackageVersions(newVersion) const changelog = await generateChangelog(previous) const contributors = await getContributors(previous) const notes = [...changelog, ...contributors] - await buildAndPublish(newVersion) + await buildPackages() + await publishAllPackages(newVersion) await gitTagAndRelease(newVersion, notes) - console.log(`\n=== Successfully published ${PACKAGE_NAME}@${newVersion} ===`) + console.log(`\n=== Successfully published ${PACKAGE_NAME}@${newVersion} (8 packages) ===`) } main() diff --git a/src/cli/index.ts b/src/cli/index.ts index d5246fbe..40100a9a 100644 --- a/src/cli/index.ts +++ b/src/cli/index.ts @@ -8,8 +8,8 @@ import type { InstallArgs } from "./types" import type { RunOptions } from "./run" import type { GetLocalVersionOptions } from "./get-local-version/types" import type { DoctorOptions } from "./doctor" +import packageJson from "../../package.json" with { type: "json" } -const packageJson = await import("../../package.json") const VERSION = packageJson.version const program = new Command() diff --git a/src/cli/install.ts b/src/cli/install.ts index e677a9a3..d7c8fce8 100644 --- a/src/cli/install.ts +++ b/src/cli/install.ts @@ -10,8 +10,8 @@ import { addProviderConfig, detectCurrentConfig, } from "./config-manager" +import packageJson from "../../package.json" with { type: "json" } -const packageJson = await import("../../package.json") const VERSION = packageJson.version const SYMBOLS = {