#!/usr/bin/env bun import { $ } from "bun" import fs from "fs" import path from "path" import { fileURLToPath } from "url" import pkg from "../package.json" const __dirname = path.dirname(fileURLToPath(import.meta.url)) const dir = path.resolve(__dirname, "..") process.chdir(dir) const TFCODE_VERSION = pkg.version const GITEA_HOST = process.env.GITEA_HOST || "gitea.toothfairyai.com" const GITEA_TOKEN = process.env.GITEA_TOKEN const GITEA_REPO = process.env.GITEA_REPO || "ToothFairyAI/tf_code" // Collect binaries const binaries: Record = {} for (const filepath of new Bun.Glob("*/package.json").scanSync({ cwd: "./dist" })) { const pkg = await Bun.file(`./dist/${filepath}`).json() if (pkg.name.startsWith("@toothfairyai/tfcode-")) { binaries[pkg.name] = pkg.version } } console.log("Binaries:", binaries) // Upload to Gitea release async function uploadToGitea() { if (!GITEA_TOKEN) { console.error("GITEA_TOKEN is required") process.exit(1) } // Check if release exists const releaseUrl = `https://${GITEA_HOST}/api/v1/repos/${GITEA_REPO}/releases/tags/v${TFCODE_VERSION}` let releaseId: string try { const res = await fetch(releaseUrl, { headers: { Authorization: `token ${GITEA_TOKEN}` }, }) if (res.ok) { const release = await res.json() releaseId = release.id console.log(`Release v${TFCODE_VERSION} exists, updating...`) } else { throw new Error("Not found") } } catch { // Create new release const createUrl = `https://${GITEA_HOST}/api/v1/repos/${GITEA_REPO}/releases` const res = await fetch(createUrl, { method: "POST", headers: { Authorization: `token ${GITEA_TOKEN}`, "Content-Type": "application/json", }, body: JSON.stringify({ tag_name: `v${TFCODE_VERSION}`, name: `v${TFCODE_VERSION}`, body: `tfcode v${TFCODE_VERSION}\n\nSee CHANGELOG.md for details.`, draft: false, prerelease: TFCODE_VERSION.includes("-"), }), }) if (!res.ok) { console.error("Failed to create release:", await res.text()) process.exit(1) } const release = await res.json() releaseId = release.id console.log(`Created release v${TFCODE_VERSION}`) } // Upload assets const assets = await fs.promises.readdir("./dist") for (const asset of assets) { if (asset.endsWith(".tar.gz") || asset.endsWith(".zip")) { const assetPath = `./dist/${asset}` const file = Bun.file(assetPath) const uploadUrl = `https://${GITEA_HOST}/api/v1/repos/${GITEA_REPO}/releases/${releaseId}/assets?name=${asset}` console.log(`Uploading ${asset}...`) const res = await fetch(uploadUrl, { method: "POST", headers: { Authorization: `token ${GITEA_TOKEN}`, "Content-Type": "application/octet-stream", }, body: file, }) if (!res.ok) { console.error(`Failed to upload ${asset}:`, await res.text()) } else { console.log(`Uploaded ${asset}`) } } } } // Create main npm package async function createMainPackage() { await $`mkdir -p ./dist/tfcode/bin` await Bun.file(`./dist/tfcode/postinstall.mjs`).write(await Bun.file("./script/postinstall-tfcode.mjs").text()) await Bun.file(`./dist/tfcode/LICENSE`).write(await Bun.file("../../LICENSE").text()) // Copy the current platform's binary to the main package // This makes installation faster (no need to download or copy from optionalDependencies) const currentPlatform = process.platform === "darwin" ? "darwin" : process.platform === "linux" ? "linux" : "windows" const currentArch = process.arch const platformBinDir = `./dist/tfcode-${currentPlatform}-${currentArch}/bin` const binaryName = process.platform === "win32" ? "tfcode.exe" : "tfcode" if (fs.existsSync(`${platformBinDir}/${binaryName}`)) { console.log(`Including ${currentPlatform}-${currentArch} binary in main package`) await $`cp ${platformBinDir}/${binaryName} ./dist/tfcode/bin/` } else { console.log(`Warning: No binary found for current platform (${currentPlatform}-${currentArch})`) console.log(`The postinstall script will need to download or copy from optionalDependencies`) } // Create a simple wrapper script that runs the installed binary const wrapper = `#!/usr/bin/env node const { spawn } = require('child_process') const path = require('path') const fs = require('fs') const binary = process.platform === 'win32' ? 'tfcode.exe' : 'tfcode' const binaryPath = path.join(__dirname, binary) if (!fs.existsSync(binaryPath)) { console.error('tfcode binary not found. Run: npm install @toothfairyai/tfcode') process.exit(1) } const child = spawn(binaryPath, process.argv.slice(2), { stdio: 'inherit', env: process.env }) child.on('exit', (code) => process.exit(code || 0)) ` await Bun.file(`./dist/tfcode/bin/tfcode.js`).write(wrapper) // Create package.json await Bun.file(`./dist/tfcode/package.json`).write( JSON.stringify( { name: "@toothfairyai/tfcode", version: TFCODE_VERSION, bin: { tfcode: "./bin/tfcode.js" }, scripts: { postinstall: "node ./postinstall.mjs" }, license: pkg.license, optionalDependencies: binaries, engines: { node: ">=18" }, homepage: "https://toothfairyai.com/developers/tfcode", repository: { type: "git", url: `https://${GITEA_HOST}/${GITEA_REPO}.git`, }, }, null, 2, ), ) // Pack and publish if (process.platform !== "win32") { await $`chmod -R 755 ./dist/tfcode` } for (const [name, version] of Object.entries(binaries)) { console.log(`Publishing ${name}...`) await $`cd ./dist/${name.replace("@toothfairyai/", "")} && bun pm pack && npm publish *.tgz --access public --tag beta` } console.log("Publishing main package...") await $`cd ./dist/tfcode && bun pm pack && npm publish *.tgz --access public --tag beta` } // Create Homebrew formula async function createHomebrewFormula() { const arm64Sha = await $`sha256sum ./dist/tfcode-linux-arm64.tar.gz | cut -d' ' -f1`.text().then((x) => x.trim()) const x64Sha = await $`sha256sum ./dist/tfcode-linux-x64.tar.gz | cut -d' ' -f1`.text().then((x) => x.trim()) const macX64Sha = await $`sha256sum ./dist/tfcode-darwin-x64.zip | cut -d' ' -f1`.text().then((x) => x.trim()) const macArm64Sha = await $`sha256sum ./dist/tfcode-darwin-arm64.zip | cut -d' ' -f1`.text().then((x) => x.trim()) const formula = [ "# typed: false", "# frozen_string_literal: true", "", "class Tfcode < Formula", ` desc "ToothFairyAI's official AI coding agent"`, ` homepage "https://toothfairyai.com/developers/tfcode"`, ` version "${TFCODE_VERSION.split("-")[0]}"`, "", ` depends_on "ripgrep"`, "", " on_macos do", " if Hardware::CPU.intel?", ` url "https://${GITEA_HOST}/${GITEA_REPO}/releases/download/v${TFCODE_VERSION}/tfcode-darwin-x64.zip"`, ` sha256 "${macX64Sha}"`, "", " def install", ' bin.install "tfcode"', " end", " end", " if Hardware::CPU.arm?", ` url "https://${GITEA_HOST}/${GITEA_REPO}/releases/download/v${TFCODE_VERSION}/tfcode-darwin-arm64.zip"`, ` sha256 "${macArm64Sha}"`, "", " def install", ' bin.install "tfcode"', " end", " end", " end", "", " on_linux do", " if Hardware::CPU.intel? and Hardware::CPU.is_64_bit?", ` url "https://${GITEA_HOST}/${GITEA_REPO}/releases/download/v${TFCODE_VERSION}/tfcode-linux-x64.tar.gz"`, ` sha256 "${x64Sha}"`, " def install", ' bin.install "tfcode"', " end", " end", " if Hardware::CPU.arm? and Hardware::CPU.is_64_bit?", ` url "https://${GITEA_HOST}/${GITEA_REPO}/releases/download/v${TFCODE_VERSION}/tfcode-linux-arm64.tar.gz"`, ` sha256 "${arm64Sha}"`, " def install", ' bin.install "tfcode"', " end", " end", " end", "end", "", ].join("\n") await Bun.file("./dist/tfcode.rb").write(formula) console.log("Created Homebrew formula: ./dist/tfcode.rb") console.log("To publish: Create a tap repo and push this formula") } // Run if (process.argv.includes("--upload")) { await uploadToGitea() } if (process.argv.includes("--npm")) { await createMainPackage() } if (process.argv.includes("--brew")) { await createHomebrewFormula() } if (!process.argv.includes("--upload") && !process.argv.includes("--npm") && !process.argv.includes("--brew")) { console.log(` Usage: bun run publish.ts --upload Upload binaries to Gitea release bun run publish.ts --npm Publish to npm bun run publish.ts --brew Create Homebrew formula bun run publish.ts --all Do all of the above `) } if (process.argv.includes("--all")) { await uploadToGitea() await createMainPackage() await createHomebrewFormula() }