tf_code/packages/tfcode/script/publish-tfcode.ts
2026-03-24 23:27:38 +11:00

276 lines
8.9 KiB
TypeScript

#!/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<string, string> = {}
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()
}