This commit is contained in:
ZacharyZhang-NY committed 2026-04-23 15:26:30 -04:00
1 parent cbef47f1ad
commit 17e416d609
9 files changed
+652 -33

No files matched your search

+10 -10
View File
@@ -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",
},
],
+4 -1
View File
@@ -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",
+199
View File
@@ -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();
+222
View File
@@ -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();
+58 -20
View File
@@ -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 });
}
}
+4 -1
View File
@@ -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();
+125
View File
@@ -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();
+7
View File
@@ -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 = "";
+23 -1
View File
@@ -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 = <T>(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.