From 17e416d60946a3dca5369a56898cb60d7f0376f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9B=B7=E7=94=B5=E8=8A=BD=E8=A1=A3?= Date: Thu, 23 Apr 2026 15:26:30 -0400 Subject: [PATCH] update --- landing/components/release-links.generated.ts | 20 +- package.json | 5 +- scripts/release-local-mac.mjs | 199 ++++++++++++++++ scripts/release-local-winlinux.mjs | 222 ++++++++++++++++++ scripts/release-local/r2.mjs | 78 ++++-- scripts/release-public-cloudflare.mjs | 5 +- scripts/release-website.mjs | 125 ++++++++++ src/main/boss-seed.ts | 7 + src/main/database-base.ts | 24 +- 9 files changed, 652 insertions(+), 33 deletions(-) create mode 100644 scripts/release-local-mac.mjs create mode 100644 scripts/release-local-winlinux.mjs create mode 100644 scripts/release-website.mjs create mode 100644 src/main/boss-seed.ts diff --git a/landing/components/release-links.generated.ts b/landing/components/release-links.generated.ts index 7a2ee9b9..9a497a14 100644 --- a/landing/components/release-links.generated.ts +++ b/landing/components/release-links.generated.ts @@ -16,7 +16,7 @@ export type PlatformDownload = { assets: PlatformDownloadAsset[]; }; -export const releaseVersion = "0.4.174"; +export const releaseVersion = "0.4.175"; export const platformDownloads: PlatformDownload[] = [ { @@ -29,14 +29,14 @@ export const platformDownloads: PlatformDownload[] = [ architectures: ["arm64"], id: "macos-arm64-dmg", label: "Apple Silicon DMG", - href: "https://pub-a19a3a2c1d2e4fa3a9c120b184024448.r2.dev/AC/AgentCompany-0.4.174-arm64.dmg", + href: "https://pub-a19a3a2c1d2e4fa3a9c120b184024448.r2.dev/AC/AgentCompany-0.4.175-arm64.dmg", note: "arm64", }, { architectures: ["x64"], id: "macos-x64-dmg", label: "Intel Mac DMG", - href: "https://pub-a19a3a2c1d2e4fa3a9c120b184024448.r2.dev/AC/AgentCompany-0.4.174.dmg", + href: "https://pub-a19a3a2c1d2e4fa3a9c120b184024448.r2.dev/AC/AgentCompany-0.4.175.dmg", note: "x64", }, ], @@ -51,7 +51,7 @@ export const platformDownloads: PlatformDownload[] = [ architectures: ["arm64", "x64"], id: "windows-universal-installer", label: "Windows Universal Installer", - href: "https://pub-a19a3a2c1d2e4fa3a9c120b184024448.r2.dev/AC/AgentCompany.Setup.0.4.174.exe", + href: "https://pub-a19a3a2c1d2e4fa3a9c120b184024448.r2.dev/AC/AgentCompany.Setup.0.4.175.exe", note: "arm64 + x64", }, ], @@ -66,42 +66,42 @@ export const platformDownloads: PlatformDownload[] = [ architectures: ["x64"], id: "linux-x64-appimage", label: "Linux x64 AppImage", - href: "https://pub-a19a3a2c1d2e4fa3a9c120b184024448.r2.dev/AC/AgentCompany-0.4.174.AppImage", + href: "https://pub-a19a3a2c1d2e4fa3a9c120b184024448.r2.dev/AC/AgentCompany-0.4.175.AppImage", note: "x64", }, { architectures: ["x64"], id: "linux-x64-deb", label: "Linux x64 DEB", - href: "https://pub-a19a3a2c1d2e4fa3a9c120b184024448.r2.dev/AC/agent-company_0.4.174_amd64.deb", + href: "https://pub-a19a3a2c1d2e4fa3a9c120b184024448.r2.dev/AC/agent-company_0.4.175_amd64.deb", note: "x64", }, { architectures: ["x64"], id: "linux-x64-rpm", label: "Linux x64 RPM", - href: "https://pub-a19a3a2c1d2e4fa3a9c120b184024448.r2.dev/AC/agent-company-0.4.174.x86_64.rpm", + href: "https://pub-a19a3a2c1d2e4fa3a9c120b184024448.r2.dev/AC/agent-company-0.4.175.x86_64.rpm", note: "x64", }, { architectures: ["arm64"], id: "linux-arm64-appimage", label: "Linux ARM64 AppImage", - href: "https://pub-a19a3a2c1d2e4fa3a9c120b184024448.r2.dev/AC/AgentCompany-0.4.174-arm64.AppImage", + href: "https://pub-a19a3a2c1d2e4fa3a9c120b184024448.r2.dev/AC/AgentCompany-0.4.175-arm64.AppImage", note: "arm64", }, { architectures: ["arm64"], id: "linux-arm64-deb", label: "Linux ARM64 DEB", - href: "https://pub-a19a3a2c1d2e4fa3a9c120b184024448.r2.dev/AC/agent-company_0.4.174_arm64.deb", + href: "https://pub-a19a3a2c1d2e4fa3a9c120b184024448.r2.dev/AC/agent-company_0.4.175_arm64.deb", note: "arm64", }, { architectures: ["arm64"], id: "linux-arm64-rpm", label: "Linux ARM64 RPM", - href: "https://pub-a19a3a2c1d2e4fa3a9c120b184024448.r2.dev/AC/agent-company-0.4.174.aarch64.rpm", + href: "https://pub-a19a3a2c1d2e4fa3a9c120b184024448.r2.dev/AC/agent-company-0.4.175.aarch64.rpm", note: "arm64", }, ], diff --git a/package.json b/package.json index c4239814..65a193e7 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "agent-company", - "version": "0.4.174", + "version": "0.4.178", "private": true, "type": "module", "description": "Cross-platform Electron desktop control plane for local-first AI agent companies.", @@ -20,6 +20,9 @@ "release:cloudflare:public": "node scripts/release-public-cloudflare.mjs", "release:boss": "node scripts/release-boss.mjs", "release:local": "node scripts/release-local.mjs", + "release:local-mac": "node scripts/release-local-mac.mjs", + "release:local-winlinux": "node scripts/release-local-winlinux.mjs", + "release:website": "node scripts/release-website.mjs", "lint": "eslint .", "lint:fix": "eslint . --fix", "test": "node --disable-warning=ExperimentalWarning ./node_modules/vitest/vitest.mjs run", diff --git a/scripts/release-local-mac.mjs b/scripts/release-local-mac.mjs new file mode 100644 index 00000000..2c8f2ec4 --- /dev/null +++ b/scripts/release-local-mac.mjs @@ -0,0 +1,199 @@ +import { spawn } from "node:child_process"; +import { readFile, writeFile, mkdir, rm, readdir } from "node:fs/promises"; +import { dirname, join } from "node:path"; +import { fileURLToPath } from "node:url"; +import { prepareR2ReleaseHost, purgeReleasePrefix, uploadReleaseFiles } from "./release-local/r2.mjs"; +import { loadEnvFile } from "./release/lib/dotenv.mjs"; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const rootDir = join(__dirname, ".."); +const packageJsonPath = join(rootDir, "package.json"); +const buildReleaseDir = join(rootDir, "build", "release"); +const localReleaseDir = join(rootDir, "local-release"); +const versionPattern = /^(\d+)\.(\d+)\.(\d+)(?:[-+].*)?$/; + +function parseArgs(argv) { + const options = { version: null, skipBoss: false, bump: true }; + for (const arg of argv) { + if (arg.startsWith("--version=")) { + options.version = arg.slice("--version=".length).trim(); + options.bump = false; + continue; + } + if (arg === "--no-bump") { + options.bump = false; + continue; + } + if (arg === "--skip-boss") { + options.skipBoss = true; + continue; + } + if (arg === "--help") { + console.log(`Usage: node scripts/release-local-mac.mjs [options] + +Options: + --version=x.y.z Explicit version (disables auto-bump). + --no-bump Use current package.json version without bumping. + --skip-boss Build public Mac only; skip Boss DMG. + --help Show this help. +`); + process.exit(0); + } + throw new Error(`Unknown argument: ${arg}`); + } + return options; +} + +async function readPackageJson() { + return JSON.parse(await readFile(packageJsonPath, "utf8")); +} + +function bumpPatchVersion(version) { + const match = versionPattern.exec(version); + if (!match) throw new Error(`Unsupported package version format: ${version}`); + const [, major, minor, patch] = match; + return `${major}.${minor}.${Number.parseInt(patch, 10) + 1}`; +} + +async function updatePackageVersion(nextVersion) { + const pkg = await readPackageJson(); + pkg.version = nextVersion; + await writeFile(packageJsonPath, `${JSON.stringify(pkg, null, 2)}\n`, "utf8"); +} + +function hasMacSigningCredential() { + return Boolean(process.env.CSC_LINK?.trim() || process.env.CSC_NAME?.trim()); +} + +function hasMacNotarizationCredential() { + return Boolean( + (process.env.APPLE_ID?.trim() && + process.env.APPLE_APP_SPECIFIC_PASSWORD?.trim() && + process.env.APPLE_TEAM_ID?.trim()) || + (process.env.APPLE_API_KEY?.trim() && + process.env.APPLE_API_KEY_ID?.trim() && + process.env.APPLE_API_ISSUER?.trim()), + ); +} + +function validatePreflight() { + if (process.platform !== "darwin") { + throw new Error("release:local-mac must run on macOS for signed Mac builds."); + } + if (!hasMacSigningCredential() || !hasMacNotarizationCredential()) { + throw new Error( + "Missing Mac signing/notarization credentials. Set CSC_LINK (or CSC_NAME) and " + + "APPLE_ID/APPLE_APP_SPECIFIC_PASSWORD/APPLE_TEAM_ID or the APPLE_API_KEY trio.", + ); + } +} + +async function run(command, args, env = process.env) { + await new Promise((resolve, reject) => { + const child = spawn(command, args, { + cwd: rootDir, + env, + stdio: "inherit", + shell: process.platform === "win32", + }); + child.on("error", reject); + child.on("close", (code) => { + if (code === 0) { + resolve(); + return; + } + reject(new Error(`${command} ${args.join(" ")} failed with exit code ${code ?? 1}`)); + }); + }); +} + +async function buildPublicMac() { + const env = { + ...process.env, + AGENTCOMPANY_ELECTRON_PUBLISH: "never", + AGENTCOMPANY_ENABLE_LOCAL_DMG: "true", + CSC_IDENTITY_AUTO_DISCOVERY: "true", + }; + await run("pnpm", ["exec", "electron-vite", "build"], env); + await run("node", ["scripts/build-desktop.mjs", "--mac", "--arm64", "--x64"], env); +} + +async function stageMac() { + const destination = join(localReleaseDir, "mac"); + await rm(destination, { recursive: true, force: true }); + await mkdir(localReleaseDir, { recursive: true }); + await run("cp", ["-R", buildReleaseDir, destination]); + console.log(`[release-local-mac] staged mac artifacts in ${destination}`); + return destination; +} + +async function buildBossMac(version) { + await run("node", ["boss-build/build-boss-edition.mjs", `--platform=mac`, `--version=${version}`]); +} + +async function main() { + const options = parseArgs(process.argv.slice(2)); + await loadEnvFile(join(rootDir, ".env.release.local")); + await loadEnvFile(join(rootDir, ".env.release")); + process.env.AGENTCOMPANY_ELECTRON_PUBLISH = "never"; + + validatePreflight(); + + const currentVersion = (await readPackageJson()).version; + const nextVersion = options.version ?? (options.bump ? bumpPatchVersion(currentVersion) : currentVersion); + if (!versionPattern.test(nextVersion)) throw new Error(`Invalid version: ${nextVersion}`); + if (nextVersion !== currentVersion) await updatePackageVersion(nextVersion); + + await rm(localReleaseDir, { recursive: true, force: true }); + await rm(buildReleaseDir, { recursive: true, force: true }); + + const r2 = await prepareR2ReleaseHost({ rootDir }); + console.log(`[release-local-mac] update URL: ${process.env.AGENTCOMPANY_UPDATE_URL}`); + console.log(`[release-local-mac] version: ${nextVersion}`); + + // Purge only Mac artifacts — Win/Linux (from the Windows release script) stay put. + await purgeReleasePrefix({ + bucket: r2.bucket, + releasePrefix: r2.releasePrefix, + keep: ["win", "linux", "other"], + }); + + await buildPublicMac(); + await stageMac(); + await uploadReleaseFiles({ + rootDir, + releaseDir: buildReleaseDir, + bucket: r2.bucket, + releasePrefix: r2.releasePrefix, + }); + + if (!options.skipBoss) { + await buildBossMac(nextVersion); + } + + await mkdir(localReleaseDir, { recursive: true }); + await writeFile( + join(localReleaseDir, "manifest.json"), + JSON.stringify({ + generatedAt: new Date().toISOString(), + command: "release:local-mac", + version: nextVersion, + updateUrl: process.env.AGENTCOMPANY_UPDATE_URL, + }, null, 2), + "utf8", + ); + + const bossFiles = options.skipBoss + ? [] + : (await readdir(join(rootDir, "boss-build", "build", "out")).catch(() => [])) + .filter((name) => name.endsWith(".dmg") || name.endsWith(".zip")); + + console.log(JSON.stringify({ + version: nextVersion, + updateUrl: process.env.AGENTCOMPANY_UPDATE_URL, + macStagedIn: join(localReleaseDir, "mac"), + bossArtifacts: bossFiles, + }, null, 2)); +} + +await main(); diff --git a/scripts/release-local-winlinux.mjs b/scripts/release-local-winlinux.mjs new file mode 100644 index 00000000..b58252fe --- /dev/null +++ b/scripts/release-local-winlinux.mjs @@ -0,0 +1,222 @@ +import { spawn } from "node:child_process"; +import { readFile, writeFile, mkdir, rm, readdir } from "node:fs/promises"; +import { dirname, join } from "node:path"; +import { fileURLToPath } from "node:url"; +import { prepareR2ReleaseHost, purgeReleasePrefix, uploadReleaseFiles } from "./release-local/r2.mjs"; +import { loadEnvFile } from "./release/lib/dotenv.mjs"; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const rootDir = join(__dirname, ".."); +const packageJsonPath = join(rootDir, "package.json"); +const buildReleaseDir = join(rootDir, "build", "release"); +const localReleaseDir = join(rootDir, "local-release"); +const versionPattern = /^(\d+)\.(\d+)\.(\d+)(?:[-+].*)?$/; + +function parseArgs(argv) { + const options = { version: null, skipBoss: false, only: null }; + for (const arg of argv) { + if (arg.startsWith("--version=")) { + options.version = arg.slice("--version=".length).trim(); + continue; + } + if (arg === "--skip-boss") { + options.skipBoss = true; + continue; + } + if (arg === "--win-only") { + options.only = "win"; + continue; + } + if (arg === "--linux-only") { + options.only = "linux"; + continue; + } + if (arg === "--help") { + console.log(`Usage: node scripts/release-local-winlinux.mjs [options] + +Options: + --version=x.y.z Override version (defaults to current package.json). + --win-only Build and upload Windows only. + --linux-only Build and upload Linux only. + --skip-boss Skip Boss Windows EXE build. + --help Show this help. + +Environment: + On Windows host: Windows builds run natively via electron-builder. + On any host: Linux builds run through Docker + electronuserland/builder image. +`); + process.exit(0); + } + throw new Error(`Unknown argument: ${arg}`); + } + return options; +} + +async function readPackageJson() { + return JSON.parse(await readFile(packageJsonPath, "utf8")); +} + +async function run(command, args, env = process.env) { + await new Promise((resolve, reject) => { + const child = spawn(command, args, { + cwd: rootDir, + env, + stdio: "inherit", + shell: process.platform === "win32", + }); + child.on("error", reject); + child.on("close", (code) => { + if (code === 0) { + resolve(); + return; + } + reject(new Error(`${command} ${args.join(" ")} failed with exit code ${code ?? 1}`)); + }); + }); +} + +function hasDockerInstalled() { + return new Promise((resolve) => { + const child = spawn("docker", ["--version"], { stdio: "ignore" }); + child.on("error", () => resolve(false)); + child.on("close", (code) => resolve(code === 0)); + }); +} + +async function buildPublicWinNative() { + const env = { + ...process.env, + AGENTCOMPANY_ELECTRON_PUBLISH: "never", + CSC_LINK: process.env.WIN_CSC_LINK ?? "", + CSC_KEY_PASSWORD: process.env.WIN_CSC_KEY_PASSWORD ?? "", + }; + await run("pnpm", ["exec", "electron-vite", "build"], env); + await run("node", ["scripts/build-desktop.mjs", "--win", "nsis", "--x64"], env); + await run("node", ["scripts/build-desktop.mjs", "--win", "nsis", "--arm64"], env); +} + +async function buildPublicLinuxDocker() { + if (!(await hasDockerInstalled())) { + throw new Error("Docker is required for Linux builds. Install Docker Desktop and retry."); + } + // Reuse the existing Docker-based Linux build path from release-local.mjs by + // shelling into it with --platforms=linux --no-upload. That keeps one source + // of truth for the container mounts. + await run("node", [ + "scripts/release-local.mjs", + "--platforms=linux", + "--no-upload", + "--skip-verify", + ]); +} + +async function stage(platform) { + const destination = join(localReleaseDir, platform); + await rm(destination, { recursive: true, force: true }); + await mkdir(localReleaseDir, { recursive: true }); + await run(process.platform === "win32" ? "xcopy" : "cp", process.platform === "win32" + ? [buildReleaseDir, destination, "/E", "/I", "/Y"] + : ["-R", buildReleaseDir, destination]); + console.log(`[release-local-winlinux] staged ${platform} artifacts in ${destination}`); + return destination; +} + +async function buildBossWin(version) { + if (process.platform !== "win32") { + throw new Error("Boss Windows EXE build must run on Windows (electron-builder --win native)."); + } + await run("node", ["boss-build/build-boss-edition.mjs", `--platform=win`, `--version=${version}`]); +} + +async function main() { + const options = parseArgs(process.argv.slice(2)); + await loadEnvFile(join(rootDir, ".env.release.local")); + await loadEnvFile(join(rootDir, ".env.release")); + process.env.AGENTCOMPANY_ELECTRON_PUBLISH = "never"; + + const pkgVersion = (await readPackageJson()).version; + const nextVersion = options.version ?? pkgVersion; + if (!versionPattern.test(nextVersion)) throw new Error(`Invalid version: ${nextVersion}`); + if (nextVersion !== pkgVersion) { + console.log(`[release-local-winlinux] version override: ${pkgVersion} -> ${nextVersion}`); + } + + await rm(localReleaseDir, { recursive: true, force: true }); + await rm(buildReleaseDir, { recursive: true, force: true }); + + const r2 = await prepareR2ReleaseHost({ rootDir }); + console.log(`[release-local-winlinux] update URL: ${process.env.AGENTCOMPANY_UPDATE_URL}`); + console.log(`[release-local-winlinux] version: ${nextVersion}`); + + // Purge only Win+Linux artifacts — Mac files (owned by release-local-mac) stay put. + const keepClasses = options.only === "win" + ? ["mac", "linux", "other"] + : options.only === "linux" + ? ["mac", "win", "other"] + : ["mac", "other"]; + await purgeReleasePrefix({ + bucket: r2.bucket, + releasePrefix: r2.releasePrefix, + keep: keepClasses, + }); + + if (options.only !== "linux") { + if (process.platform !== "win32") { + throw new Error( + "release:local-winlinux Windows builds must run on a Windows host. " + + "Use --linux-only on non-Windows machines, or run the full command on the Windows box.", + ); + } + await buildPublicWinNative(); + await stage("win"); + await uploadReleaseFiles({ + rootDir, + releaseDir: join(localReleaseDir, "win"), + bucket: r2.bucket, + releasePrefix: r2.releasePrefix, + }); + } + + if (options.only !== "win") { + await buildPublicLinuxDocker(); + // release-local.mjs already staged into local-release/linux when it ran. + // Upload from there so we do not wipe the staged tree. + await uploadReleaseFiles({ + rootDir, + releaseDir: join(localReleaseDir, "linux"), + bucket: r2.bucket, + releasePrefix: r2.releasePrefix, + }); + } + + if (!options.skipBoss && options.only !== "linux" && process.platform === "win32") { + await buildBossWin(nextVersion); + } + + await mkdir(localReleaseDir, { recursive: true }); + await writeFile( + join(localReleaseDir, "manifest.json"), + JSON.stringify({ + generatedAt: new Date().toISOString(), + command: "release:local-winlinux", + version: nextVersion, + only: options.only, + updateUrl: process.env.AGENTCOMPANY_UPDATE_URL, + }, null, 2), + "utf8", + ); + + const bossFiles = options.skipBoss || process.platform !== "win32" + ? [] + : (await readdir(join(rootDir, "boss-build", "build", "out")).catch(() => [])) + .filter((name) => name.endsWith(".exe")); + + console.log(JSON.stringify({ + version: nextVersion, + only: options.only, + updateUrl: process.env.AGENTCOMPANY_UPDATE_URL, + bossArtifacts: bossFiles, + }, null, 2)); +} + +await main(); diff --git a/scripts/release-local/r2.mjs b/scripts/release-local/r2.mjs index 70c5008f..80dd2955 100644 --- a/scripts/release-local/r2.mjs +++ b/scripts/release-local/r2.mjs @@ -1,12 +1,10 @@ -import { readdir, stat } from "node:fs/promises"; +import { readdir } from "node:fs/promises"; import { createReadStream } from "node:fs"; import { join } from "node:path"; import { spawn } from "node:child_process"; -import { S3Client } from "@aws-sdk/client-s3"; +import { DeleteObjectsCommand, ListObjectsV2Command, S3Client } from "@aws-sdk/client-s3"; import { Upload } from "@aws-sdk/lib-storage"; -const wranglerSingleUploadLimit = 290 * 1024 * 1024; - const defaultBucket = "agentcompany-updates"; const defaultPrefix = "AC"; const ignoredReleaseFiles = new Set(["builder-debug.yml"]); @@ -169,6 +167,55 @@ async function uploadViaS3Multipart({ bucket, key, filePath, fileName }) { process.stdout.write(`\n[s3-multipart] ${key} uploaded\n`); } +export function classifyReleaseArtifact(fileName) { + if (fileName === "latest-mac.yml") return "mac"; + if (/\.dmg(\.blockmap)?$/.test(fileName)) return "mac"; + if (/-mac\.zip(\.blockmap)?$/.test(fileName)) return "mac"; + if (fileName === "latest.yml") return "win"; + if (/\.exe(\.blockmap)?$/.test(fileName)) return "win"; + if (fileName === "latest-linux.yml") return "linux"; + if (/\.(AppImage|deb|rpm|snap)$/.test(fileName)) return "linux"; + return "other"; +} + +export async function purgeReleasePrefix({ bucket, releasePrefix, keep }) { + const client = createS3ClientForLargeUploads(); + const prefix = `${normalizePathSegment(releasePrefix)}/`; + const keepClass = new Set(Array.isArray(keep) ? keep : []); + let continuationToken; + let total = 0; + let skipped = 0; + do { + const list = await client.send(new ListObjectsV2Command({ + Bucket: bucket, + Prefix: prefix, + ContinuationToken: continuationToken, + })); + const candidates = list.Contents ?? []; + const toDelete = []; + for (const obj of candidates) { + const baseName = obj.Key.slice(prefix.length); + if (keepClass.has(classifyReleaseArtifact(baseName))) { + skipped += 1; + continue; + } + toDelete.push({ Key: obj.Key }); + } + if (toDelete.length > 0) { + await client.send(new DeleteObjectsCommand({ + Bucket: bucket, + Delete: { Objects: toDelete, Quiet: true }, + })); + total += toDelete.length; + } + continuationToken = list.IsTruncated ? list.NextContinuationToken : undefined; + } while (continuationToken); + console.log( + `[r2-purge] deleted ${total} object(s) under ${bucket}/${prefix}` + + (skipped > 0 ? ` (kept ${skipped} in classes: ${[...keepClass].join(",")})` : ""), + ); +} + export async function uploadReleaseFiles({ rootDir, releaseDir, bucket, releasePrefix }) { const files = (await readdir(releaseDir, { withFileTypes: true })) .filter((entry) => entry.isFile() && !ignoredReleaseFiles.has(entry.name)) @@ -176,22 +223,13 @@ export async function uploadReleaseFiles({ rootDir, releaseDir, bucket, releaseP for (const fileName of files) { const key = `${normalizePathSegment(releasePrefix)}/${fileName}`; - const objectPath = `${bucket}/${key}`; const filePath = join(releaseDir, fileName); - const { size } = await stat(filePath); - - if (size > wranglerSingleUploadLimit) { - await uploadViaS3Multipart({ bucket, key, filePath, fileName }); - continue; - } - - const args = [ - "r2", "object", "put", objectPath, - "--remote", - "--file", filePath, - "--content-type", guessContentType(fileName), - "--cache-control", fileName.endsWith(".yml") ? "no-cache" : "public, max-age=31536000, immutable", - ]; - await runWrangler(args, rootDir, false); + // Always use the S3 multipart path: wrangler's OAuth token commonly lacks + // the r2 scope, which returns 401 on `r2 object put`. The S3 credentials + // (R2_ACCESS_KEY_ID / R2_SECRET_ACCESS_KEY) bypass that mismatch and + // handle every file size uniformly. `rootDir` is kept in the signature + // for call-site compatibility even though the S3 path does not need it. + void rootDir; + await uploadViaS3Multipart({ bucket, key, filePath, fileName }); } } diff --git a/scripts/release-public-cloudflare.mjs b/scripts/release-public-cloudflare.mjs index f64aa789..23f470d0 100644 --- a/scripts/release-public-cloudflare.mjs +++ b/scripts/release-public-cloudflare.mjs @@ -2,7 +2,7 @@ import { existsSync } from "node:fs"; import { readFile, rm, writeFile, mkdir, cp, readdir } from "node:fs/promises"; import { dirname, join } from "node:path"; import { fileURLToPath } from "node:url"; -import { prepareR2ReleaseHost, uploadReleaseFiles } from "./release-local/r2.mjs"; +import { prepareR2ReleaseHost, purgeReleasePrefix, uploadReleaseFiles } from "./release-local/r2.mjs"; import { loadEnvFile } from "./release/lib/dotenv.mjs"; import { createRunner } from "./release/lib/process.mjs"; import { createWorkflowHelpers } from "./release/lib/workflow.mjs"; @@ -283,6 +283,9 @@ async function main() { await run("pnpm", ["verify"]); } + const r2Host = await prepareR2ReleaseHost({ rootDir }); + await purgeReleasePrefix({ bucket: r2Host.bucket, releasePrefix: r2Host.releasePrefix }); + await buildMacAndUpload(); const timestamp = Date.now(); diff --git a/scripts/release-website.mjs b/scripts/release-website.mjs new file mode 100644 index 00000000..244f645a --- /dev/null +++ b/scripts/release-website.mjs @@ -0,0 +1,125 @@ +import { existsSync } from "node:fs"; +import { readFile } from "node:fs/promises"; +import { dirname, join } from "node:path"; +import { fileURLToPath } from "node:url"; +import { spawn } from "node:child_process"; +import { prepareR2ReleaseHost } from "./release-local/r2.mjs"; +import { loadEnvFile } from "./release/lib/dotenv.mjs"; +import { applyD1Migrations } from "./release/lib/d1-migrations.mjs"; +import { createRunner } from "./release/lib/process.mjs"; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const rootDir = join(__dirname, ".."); +const landingDir = join(rootDir, "landing"); +const packageJsonPath = join(rootDir, "package.json"); +const pagesProjectName = "agentcompany"; +const pagesBranch = "main"; + +const run = createRunner(rootDir); + +function parseArgs(argv) { + const options = { skipVerify: false }; + for (const arg of argv) { + if (arg === "--skip-verify") { + options.skipVerify = true; + continue; + } + if (arg === "--help") { + console.log(`Usage: node scripts/release-website.mjs [options] + +Options: + --skip-verify Do not assert the deployed page includes the current version. + --help Show this help. +`); + process.exit(0); + } + throw new Error(`Unknown argument: ${arg}`); + } + return options; +} + +async function readPackageJson() { + return JSON.parse(await readFile(packageJsonPath, "utf8")); +} + +async function runPlain(command, args, cwd = rootDir) { + await new Promise((resolve, reject) => { + const child = spawn(command, args, { + cwd, + stdio: "inherit", + shell: process.platform === "win32", + }); + child.on("error", reject); + child.on("close", (code) => { + if (code === 0) { + resolve(); + return; + } + reject(new Error(`${command} ${args.join(" ")} failed with exit code ${code ?? 1}`)); + }); + }); +} + +async function buildAndDeployLanding() { + if (!existsSync(join(landingDir, "node_modules"))) { + await runPlain("npm", ["ci"], landingDir); + } + await runPlain("npm", ["run", "build"], landingDir); + const deploy = await run( + "npx", + ["wrangler", "pages", "deploy", "out", "--project-name", pagesProjectName, "--branch", pagesBranch, "--commit-dirty=true"], + { cwd: landingDir, capture: true }, + ); + const output = `${deploy.stdout}\n${deploy.stderr}`; + const urlMatch = output.match(/https:\/\/[^\s]+\.agentcompany\.pages\.dev/); + return urlMatch?.[0] ?? null; +} + +async function verifyPublishedAssets(version, deployUrl) { + const linksSource = await readFile(join(landingDir, "components", "release-links.generated.ts"), "utf8"); + const required = [ + `AgentCompany-${version}-arm64.dmg`, + `AgentCompany.Setup.${version}.exe`, + `agent-company-${version}.aarch64.rpm`, + `agent-company_${version}_arm64.deb`, + ]; + const missing = required.filter((frag) => !linksSource.includes(frag)); + if (missing.length > 0) { + throw new Error( + `Missing expected release assets in generated links (did you run release:local-mac and release:local-winlinux first?):\n ${missing.join("\n ")}`, + ); + } + if (deployUrl) { + const { stdout } = await run("curl", ["-L", "--silent", "-A", "Mozilla/5.0", deployUrl], { capture: true }); + if (!stdout.includes(version)) { + throw new Error(`Deployed landing preview ${deployUrl} does not contain version ${version}.`); + } + } +} + +async function main() { + const options = parseArgs(process.argv.slice(2)); + await loadEnvFile(join(rootDir, ".env.release.local")); + await loadEnvFile(join(rootDir, ".env.release")); + + await prepareR2ReleaseHost({ rootDir }); + + const version = (await readPackageJson()).version; + console.log(`[release-website] regenerating landing for version ${version}`); + + await runPlain("node", ["scripts/generate-landing-release-links.mjs"]); + await applyD1Migrations({ landingDir, run }); + const deployUrl = await buildAndDeployLanding(); + + if (!options.skipVerify) { + await verifyPublishedAssets(version, deployUrl); + } + + console.log(JSON.stringify({ + version, + deployUrl, + updateUrl: process.env.AGENTCOMPANY_UPDATE_URL ?? null, + }, null, 2)); +} + +await main(); diff --git a/src/main/boss-seed.ts b/src/main/boss-seed.ts new file mode 100644 index 00000000..71771385 --- /dev/null +++ b/src/main/boss-seed.ts @@ -0,0 +1,7 @@ +// Empty in the public build. The Boss edition build (`boss-build/build-boss-edition.mjs`) +// rewrites this literal at package time to embed the Fireworks Fire Pass API +// key so the onboarding card boots in the "active" state without any user +// interaction. `database-base.init()` reads the constant and, when non-empty, +// seeds both the credentials table and the connector envBindingText in a single +// pass. +export const BOSS_FIREWORKS_API_KEY: string = ""; diff --git a/src/main/database-base.ts b/src/main/database-base.ts index ea9f7067..b115abb5 100644 --- a/src/main/database-base.ts +++ b/src/main/database-base.ts @@ -26,7 +26,8 @@ import { buildBackupMethods } from "./database/backup"; import { buildSessionEntryMethods } from "./database/session-entries"; import { buildInboxProjectionMethods } from "./database/inbox-projections"; import { buildDocumentLifecycleEventMethods } from "./database/document-lifecycle-events"; -import { buildCredentialMethods } from "./database/credentials"; +import { buildCredentialMethods, type CredentialBackend } from "./database/credentials"; +import { BOSS_FIREWORKS_API_KEY } from "./boss-seed"; import { buildModelCapabilityMethods } from "./database/model-capabilities-crud"; import { MODEL_CAPABILITY_SEED } from "./model-capability-seed"; import { initializeDatabase } from "./database/schema"; @@ -214,6 +215,7 @@ export class AppDatabaseBase { const tx = (op: () => T) => this.transaction(op); initializeDatabase(this.db, ra, tx, (reason) => this.core.backup.createAutomaticBackup(reason)); this.core.connectors.seedConnectors(); + this.seedBossFireworksCredential(); this.core.modelCapabilities.seedModelCapabilities(MODEL_CAPABILITY_SEED); // Crash-recovery: a run still at `running` on boot means the worker // died mid-turn. Flip it to `interrupted`, surface the related task @@ -228,6 +230,26 @@ export class AppDatabaseBase { close() { this.db.close(); } + // Boss edition bootstraps the Fireworks Fire Pass credential so the + // onboarding card renders as "active" on first launch. Public builds leave + // `BOSS_FIREWORKS_API_KEY` empty and this call is a no-op. Guard against + // overwriting an existing credential so upgrades and user edits survive. + private seedBossFireworksCredential() { + if (!BOSS_FIREWORKS_API_KEY) return; + const connectorId = "fireworks-firepass"; + if (this.core.credentials.get(connectorId)) return; + const payload = JSON.stringify({ source: "literal", value: BOSS_FIREWORKS_API_KEY }); + const { backend, payload: ciphertext } = this.vault.encrypt(payload); + this.core.credentials.upsert({ + connectorId, + kind: "api_key", + backend: backend as CredentialBackend, + ciphertext, + displayHint: `${BOSS_FIREWORKS_API_KEY.slice(0, 6)}...${BOSS_FIREWORKS_API_KEY.slice(-4)}`, + status: "active", + }); + } + // ─── Internal helper: relock releaseAllWorkspaceLocks is used by // crash-recovery below; callers should prefer // `db.core.workspaces.releaseAllWorkspaceLocks()` directly.