History

Stage production icons for desktop builds and publish (#168)

* Stage production icons for desktop builds and publish - add shared brand asset paths and a Windows .ico asset - stage Linux/Windows icons in desktop artifact builds - override and restore publish favicon during server CLI publish * 'blip' * Fix PR review issues for publish icon staging * Refactor brand assets and harden Windows desktop build setup * Apply remaining server orchestration updates * Fix production mac icon asset Co-authored-by: Theo Browne <t3dotgg@users.noreply.github.com> ---------

Julius Marminge committed 8 months ago

Commit d249e621 parent 40fcb6b42 files · +237 −60Browse files

Unified · Split

42 files
  1. apps/server/integration/OrchestrationEngineHarness.integration.ts +5 −10
  2. apps/server/scripts/cli.ts +95 −6
  3. apps/server/src/orchestration/Layers/CheckpointReactor.test.ts +0 −1
  4. apps/server/src/orchestration/Layers/ProviderCommandReactor.test.ts +0 −1
  5. apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.test.ts +0 −1
  6. apps/server/src/provider/Layers/ProviderService.test.ts +3 −16
  7. apps/server/src/provider/Layers/ProviderService.ts +7 −2
  8. apps/server/src/provider/Services/ProviderService.ts +0 −10
  9. apps/server/src/serverLogger.ts +3 −6
  10. apps/server/src/wsServer.test.ts +0 −1
  11. apps/server/src/wsServer.ts +0 −6
  12. apps/web/public/apple-touch-icon.png +0 −0
  13. apps/web/public/favicon-16x16.png +0 −0
  14. apps/web/public/favicon-32x32.png +0 −0
  15. apps/web/public/favicon.ico +0 −0
  16. apps/web/public/favicon.svg +0 −4
  17. assets/T3 Code blueprint-macOS-Default-1024x1024@1x.png +0 −0
  18. assets/T3 Code logo.svg +0 −4
  19. assets/T3 Code logo@16x.png +0 −0
  20. assets/T3 Code logo@32x.png +0 −0
  21. assets/T3 Code-macOS-Default-1024x1024@4x.png +0 −0
  22. assets/dev/blueprint-ios-1024.png +0 −0
  23. assets/dev/blueprint-macos-1024.png +0 −0
  24. assets/dev/blueprint-universal-1024.png +0 −0
  25. assets/dev/blueprint-web-apple-touch-180.png +0 −0
  26. assets/dev/blueprint-web-favicon-16x16.png +0 −0
  27. assets/dev/blueprint-web-favicon-32x32.png +0 −0
  28. assets/dev/blueprint-web-favicon.ico +0 −0
  29. assets/dev/blueprint-windows.ico +0 −0
  30. assets/logo-1024.png +0 −0
  31. assets/macos-icon-1024.png +0 −0
  32. assets/prod/black-ios-1024.png +0 −0
  33. assets/prod/black-macos-1024.png +0 −0
  34. assets/prod/black-universal-1024.png +0 −0
  35. assets/prod/logo.svg +0 −0
  36. assets/prod/t3-black-web-apple-touch-180.png +0 −0
  37. assets/prod/t3-black-web-favicon-16x16.png +0 −0
  38. assets/prod/t3-black-web-favicon-32x32.png +0 −0
  39. assets/prod/t3-black-web-favicon.ico +0 −0
  40. assets/prod/t3-black-windows.ico +0 −0
  41. scripts/build-desktop-artifact.ts +93 −18
  42. scripts/lib/brand-assets.ts +57 −0
apps/server/integration/OrchestrationEngineHarness.integration.ts+5−10
@@ -406,19 +406,14 @@ export const makeOrchestrationIntegrationHarness = ( · 405 unmodified lines
406 disposed = true;406 disposed = true;
407 407
408 const shutdown = Effect.gen(function* () {408 const shutdown = Effect.gen(function* () {
409 const stopAllExit = yield* Effect.exit(
410 Effect.promise(() => runtime.runPromise(providerService.stopAll())),
411 );
412 const closeScopeExit = yield* Effect.exit(Scope.close(scope, Exit.void));409 const closeScopeExit = yield* Effect.exit(Scope.close(scope, Exit.void));
413 const disposeRuntimeExit = yield* Effect.exit(Effect.promise(() => runtime.dispose()));410 const disposeRuntimeExit = yield* Effect.exit(Effect.promise(() => runtime.dispose()));
414 411
415 const failureCause = Exit.isFailure(stopAllExit)412 const failureCause = Exit.isFailure(closeScopeExit)
416 ? stopAllExit.cause413 ? closeScopeExit.cause
417 : Exit.isFailure(closeScopeExit)414 : Exit.isFailure(disposeRuntimeExit)
418 ? closeScopeExit.cause415 ? disposeRuntimeExit.cause
419 : Exit.isFailure(disposeRuntimeExit)416 : null;
420 ? disposeRuntimeExit.cause
421 : null;
422 417
423 if (failureCause) {418 if (failureCause) {
424 return yield* Effect.failCause(failureCause);419 return yield* Effect.failCause(failureCause);
apps/server/scripts/cli.ts+95−6
@@ -6,6 +6,7 @@ import { Data, Effect, FileSystem, Logger, Option, Path } from "effect"; · 5 unmodified lines
6import { Command, Flag } from "effect/unstable/cli";6import { Command, Flag } from "effect/unstable/cli";
7import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process";7import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process";
8 8
9import { DEVELOPMENT_ICON_OVERRIDES, PUBLISH_ICON_OVERRIDES } from "../../../scripts/lib/brand-assets.ts";
9import { resolveCatalogDependencies } from "../../../scripts/lib/resolve-catalog.ts";10import { resolveCatalogDependencies } from "../../../scripts/lib/resolve-catalog.ts";
10import rootPackageJson from "../../../package.json" with { type: "json" };11import rootPackageJson from "../../../package.json" with { type: "json" };
11import serverPackageJson from "../package.json" with { type: "json" };12import serverPackageJson from "../package.json" with { type: "json" };
@@ -31,6 +32,84 @@ const runCommand = Effect.fn("runCommand")(function* (command: ChildProcess.Comm · 19 unmodified lines
31 }32 }
32});33});
33 34
35interface PublishIconBackup {
36 readonly targetPath: string;
37 readonly backupPath: string;
38}
39
40const applyPublishIconOverrides = Effect.fn("applyPublishIconOverrides")(function* (
41 repoRoot: string,
42 serverDir: string,
43) {
44 const path = yield* Path.Path;
45 const fs = yield* FileSystem.FileSystem;
46 const backups: PublishIconBackup[] = [];
47
48 for (const override of PUBLISH_ICON_OVERRIDES) {
49 const sourcePath = path.join(repoRoot, override.sourceRelativePath);
50 const targetPath = path.join(serverDir, override.targetRelativePath);
51 const backupPath = `${targetPath}.publish-bak`;
52
53 if (!(yield* fs.exists(sourcePath))) {
54 return yield* new CliError({
55 message: `Missing publish icon source: ${sourcePath}`,
56 });
57 }
58 if (!(yield* fs.exists(targetPath))) {
59 return yield* new CliError({
60 message: `Missing publish icon target: ${targetPath}. Run the build subcommand first.`,
61 });
62 }
63
64 yield* fs.copyFile(targetPath, backupPath);
65 yield* fs.copyFile(sourcePath, targetPath);
66 backups.push({ targetPath, backupPath });
67 }
68
69 yield* Effect.log("[cli] Applied publish icon overrides to dist/client");
70 return backups as ReadonlyArray<PublishIconBackup>;
71});
72
73const restorePublishIconOverrides = Effect.fn("restorePublishIconOverrides")(function* (
74 backups: ReadonlyArray<PublishIconBackup>,
75) {
76 const fs = yield* FileSystem.FileSystem;
77 for (const backup of backups) {
78 if (!(yield* fs.exists(backup.backupPath))) {
79 continue;
80 }
81 yield* fs.rename(backup.backupPath, backup.targetPath);
82 }
83});
84
85const applyDevelopmentIconOverrides = Effect.fn("applyDevelopmentIconOverrides")(function* (
86 repoRoot: string,
87 serverDir: string,
88) {
89 const path = yield* Path.Path;
90 const fs = yield* FileSystem.FileSystem;
91
92 for (const override of DEVELOPMENT_ICON_OVERRIDES) {
93 const sourcePath = path.join(repoRoot, override.sourceRelativePath);
94 const targetPath = path.join(serverDir, override.targetRelativePath);
95
96 if (!(yield* fs.exists(sourcePath))) {
97 return yield* new CliError({
98 message: `Missing development icon source: ${sourcePath}`,
99 });
100 }
101 if (!(yield* fs.exists(targetPath))) {
102 return yield* new CliError({
103 message: `Missing development icon target: ${targetPath}. Build web first.`,
104 });
105 }
106
107 yield* fs.copyFile(sourcePath, targetPath);
108 }
109
110 yield* Effect.log("[cli] Applied development icon overrides to dist/client");
111});
112
34// ---------------------------------------------------------------------------113// ---------------------------------------------------------------------------
35// build subcommand114// build subcommand
36// ---------------------------------------------------------------------------115// ---------------------------------------------------------------------------
@@ -61,6 +140,7 @@ const buildCmd = Command.make( · 24 unmodified lines
61 140
62 if (yield* fs.exists(webDist)) {141 if (yield* fs.exists(webDist)) {
63 yield* fs.copy(webDist, clientTarget);142 yield* fs.copy(webDist, clientTarget);
143 yield* applyDevelopmentIconOverrides(repoRoot, serverDir);
64 yield* Effect.log("[cli] Bundled web app into dist/client");144 yield* Effect.log("[cli] Bundled web app into dist/client");
65 } else {145 } else {
66 yield* Effect.logWarning("[cli] Web dist not found — skipping client bundle.");146 yield* Effect.logWarning("[cli] Web dist not found — skipping client bundle.");
@@ -104,10 +184,8 @@ const publishCmd = Command.make( · 37 unmodified lines
104 yield* Effect.acquireUseRelease(184 yield* Effect.acquireUseRelease(
105 // Acquire: backup package.json, resolve catalog: deps, strip devDependencies/scripts185 // Acquire: backup package.json, resolve catalog: deps, strip devDependencies/scripts
106 Effect.gen(function* () {186 Effect.gen(function* () {
107 const original = yield* fs.readFileString(packageJsonPath);187 // Resolve catalog dependencies before any file mutations. If this throws,
108 yield* fs.writeFileString(backupPath, original);188 // acquire fails and no release hook runs, so filesystem must still be untouched.
109
110 // Build package.json for publish
111 const version = Option.getOrElse(config.appVersion, () => serverPackageJson.version);189 const version = Option.getOrElse(config.appVersion, () => serverPackageJson.version);
112 const pkg = {190 const pkg = {
113 name: serverPackageJson.name,191 name: serverPackageJson.name,
@@ -120,15 +198,19 @@ const publishCmd = Command.make( · 6 unmodified lines
120 dependencies: serverPackageJson.dependencies as Record<string, unknown>,198 dependencies: serverPackageJson.dependencies as Record<string, unknown>,
121 };199 };
122 200
123 // Resolve catalog: entries in production dependencies
124 pkg.dependencies = resolveCatalogDependencies(201 pkg.dependencies = resolveCatalogDependencies(
125 pkg.dependencies,202 pkg.dependencies,
126 rootPackageJson.workspaces.catalog,203 rootPackageJson.workspaces.catalog,
127 "apps/server dependencies",204 "apps/server dependencies",
128 );205 );
129 206
207 const original = yield* fs.readFileString(packageJsonPath);
208 yield* fs.writeFileString(backupPath, original);
130 yield* fs.writeFileString(packageJsonPath, `${JSON.stringify(pkg, null, 2)}\n`);209 yield* fs.writeFileString(packageJsonPath, `${JSON.stringify(pkg, null, 2)}\n`);
131 yield* Effect.log("[cli] Resolved package.json for publish");210 yield* Effect.log("[cli] Resolved package.json for publish");
211
212 const iconBackups = yield* applyPublishIconOverrides(repoRoot, serverDir);
213 return { iconBackups };
132 }),214 }),
133 // Use: npm publish215 // Use: npm publish
134 () =>216 () =>
@@ -147,8 +229,15 @@ const publishCmd = Command.make( · 12 unmodified lines
147 );229 );
148 }),230 }),
149 // Release: restore231 // Release: restore
150 () =>232 (resource: { readonly iconBackups: ReadonlyArray<PublishIconBackup> }) =>
151 Effect.gen(function* () {233 Effect.gen(function* () {
234 yield* restorePublishIconOverrides(resource.iconBackups).pipe(
235 Effect.catch((error) =>
236 Effect.logError(
237 `[cli] Failed to restore publish icon overrides: ${String(error)}`,
238 ),
239 ),
240 );
152 yield* fs.rename(backupPath, packageJsonPath);241 yield* fs.rename(backupPath, packageJsonPath);
153 if (config.verbose) yield* Effect.log("[cli] Restored original package.json");242 if (config.verbose) yield* Effect.log("[cli] Restored original package.json");
154 }),243 }),
apps/server/src/orchestration/Layers/CheckpointReactor.test.ts+0−1
@@ -91,7 +91,6 @@ function createProviderServiceHarness( · 90 unmodified lines
91 listSessions,91 listSessions,
92 getCapabilities: () => Effect.succeed({ sessionModelSwitch: "in-session" }),92 getCapabilities: () => Effect.succeed({ sessionModelSwitch: "in-session" }),
93 rollbackConversation,93 rollbackConversation,
94 stopAll: () => Effect.void,
95 streamEvents: Stream.fromPubSub(runtimeEventPubSub),94 streamEvents: Stream.fromPubSub(runtimeEventPubSub),
96 };95 };
97 96
apps/server/src/orchestration/Layers/ProviderCommandReactor.test.ts+0−1
@@ -188,7 +188,6 @@ describe("ProviderCommandReactor", () => { · 187 unmodified lines
188 sessionModelSwitch: provider === "codex" ? "in-session" : "in-session",188 sessionModelSwitch: provider === "codex" ? "in-session" : "in-session",
189 }),189 }),
190 rollbackConversation: () => unsupported(),190 rollbackConversation: () => unsupported(),
191 stopAll: () => Effect.void,
192 streamEvents: Stream.fromPubSub(runtimeEventPubSub),191 streamEvents: Stream.fromPubSub(runtimeEventPubSub),
193 };192 };
194 193
apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.test.ts+0−1
@@ -65,7 +65,6 @@ function createProviderServiceHarness() { · 64 unmodified lines
65 listSessions: () => Effect.succeed([]),65 listSessions: () => Effect.succeed([]),
66 getCapabilities: () => Effect.succeed({ sessionModelSwitch: "in-session" }),66 getCapabilities: () => Effect.succeed({ sessionModelSwitch: "in-session" }),
67 rollbackConversation: () => unsupported(),67 rollbackConversation: () => unsupported(),
68 stopAll: () => Effect.void,
69 streamEvents: Stream.fromPubSub(runtimeEventPubSub),68 streamEvents: Stream.fromPubSub(runtimeEventPubSub),
70 };69 };
71 70
apps/server/src/provider/Layers/ProviderService.test.ts+3−16
@@ -356,11 +356,6 @@ it.effect( · 355 unmodified lines
356 });356 });
357 }).pipe(Effect.provide(firstProviderLayer));357 }).pipe(Effect.provide(firstProviderLayer));
358 358
359 yield* Effect.gen(function* () {
360 const provider = yield* ProviderService;
361 yield* provider.stopAll();
362 }).pipe(Effect.provide(firstProviderLayer));
363
364 const persistedAfterStopAll = yield* Effect.gen(function* () {359 const persistedAfterStopAll = yield* Effect.gen(function* () {
365 const repository = yield* ProviderSessionRuntimeRepository;360 const repository = yield* ProviderSessionRuntimeRepository;
366 return yield* repository.getByThreadId({ threadId: startedSession.threadId });361 return yield* repository.getByThreadId({ threadId: startedSession.threadId });
@@ -549,7 +544,7 @@ routing.layer("ProviderServiceLive routing", (it) => { · 182 unmodified lines
549 runtimeMode: "full-access",544 runtimeMode: "full-access",
550 });545 });
551 546
552 yield* provider.stopAll();547 yield* routing.codex.stopAll();
553 routing.codex.startSession.mockClear();548 routing.codex.startSession.mockClear();
554 routing.codex.sendTurn.mockClear();549 routing.codex.sendTurn.mockClear();
555 550
@@ -578,7 +573,7 @@ routing.layer("ProviderServiceLive routing", (it) => { · 22 unmodified lines
578 }),573 }),
579 );574 );
580 575
581 it.effect("stops all sessions and clears adapter runtime", () =>576 it.effect("lists no sessions after adapter runtime clears", () =>
582 Effect.gen(function* () {577 Effect.gen(function* () {
583 const provider = yield* ProviderService;578 const provider = yield* ProviderService;
584 579
@@ -593,7 +588,7 @@ routing.layer("ProviderServiceLive routing", (it) => { · 8 unmodified lines
593 runtimeMode: "full-access",588 runtimeMode: "full-access",
594 });589 });
595 590
596 yield* provider.stopAll();591 yield* routing.codex.stopAll();
597 592
598 const remaining = yield* provider.listSessions();593 const remaining = yield* provider.listSessions();
599 assert.equal(remaining.length, 0);594 assert.equal(remaining.length, 0);
@@ -641,14 +636,6 @@ routing.layer("ProviderServiceLive routing", (it) => { · 41 unmodified lines
641 }636 }
642 }637 }
643 638
644 yield* provider.stopAll();
645 const stoppedRuntime = yield* runtimeRepository.getByThreadId({
646 threadId: session.threadId,
647 });
648 assert.equal(Option.isSome(stoppedRuntime), true);
649 if (Option.isSome(stoppedRuntime)) {
650 assert.equal(stoppedRuntime.value.status, "stopped");
651 }
652 }),639 }),
653 );640 );
654});641});
apps/server/src/provider/Layers/ProviderService.ts+7−2
@@ -471,7 +471,7 @@ const makeProviderService = (options?: ProviderServiceLiveOptions) => · 470 unmodified lines
471 });471 });
472 });472 });
473 473
474 const stopAll: ProviderServiceShape["stopAll"] = () =>474 const runStopAll = () =>
475 Effect.gen(function* () {475 Effect.gen(function* () {
476 const threadIds = yield* directory.listThreadIds();476 const threadIds = yield* directory.listThreadIds();
477 yield* Effect.forEach(adapters, (adapter) => adapter.stopAll()).pipe(Effect.asVoid);477 yield* Effect.forEach(adapters, (adapter) => adapter.stopAll()).pipe(Effect.asVoid);
@@ -497,6 +497,12 @@ const makeProviderService = (options?: ProviderServiceLiveOptions) => · 19 unmodified lines
497 yield* analytics.flush;497 yield* analytics.flush;
498 });498 });
499 499
500 yield* Effect.addFinalizer(() =>
501 Effect.catch(runStopAll(), (cause) =>
502 Effect.logWarning("failed to stop provider service", { cause }),
503 ),
504 );
505
500 return {506 return {
501 startSession,507 startSession,
502 sendTurn,508 sendTurn,
@@ -507,7 +513,6 @@ const makeProviderService = (options?: ProviderServiceLiveOptions) => · 4 unmodified lines
507 listSessions,513 listSessions,
508 getCapabilities,514 getCapabilities,
509 rollbackConversation,515 rollbackConversation,
510 stopAll,
511 streamEvents: Stream.fromPubSub(runtimeEventPubSub),516 streamEvents: Stream.fromPubSub(runtimeEventPubSub),
512 } satisfies ProviderServiceShape;517 } satisfies ProviderServiceShape;
513 });518 });
apps/server/src/provider/Services/ProviderService.ts+0−10
@@ -99,16 +99,6 @@ export interface ProviderServiceShape { · 98 unmodified lines
99 readonly numTurns: number;99 readonly numTurns: number;
100 }) => Effect.Effect<void, ProviderServiceError>;100 }) => Effect.Effect<void, ProviderServiceError>;
101 101
102 /**
103 * Stop all active provider sessions.
104 *
105 * Persisted session bindings remain so stale sessions can be resumed after
106 * process restart.
107 *
108 * Runtime alias mappings are cleared as part of shutdown.
109 */
110 readonly stopAll: () => Effect.Effect<void, ProviderServiceError>;
111
112 /**102 /**
113 * Canonical provider runtime event stream.103 * Canonical provider runtime event stream.
114 *104 *
apps/server/src/serverLogger.ts+3−6
@@ -1,18 +1,15 @@
1import fs from "node:fs";1import fs from "node:fs";
2import path from "node:path";2import path from "node:path";
3 3
4import { Effect, Logger, Option } from "effect";4import { Effect, Logger } from "effect";
5import * as Layer from "effect/Layer";5import * as Layer from "effect/Layer";
6 6
7import { ServerConfig } from "./config";7import { ServerConfig } from "./config";
8 8
9export const ServerLoggerLive = Effect.gen(function* () {9export const ServerLoggerLive = Effect.gen(function* () {
10 const config = yield* Effect.serviceOption(ServerConfig);10 const config = yield* ServerConfig;
11 if (Option.isNone(config)) {
12 return Logger.layer([Logger.defaultLogger]);
13 }
14 11
15 const logDir = path.join(config.value.stateDir, "logs");12 const logDir = path.join(config.stateDir, "logs");
16 const logPath = path.join(logDir, "server.log");13 const logPath = path.join(logDir, "server.log");
17 14
18 yield* Effect.sync(() => {15 yield* Effect.sync(() => {
apps/server/src/wsServer.test.ts+0−1
@@ -1171,7 +1171,6 @@ describe("WebSocket Server", () => { · 1170 unmodified lines
1171 listSessions: () => Effect.succeed([]),1171 listSessions: () => Effect.succeed([]),
1172 getCapabilities: () => Effect.succeed({ sessionModelSwitch: "in-session" }),1172 getCapabilities: () => Effect.succeed({ sessionModelSwitch: "in-session" }),
1173 rollbackConversation: () => unsupported(),1173 rollbackConversation: () => unsupported(),
1174 stopAll: () => Effect.void,
1175 streamEvents: Stream.fromPubSub(runtimeEventPubSub),1174 streamEvents: Stream.fromPubSub(runtimeEventPubSub),
1176 };1175 };
1177 const providerLayer = Layer.succeed(ProviderService, providerService);1176 const providerLayer = Layer.succeed(ProviderService, providerService);
apps/server/src/wsServer.ts+0−6
@@ -568,7 +568,6 @@ export const createServer = Effect.fn(function* (): Effect.fn.Return< · 567 unmodified lines
568 const orchestrationEngine = yield* OrchestrationEngineService;568 const orchestrationEngine = yield* OrchestrationEngineService;
569 const projectionReadModelQuery = yield* ProjectionSnapshotQuery;569 const projectionReadModelQuery = yield* ProjectionSnapshotQuery;
570 const checkpointDiffQuery = yield* CheckpointDiffQuery;570 const checkpointDiffQuery = yield* CheckpointDiffQuery;
571 const liveProviderService = yield* ProviderService;
572 const orchestrationReactor = yield* OrchestrationReactor;571 const orchestrationReactor = yield* OrchestrationReactor;
573 const { openInEditor } = yield* Open;572 const { openInEditor } = yield* Open;
574 573
@@ -660,11 +659,6 @@ export const createServer = Effect.fn(function* (): Effect.fn.Return< · 85 unmodified lines
660 }659 }
661 660
662 const runPromise = yield* Effect.map(Effect.services<never>(), Effect.runPromiseWith);661 const runPromise = yield* Effect.map(Effect.services<never>(), Effect.runPromiseWith);
663 yield* Effect.addFinalizer(() =>
664 Effect.catch(liveProviderService.stopAll(), (cause) =>
665 Effect.logWarning("failed to stop provider service", { cause }),
666 ),
667 );
668 662
669 const unsubscribeTerminalEvents = yield* terminalManager.subscribe(663 const unsubscribeTerminalEvents = yield* terminalManager.subscribe(
670 (event) => void Effect.runPromise(onTerminalEvent(event)),664 (event) => void Effect.runPromise(onTerminalEvent(event)),
apps/web/public/apple-touch-icon.png+0−0

Binary file not shown.

apps/web/public/favicon-16x16.png+0−0

Binary file not shown.

apps/web/public/favicon-32x32.png+0−0

Binary file not shown.

apps/web/public/favicon.ico+0−0

Binary file not shown.

apps/web/public/favicon.svg+0−4
@@ -1,4 +0,0 @@
1<svg width="128" height="128" viewBox="0 0 128 128" fill="none" xmlns="http://www.w3.org/2000/svg">
2<path d="M0 10C0 4.47715 4.47715 0 10 0H118C123.523 0 128 4.47715 128 10V118C128 123.523 123.523 128 118 128H10C4.47715 128 0 123.523 0 118V10Z" fill="black"/>
3<path d="M33.4509 93V47.56H15.5309V37H64.3309V47.56H46.4109V93H33.4509ZM86.7253 93.96C82.832 93.96 78.9653 93.4533 75.1253 92.44C71.2853 91.3733 68.032 89.88 65.3653 87.96L70.4053 78.04C72.5386 79.5867 75.0186 80.8133 77.8453 81.72C80.672 82.6267 83.5253 83.08 86.4053 83.08C89.6586 83.08 92.2186 82.44 94.0853 81.16C95.952 79.88 96.8853 78.12 96.8853 75.88C96.8853 73.7467 96.0586 72.0667 94.4053 70.84C92.752 69.6133 90.0853 69 86.4053 69H80.4853V60.44L96.0853 42.76L97.5253 47.4H68.1653V37H107.365V45.4L91.8453 63.08L85.2853 59.32H89.0453C95.9253 59.32 101.125 60.8667 104.645 63.96C108.165 67.0533 109.925 71.0267 109.925 75.88C109.925 79.0267 109.099 81.9867 107.445 84.76C105.792 87.48 103.259 89.6933 99.8453 91.4C96.432 93.1067 92.0586 93.96 86.7253 93.96Z" fill="white"/>
4</svg>
assets/T3 Code blueprint-macOS-Default-1024x1024@1x.png+0−0

Binary file not shown.

assets/T3 Code logo.svg+0−4
@@ -1,4 +0,0 @@
1<svg width="128" height="128" viewBox="0 0 128 128" fill="none" xmlns="http://www.w3.org/2000/svg">
2<path d="M0 10C0 4.47715 4.47715 0 10 0H118C123.523 0 128 4.47715 128 10V118C128 123.523 123.523 128 118 128H10C4.47715 128 0 123.523 0 118V10Z" fill="black"/>
3<path d="M33.4509 93V47.56H15.5309V37H64.3309V47.56H46.4109V93H33.4509ZM86.7253 93.96C82.832 93.96 78.9653 93.4533 75.1253 92.44C71.2853 91.3733 68.032 89.88 65.3653 87.96L70.4053 78.04C72.5386 79.5867 75.0186 80.8133 77.8453 81.72C80.672 82.6267 83.5253 83.08 86.4053 83.08C89.6586 83.08 92.2186 82.44 94.0853 81.16C95.952 79.88 96.8853 78.12 96.8853 75.88C96.8853 73.7467 96.0586 72.0667 94.4053 70.84C92.752 69.6133 90.0853 69 86.4053 69H80.4853V60.44L96.0853 42.76L97.5253 47.4H68.1653V37H107.365V45.4L91.8453 63.08L85.2853 59.32H89.0453C95.9253 59.32 101.125 60.8667 104.645 63.96C108.165 67.0533 109.925 71.0267 109.925 75.88C109.925 79.0267 109.099 81.9867 107.445 84.76C105.792 87.48 103.259 89.6933 99.8453 91.4C96.432 93.1067 92.0586 93.96 86.7253 93.96Z" fill="white"/>
4</svg>
assets/T3 Code logo@16x.png+0−0

Binary file not shown.

assets/T3 Code logo@32x.png+0−0

Binary file not shown.

assets/T3 Code-macOS-Default-1024x1024@4x.png+0−0

Binary file not shown.

assets/dev/blueprint-ios-1024.png+0−0

Binary file not shown.

assets/dev/blueprint-macos-1024.pngrenamed from assets/T3 Code blueprint-macOS-Default-1024x1024@2x.png+0−0

File renamed without changes.

assets/dev/blueprint-universal-1024.png+0−0

Binary file not shown.

assets/dev/blueprint-web-apple-touch-180.png+0−0

Binary file not shown.

assets/dev/blueprint-web-favicon-16x16.png+0−0

Binary file not shown.

assets/dev/blueprint-web-favicon-32x32.png+0−0

Binary file not shown.

assets/dev/blueprint-web-favicon.ico+0−0

Binary file not shown.

assets/dev/blueprint-windows.ico+0−0

Binary file not shown.

assets/logo-1024.png+0−0

Binary file not shown.

assets/macos-icon-1024.png+0−0

Binary file not shown.

assets/prod/black-ios-1024.png+0−0

Binary file not shown.

assets/prod/black-macos-1024.pngrenamed from assets/T3 Code-macOS-Default-1024x1024@1x.png+0−0

File renamed without changes.

assets/prod/black-universal-1024.pngrenamed from assets/T3 Code logo@8x.png+0−0

File renamed without changes.

assets/prod/logo.svgrenamed from assets/logo.svg+0−0

File renamed without changes.

assets/prod/t3-black-web-apple-touch-180.png+0−0

Binary file not shown.

assets/prod/t3-black-web-favicon-16x16.png+0−0

Binary file not shown.

assets/prod/t3-black-web-favicon-32x32.png+0−0

Binary file not shown.

assets/prod/t3-black-web-favicon.ico+0−0

Binary file not shown.

assets/prod/t3-black-windows.ico+0−0

Binary file not shown.

scripts/build-desktop-artifact.ts+93−18
@@ -1,11 +1,14 @@
1#!/usr/bin/env node1#!/usr/bin/env node
2 2
3import { spawnSync } from "node:child_process";3import { spawnSync } from "node:child_process";
4import { existsSync } from "node:fs";
5import { join } from "node:path";
4 6
5import rootPackageJson from "../package.json" with { type: "json" };7import rootPackageJson from "../package.json" with { type: "json" };
6import desktopPackageJson from "../apps/desktop/package.json" with { type: "json" };8import desktopPackageJson from "../apps/desktop/package.json" with { type: "json" };
7import serverPackageJson from "../apps/server/package.json" with { type: "json" };9import serverPackageJson from "../apps/server/package.json" with { type: "json" };
8 10
11import { BRAND_ASSET_PATHS } from "./lib/brand-assets.ts";
9import { resolveCatalogDependencies } from "./lib/resolve-catalog.ts";12import { resolveCatalogDependencies } from "./lib/resolve-catalog.ts";
10 13
11import * as NodeRuntime from "@effect/platform-node/NodeRuntime";14import * as NodeRuntime from "@effect/platform-node/NodeRuntime";
@@ -20,8 +23,18 @@ const BuildArch = Schema.Literals(["arm64", "x64", "universal"]); · 8 unmodified lines
20const RepoRoot = Effect.service(Path.Path).pipe(23const RepoRoot = Effect.service(Path.Path).pipe(
21 Effect.flatMap((path) => path.fromFileUrl(new URL("..", import.meta.url))),24 Effect.flatMap((path) => path.fromFileUrl(new URL("..", import.meta.url))),
22);25);
23const IconSource = Effect.zipWith(RepoRoot, Effect.service(Path.Path), (repoRoot, path) =>26const ProductionMacIconSource = Effect.zipWith(RepoRoot, Effect.service(Path.Path), (repoRoot, path) =>
24 path.join(repoRoot, "assets/macos-icon-1024.png"),27 path.join(repoRoot, BRAND_ASSET_PATHS.productionMacIconPng),
28);
29const ProductionLinuxIconSource = Effect.zipWith(
30 RepoRoot,
31 Effect.service(Path.Path),
32 (repoRoot, path) => path.join(repoRoot, BRAND_ASSET_PATHS.productionLinuxIconPng),
33);
34const ProductionWindowsIconSource = Effect.zipWith(
35 RepoRoot,
36 Effect.service(Path.Path),
37 (repoRoot, path) => path.join(repoRoot, BRAND_ASSET_PATHS.productionWindowsIconIco),
25);38);
26const encodeJsonString = Schema.encodeEffect(Schema.UnknownFromJsonString);39const encodeJsonString = Schema.encodeEffect(Schema.UnknownFromJsonString);
27 40
@@ -104,6 +117,39 @@ function resolveGitCommitHash(repoRoot: string): string { · 76 unmodified lines
104 return hash.toLowerCase();117 return hash.toLowerCase();
105}118}
106 119
120function resolvePythonForNodeGyp(): string | undefined {
121 const configured = process.env.npm_config_python ?? process.env.PYTHON;
122 if (configured && existsSync(configured)) {
123 return configured;
124 }
125
126 if (process.platform === "win32") {
127 const localAppData = process.env.LOCALAPPDATA;
128 if (localAppData) {
129 for (const version of ["Python313", "Python312", "Python311", "Python310"]) {
130 const candidate = join(localAppData, "Programs", "Python", version, "python.exe");
131 if (existsSync(candidate)) {
132 return candidate;
133 }
134 }
135 }
136 }
137
138 const probe = spawnSync("python", ["-c", "import sys;print(sys.executable)"], {
139 encoding: "utf8",
140 });
141 if (probe.status !== 0) {
142 return undefined;
143 }
144
145 const executable = probe.stdout.trim();
146 if (!executable || !existsSync(executable)) {
147 return undefined;
148 }
149
150 return executable;
151}
152
107interface ResolvedBuildOptions {153interface ResolvedBuildOptions {
108 readonly platform: typeof BuildPlatform.Type;154 readonly platform: typeof BuildPlatform.Type;
109 readonly target: string;155 readonly target: string;
@@ -261,7 +307,7 @@ function stageMacIcons(stageResourcesDir: string, verbose: boolean) { · 151 unmodified lines
261 return Effect.gen(function* () {307 return Effect.gen(function* () {
262 const fs = yield* FileSystem.FileSystem;308 const fs = yield* FileSystem.FileSystem;
263 const path = yield* Path.Path;309 const path = yield* Path.Path;
264 const iconSource = yield* IconSource;310 const iconSource = yield* ProductionMacIconSource;
265 if (!(yield* fs.exists(iconSource))) {311 if (!(yield* fs.exists(iconSource))) {
266 return yield* new BuildScriptError({312 return yield* new BuildScriptError({
267 message: `Production icon source is missing at ${iconSource}`,313 message: `Production icon source is missing at ${iconSource}`,
@@ -285,6 +331,38 @@ function stageMacIcons(stageResourcesDir: string, verbose: boolean) { · 17 unmodified lines
285 });331 });
286}332}
287 333
334function stageLinuxIcons(stageResourcesDir: string) {
335 return Effect.gen(function* () {
336 const fs = yield* FileSystem.FileSystem;
337 const path = yield* Path.Path;
338 const iconSource = yield* ProductionLinuxIconSource;
339 if (!(yield* fs.exists(iconSource))) {
340 return yield* new BuildScriptError({
341 message: `Production icon source is missing at ${iconSource}`,
342 });
343 }
344
345 const iconPath = path.join(stageResourcesDir, "icon.png");
346 yield* fs.copyFile(iconSource, iconPath);
347 });
348}
349
350function stageWindowsIcons(stageResourcesDir: string) {
351 return Effect.gen(function* () {
352 const fs = yield* FileSystem.FileSystem;
353 const path = yield* Path.Path;
354 const iconSource = yield* ProductionWindowsIconSource;
355 if (!(yield* fs.exists(iconSource))) {
356 return yield* new BuildScriptError({
357 message: `Production Windows icon source is missing at ${iconSource}`,
358 });
359 }
360
361 const iconPath = path.join(stageResourcesDir, "icon.ico");
362 yield* fs.copyFile(iconSource, iconPath);
363 });
364}
365
288function validateBundledClientAssets(clientDir: string) {366function validateBundledClientAssets(clientDir: string) {
289 return Effect.gen(function* () {367 return Effect.gen(function* () {
290 const fs = yield* FileSystem.FileSystem;368 const fs = yield* FileSystem.FileSystem;
@@ -416,31 +494,18 @@ const assertPlatformBuildResources = Effect.fn("assertPlatformBuildResources")(f · 125 unmodified lines
416 stageResourcesDir: string,494 stageResourcesDir: string,
417 verbose: boolean,495 verbose: boolean,
418) {496) {
419 const fs = yield* FileSystem.FileSystem;
420 const path = yield* Path.Path;
421
422 if (platform === "mac") {497 if (platform === "mac") {
423 yield* stageMacIcons(stageResourcesDir, verbose);498 yield* stageMacIcons(stageResourcesDir, verbose);
424 return;499 return;
425 }500 }
426 501
427 if (platform === "linux") {502 if (platform === "linux") {
428 const iconPath = path.join(stageResourcesDir, "icon.png");503 yield* stageLinuxIcons(stageResourcesDir);
429 if (!(yield* fs.exists(iconPath))) {
430 return yield* new BuildScriptError({
431 message: `Missing Linux icon at ${iconPath}`,
432 });
433 }
434 return;504 return;
435 }505 }
436 506
437 if (platform === "win") {507 if (platform === "win") {
438 const iconPath = path.join(stageResourcesDir, "icon.ico");508 yield* stageWindowsIcons(stageResourcesDir);
439 if (!(yield* fs.exists(iconPath))) {
440 return yield* new BuildScriptError({
441 message: `Missing Windows icon at ${iconPath}`,
442 });
443 }
444 }509 }
445});510});
446 511
@@ -597,6 +662,16 @@ const buildDesktopArtifact = Effect.fn("buildDesktopArtifact")(function* ( · 150 unmodified lines
597 delete buildEnv.APPLE_API_ISSUER;662 delete buildEnv.APPLE_API_ISSUER;
598 }663 }
599 664
665 if (process.platform === "win32") {
666 const python = resolvePythonForNodeGyp();
667 if (python) {
668 buildEnv.PYTHON = python;
669 buildEnv.npm_config_python = python;
670 }
671 buildEnv.npm_config_msvs_version = buildEnv.npm_config_msvs_version ?? "2022";
672 buildEnv.GYP_MSVS_VERSION = buildEnv.GYP_MSVS_VERSION ?? "2022";
673 }
674
600 yield* Effect.log(675 yield* Effect.log(
601 `[desktop-artifact] Building ${options.platform}/${options.target} (arch=${options.arch}, version=${appVersion})...`,676 `[desktop-artifact] Building ${options.platform}/${options.target} (arch=${options.arch}, version=${appVersion})...`,
602 );677 );
scripts/lib/brand-assets.ts+57−0
@@ -0,0 +1,57 @@
1export const BRAND_ASSET_PATHS = {
2 productionMacIconPng: "assets/prod/black-macos-1024.png",
3 productionLinuxIconPng: "assets/prod/black-universal-1024.png",
4 productionWindowsIconIco: "assets/prod/t3-black-windows.ico",
5 productionWebFaviconIco: "assets/prod/t3-black-web-favicon.ico",
6 productionWebFavicon16Png: "assets/prod/t3-black-web-favicon-16x16.png",
7 productionWebFavicon32Png: "assets/prod/t3-black-web-favicon-32x32.png",
8 productionWebAppleTouchIconPng: "assets/prod/t3-black-web-apple-touch-180.png",
9 developmentWindowsIconIco: "assets/dev/blueprint-windows.ico",
10 developmentWebFaviconIco: "assets/dev/blueprint-web-favicon.ico",
11 developmentWebFavicon16Png: "assets/dev/blueprint-web-favicon-16x16.png",
12 developmentWebFavicon32Png: "assets/dev/blueprint-web-favicon-32x32.png",
13 developmentWebAppleTouchIconPng: "assets/dev/blueprint-web-apple-touch-180.png",
14} as const;
15
16export interface IconOverride {
17 readonly sourceRelativePath: string;
18 readonly targetRelativePath: string;
19}
20
21export const DEVELOPMENT_ICON_OVERRIDES: ReadonlyArray<IconOverride> = [
22 {
23 sourceRelativePath: BRAND_ASSET_PATHS.developmentWebFaviconIco,
24 targetRelativePath: "dist/client/favicon.ico",
25 },
26 {
27 sourceRelativePath: BRAND_ASSET_PATHS.developmentWebFavicon16Png,
28 targetRelativePath: "dist/client/favicon-16x16.png",
29 },
30 {
31 sourceRelativePath: BRAND_ASSET_PATHS.developmentWebFavicon32Png,
32 targetRelativePath: "dist/client/favicon-32x32.png",
33 },
34 {
35 sourceRelativePath: BRAND_ASSET_PATHS.developmentWebAppleTouchIconPng,
36 targetRelativePath: "dist/client/apple-touch-icon.png",
37 },
38];
39
40export const PUBLISH_ICON_OVERRIDES: ReadonlyArray<IconOverride> = [
41 {
42 sourceRelativePath: BRAND_ASSET_PATHS.productionWebFaviconIco,
43 targetRelativePath: "dist/client/favicon.ico",
44 },
45 {
46 sourceRelativePath: BRAND_ASSET_PATHS.productionWebFavicon16Png,
47 targetRelativePath: "dist/client/favicon-16x16.png",
48 },
49 {
50 sourceRelativePath: BRAND_ASSET_PATHS.productionWebFavicon32Png,
51 targetRelativePath: "dist/client/favicon-32x32.png",
52 },
53 {
54 sourceRelativePath: BRAND_ASSET_PATHS.productionWebAppleTouchIconPng,
55 targetRelativePath: "dist/client/apple-touch-icon.png",
56 },
57];