t3-code-android-nightly/scripts/export-brand-icons.ts
Julius Marminge 194c73f3f9
chore(deps): upgrade Effect to stable 4.0.1 (#16138)
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-05 13:15:14 -07:00

810 lines
25 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env node
import * as NodeRuntime from "@effect/platform-node/NodeRuntime";
import * as NodeServices from "@effect/platform-node/NodeServices";
import { HostProcessEnvironment } from "@t3tools/shared/hostProcess";
import * as Console from "effect/Console";
import * as Effect from "effect/Effect";
import * as FileSystem from "effect/FileSystem";
import * as Option from "effect/Option";
import * as Path from "effect/Path";
import * as Schema from "effect/Schema";
import * as Stream from "effect/Stream";
import { Command, Flag } from "effect/cli";
import { ChildProcess, ChildProcessSpawner } from "effect/process";
import { BRAND_ASSET_PATHS, DEVELOPMENT_PUBLIC_ICON_OVERRIDES } from "./lib/brand-assets.ts";
import { encodePngIco, readPngDimensions, WINDOWS_ICON_SIZES } from "./lib/icon-export.ts";
const DESIGN_GENERATION = 26;
const ICON_COMPOSER_EXECUTABLE_PARTS = [
"Contents",
"Applications",
"Icon Composer.app",
"Contents",
"Executables",
"ictool",
] as const;
const STANDALONE_ICON_COMPOSER_EXECUTABLE_PARTS = [
"Icon Composer.app",
"Contents",
"Executables",
"ictool",
] as const;
const NonNegativeInt = Schema.Int.check(Schema.isGreaterThanOrEqualTo(0));
const IconComposerVersion = Schema.Struct({
"bundle-version": Schema.NonEmptyString,
"short-bundle-version": Schema.NonEmptyString,
});
const decodeIconComposerVersion = Schema.decodeUnknownEffect(
Schema.fromJsonString(IconComposerVersion),
);
type IconPlatform = "iOS";
interface VariantOutputs {
readonly ios: string;
readonly macos: string;
readonly universal: string;
readonly appleTouch: string;
readonly favicon16: string;
readonly favicon32: string;
readonly faviconIco: string;
readonly windowsIco: string;
}
interface IconVariant {
readonly label: string;
readonly source: string;
readonly outputs: VariantOutputs;
}
interface IconComposerTool {
readonly path: string;
readonly version: string;
readonly bundleVersion: string;
readonly supportsDesignGeneration: boolean;
}
interface CommandResult {
readonly stdout: string;
readonly stderr: string;
readonly exitCode: number;
}
export class IconExportFileSystemError extends Schema.TaggedError<IconExportFileSystemError>()(
"IconExportFileSystemError",
{
operation: Schema.Literals([
"resolve-repository-root",
"check-path",
"read-directory",
"read-file",
"make-directory",
"make-temp-directory",
"make-temp-file",
"write-file",
"rename-file",
]),
path: Schema.String,
cause: Schema.Defect(),
},
) {
override get message(): string {
return `Icon export file-system operation '${this.operation}' failed for ${this.path}.`;
}
}
export class IconExportProcessError extends Schema.TaggedError<IconExportProcessError>()(
"IconExportProcessError",
{
operation: Schema.Literals(["spawn", "collect-stdout", "collect-stderr", "wait-for-exit"]),
command: Schema.String,
argumentCount: NonNegativeInt,
cause: Schema.Defect(),
},
) {
override get message(): string {
return `Icon export process operation '${this.operation}' failed for ${this.command}.`;
}
}
export class IconExportCommandFailedError extends Schema.TaggedError<IconExportCommandFailedError>()(
"IconExportCommandFailedError",
{
command: Schema.String,
argumentCount: NonNegativeInt,
exitCode: Schema.Int,
sourcePath: Schema.String,
size: Schema.Int,
stdout: Schema.optional(Schema.String),
stderr: Schema.optional(Schema.String),
},
) {
override get message(): string {
return `Icon Composer failed to export ${this.sourcePath} at ${this.size}x${this.size}.`;
}
}
export class IconExportToolResolutionError extends Schema.TaggedError<IconExportToolResolutionError>()(
"IconExportToolResolutionError",
{
reason: Schema.Literals(["configured-invalid", "configured-outdated", "not-found"]),
designGeneration: Schema.Int,
toolPath: Schema.optional(Schema.String),
version: Schema.optional(Schema.String),
},
) {
override get message(): string {
switch (this.reason) {
case "configured-invalid":
return `ICON_COMPOSER_TOOL does not point to Icon Composer's export-capable ictool: ${this.toolPath}`;
case "configured-outdated":
return `ICON_COMPOSER_TOOL points to Icon Composer ${this.version}, but version 2 or newer is required for design generation ${this.designGeneration}.`;
case "not-found":
return `Could not find an Icon Composer 2.x exporter compatible with design generation ${this.designGeneration}. Install a compatible Icon Composer/Xcode or set ICON_COMPOSER_TOOL to Icon Composer.app/Contents/Executables/ictool.`;
}
}
}
export class IconExportSourceMissingError extends Schema.TaggedError<IconExportSourceMissingError>()(
"IconExportSourceMissingError",
{
sourcePath: Schema.String,
},
) {
override get message(): string {
return `Missing Icon Composer source project: ${this.sourcePath}`;
}
}
export class IconExportRenditionError extends Schema.TaggedError<IconExportRenditionError>()(
"IconExportRenditionError",
{
sourcePath: Schema.String,
outputPath: Schema.String,
expectedSize: Schema.Int,
actualWidth: Schema.optional(Schema.Int),
actualHeight: Schema.optional(Schema.Int),
cause: Schema.optional(Schema.Defect()),
},
) {
override get message(): string {
const actual =
this.actualWidth === undefined || this.actualHeight === undefined
? "an invalid PNG"
: `${this.actualWidth}x${this.actualHeight}`;
return `Icon Composer produced ${actual}; expected ${this.expectedSize}x${this.expectedSize} for ${this.sourcePath}.`;
}
}
export class IconExportEncodingError extends Schema.TaggedError<IconExportEncodingError>()(
"IconExportEncodingError",
{
variant: Schema.String,
cause: Schema.Defect(),
},
) {
override get message(): string {
return `Failed to encode ICO renditions for the ${this.variant} icon.`;
}
}
export class IconExportAssetsStaleError extends Schema.TaggedError<IconExportAssetsStaleError>()(
"IconExportAssetsStaleError",
{
paths: Schema.Array(Schema.String),
},
) {
override get message(): string {
return `Generated icon assets are stale:\n${this.paths.map((path) => `- ${path}`).join("\n")}`;
}
}
const ICON_VARIANTS = [
{
label: "development",
source: BRAND_ASSET_PATHS.developmentIconComposerProject,
outputs: {
ios: BRAND_ASSET_PATHS.developmentIosIconPng,
macos: BRAND_ASSET_PATHS.developmentDesktopIconPng,
universal: BRAND_ASSET_PATHS.developmentUniversalIconPng,
appleTouch: BRAND_ASSET_PATHS.developmentWebAppleTouchIconPng,
favicon16: BRAND_ASSET_PATHS.developmentWebFavicon16Png,
favicon32: BRAND_ASSET_PATHS.developmentWebFavicon32Png,
faviconIco: BRAND_ASSET_PATHS.developmentWebFaviconIco,
windowsIco: BRAND_ASSET_PATHS.developmentWindowsIconIco,
},
},
{
label: "preview",
source: BRAND_ASSET_PATHS.nightlyIconComposerProject,
outputs: {
ios: BRAND_ASSET_PATHS.nightlyIosIconPng,
macos: BRAND_ASSET_PATHS.nightlyMacIconPng,
universal: BRAND_ASSET_PATHS.nightlyLinuxIconPng,
appleTouch: BRAND_ASSET_PATHS.nightlyWebAppleTouchIconPng,
favicon16: BRAND_ASSET_PATHS.nightlyWebFavicon16Png,
favicon32: BRAND_ASSET_PATHS.nightlyWebFavicon32Png,
faviconIco: BRAND_ASSET_PATHS.nightlyWebFaviconIco,
windowsIco: BRAND_ASSET_PATHS.nightlyWindowsIconIco,
},
},
{
label: "production",
source: BRAND_ASSET_PATHS.productionIconComposerProject,
outputs: {
ios: BRAND_ASSET_PATHS.productionIosIconPng,
macos: BRAND_ASSET_PATHS.productionMacIconPng,
universal: BRAND_ASSET_PATHS.productionLinuxIconPng,
appleTouch: BRAND_ASSET_PATHS.productionWebAppleTouchIconPng,
favicon16: BRAND_ASSET_PATHS.productionWebFavicon16Png,
favicon32: BRAND_ASSET_PATHS.productionWebFavicon32Png,
faviconIco: BRAND_ASSET_PATHS.productionWebFaviconIco,
windowsIco: BRAND_ASSET_PATHS.productionWindowsIconIco,
},
},
] as const satisfies ReadonlyArray<IconVariant>;
const MACOS_EXPORT_CODEX_PROMPT = [
"Use [@Computer](plugin://computer-use@openai-bundled) and the Icon Composer app to export the three macOS app icons in this repository.",
"For each project below, use Platform: macOS pre-Tahoe, Appearance: Default, Size: 1024pt, and Scale: 1×, then save the PNG to the exact destination:",
...ICON_VARIANTS.map((variant) => `- ${variant.source} -> ${variant.outputs.macos}`),
"Do not resize, composite, or otherwise post-process the exported PNGs.",
"Verify every result is 1024×1024 and has the classic macOS safe area: an 824×824 opaque body inset 100px on every side, with only Icon Composer's native shadow extending beyond it.",
];
const RepositoryRoot = Effect.service(Path.Path).pipe(
Effect.flatMap((path) => path.fromFileUrl(new URL("..", import.meta.url))),
Effect.mapError(
(cause) =>
new IconExportFileSystemError({
operation: "resolve-repository-root",
path: new URL("..", import.meta.url).pathname,
cause,
}),
),
);
const collectStreamAsString = <E>(stream: Stream.Stream<Uint8Array, E>) =>
stream.pipe(
Stream.decodeText(),
Stream.runFold(
() => "",
(acc, chunk) => acc + chunk,
),
);
const runCommand = Effect.fn("iconExport.runCommand")(function* (
command: string,
args: ReadonlyArray<string>,
) {
const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
const child = yield* spawner.spawn(ChildProcess.make(command, args)).pipe(
Effect.mapError(
(cause) =>
new IconExportProcessError({
operation: "spawn",
command,
argumentCount: args.length,
cause,
}),
),
);
const [stdout, stderr, exitCode] = yield* Effect.all(
[
collectStreamAsString(child.stdout).pipe(
Effect.mapError(
(cause) =>
new IconExportProcessError({
operation: "collect-stdout",
command,
argumentCount: args.length,
cause,
}),
),
),
collectStreamAsString(child.stderr).pipe(
Effect.mapError(
(cause) =>
new IconExportProcessError({
operation: "collect-stderr",
command,
argumentCount: args.length,
cause,
}),
),
),
child.exitCode.pipe(
Effect.map(Number),
Effect.mapError(
(cause) =>
new IconExportProcessError({
operation: "wait-for-exit",
command,
argumentCount: args.length,
cause,
}),
),
),
],
{ concurrency: "unbounded" },
);
return { stdout, stderr, exitCode } satisfies CommandResult;
});
const iconComposerToolFromDeveloperDirectory = (developerDirectory: string, path: Path.Path) =>
path.resolve(developerDirectory, "..", ...ICON_COMPOSER_EXECUTABLE_PARTS.slice(1));
const readSelectedDeveloperDirectory = Effect.fn("iconExport.readSelectedDeveloperDirectory")(
function* () {
const result = yield* runCommand("xcode-select", ["-p"]).pipe(Effect.option);
return Option.flatMap(result, (output) => {
const developerDirectory = output.stdout.trim();
return output.exitCode === 0 && developerDirectory.length > 0
? Option.some(developerDirectory)
: Option.none();
});
},
);
const findXcodeAppCandidates = Effect.fn("iconExport.findXcodeAppCandidates")(function* (
directory: string,
) {
const fs = yield* FileSystem.FileSystem;
const path = yield* Path.Path;
const entries = yield* fs.readDirectory(directory).pipe(
Effect.mapError(
(cause) =>
new IconExportFileSystemError({
operation: "read-directory",
path: directory,
cause,
}),
),
Effect.orElseSucceed(() => []),
);
return entries
.filter((entry) => /^Xcode.*\.app$/.test(entry))
.map((entry) => path.join(directory, entry, ...ICON_COMPOSER_EXECUTABLE_PARTS));
});
const probeIconComposerTool = Effect.fn("iconExport.probeIconComposerTool")(function* (
candidate: string,
) {
const fs = yield* FileSystem.FileSystem;
const exists = yield* fs.exists(candidate).pipe(
Effect.mapError(
(cause) =>
new IconExportFileSystemError({
operation: "check-path",
path: candidate,
cause,
}),
),
Effect.orElseSucceed(() => false),
);
if (!exists) return Option.none<IconComposerTool>();
const result = yield* runCommand(candidate, ["--version"]).pipe(Effect.option);
if (Option.isNone(result) || result.value.exitCode !== 0) {
return Option.none<IconComposerTool>();
}
const version = yield* decodeIconComposerVersion(result.value.stdout).pipe(Effect.option);
if (Option.isNone(version)) return Option.none<IconComposerTool>();
const bundleVersion = version.value["bundle-version"];
const shortVersion = version.value["short-bundle-version"];
return Option.some({
path: candidate,
version: `${shortVersion} (${bundleVersion})`,
bundleVersion,
supportsDesignGeneration: Number.parseInt(shortVersion, 10) >= 2,
});
});
const resolveIconComposerTool = Effect.fn("iconExport.resolveIconComposerTool")(function* () {
const path = yield* Path.Path;
const environment = yield* HostProcessEnvironment;
const configuredTool = environment.ICON_COMPOSER_TOOL?.trim();
if (configuredTool) {
const tool = yield* probeIconComposerTool(configuredTool);
if (Option.isNone(tool)) {
return yield* new IconExportToolResolutionError({
reason: "configured-invalid",
designGeneration: DESIGN_GENERATION,
toolPath: configuredTool,
});
}
if (!tool.value.supportsDesignGeneration) {
return yield* new IconExportToolResolutionError({
reason: "configured-outdated",
designGeneration: DESIGN_GENERATION,
toolPath: configuredTool,
version: tool.value.version,
});
}
return tool.value;
}
const selectedDeveloperDirectory = yield* readSelectedDeveloperDirectory();
const configuredDeveloperDirectory = environment.DEVELOPER_DIR?.trim();
const homeDirectory = environment.HOME?.trim();
const searchDirectories = [
"/Applications",
...(homeDirectory ? [path.join(homeDirectory, "Downloads")] : []),
];
const xcodeCandidates = yield* Effect.forEach(searchDirectories, findXcodeAppCandidates, {
concurrency: "unbounded",
});
const candidates = new Set<string>([
...(configuredDeveloperDirectory
? [iconComposerToolFromDeveloperDirectory(configuredDeveloperDirectory, path)]
: []),
...Option.match(selectedDeveloperDirectory, {
onNone: () => [],
onSome: (developerDirectory) => [
iconComposerToolFromDeveloperDirectory(developerDirectory, path),
],
}),
path.join("/Applications", ...STANDALONE_ICON_COMPOSER_EXECUTABLE_PARTS),
...(homeDirectory
? [path.join(homeDirectory, "Applications", ...STANDALONE_ICON_COMPOSER_EXECUTABLE_PARTS)]
: []),
...xcodeCandidates.flat(),
]);
const probed = yield* Effect.forEach([...candidates], probeIconComposerTool, {
concurrency: "unbounded",
});
const compatibleTools = probed
.filter(Option.isSome)
.map((tool) => tool.value)
.filter((tool) => tool.supportsDesignGeneration)
.sort((left, right) =>
right.bundleVersion.localeCompare(left.bundleVersion, undefined, { numeric: true }),
);
const newestTool = compatibleTools[0];
if (newestTool) return newestTool;
return yield* new IconExportToolResolutionError({
reason: "not-found",
designGeneration: DESIGN_GENERATION,
});
});
const renderIcon = Effect.fn("iconExport.renderIcon")(function* (
toolPath: string,
sourcePath: string,
outputPath: string,
platform: IconPlatform,
size: number,
) {
const fs = yield* FileSystem.FileSystem;
const args = [
sourcePath,
"--export-image",
"--output-file",
outputPath,
"--platform",
platform,
"--rendition",
"Default",
"--width",
String(size),
"--height",
String(size),
"--scale",
"1",
"--design-generation",
String(DESIGN_GENERATION),
];
const result = yield* runCommand(toolPath, args);
if (result.exitCode !== 0) {
return yield* new IconExportCommandFailedError({
command: toolPath,
argumentCount: args.length,
exitCode: result.exitCode,
sourcePath,
size,
...(result.stdout.trim() ? { stdout: result.stdout.trim() } : {}),
...(result.stderr.trim() ? { stderr: result.stderr.trim() } : {}),
});
}
const contents = yield* fs.readFile(outputPath).pipe(
Effect.mapError(
(cause) =>
new IconExportFileSystemError({
operation: "read-file",
path: outputPath,
cause,
}),
),
);
const buffer = Buffer.from(contents);
const dimensions = yield* Effect.try({
try: () => readPngDimensions(buffer),
catch: (cause) =>
new IconExportRenditionError({
sourcePath,
outputPath,
expectedSize: size,
cause,
}),
});
if (dimensions.width !== size || dimensions.height !== size) {
return yield* new IconExportRenditionError({
sourcePath,
outputPath,
expectedSize: size,
actualWidth: dimensions.width,
actualHeight: dimensions.height,
});
}
return buffer;
});
const renderVariant = Effect.fn("iconExport.renderVariant")(function* (
toolPath: string,
repositoryRoot: string,
temporaryDirectory: string,
variant: IconVariant,
) {
const fs = yield* FileSystem.FileSystem;
const path = yield* Path.Path;
const sourcePath = path.join(repositoryRoot, variant.source);
const sourceExists = yield* fs.exists(sourcePath).pipe(
Effect.mapError(
(cause) =>
new IconExportFileSystemError({
operation: "check-path",
path: sourcePath,
cause,
}),
),
);
if (!sourceExists) {
return yield* new IconExportSourceMissingError({ sourcePath: variant.source });
}
const renditionCache = new Map<string, Buffer>();
const render = Effect.fn("iconExport.renderVariant.rendition")(function* (
platform: IconPlatform,
size: number,
) {
const cacheKey = `${platform}-${size}`;
const cached = renditionCache.get(cacheKey);
if (cached) return cached;
const outputPath = path.join(temporaryDirectory, `${variant.label}-${platform}-${size}.png`);
const contents = yield* renderIcon(toolPath, sourcePath, outputPath, platform, size);
renditionCache.set(cacheKey, contents);
return contents;
});
const ios = yield* render("iOS", 1024);
const icoRenditions = yield* Effect.forEach(
WINDOWS_ICON_SIZES,
(size) => render("iOS", size).pipe(Effect.map((contents) => ({ size, contents }))),
{ concurrency: 1 },
);
const ico = yield* Effect.try({
try: () => encodePngIco(icoRenditions),
catch: (cause) => new IconExportEncodingError({ variant: variant.label, cause }),
});
return new Map<string, Buffer>([
[variant.outputs.ios, ios],
[variant.outputs.universal, ios],
[variant.outputs.appleTouch, yield* render("iOS", 180)],
[variant.outputs.favicon16, yield* render("iOS", 16)],
[variant.outputs.favicon32, yield* render("iOS", 32)],
[variant.outputs.faviconIco, ico],
[variant.outputs.windowsIco, ico],
]);
});
const logManualMacOsExportInstructions = Effect.fn("iconExport.logManualMacOsExportInstructions")(
function* () {
yield* Console.warn(
[
"macOS icons require Icon Composer's GUI-only pre-Tahoe preset and were not changed.",
"Export each source with Platform: macOS pre-Tahoe, Appearance: Default, Size: 1024pt, Scale: 1×:",
...ICON_VARIANTS.map((variant) => `- ${variant.source} -> ${variant.outputs.macos}`),
"See assets/README.md for the complete workflow.",
"",
"Copy/paste this prompt into Codex to perform the native exports:",
"---",
...MACOS_EXPORT_CODEX_PROMPT,
"---",
].join("\n"),
);
},
);
const writeAtomically = Effect.fn("iconExport.writeAtomically")(function* (
repositoryRoot: string,
relativePath: string,
contents: Buffer,
) {
const fs = yield* FileSystem.FileSystem;
const path = yield* Path.Path;
const targetPath = path.join(repositoryRoot, relativePath);
const targetDirectory = path.dirname(targetPath);
yield* fs.makeDirectory(targetDirectory, { recursive: true }).pipe(
Effect.mapError(
(cause) =>
new IconExportFileSystemError({
operation: "make-directory",
path: targetDirectory,
cause,
}),
),
);
const temporaryPath = yield* fs
.makeTempFileScoped({
directory: targetDirectory,
prefix: ".t3-icon-export-",
suffix: ".tmp",
})
.pipe(
Effect.mapError(
(cause) =>
new IconExportFileSystemError({
operation: "make-temp-file",
path: targetDirectory,
cause,
}),
),
);
yield* fs.writeFile(temporaryPath, contents).pipe(
Effect.mapError(
(cause) =>
new IconExportFileSystemError({
operation: "write-file",
path: temporaryPath,
cause,
}),
),
);
yield* fs.rename(temporaryPath, targetPath).pipe(
Effect.mapError(
(cause) =>
new IconExportFileSystemError({
operation: "rename-file",
path: targetPath,
cause,
}),
),
);
});
const isCurrent = Effect.fn("iconExport.isCurrent")(function* (
repositoryRoot: string,
relativePath: string,
expected: Buffer,
) {
const fs = yield* FileSystem.FileSystem;
const path = yield* Path.Path;
const targetPath = path.join(repositoryRoot, relativePath);
const exists = yield* fs.exists(targetPath).pipe(
Effect.mapError(
(cause) =>
new IconExportFileSystemError({
operation: "check-path",
path: targetPath,
cause,
}),
),
);
if (!exists) return false;
const actual = yield* fs.readFile(targetPath).pipe(
Effect.mapError(
(cause) =>
new IconExportFileSystemError({
operation: "read-file",
path: targetPath,
cause,
}),
),
);
return Buffer.from(actual).equals(expected);
});
export const exportBrandIcons = Effect.fn("exportBrandIcons")(function* (checkOnly: boolean) {
const fs = yield* FileSystem.FileSystem;
const repositoryRoot = yield* RepositoryRoot;
const tool = yield* resolveIconComposerTool();
const temporaryDirectory = yield* fs
.makeTempDirectoryScoped({
prefix: "t3-icon-export-",
})
.pipe(
Effect.mapError(
(cause) =>
new IconExportFileSystemError({
operation: "make-temp-directory",
path: "system temporary directory",
cause,
}),
),
);
yield* Console.log(
`Exporting icons with Icon Composer ${tool.version}, design generation ${DESIGN_GENERATION}.`,
);
const generated = new Map<string, Buffer>();
for (const variant of ICON_VARIANTS) {
yield* Console.log(`Rendering ${variant.label} from ${variant.source}...`);
const variantAssets = yield* renderVariant(
tool.path,
repositoryRoot,
temporaryDirectory,
variant,
);
for (const [relativePath, contents] of variantAssets) {
generated.set(relativePath, contents);
}
}
for (const override of DEVELOPMENT_PUBLIC_ICON_OVERRIDES) {
const sourceContents = generated.get(override.sourceRelativePath);
if (sourceContents === undefined) {
return yield* Effect.die(
new Error(`Generated development web icon is missing: ${override.sourceRelativePath}`),
);
}
generated.set(override.targetRelativePath, sourceContents);
}
if (checkOnly) {
const stale = yield* Effect.filter(
[...generated.entries()],
([relativePath, contents]) =>
isCurrent(repositoryRoot, relativePath, contents).pipe(Effect.map((current) => !current)),
{ concurrency: "unbounded" },
);
if (stale.length > 0) {
return yield* new IconExportAssetsStaleError({
paths: stale.map(([relativePath]) => relativePath),
});
}
yield* Console.log(`All ${generated.size} generated icon assets are current.`);
yield* logManualMacOsExportInstructions();
return;
}
yield* Effect.forEach(
generated,
([relativePath, contents]) => writeAtomically(repositoryRoot, relativePath, contents),
{ concurrency: 1, discard: true },
);
yield* Console.log(`Updated ${generated.size} generated icon assets.`);
yield* logManualMacOsExportInstructions();
});
export const exportBrandIconsCommand = Command.make(
"export-brand-icons",
{
check: Flag.Boolean("check").pipe(
Flag.withDescription("Verify generated icon assets without modifying files."),
Flag.withDefault(false),
),
},
({ check }) => exportBrandIcons(check).pipe(Effect.scoped),
).pipe(
Command.withDescription(
"Export development, preview, and production assets from Icon Composer projects.",
),
);
if (import.meta.main) {
Command.run(exportBrandIconsCommand, { version: "0.0.0" }).pipe(
Effect.provide(NodeServices.layer),
NodeRuntime.runMain,
);
}