#!/usr/bin/env node // publish-codex-installer.js —— 发布 Codex 安装器资源到自建 gitea。 // // 用法: // node scripts/publish-codex-installer.js /Users/mac/Downloads/Codex-Windows-x64.msix // node scripts/publish-codex-installer.js /Users/mac/Downloads/ChatGPT.dmg // token 可设置 GIT_TOKEN,或写入 ~/.tokendun-git-token。 "use strict"; const fs = require("fs"); const os = require("os"); const path = require("path"); const crypto = require("crypto"); const { execFileSync, spawnSync } = require("child_process"); const OWNER = "mhgmh"; const REPO = "codex-market-app"; const TAG = "codex-installers"; const MSIX_NAME = "Codex-Windows-x64.msix"; const DMG_NAME = "ChatGPT.dmg"; const ALLOWED_NAMES = new Set([MSIX_NAME, DMG_NAME]); const API = `https://up.tokendun.cc/api/v1/repos/${OWNER}/${REPO}`; const DL = `https://update.tokendun.cc/${OWNER}/${REPO}/releases/download/${TAG}`; const installerPath = path.resolve(process.argv[2] || "/Users/mac/Downloads/Codex-Windows-x64.msix"); const installerName = path.basename(installerPath); if (!ALLOWED_NAMES.has(installerName)) { console.error(`安装包文件名必须是 ${Array.from(ALLOWED_NAMES).join(" 或 ")},当前是 ${installerName}`); process.exit(1); } if (!fs.existsSync(installerPath)) { console.error(`安装包不存在:${installerPath}`); process.exit(1); } let token = process.env.GIT_TOKEN || ""; if (!token) { try { token = fs.readFileSync(path.join(os.homedir(), ".tokendun-git-token"), "utf8").trim(); } catch {} } if (!token) { console.error("缺少 gitea token:设置 GIT_TOKEN 或写入 ~/.tokendun-git-token"); process.exit(1); } function hashFile(file) { return new Promise((resolve, reject) => { const h = crypto.createHash("sha256"); fs.createReadStream(file) .on("data", (chunk) => h.update(chunk)) .on("error", reject) .on("end", () => resolve(h.digest("hex"))); }); } function api(method, urlPath, body) { const args = ["-s", "-X", method, "-H", `Authorization: token ${token}`, `${API}${urlPath}`]; if (body !== undefined) { args.push("-H", "Content-Type: application/json", "-d", JSON.stringify(body)); } return execFileSync("curl", args, { encoding: "utf8", maxBuffer: 64 * 1024 * 1024 }); } function apiJson(method, urlPath, body) { const out = api(method, urlPath, body); try { return JSON.parse(out); } catch { return null; } } function upload(rel, file, name) { const result = spawnSync("curl", [ "--progress-bar", "-o", "/dev/null", "-w", "%{http_code}", "-X", "POST", "-H", `Authorization: token ${token}`, "-F", `attachment=@${file};type=application/octet-stream`, `${API}/releases/${rel.id}/assets?name=${encodeURIComponent(name)}`, ], { encoding: "utf8", stdio: ["ignore", "pipe", "inherit"], maxBuffer: 64 * 1024 * 1024 }); if (result.error) throw result.error; const code = String(result.stdout || "").trim(); console.log(`上传 ${name} → HTTP ${code}`); if (code !== "201") { console.error(`上传失败:${name}`); process.exit(1); } } (async () => { const stat = fs.statSync(installerPath); console.log(`准备发布 ${installerName}:${(stat.size / 1048576).toFixed(2)}MB`); const sha256 = await hashFile(installerPath); const shaPath = path.join(os.tmpdir(), `${installerName}.sha256`); fs.writeFileSync(shaPath, `${sha256} ${installerName}\n`); console.log(`sha256=${sha256}`); let rel = apiJson("GET", `/releases/tags/${encodeURIComponent(TAG)}`); if (!rel || !rel.id) { console.log(`创建 release ${TAG} …`); rel = apiJson("POST", "/releases", { tag_name: TAG, target_commitish: "main", name: "Codex 安装器资源", body: "TD工具箱内置安装器使用的远程安装包资源。", draft: false, prerelease: false, }); } if (!rel || !rel.id) { console.error(`无法创建/获取 release ${TAG}`); process.exit(1); } const existing = new Map((rel.assets || []).map((a) => [a.name, a.id])); for (const name of [installerName, `${installerName}.sha256`]) { if (existing.has(name)) { api("DELETE", `/releases/${rel.id}/assets/${existing.get(name)}`); console.log(`删除旧 asset: ${name}`); } } upload(rel, installerPath, installerName); upload(rel, shaPath, `${installerName}.sha256`); console.log("\n发布完成:"); console.log(` ${DL}/${installerName}`); console.log(` ${DL}/${installerName}.sha256`); })();