810 lines · 25.1 KB · TypeScript
RawDownload
#!/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/unstable/cli";import { ChildProcess, ChildProcessSpawner } from "effect/unstable/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.TaggedErrorClass<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.TaggedErrorClass<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.TaggedErrorClass<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.TaggedErrorClass<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.TaggedErrorClass<IconExportSourceMissingError>()(  "IconExportSourceMissingError",  {    sourcePath: Schema.String,  },) {  override get message(): string {    return `Missing Icon Composer source project: ${this.sourcePath}`;  }} export class IconExportRenditionError extends Schema.TaggedErrorClass<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.TaggedErrorClass<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.TaggedErrorClass<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",

Lines 1–500 of 810.