#!/usr/bin/env node import rootPackageJson from "../package.json" with { type: "json" };import desktopPackageJson from "../apps/desktop/package.json" with { type: "json" };import serverPackageJson from "../apps/server/package.json" with { type: "json" }; import * as NodeRuntime from "@effect/platform-node/NodeRuntime";import * as NodeServices from "@effect/platform-node/NodeServices";import { Config, Data, Effect, FileSystem, Logger, Option, Path, Schema } from "effect";import { Command, Flag } from "effect/unstable/cli";import { ChildProcess } from "effect/unstable/process"; const BuildPlatform = Schema.Literals(["mac", "linux", "win"]);const BuildArch = Schema.Literals(["arm64", "x64", "universal"]); const RepoRoot = Effect.service(Path.Path).pipe( Effect.flatMap((path) => path.fromFileUrl(new URL("..", import.meta.url))),);const IconSource = Effect.zipWith(RepoRoot, Effect.service(Path.Path), (repoRoot, path) => path.join(repoRoot, "assets/macos-icon-1024.png"),); interface PlatformConfig { readonly cliFlag: "--mac" | "--linux" | "--win"; readonly defaultTarget: string; readonly archChoices: ReadonlyArray<typeof BuildArch.Type>;} const PLATFORM_CONFIG: Record<typeof BuildPlatform.Type, PlatformConfig> = { mac: { cliFlag: "--mac", defaultTarget: "dmg", archChoices: ["arm64", "x64", "universal"], }, linux: { cliFlag: "--linux", defaultTarget: "AppImage", archChoices: ["x64", "arm64"], }, win: { cliFlag: "--win", defaultTarget: "nsis", archChoices: ["x64", "arm64"], },}; interface BuildCliInput { readonly platform: Option.Option<typeof BuildPlatform.Type>; readonly target: Option.Option<string>; readonly arch: Option.Option<typeof BuildArch.Type>; readonly buildVersion: Option.Option<string>; readonly outputDir: Option.Option<string>; readonly skipBuild: Option.Option<boolean>; readonly keepStage: Option.Option<boolean>; readonly signed: Option.Option<boolean>; readonly verbose: Option.Option<boolean>;} function detectHostBuildPlatform(hostPlatform: string): typeof BuildPlatform.Type | undefined { if (hostPlatform === "darwin") return "mac"; if (hostPlatform === "linux") return "linux"; if (hostPlatform === "win32") return "win"; return undefined;} function getDefaultArch(platform: typeof BuildPlatform.Type): typeof BuildArch.Type { const config = PLATFORM_CONFIG[platform]; if (!config) { return "x64"; } if (process.arch === "arm64" && config.archChoices.includes("arm64")) { return "arm64"; } if (process.arch === "x64" && config.archChoices.includes("x64")) { return "x64"; } return config.archChoices[0] ?? "x64";} class BuildScriptError extends Data.TaggedError("BuildScriptError")<{ readonly message: string; readonly cause?: unknown;}> {} interface ResolvedBuildOptions { readonly platform: typeof BuildPlatform.Type; readonly target: string; readonly arch: typeof BuildArch.Type; readonly version: string | undefined; readonly outputDir: string; readonly skipBuild: boolean; readonly keepStage: boolean; readonly signed: boolean; readonly verbose: boolean;} interface StagePackageJson { readonly name: string; readonly version: string; readonly private: true; readonly description: string; readonly author: string; readonly main: string; readonly build: Record<string, unknown>; readonly dependencies: Record<string, unknown>; readonly devDependencies: { readonly electron: string; };} const AzureTrustedSigningOptionsConfig = Config.all({ publisherName: Config.string("AZURE_TRUSTED_SIGNING_PUBLISHER_NAME"), endpoint: Config.string("AZURE_TRUSTED_SIGNING_ENDPOINT"), certificateProfileName: Config.string("AZURE_TRUSTED_SIGNING_CERTIFICATE_PROFILE_NAME"), codeSigningAccountName: Config.string("AZURE_TRUSTED_SIGNING_ACCOUNT_NAME"), fileDigest: Config.string("AZURE_TRUSTED_SIGNING_FILE_DIGEST").pipe( Config.withDefault(() => "SHA256"), ), timestampDigest: Config.string("AZURE_TRUSTED_SIGNING_TIMESTAMP_DIGEST").pipe( Config.withDefault(() => "SHA256"), ), timestampRfc3161: Config.string("AZURE_TRUSTED_SIGNING_TIMESTAMP_RFC3161").pipe( Config.withDefault(() => "http://timestamp.acs.microsoft.com"), ),}); const BuildEnvConfig = Config.all({ platform: Config.schema(BuildPlatform, "T3CODE_DESKTOP_PLATFORM").pipe(Config.option), target: Config.string("T3CODE_DESKTOP_TARGET").pipe(Config.option), arch: Config.schema(BuildArch, "T3CODE_DESKTOP_ARCH").pipe(Config.option), version: Config.string("T3CODE_DESKTOP_VERSION").pipe(Config.option), outputDir: Config.string("T3CODE_DESKTOP_OUTPUT_DIR").pipe(Config.option), skipBuild: Config.boolean("T3CODE_DESKTOP_SKIP_BUILD").pipe(Config.withDefault(() => false)), keepStage: Config.boolean("T3CODE_DESKTOP_KEEP_STAGE").pipe(Config.withDefault(() => false)), signed: Config.boolean("T3CODE_DESKTOP_SIGNED").pipe(Config.withDefault(() => false)), verbose: Config.boolean("T3CODE_DESKTOP_VERBOSE").pipe(Config.withDefault(() => false)),}); const resolveBooleanFlag = (flag: Option.Option<boolean>, envValue: boolean) => Option.getOrElse(Option.filter(flag, Boolean), () => envValue);const mergeOptions = <A>(a: Option.Option<A>, b: Option.Option<A>, defaultValue: A) => Option.getOrElse(a, () => Option.getOrElse(b, () => defaultValue)); const resolveBuildOptions = Effect.fn(function* (input: BuildCliInput) { const path = yield* Path.Path; const repoRoot = yield* RepoRoot; const env = yield* BuildEnvConfig.asEffect(); const platform = mergeOptions( input.platform, env.platform, detectHostBuildPlatform(process.platform), ); if (!platform) { return yield* new BuildScriptError({ message: `Unsupported host platform '${process.platform}'.`, }); } const target = mergeOptions(input.target, env.target, PLATFORM_CONFIG[platform].defaultTarget); const arch = mergeOptions(input.arch, env.arch, getDefaultArch(platform)); const version = mergeOptions(input.buildVersion, env.version, undefined); const outputDir = path.resolve(repoRoot, mergeOptions(input.outputDir, env.outputDir, "release")); const skipBuild = resolveBooleanFlag(input.skipBuild, env.skipBuild); const keepStage = resolveBooleanFlag(input.keepStage, env.keepStage); const signed = resolveBooleanFlag(input.signed, env.signed); const verbose = resolveBooleanFlag(input.verbose, env.verbose); return { platform, target, arch, version, outputDir, skipBuild, keepStage, signed, verbose, } satisfies ResolvedBuildOptions;}); const commandOutputOptions = (verbose: boolean) => ({ stdout: verbose ? "inherit" : "ignore", stderr: "inherit", }) as const; const runCommand = Effect.fn(function* (command: ChildProcess.Command) { const exitCode = yield* ChildProcess.exitCode(command); if (exitCode !== 0) { return yield* new BuildScriptError({ message: "Command exited with non-zero exit code", }); }}); function generateMacIconSet( sourcePng: string, targetIcns: string, tmpRoot: string, path: Path.Path, verbose: boolean,) { return Effect.gen(function* () { const fs = yield* FileSystem.FileSystem; const iconsetDir = path.join(tmpRoot, "icon.iconset"); yield* fs.makeDirectory(iconsetDir, { recursive: true }); const iconSizes = [16, 32, 128, 256, 512] as const; for (const size of iconSizes) { yield* runCommand( ChildProcess.make({ ...commandOutputOptions(verbose), })`sips -z ${size} ${size} ${sourcePng} --out ${path.join(iconsetDir, `icon_${size}x${size}.png`)}`, ); const retinaSize = size * 2; yield* runCommand( ChildProcess.make({ ...commandOutputOptions(verbose), })`sips -z ${retinaSize} ${retinaSize} ${sourcePng} --out ${path.join(iconsetDir, `icon_${size}x${size}@2x.png`)}`, ); } yield* runCommand( ChildProcess.make({ ...commandOutputOptions(verbose), })`iconutil -c icns ${iconsetDir} -o ${targetIcns}`, ); });} function stageMacIcons(stageResourcesDir: string, verbose: boolean) { return Effect.gen(function* () { const fs = yield* FileSystem.FileSystem; const path = yield* Path.Path; const iconSource = yield* IconSource; if (!(yield* fs.exists(iconSource))) { return yield* new BuildScriptError({ message: `Production icon source is missing at ${iconSource}`, }); } const tmpRoot = yield* fs.makeTempDirectoryScoped({ prefix: "t3code-icon-build-", }); const iconPngPath = path.join(stageResourcesDir, "icon.png"); const iconIcnsPath = path.join(stageResourcesDir, "icon.icns"); yield* runCommand( ChildProcess.make({ ...commandOutputOptions(verbose), })`sips -z 512 512 ${iconSource} --out ${iconPngPath}`, ); yield* generateMacIconSet(iconSource, iconIcnsPath, tmpRoot, path, verbose); });} function validateBundledClientAssets(clientDir: string) { return Effect.gen(function* () { const fs = yield* FileSystem.FileSystem; const path = yield* Path.Path; const indexPath = path.join(clientDir, "index.html"); const indexHtml = yield* fs.readFileString(indexPath); const refs = [...indexHtml.matchAll(/\b(?:src|href)=["']([^"']+)["']/g)].map( (match) => match[1], ); const missing: string[] = []; for (const ref of refs) { const normalizedRef = ref.split("#")[0]?.split("?")[0] ?? ""; if (!normalizedRef) continue; if (normalizedRef.startsWith("http://") || normalizedRef.startsWith("https://")) continue; if (normalizedRef.startsWith("data:") || normalizedRef.startsWith("mailto:")) continue; const ext = path.extname(normalizedRef); if (!ext) continue; const relativePath = normalizedRef.replace(/^\/+/, ""); const assetPath = path.join(clientDir, relativePath); if (!(yield* fs.exists(assetPath))) { missing.push(normalizedRef); } } if (missing.length > 0) { const preview = missing.slice(0, 6).join(", "); const suffix = missing.length > 6 ? ` (+${missing.length - 6} more)` : ""; return yield* new BuildScriptError({ message: `Bundled client references missing files in ${indexPath}: ${preview}${suffix}. Rebuild web/server artifacts.`, }); } });} function resolveCatalogDependencies( dependencies: Record<string, unknown>, catalog: Record<string, unknown>, dependencySourceLabel: string,): Record<string, unknown> { return Object.fromEntries( Object.entries(dependencies).map(([dependencyName, spec]) => { if (typeof spec !== "string" || !spec.startsWith("catalog:")) { return [dependencyName, spec]; } const catalogKey = spec.slice("catalog:".length).trim(); const lookupKey = catalogKey.length > 0 ? catalogKey : dependencyName; const resolvedSpec = catalog[lookupKey]; if (typeof resolvedSpec !== "string" || resolvedSpec.length === 0) { throw new BuildScriptError({ message: `Unable to resolve '${spec}' for ${dependencySourceLabel} dependency '${dependencyName}'. Expected key '${lookupKey}' in root workspace catalog.`, }); } return [dependencyName, resolvedSpec]; }), );} const createBuildConfig = Effect.fn(function* ( platform: typeof BuildPlatform.Type, target: string, productName: string, signed: boolean,) { const buildConfig: Record<string, unknown> = { appId: "com.t3tools.t3code", productName, artifactName: "T3-Code-${version}-${arch}.${ext}", directories: { buildResources: "apps/desktop/resources", }, }; if (platform === "mac") { buildConfig.mac = { target: [target], icon: "icon.icns", category: "public.app-category.developer-tools", }; } if (platform === "linux") { buildConfig.linux = { target: [target], icon: "icon.png", category: "Development", }; } if (platform === "win") { const winConfig: Record<string, unknown> = { target: [target], icon: "icon.ico", }; if (signed) { winConfig.azureSignOptions = yield* AzureTrustedSigningOptionsConfig; } buildConfig.win = winConfig; } return buildConfig;}); const assertPlatformBuildResources = Effect.fn(function* ( platform: typeof BuildPlatform.Type, stageResourcesDir: string, verbose: boolean,) { const fs = yield* FileSystem.FileSystem; const path = yield* Path.Path; if (platform === "mac") { yield* stageMacIcons(stageResourcesDir, verbose); return; } if (platform === "linux") { const iconPath = path.join(stageResourcesDir, "icon.png"); if (!(yield* fs.exists(iconPath))) { return yield* new BuildScriptError({ message: `Missing Linux icon at ${iconPath}`, }); } return; } if (platform === "win") { const iconPath = path.join(stageResourcesDir, "icon.ico"); if (!(yield* fs.exists(iconPath))) { return yield* new BuildScriptError({ message: `Missing Windows icon at ${iconPath}`, }); } }}); const buildDesktopArtifact = Effect.fn(function* (options: ResolvedBuildOptions) { const repoRoot = yield* RepoRoot; const path = yield* Path.Path; const fs = yield* FileSystem.FileSystem; const platformConfig = PLATFORM_CONFIG[options.platform]; if (!platformConfig) { return yield* new BuildScriptError({ message: `Unsupported platform '${options.platform}'.`, }); } const electronVersion = desktopPackageJson.dependencies.electron; const dependencies = serverPackageJson.dependencies; if (!dependencies || Object.keys(dependencies).length === 0) { return yield* new BuildScriptError({ message: "Could not resolve production dependencies from apps/server/package.json.", }); } const resolvedDependencies = yield* Effect.try({ try: () => resolveCatalogDependencies(dependencies, rootPackageJson.workspaces.catalog, "apps/server"), catch: (cause) => new BuildScriptError({ message: "Could not resolve production dependencies from apps/server/package.json.", cause, }), }); const appVersion = options.version ?? serverPackageJson.version; const mkdir = options.keepStage ? fs.makeTempDirectory : fs.makeTempDirectoryScoped; const stageRoot = yield* mkdir({ prefix: `t3code-desktop-${options.platform}-stage-`, }); const stageAppDir = path.join(stageRoot, "app"); const stageResourcesDir = path.join(stageAppDir, "apps/desktop/resources"); const distDirs = { desktopDist: path.join(repoRoot, "apps/desktop/dist-electron"), desktopResources: path.join(repoRoot, "apps/desktop/resources"), serverDist: path.join(repoRoot, "apps/server/dist"), }; const bundledClientEntry = path.join(distDirs.serverDist, "client/index.html"); if (!options.skipBuild) { yield* Effect.log("[desktop-artifact] Building desktop/server/web artifacts..."); yield* runCommand( ChildProcess.make({ cwd: repoRoot, ...commandOutputOptions(options.verbose), })`bun run build:desktop`, ); } for (const [label, dir] of Object.entries(distDirs)) { if (!(yield* fs.exists(dir))) { return yield* new BuildScriptError({ message: `Missing ${label} at ${dir}. Run 'bun run build:desktop' first.`, }); } } if (!(yield* fs.exists(bundledClientEntry))) { return yield* new BuildScriptError({ message: `Missing bundled server client at ${bundledClientEntry}. Run 'bun run build:desktop' first.`, }); } yield* validateBundledClientAssets(path.dirname(bundledClientEntry)); yield* fs.makeDirectory(path.join(stageAppDir, "apps/desktop"), { recursive: true }); yield* fs.makeDirectory(path.join(stageAppDir, "apps/server"), { recursive: true }); yield* Effect.log("[desktop-artifact] Staging release app..."); yield* fs.copy(distDirs.desktopDist, path.join(stageAppDir, "apps/desktop/dist-electron")); yield* fs.copy(distDirs.desktopResources, stageResourcesDir); yield* fs.copy(distDirs.serverDist, path.join(stageAppDir, "apps/server/dist")); yield* assertPlatformBuildResources(options.platform, stageResourcesDir, options.verbose); const stagePackageJson: StagePackageJson = { name: "t3-code-desktop", version: appVersion, private: true, description: "T3 Code desktop build", author: "T3 Tools", main: "apps/desktop/dist-electron/main.js", build: yield* createBuildConfig( options.platform, options.target, desktopPackageJson.productName ?? "T3 Code", options.signed, ),Lines 1–500 of 633. Load more
#!/usr/bin/env node import rootPackageJson from "../package.json" with { type: "json" }; import desktopPackageJson from "../apps/desktop/package.json" with { type: "json" }; import serverPackageJson from "../apps/server/package.json" with { type: "json" }; import * as NodeRuntime from "@effect/platform-node/NodeRuntime"; import * as NodeServices from "@effect/platform-node/NodeServices"; import { Config, Data, Effect, FileSystem, Logger, Option, Path, Schema } from "effect"; import { Command, Flag } from "effect/unstable/cli"; import { ChildProcess } from "effect/unstable/process"; const BuildPlatform = Schema.Literals(["mac", "linux", "win"]); const BuildArch = Schema.Literals(["arm64", "x64", "universal"]); const RepoRoot = Effect.service(Path.Path).pipe( Effect.flatMap((path) => path.fromFileUrl(new URL("..", import.meta.url))), ); const IconSource = Effect.zipWith(RepoRoot, Effect.service(Path.Path), (repoRoot, path) => path.join(repoRoot, "assets/macos-icon-1024.png"), ); interface PlatformConfig { readonly cliFlag: "--mac" | "--linux" | "--win"; readonly defaultTarget: string; readonly archChoices: ReadonlyArray<typeof BuildArch.Type>; } const PLATFORM_CONFIG: Record<typeof BuildPlatform.Type, PlatformConfig> = { mac: { cliFlag: "--mac", defaultTarget: "dmg", archChoices: ["arm64", "x64", "universal"], }, linux: { cliFlag: "--linux", defaultTarget: "AppImage", archChoices: ["x64", "arm64"], }, win: { cliFlag: "--win", defaultTarget: "nsis", archChoices: ["x64", "arm64"], }, }; interface BuildCliInput { readonly platform: Option.Option<typeof BuildPlatform.Type>; readonly target: Option.Option<string>; readonly arch: Option.Option<typeof BuildArch.Type>; readonly buildVersion: Option.Option<string>; readonly outputDir: Option.Option<string>; readonly skipBuild: Option.Option<boolean>; readonly keepStage: Option.Option<boolean>; readonly signed: Option.Option<boolean>; readonly verbose: Option.Option<boolean>; } function detectHostBuildPlatform(hostPlatform: string): typeof BuildPlatform.Type | undefined { if (hostPlatform === "darwin") return "mac"; if (hostPlatform === "linux") return "linux"; if (hostPlatform === "win32") return "win"; return undefined; } function getDefaultArch(platform: typeof BuildPlatform.Type): typeof BuildArch.Type { const config = PLATFORM_CONFIG[platform]; if (!config) { return "x64"; } if (process.arch === "arm64" && config.archChoices.includes("arm64")) { return "arm64"; } if (process.arch === "x64" && config.archChoices.includes("x64")) { return "x64"; } return config.archChoices[0] ?? "x64"; } class BuildScriptError extends Data.TaggedError("BuildScriptError")<{ readonly message: string; readonly cause?: unknown; }> {} interface ResolvedBuildOptions { readonly platform: typeof BuildPlatform.Type; readonly target: string; readonly arch: typeof BuildArch.Type; readonly version: string | undefined; readonly outputDir: string; readonly skipBuild: boolean; readonly keepStage: boolean; readonly signed: boolean; readonly verbose: boolean; } interface StagePackageJson { readonly name: string; readonly version: string; readonly private: true; readonly description: string; readonly author: string; readonly main: string; readonly build: Record<string, unknown>; readonly dependencies: Record<string, unknown>; readonly devDependencies: { readonly electron: string; }; } const AzureTrustedSigningOptionsConfig = Config.all({ publisherName: Config.string("AZURE_TRUSTED_SIGNING_PUBLISHER_NAME"), endpoint: Config.string("AZURE_TRUSTED_SIGNING_ENDPOINT"), certificateProfileName: Config.string("AZURE_TRUSTED_SIGNING_CERTIFICATE_PROFILE_NAME"), codeSigningAccountName: Config.string("AZURE_TRUSTED_SIGNING_ACCOUNT_NAME"), fileDigest: Config.string("AZURE_TRUSTED_SIGNING_FILE_DIGEST").pipe( Config.withDefault(() => "SHA256"), ), timestampDigest: Config.string("AZURE_TRUSTED_SIGNING_TIMESTAMP_DIGEST").pipe( Config.withDefault(() => "SHA256"), ), timestampRfc3161: Config.string("AZURE_TRUSTED_SIGNING_TIMESTAMP_RFC3161").pipe( Config.withDefault(() => "http://timestamp.acs.microsoft.com"), ), }); const BuildEnvConfig = Config.all({ platform: Config.schema(BuildPlatform, "T3CODE_DESKTOP_PLATFORM").pipe(Config.option), target: Config.string("T3CODE_DESKTOP_TARGET").pipe(Config.option), arch: Config.schema(BuildArch, "T3CODE_DESKTOP_ARCH").pipe(Config.option), version: Config.string("T3CODE_DESKTOP_VERSION").pipe(Config.option), outputDir: Config.string("T3CODE_DESKTOP_OUTPUT_DIR").pipe(Config.option), skipBuild: Config.boolean("T3CODE_DESKTOP_SKIP_BUILD").pipe(Config.withDefault(() => false)), keepStage: Config.boolean("T3CODE_DESKTOP_KEEP_STAGE").pipe(Config.withDefault(() => false)), signed: Config.boolean("T3CODE_DESKTOP_SIGNED").pipe(Config.withDefault(() => false)), verbose: Config.boolean("T3CODE_DESKTOP_VERBOSE").pipe(Config.withDefault(() => false)), }); const resolveBooleanFlag = (flag: Option.Option<boolean>, envValue: boolean) => Option.getOrElse(Option.filter(flag, Boolean), () => envValue); const mergeOptions = <A>(a: Option.Option<A>, b: Option.Option<A>, defaultValue: A) => Option.getOrElse(a, () => Option.getOrElse(b, () => defaultValue)); const resolveBuildOptions = Effect.fn(function* (input: BuildCliInput) { const path = yield* Path.Path; const repoRoot = yield* RepoRoot; const env = yield* BuildEnvConfig.asEffect(); const platform = mergeOptions( input.platform, env.platform, detectHostBuildPlatform(process.platform), ); if (!platform) { return yield* new BuildScriptError({ message: `Unsupported host platform '${process.platform}'.`, }); } const target = mergeOptions(input.target, env.target, PLATFORM_CONFIG[platform].defaultTarget); const arch = mergeOptions(input.arch, env.arch, getDefaultArch(platform)); const version = mergeOptions(input.buildVersion, env.version, undefined); const outputDir = path.resolve(repoRoot, mergeOptions(input.outputDir, env.outputDir, "release")); const skipBuild = resolveBooleanFlag(input.skipBuild, env.skipBuild); const keepStage = resolveBooleanFlag(input.keepStage, env.keepStage); const signed = resolveBooleanFlag(input.signed, env.signed); const verbose = resolveBooleanFlag(input.verbose, env.verbose); return { platform, target, arch, version, outputDir, skipBuild, keepStage, signed, verbose, } satisfies ResolvedBuildOptions; }); const commandOutputOptions = (verbose: boolean) => ({ stdout: verbose ? "inherit" : "ignore", stderr: "inherit", }) as const; const runCommand = Effect.fn(function* (command: ChildProcess.Command) { const exitCode = yield* ChildProcess.exitCode(command); if (exitCode !== 0) { return yield* new BuildScriptError({ message: "Command exited with non-zero exit code", }); } }); function generateMacIconSet( sourcePng: string, targetIcns: string, tmpRoot: string, path: Path.Path, verbose: boolean, ) { return Effect.gen(function* () { const fs = yield* FileSystem.FileSystem; const iconsetDir = path.join(tmpRoot, "icon.iconset"); yield* fs.makeDirectory(iconsetDir, { recursive: true }); const iconSizes = [16, 32, 128, 256, 512] as const; for (const size of iconSizes) { yield* runCommand( ChildProcess.make({ ...commandOutputOptions(verbose), })`sips -z ${size} ${size} ${sourcePng} --out ${path.join(iconsetDir, `icon_${size}x${size}.png`)}`, ); const retinaSize = size * 2; yield* runCommand( ChildProcess.make({ ...commandOutputOptions(verbose), })`sips -z ${retinaSize} ${retinaSize} ${sourcePng} --out ${path.join(iconsetDir, `icon_${size}x${size}@2x.png`)}`, ); } yield* runCommand( ChildProcess.make({ ...commandOutputOptions(verbose), })`iconutil -c icns ${iconsetDir} -o ${targetIcns}`, ); }); } function stageMacIcons(stageResourcesDir: string, verbose: boolean) { return Effect.gen(function* () { const fs = yield* FileSystem.FileSystem; const path = yield* Path.Path; const iconSource = yield* IconSource; if (!(yield* fs.exists(iconSource))) { return yield* new BuildScriptError({ message: `Production icon source is missing at ${iconSource}`, }); } const tmpRoot = yield* fs.makeTempDirectoryScoped({ prefix: "t3code-icon-build-", }); const iconPngPath = path.join(stageResourcesDir, "icon.png"); const iconIcnsPath = path.join(stageResourcesDir, "icon.icns"); yield* runCommand( ChildProcess.make({ ...commandOutputOptions(verbose), })`sips -z 512 512 ${iconSource} --out ${iconPngPath}`, ); yield* generateMacIconSet(iconSource, iconIcnsPath, tmpRoot, path, verbose); }); } function validateBundledClientAssets(clientDir: string) { return Effect.gen(function* () { const fs = yield* FileSystem.FileSystem; const path = yield* Path.Path; const indexPath = path.join(clientDir, "index.html"); const indexHtml = yield* fs.readFileString(indexPath); const refs = [...indexHtml.matchAll(/\b(?:src|href)=["']([^"']+)["']/g)].map( (match) => match[1], ); const missing: string[] = []; for (const ref of refs) { const normalizedRef = ref.split("#")[0]?.split("?")[0] ?? ""; if (!normalizedRef) continue; if (normalizedRef.startsWith("http://") || normalizedRef.startsWith("https://")) continue; if (normalizedRef.startsWith("data:") || normalizedRef.startsWith("mailto:")) continue; const ext = path.extname(normalizedRef); if (!ext) continue; const relativePath = normalizedRef.replace(/^\/+/, ""); const assetPath = path.join(clientDir, relativePath); if (!(yield* fs.exists(assetPath))) { missing.push(normalizedRef); } } if (missing.length > 0) { const preview = missing.slice(0, 6).join(", "); const suffix = missing.length > 6 ? ` (+${missing.length - 6} more)` : ""; return yield* new BuildScriptError({ message: `Bundled client references missing files in ${indexPath}: ${preview}${suffix}. Rebuild web/server artifacts.`, }); } }); } function resolveCatalogDependencies( dependencies: Record<string, unknown>, catalog: Record<string, unknown>, dependencySourceLabel: string, ): Record<string, unknown> { return Object.fromEntries( Object.entries(dependencies).map(([dependencyName, spec]) => { if (typeof spec !== "string" || !spec.startsWith("catalog:")) { return [dependencyName, spec]; } const catalogKey = spec.slice("catalog:".length).trim(); const lookupKey = catalogKey.length > 0 ? catalogKey : dependencyName; const resolvedSpec = catalog[lookupKey]; if (typeof resolvedSpec !== "string" || resolvedSpec.length === 0) { throw new BuildScriptError({ message: `Unable to resolve '${spec}' for ${dependencySourceLabel} dependency '${dependencyName}'. Expected key '${lookupKey}' in root workspace catalog.`, }); } return [dependencyName, resolvedSpec]; }), ); } const createBuildConfig = Effect.fn(function* ( platform: typeof BuildPlatform.Type, target: string, productName: string, signed: boolean, ) { const buildConfig: Record<string, unknown> = { appId: "com.t3tools.t3code", productName, artifactName: "T3-Code-${version}-${arch}.${ext}", directories: { buildResources: "apps/desktop/resources", }, }; if (platform === "mac") { buildConfig.mac = { target: [target], icon: "icon.icns", category: "public.app-category.developer-tools", }; } if (platform === "linux") { buildConfig.linux = { target: [target], icon: "icon.png", category: "Development", }; } if (platform === "win") { const winConfig: Record<string, unknown> = { target: [target], icon: "icon.ico", }; if (signed) { winConfig.azureSignOptions = yield* AzureTrustedSigningOptionsConfig; } buildConfig.win = winConfig; } return buildConfig; }); const assertPlatformBuildResources = Effect.fn(function* ( platform: typeof BuildPlatform.Type, stageResourcesDir: string, verbose: boolean, ) { const fs = yield* FileSystem.FileSystem; const path = yield* Path.Path; if (platform === "mac") { yield* stageMacIcons(stageResourcesDir, verbose); return; } if (platform === "linux") { const iconPath = path.join(stageResourcesDir, "icon.png"); if (!(yield* fs.exists(iconPath))) { return yield* new BuildScriptError({ message: `Missing Linux icon at ${iconPath}`, }); } return; } if (platform === "win") { const iconPath = path.join(stageResourcesDir, "icon.ico"); if (!(yield* fs.exists(iconPath))) { return yield* new BuildScriptError({ message: `Missing Windows icon at ${iconPath}`, }); } } }); const buildDesktopArtifact = Effect.fn(function* (options: ResolvedBuildOptions) { const repoRoot = yield* RepoRoot; const path = yield* Path.Path; const fs = yield* FileSystem.FileSystem; const platformConfig = PLATFORM_CONFIG[options.platform]; if (!platformConfig) { return yield* new BuildScriptError({ message: `Unsupported platform '${options.platform}'.`, }); } const electronVersion = desktopPackageJson.dependencies.electron; const dependencies = serverPackageJson.dependencies; if (!dependencies || Object.keys(dependencies).length === 0) { return yield* new BuildScriptError({ message: "Could not resolve production dependencies from apps/server/package.json.", }); } const resolvedDependencies = yield* Effect.try({ try: () => resolveCatalogDependencies(dependencies, rootPackageJson.workspaces.catalog, "apps/server"), catch: (cause) => new BuildScriptError({ message: "Could not resolve production dependencies from apps/server/package.json.", cause, }), }); const appVersion = options.version ?? serverPackageJson.version; const mkdir = options.keepStage ? fs.makeTempDirectory : fs.makeTempDirectoryScoped; const stageRoot = yield* mkdir({ prefix: `t3code-desktop-${options.platform}-stage-`, }); const stageAppDir = path.join(stageRoot, "app"); const stageResourcesDir = path.join(stageAppDir, "apps/desktop/resources"); const distDirs = { desktopDist: path.join(repoRoot, "apps/desktop/dist-electron"), desktopResources: path.join(repoRoot, "apps/desktop/resources"), serverDist: path.join(repoRoot, "apps/server/dist"), }; const bundledClientEntry = path.join(distDirs.serverDist, "client/index.html"); if (!options.skipBuild) { yield* Effect.log("[desktop-artifact] Building desktop/server/web artifacts..."); yield* runCommand( ChildProcess.make({ cwd: repoRoot, ...commandOutputOptions(options.verbose), })`bun run build:desktop`, ); } for (const [label, dir] of Object.entries(distDirs)) { if (!(yield* fs.exists(dir))) { return yield* new BuildScriptError({ message: `Missing ${label} at ${dir}. Run 'bun run build:desktop' first.`, }); } } if (!(yield* fs.exists(bundledClientEntry))) { return yield* new BuildScriptError({ message: `Missing bundled server client at ${bundledClientEntry}. Run 'bun run build:desktop' first.`, }); } yield* validateBundledClientAssets(path.dirname(bundledClientEntry)); yield* fs.makeDirectory(path.join(stageAppDir, "apps/desktop"), { recursive: true }); yield* fs.makeDirectory(path.join(stageAppDir, "apps/server"), { recursive: true }); yield* Effect.log("[desktop-artifact] Staging release app..."); yield* fs.copy(distDirs.desktopDist, path.join(stageAppDir, "apps/desktop/dist-electron")); yield* fs.copy(distDirs.desktopResources, stageResourcesDir); yield* fs.copy(distDirs.serverDist, path.join(stageAppDir, "apps/server/dist")); yield* assertPlatformBuildResources(options.platform, stageResourcesDir, options.verbose); const stagePackageJson: StagePackageJson = { name: "t3-code-desktop", version: appVersion, private: true, description: "T3 Code desktop build", author: "T3 Tools", main: "apps/desktop/dist-electron/main.js", build: yield* createBuildConfig( options.platform, options.target, desktopPackageJson.productName ?? "T3 Code", options.signed, ),