update
This commit is contained in:
1 parent
cbef47f1ad
commit
17e416d609
9 files changed
+652
-33
No files matched your search
@@ -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
@@ -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",
|
||||
|
||||
@@ -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();
|
||||
@@ -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();
|
||||
@@ -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 });
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
@@ -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 = "";
|
||||
@@ -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.
|
||||
|
||||
Reference in new issue
Block a user