feat: migrate packaging and auto updates

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
David Kaya
2026-03-29 22:36:44 +02:00
co-authored by Copilot
parent 66b2a94977
commit 15071fdc47
28 changed files with 1254 additions and 901 deletions
+9
View File
@@ -0,0 +1,9 @@
import { rm } from 'node:fs/promises';
import { dirname, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
const scriptDirectory = dirname(fileURLToPath(import.meta.url));
const repositoryRoot = resolve(scriptDirectory, '..');
const releaseDirectory = resolve(repositoryRoot, 'release');
await rm(releaseDirectory, { recursive: true, force: true });
-227
View File
@@ -1,227 +0,0 @@
import { spawn } from 'node:child_process';
import { constants } from 'node:fs';
import {
access,
cp,
mkdir,
readFile,
symlink,
writeFile,
} from 'node:fs/promises';
import { dirname, join, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
import { productName, resolveReleaseTarget } from './releaseTarget';
function runCommand(command: string, args: string[], cwd: string): Promise<void> {
return new Promise((resolvePromise, rejectPromise) => {
const child = spawn(command, args, {
cwd,
stdio: 'inherit',
});
child.on('error', rejectPromise);
child.on('exit', (code, signal) => {
if (code === 0) {
resolvePromise();
return;
}
if (signal) {
rejectPromise(new Error(`${command} exited because of signal ${signal}.`));
return;
}
rejectPromise(new Error(`${command} exited with code ${code ?? 'unknown'}.`));
});
});
}
async function pathExists(path: string): Promise<boolean> {
try {
await access(path, constants.F_OK);
return true;
} catch {
return false;
}
}
const scriptDirectory = dirname(fileURLToPath(import.meta.url));
const repositoryRoot = resolve(scriptDirectory, '..');
const releaseTarget = resolveReleaseTarget(process.platform, process.arch);
const releaseRootDirectory = join(repositoryRoot, 'release');
const packagedAppDirectory = join(releaseRootDirectory, releaseTarget.outputDirectoryName);
const installerOutputPath = join(releaseRootDirectory, releaseTarget.installerAssetName);
const installerAssetsDirectory = join(repositoryRoot, 'assets', 'installer');
async function readVersion(): Promise<string> {
const packageJson = JSON.parse(
await readFile(join(repositoryRoot, 'package.json'), 'utf8'),
) as { version: string };
return packageJson.version;
}
// --- Windows: Inno Setup installer ---
async function resolveInnoSetupCompilerPath(): Promise<string> {
const candidates = [
'C:\\Program Files (x86)\\Inno Setup 6\\ISCC.exe',
'C:\\Program Files\\Inno Setup 6\\ISCC.exe',
'iscc',
];
for (const candidate of candidates) {
if (candidate.includes('\\') && (await pathExists(candidate))) {
return candidate;
}
}
return 'iscc';
}
async function createWindowsInstaller(version: string): Promise<void> {
const issScript = join(installerAssetsDirectory, 'windows.iss');
const isccPath = await resolveInnoSetupCompilerPath();
const outputFilename = releaseTarget.installerAssetName.replace(/\.exe$/, '');
const iconPath = join(repositoryRoot, 'assets', 'icons', 'windows', 'icon.ico');
process.env.ARYX_BUILD_VERSION = version;
process.env.ARYX_BUILD_SOURCE_DIR = packagedAppDirectory;
process.env.ARYX_BUILD_OUTPUT_DIR = releaseRootDirectory;
process.env.ARYX_BUILD_OUTPUT_FILENAME = outputFilename;
process.env.ARYX_BUILD_ICON_PATH = iconPath;
await runCommand(isccPath, [issScript], repositoryRoot);
}
// --- macOS: DMG disk image ---
async function createMacInstaller(): Promise<void> {
const appBundleName = releaseTarget.appBundleName;
if (!appBundleName) {
throw new Error('macOS installer requires an app bundle name.');
}
const appBundlePath = join(packagedAppDirectory, appBundleName);
// Use macOS's native disk image tooling so packaging does not depend on
// create-dmg's native node-gyp install path on CI runners.
await runCommand(
'hdiutil',
[
'create',
'-volname',
productName,
'-srcfolder',
appBundlePath,
'-ov',
'-format',
'UDZO',
installerOutputPath,
],
repositoryRoot,
);
}
// --- Linux: .deb package ---
const linuxIconSizes = ['16x16', '32x32', '48x48', '64x64', '128x128', '256x256', '512x512'];
async function createLinuxInstaller(version: string): Promise<void> {
const stagingDirectory = join(releaseRootDirectory, 'deb-staging');
const debianDirectory = join(stagingDirectory, 'DEBIAN');
const optDirectory = join(stagingDirectory, 'opt', 'aryx');
const binDirectory = join(stagingDirectory, 'usr', 'bin');
const applicationsDirectory = join(stagingDirectory, 'usr', 'share', 'applications');
await mkdir(debianDirectory, { recursive: true });
await mkdir(binDirectory, { recursive: true });
await mkdir(applicationsDirectory, { recursive: true });
// Copy packaged app into /opt/aryx/
await cp(packagedAppDirectory, optDirectory, { recursive: true });
// Create symlink /usr/bin/aryx -> /opt/aryx/Aryx
await symlink('/opt/aryx/Aryx', join(binDirectory, 'aryx'));
// Copy desktop entry
await cp(
join(installerAssetsDirectory, 'linux', 'aryx.desktop'),
join(applicationsDirectory, 'aryx.desktop'),
);
// Install icons into hicolor theme
const sourceIconsDirectory = join(repositoryRoot, 'assets', 'icons', 'linux', 'icons');
for (const size of linuxIconSizes) {
const sourceIcon = join(sourceIconsDirectory, `${size}.png`);
if (!(await pathExists(sourceIcon))) {
continue;
}
const targetIconDirectory = join(
stagingDirectory, 'usr', 'share', 'icons', 'hicolor', size, 'apps',
);
await mkdir(targetIconDirectory, { recursive: true });
await cp(sourceIcon, join(targetIconDirectory, 'aryx.png'));
}
// Determine installed size (in KB)
const { stdout } = await new Promise<{ stdout: string }>((resolvePromise, rejectPromise) => {
const child = spawn('du', ['-sk', optDirectory], { stdio: ['pipe', 'pipe', 'pipe'] });
let out = '';
child.stdout.on('data', (data: Buffer) => { out += data.toString(); });
child.on('error', rejectPromise);
child.on('exit', () => resolvePromise({ stdout: out }));
});
const installedSizeKb = parseInt(stdout.split('\t')[0] ?? '0', 10);
const debArch = releaseTarget.arch === 'x64' ? 'amd64' : 'arm64';
// Write DEBIAN/control
const controlContent = [
`Package: aryx`,
`Version: ${version}`,
`Section: devel`,
`Priority: optional`,
`Architecture: ${debArch}`,
`Installed-Size: ${installedSizeKb}`,
`Depends: libsecret-1-0`,
`Maintainer: David Kaya`,
`Description: ${productName} — Copilot-powered agent workflow orchestrator`,
` Electron desktop app for orchestrating Copilot-driven agent workflows`,
` across multiple projects.`,
'',
].join('\n');
await writeFile(join(debianDirectory, 'control'), controlContent);
// Build the .deb
await runCommand(
'dpkg-deb',
['--build', '--root-owner-group', stagingDirectory, installerOutputPath],
repositoryRoot,
);
}
// --- Entry point ---
if (!(await pathExists(packagedAppDirectory))) {
throw new Error(
`Packaged app not found at ${packagedAppDirectory}. Run "bun run package" first.`,
);
}
const version = await readVersion();
switch (releaseTarget.platform) {
case 'win32':
await createWindowsInstaller(version);
break;
case 'darwin':
await createMacInstaller();
break;
case 'linux':
await createLinuxInstaller(version);
break;
}
console.log(`Created installer: ${installerOutputPath}`);
-332
View File
@@ -1,332 +0,0 @@
import { constants } from 'node:fs';
import { access, chmod, cp, mkdir, readdir, readFile, rename, rm, writeFile } from 'node:fs/promises';
import { dirname, join, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
import { createPackageWithOptions } from '@electron/asar';
import {
macBundleIdentifier,
productName,
resolveReleaseTarget,
type ReleaseTarget,
} from './releaseTarget';
interface PackageManifest {
readonly name: string;
readonly productName: string;
readonly version: string;
readonly description?: string;
readonly main: string;
readonly author?: string;
readonly license?: string;
}
interface RootPackageJson {
readonly name: string;
readonly version: string;
readonly description?: string;
readonly main: string;
readonly author?: string;
readonly license?: string;
readonly dependencies?: Record<string, string>;
}
const scriptDirectory = dirname(fileURLToPath(import.meta.url));
const repositoryRoot = resolve(scriptDirectory, '..');
const assetDirectory = join(repositoryRoot, 'assets');
const genericIconPath = join(assetDirectory, 'icons', 'icon.png');
const windowsIconPath = join(assetDirectory, 'icons', 'windows', 'icon.ico');
const macosIconPath = join(assetDirectory, 'icons', 'macos', 'icon.icns');
const rendererBuildDirectory = join(repositoryRoot, 'dist');
const electronBuildDirectory = join(repositoryRoot, 'dist-electron');
const releaseTarget = resolveReleaseTarget(process.platform, process.arch);
const releaseRootDirectory = join(repositoryRoot, 'release');
const outputDirectory = join(releaseRootDirectory, releaseTarget.outputDirectoryName);
const electronDistributionDirectory = releaseTarget.platform === 'darwin'
? join(repositoryRoot, 'node_modules', 'electron', 'dist', 'Electron.app')
: join(repositoryRoot, 'node_modules', 'electron', 'dist');
const publishedSidecarDirectory = join(repositoryRoot, 'dist-sidecar', releaseTarget.dotnetRuntime);
async function ensurePathExists(path: string, label: string): Promise<void> {
try {
await access(path, constants.F_OK);
} catch {
throw new Error(`${label} was not found at ${path}.`);
}
}
async function pathExists(path: string): Promise<boolean> {
try {
await access(path, constants.F_OK);
return true;
} catch {
return false;
}
}
async function readJson<T>(path: string): Promise<T> {
return JSON.parse(await readFile(path, 'utf8')) as T;
}
async function collectRuntimeDependencies(): Promise<string[]> {
const rootPackageJson = await readJson<RootPackageJson>(join(repositoryRoot, 'package.json'));
const dependencies = new Set(Object.keys(rootPackageJson.dependencies ?? {}));
const queue = [...dependencies];
while (queue.length > 0) {
const dependencyName = queue.shift();
if (!dependencyName) {
continue;
}
const dependencyPackageJsonPath = join(
repositoryRoot,
'node_modules',
...dependencyName.split('/'),
'package.json',
);
if (!(await pathExists(dependencyPackageJsonPath))) {
dependencies.delete(dependencyName);
continue;
}
const dependencyPackageJson = await readJson<{
readonly dependencies?: Record<string, string>;
readonly optionalDependencies?: Record<string, string>;
}>(dependencyPackageJsonPath);
for (const transitiveDependency of Object.keys({
...(dependencyPackageJson.dependencies ?? {}),
...(dependencyPackageJson.optionalDependencies ?? {}),
})) {
if (!dependencies.has(transitiveDependency)) {
dependencies.add(transitiveDependency);
queue.push(transitiveDependency);
}
}
}
return [...dependencies].sort();
}
async function copyRuntimeDependencies(
packagedAppDirectory: string,
dependencyNames: string[],
): Promise<void> {
const packagedNodeModulesDirectory = join(packagedAppDirectory, 'node_modules');
await mkdir(packagedNodeModulesDirectory, { recursive: true });
for (const dependencyName of dependencyNames) {
const dependencyPathParts = dependencyName.split('/');
const sourceDirectory = join(repositoryRoot, 'node_modules', ...dependencyPathParts);
const targetDirectory = join(packagedNodeModulesDirectory, ...dependencyPathParts);
await mkdir(dirname(targetDirectory), { recursive: true });
await cp(sourceDirectory, targetDirectory, { recursive: true });
}
}
async function writePackagedManifest(packagedAppDirectory: string): Promise<PackageManifest> {
const sourcePackageJson = await readJson<RootPackageJson>(join(repositoryRoot, 'package.json'));
const packagedManifest: PackageManifest = {
name: sourcePackageJson.name,
productName,
version: sourcePackageJson.version,
description: sourcePackageJson.description,
main: sourcePackageJson.main,
author: sourcePackageJson.author,
license: sourcePackageJson.license,
};
await writeFile(
join(packagedAppDirectory, 'package.json'),
`${JSON.stringify(packagedManifest, null, 2)}\n`,
);
return packagedManifest;
}
async function copyApplicationPayload(
packagedAppDirectory: string,
outputResourcesDirectory: string,
dependencyNames: string[],
): Promise<PackageManifest> {
await mkdir(packagedAppDirectory, { recursive: true });
const manifest = await writePackagedManifest(packagedAppDirectory);
await Promise.all([
cp(assetDirectory, join(packagedAppDirectory, 'assets'), { recursive: true }),
cp(rendererBuildDirectory, join(packagedAppDirectory, 'dist'), { recursive: true }),
cp(electronBuildDirectory, join(packagedAppDirectory, 'dist-electron'), { recursive: true }),
cp(publishedSidecarDirectory, join(outputResourcesDirectory, 'sidecar'), { recursive: true }),
]);
await copyRuntimeDependencies(packagedAppDirectory, dependencyNames);
const asarPath = join(outputResourcesDirectory, 'app.asar');
await createPackageWithOptions(packagedAppDirectory, asarPath, {
unpack: '**/*.node',
});
await rm(packagedAppDirectory, { recursive: true });
return manifest;
}
async function ensureExecutable(path: string, mode = 0o755): Promise<void> {
await chmod(path, mode);
}
function replacePlistValue(plistContents: string, key: string, value: string): string {
const pattern = new RegExp(`(<key>${key}</key>\\s*<string>)([^<]*)(</string>)`);
if (!pattern.test(plistContents)) {
throw new Error(`Could not find ${key} in macOS Info.plist.`);
}
return plistContents.replace(pattern, `$1${value}$3`);
}
async function applyMacMetadata(appBundleDirectory: string, version: string): Promise<void> {
const infoPlistPath = join(appBundleDirectory, 'Contents', 'Info.plist');
let infoPlistContents = await readFile(infoPlistPath, 'utf8');
infoPlistContents = replacePlistValue(infoPlistContents, 'CFBundleDisplayName', productName);
infoPlistContents = replacePlistValue(infoPlistContents, 'CFBundleExecutable', productName);
infoPlistContents = replacePlistValue(infoPlistContents, 'CFBundleIconFile', 'icon.icns');
infoPlistContents = replacePlistValue(infoPlistContents, 'CFBundleIdentifier', macBundleIdentifier);
infoPlistContents = replacePlistValue(infoPlistContents, 'CFBundleName', productName);
infoPlistContents = replacePlistValue(infoPlistContents, 'CFBundleShortVersionString', version);
infoPlistContents = replacePlistValue(infoPlistContents, 'CFBundleVersion', version);
await writeFile(infoPlistPath, infoPlistContents);
const sourceExecutablePath = join(appBundleDirectory, 'Contents', 'MacOS', 'Electron');
const targetExecutablePath = join(appBundleDirectory, 'Contents', 'MacOS', productName);
await rename(sourceExecutablePath, targetExecutablePath);
await ensureExecutable(targetExecutablePath);
await cp(macosIconPath, join(appBundleDirectory, 'Contents', 'Resources', 'icon.icns'));
}
async function stripUnneededElectronFiles(electronOutputDirectory: string): Promise<void> {
const filesToRemove = ['LICENSES.chromium.html', 'LICENSE'];
const resourcesToRemove = ['default_app.asar'];
await Promise.all([
...filesToRemove.map((file) => rm(join(electronOutputDirectory, file), { force: true })),
...resourcesToRemove.map((file) =>
rm(join(electronOutputDirectory, 'resources', file), { force: true }),
),
]);
const localesDirectory = join(electronOutputDirectory, 'locales');
if (await pathExists(localesDirectory)) {
const localeFiles = await readdir(localesDirectory);
await Promise.all(
localeFiles
.filter((file) => file !== 'en-US.pak')
.map((file) => rm(join(localesDirectory, file))),
);
}
}
async function stripMacElectronFiles(resourcesDirectory: string): Promise<void> {
await rm(join(resourcesDirectory, 'LICENSES.chromium.html'), { force: true });
const entries = await readdir(resourcesDirectory);
const unusedLproj = entries.filter(
(entry) => entry.endsWith('.lproj') && entry !== 'en.lproj',
);
await Promise.all(
unusedLproj.map((dir) => rm(join(resourcesDirectory, dir), { recursive: true })),
);
}
async function packageWindows(dependencyNames: string[]): Promise<void> {
const packagedExecutablePath = join(outputDirectory, `${productName}.exe`);
const packagedAppDirectory = join(outputDirectory, 'resources', 'app');
const outputResourcesDirectory = join(outputDirectory, 'resources');
await cp(electronDistributionDirectory, outputDirectory, { recursive: true });
await stripUnneededElectronFiles(outputDirectory);
await rename(join(outputDirectory, 'electron.exe'), packagedExecutablePath);
await copyApplicationPayload(packagedAppDirectory, outputResourcesDirectory, dependencyNames);
const { rcedit } = await import('rcedit');
await rcedit(packagedExecutablePath, { icon: windowsIconPath });
}
async function packageMac(dependencyNames: string[]): Promise<void> {
const appBundleName = releaseTarget.appBundleName;
if (!appBundleName) {
throw new Error('macOS packaging requires an app bundle name.');
}
const appBundleDirectory = join(outputDirectory, appBundleName);
const packagedAppDirectory = join(appBundleDirectory, 'Contents', 'Resources', 'app');
const outputResourcesDirectory = join(appBundleDirectory, 'Contents', 'Resources');
await cp(electronDistributionDirectory, appBundleDirectory, { recursive: true });
await stripMacElectronFiles(join(appBundleDirectory, 'Contents', 'Resources'));
const manifest = await copyApplicationPayload(packagedAppDirectory, outputResourcesDirectory, dependencyNames);
await applyMacMetadata(appBundleDirectory, manifest.version);
await ensureExecutable(join(outputResourcesDirectory, 'sidecar', releaseTarget.sidecarExecutableName));
}
async function packageLinux(dependencyNames: string[]): Promise<void> {
const packagedExecutableName = releaseTarget.packagedExecutableName;
if (!packagedExecutableName) {
throw new Error('Linux packaging requires a packaged executable name.');
}
const packagedExecutablePath = join(outputDirectory, packagedExecutableName);
const packagedAppDirectory = join(outputDirectory, 'resources', 'app');
const outputResourcesDirectory = join(outputDirectory, 'resources');
const chromeSandboxPath = join(outputDirectory, 'chrome-sandbox');
await cp(electronDistributionDirectory, outputDirectory, { recursive: true });
await stripUnneededElectronFiles(outputDirectory);
await rename(join(outputDirectory, 'electron'), packagedExecutablePath);
await ensureExecutable(packagedExecutablePath);
await copyApplicationPayload(packagedAppDirectory, outputResourcesDirectory, dependencyNames);
await ensureExecutable(join(outputResourcesDirectory, 'sidecar', releaseTarget.sidecarExecutableName));
if (await pathExists(chromeSandboxPath)) {
await chmod(chromeSandboxPath, 0o4755);
}
}
async function packageCurrentPlatform(target: ReleaseTarget, dependencyNames: string[]): Promise<void> {
switch (target.platform) {
case 'win32':
await packageWindows(dependencyNames);
return;
case 'darwin':
await packageMac(dependencyNames);
return;
case 'linux':
await packageLinux(dependencyNames);
return;
}
}
await Promise.all([
ensurePathExists(assetDirectory, 'Application assets'),
ensurePathExists(genericIconPath, 'Source application icon'),
ensurePathExists(electronDistributionDirectory, 'Electron runtime'),
ensurePathExists(rendererBuildDirectory, 'Renderer build output'),
ensurePathExists(electronBuildDirectory, 'Electron build output'),
ensurePathExists(publishedSidecarDirectory, 'Published sidecar output'),
]);
if (releaseTarget.platform === 'win32') {
await ensurePathExists(windowsIconPath, 'Windows application icon');
}
if (releaseTarget.platform === 'darwin') {
await ensurePathExists(macosIconPath, 'macOS application icon');
}
const runtimeDependencies = await collectRuntimeDependencies();
await rm(outputDirectory, { recursive: true, force: true });
await mkdir(releaseRootDirectory, { recursive: true });
await packageCurrentPlatform(releaseTarget, runtimeDependencies);
console.log(`Packaged ${productName} for ${releaseTarget.platformLabel} to ${outputDirectory}`);
console.log(`Bundled ${runtimeDependencies.length} runtime dependencies and the self-contained .NET sidecar.`);
+35 -6
View File
@@ -3,8 +3,6 @@ import { rm } from 'node:fs/promises';
import { dirname, join, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
import { resolveReleaseTarget } from './releaseTarget';
function runCommand(command: string, args: string[], cwd: string): Promise<void> {
return new Promise((resolvePromise, rejectPromise) => {
const child = spawn(command, args, {
@@ -29,9 +27,40 @@ function runCommand(command: string, args: string[], cwd: string): Promise<void>
});
}
type SupportedPlatform = 'win32' | 'darwin' | 'linux';
type SupportedArch = 'x64' | 'arm64';
function resolveDotnetRuntime(platform: NodeJS.Platform, arch: NodeJS.Architecture): `${string}-${SupportedArch}` {
if (arch !== 'x64' && arch !== 'arm64') {
throw new Error(`Unsupported architecture for sidecar publish: ${arch}`);
}
switch (platform) {
case 'win32':
return `win-${arch}`;
case 'darwin':
return `osx-${arch}`;
case 'linux':
return `linux-${arch}`;
default:
throw new Error(`Unsupported platform for sidecar publish: ${platform}`);
}
}
function resolvePlatformLabel(platform: SupportedPlatform): 'windows' | 'macos' | 'linux' {
switch (platform) {
case 'win32':
return 'windows';
case 'darwin':
return 'macos';
case 'linux':
return 'linux';
}
}
const scriptDirectory = dirname(fileURLToPath(import.meta.url));
const repositoryRoot = resolve(scriptDirectory, '..');
const releaseTarget = resolveReleaseTarget(process.platform, process.arch);
const dotnetRuntime = resolveDotnetRuntime(process.platform, process.arch);
const sidecarProjectPath = join(
repositoryRoot,
'sidecar',
@@ -39,7 +68,7 @@ const sidecarProjectPath = join(
'Aryx.AgentHost',
'Aryx.AgentHost.csproj',
);
const outputDirectory = join(repositoryRoot, 'dist-sidecar', releaseTarget.dotnetRuntime);
const outputDirectory = join(repositoryRoot, 'dist-sidecar');
await rm(outputDirectory, { recursive: true, force: true });
@@ -51,7 +80,7 @@ await runCommand(
'-c',
'Release',
'-r',
releaseTarget.dotnetRuntime,
dotnetRuntime,
'--self-contained',
'true',
'-p:DebugType=None',
@@ -65,4 +94,4 @@ await runCommand(
repositoryRoot,
);
console.log(`Published sidecar for ${releaseTarget.platformLabel} (${releaseTarget.dotnetRuntime}) to ${outputDirectory}`);
console.log(`Published sidecar for ${resolvePlatformLabel(process.platform as SupportedPlatform)} (${dotnetRuntime}) to ${outputDirectory}`);
-87
View File
@@ -1,87 +0,0 @@
export const productName = 'Aryx';
export const macBundleIdentifier = 'com.davidkaya.aryx';
type SupportedPlatform = 'win32' | 'darwin' | 'linux';
type SupportedArch = 'x64' | 'arm64';
export interface ReleaseTarget {
readonly platform: SupportedPlatform;
readonly arch: SupportedArch;
readonly platformLabel: 'windows' | 'macos' | 'linux';
readonly dotnetRuntime: `${string}-${SupportedArch}`;
readonly outputDirectoryName: string;
readonly archiveBaseName: string;
readonly installerAssetName: string;
readonly sidecarExecutableName: string;
readonly packagedExecutableName?: string;
readonly appBundleName?: string;
}
function resolveSupportedArch(
platform: SupportedPlatform,
arch: NodeJS.Architecture,
): SupportedArch {
if (arch === 'x64' || arch === 'arm64') {
return arch;
}
throw new Error(`Unsupported architecture for ${platform}: ${arch}`);
}
export function resolveReleaseTarget(
platform: NodeJS.Platform,
arch: NodeJS.Architecture,
): ReleaseTarget {
switch (platform) {
case 'win32': {
const supportedArch = resolveSupportedArch(platform, arch);
const archiveBaseName = `${productName}-windows-${supportedArch}`;
return {
platform,
arch: supportedArch,
platformLabel: 'windows',
dotnetRuntime: `win-${supportedArch}`,
outputDirectoryName: archiveBaseName,
archiveBaseName,
installerAssetName: `${archiveBaseName}-setup.exe`,
sidecarExecutableName: 'Aryx.AgentHost.exe',
packagedExecutableName: `${productName}.exe`,
};
}
case 'darwin': {
const supportedArch = resolveSupportedArch(platform, arch);
const archiveBaseName = `${productName}-macos-${supportedArch}`;
return {
platform,
arch: supportedArch,
platformLabel: 'macos',
dotnetRuntime: `osx-${supportedArch}`,
outputDirectoryName: archiveBaseName,
archiveBaseName,
installerAssetName: `${archiveBaseName}.dmg`,
sidecarExecutableName: 'Aryx.AgentHost',
appBundleName: `${productName}.app`,
};
}
case 'linux': {
const supportedArch = resolveSupportedArch(platform, arch);
const archiveBaseName = `${productName}-linux-${supportedArch}`;
return {
platform,
arch: supportedArch,
platformLabel: 'linux',
dotnetRuntime: `linux-${supportedArch}`,
outputDirectoryName: archiveBaseName,
archiveBaseName,
installerAssetName: `aryx-linux-${supportedArch}.deb`,
sidecarExecutableName: 'Aryx.AgentHost',
packagedExecutableName: productName,
};
}
default:
throw new Error(`Unsupported release platform: ${platform}`);
}
}