Skip to content

Commit a405818

Browse files
authored
Merge branch 'main' into feature/fix/composer-image-hydration
2 parents 8b2803e + b96308f commit a405818

123 files changed

Lines changed: 11444 additions & 5299 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

CLAUDE.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
AGENTS.md
1+
AGENTS.md

apps/desktop/src/main.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ const UPDATE_DOWNLOAD_CHANNEL = "desktop:update-download";
6060
const UPDATE_INSTALL_CHANNEL = "desktop:update-install";
6161
const UPDATE_CHECK_CHANNEL = "desktop:update-check";
6262
const GET_WS_URL_CHANNEL = "desktop:get-ws-url";
63+
const GET_LOCAL_ENVIRONMENT_BOOTSTRAP_CHANNEL = "desktop:get-local-environment-bootstrap";
6364
const BASE_DIR = process.env.T3CODE_HOME?.trim() || Path.join(OS.homedir(), ".t3");
6465
const STATE_DIR = Path.join(BASE_DIR, "userdata");
6566
const DESKTOP_SCHEME = "t3";
@@ -1172,6 +1173,14 @@ function registerIpcHandlers(): void {
11721173
event.returnValue = backendWsUrl;
11731174
});
11741175

1176+
ipcMain.removeAllListeners(GET_LOCAL_ENVIRONMENT_BOOTSTRAP_CHANNEL);
1177+
ipcMain.on(GET_LOCAL_ENVIRONMENT_BOOTSTRAP_CHANNEL, (event) => {
1178+
event.returnValue = {
1179+
label: "Local environment",
1180+
wsUrl: backendWsUrl || null,
1181+
} as const;
1182+
});
1183+
11751184
ipcMain.removeHandler(PICK_FOLDER_CHANNEL);
11761185
ipcMain.handle(PICK_FOLDER_CHANNEL, async () => {
11771186
const owner = BrowserWindow.getFocusedWindow() ?? mainWindow;

apps/desktop/src/preload.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,20 @@ const UPDATE_CHECK_CHANNEL = "desktop:update-check";
1313
const UPDATE_DOWNLOAD_CHANNEL = "desktop:update-download";
1414
const UPDATE_INSTALL_CHANNEL = "desktop:update-install";
1515
const GET_WS_URL_CHANNEL = "desktop:get-ws-url";
16+
const GET_LOCAL_ENVIRONMENT_BOOTSTRAP_CHANNEL = "desktop:get-local-environment-bootstrap";
1617

1718
contextBridge.exposeInMainWorld("desktopBridge", {
1819
getWsUrl: () => {
1920
const result = ipcRenderer.sendSync(GET_WS_URL_CHANNEL);
2021
return typeof result === "string" ? result : null;
2122
},
23+
getLocalEnvironmentBootstrap: () => {
24+
const result = ipcRenderer.sendSync(GET_LOCAL_ENVIRONMENT_BOOTSTRAP_CHANNEL);
25+
if (typeof result !== "object" || result === null) {
26+
return null;
27+
}
28+
return result as ReturnType<DesktopBridge["getLocalEnvironmentBootstrap"]>;
29+
},
2230
pickFolder: () => ipcRenderer.invoke(PICK_FOLDER_CHANNEL),
2331
confirm: (message) => ipcRenderer.invoke(CONFIRM_CHANNEL, message),
2432
setTheme: (theme) => ipcRenderer.invoke(SET_THEME_CHANNEL, theme),

apps/server/integration/OrchestrationEngineHarness.integration.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ import { CodexAdapter } from "../src/provider/Services/CodexAdapter.ts";
4545
import { ProviderService } from "../src/provider/Services/ProviderService.ts";
4646
import { AnalyticsService } from "../src/telemetry/Services/AnalyticsService.ts";
4747
import { CheckpointReactorLive } from "../src/orchestration/Layers/CheckpointReactor.ts";
48+
import { RepositoryIdentityResolverLive } from "../src/project/Layers/RepositoryIdentityResolver.ts";
4849
import { OrchestrationEngineLive } from "../src/orchestration/Layers/OrchestrationEngine.ts";
4950
import { OrchestrationProjectionPipelineLive } from "../src/orchestration/Layers/ProjectionPipeline.ts";
5051
import { OrchestrationProjectionSnapshotQueryLive } from "../src/orchestration/Layers/ProjectionSnapshotQuery.ts";
@@ -338,6 +339,7 @@ export const makeOrchestrationIntegrationHarness = (
338339
Layer.provideMerge(runtimeServicesLayer),
339340
Layer.provideMerge(orchestrationReactorLayer),
340341
Layer.provide(persistenceLayer),
342+
Layer.provideMerge(RepositoryIdentityResolverLive),
341343
Layer.provideMerge(ServerSettingsService.layerTest()),
342344
Layer.provideMerge(ServerConfig.layerTest(workspaceDir, rootDir)),
343345
Layer.provideMerge(NodeServices.layer),

apps/server/src/config.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ export interface ServerDerivedPaths {
3030
readonly providerEventLogPath: string;
3131
readonly terminalLogsDir: string;
3232
readonly anonymousIdPath: string;
33+
readonly environmentIdPath: string;
3334
}
3435

3536
/**
@@ -83,6 +84,7 @@ export const deriveServerPaths = Effect.fn(function* (
8384
providerEventLogPath: join(providerLogsDir, "events.log"),
8485
terminalLogsDir: join(logsDir, "terminals"),
8586
anonymousIdPath: join(stateDir, "anonymous-id"),
87+
environmentIdPath: join(stateDir, "environment-id"),
8688
};
8789
});
8890

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
import * as nodePath from "node:path";
2+
import * as NodeServices from "@effect/platform-node/NodeServices";
3+
import { expect, it } from "@effect/vitest";
4+
import { Effect, Exit, FileSystem, Layer, PlatformError } from "effect";
5+
6+
import { deriveServerPaths, ServerConfig, type ServerConfigShape } from "../../config.ts";
7+
import { ServerEnvironment } from "../Services/ServerEnvironment.ts";
8+
import { ServerEnvironmentLive } from "./ServerEnvironment.ts";
9+
10+
const makeServerEnvironmentLayer = (baseDir: string) =>
11+
ServerEnvironmentLive.pipe(Layer.provide(ServerConfig.layerTest(process.cwd(), baseDir)));
12+
13+
const makeServerConfig = Effect.fn(function* (baseDir: string) {
14+
const derivedPaths = yield* deriveServerPaths(baseDir, undefined);
15+
16+
return {
17+
...derivedPaths,
18+
logLevel: "Error",
19+
traceMinLevel: "Info",
20+
traceTimingEnabled: true,
21+
traceBatchWindowMs: 200,
22+
traceMaxBytes: 10 * 1024 * 1024,
23+
traceMaxFiles: 10,
24+
otlpTracesUrl: undefined,
25+
otlpMetricsUrl: undefined,
26+
otlpExportIntervalMs: 10_000,
27+
otlpServiceName: "t3-server",
28+
cwd: process.cwd(),
29+
baseDir,
30+
mode: "web",
31+
autoBootstrapProjectFromCwd: false,
32+
logWebSocketEvents: false,
33+
port: 0,
34+
host: undefined,
35+
authToken: undefined,
36+
staticDir: undefined,
37+
devUrl: undefined,
38+
noBrowser: false,
39+
} satisfies ServerConfigShape;
40+
});
41+
42+
it.layer(NodeServices.layer)("ServerEnvironmentLive", (it) => {
43+
it.effect("persists the environment id across service restarts", () =>
44+
Effect.gen(function* () {
45+
const fileSystem = yield* FileSystem.FileSystem;
46+
const baseDir = yield* fileSystem.makeTempDirectoryScoped({
47+
prefix: "t3-server-environment-test-",
48+
});
49+
50+
const first = yield* Effect.gen(function* () {
51+
const serverEnvironment = yield* ServerEnvironment;
52+
return yield* serverEnvironment.getDescriptor;
53+
}).pipe(Effect.provide(makeServerEnvironmentLayer(baseDir)));
54+
const second = yield* Effect.gen(function* () {
55+
const serverEnvironment = yield* ServerEnvironment;
56+
return yield* serverEnvironment.getDescriptor;
57+
}).pipe(Effect.provide(makeServerEnvironmentLayer(baseDir)));
58+
59+
expect(first.environmentId).toBe(second.environmentId);
60+
expect(second.capabilities.repositoryIdentity).toBe(true);
61+
}),
62+
);
63+
64+
it.effect("fails instead of overwriting a persisted id when reading the file errors", () =>
65+
Effect.gen(function* () {
66+
const fileSystem = yield* FileSystem.FileSystem;
67+
const baseDir = yield* fileSystem.makeTempDirectoryScoped({
68+
prefix: "t3-server-environment-read-error-test-",
69+
});
70+
const serverConfig = yield* makeServerConfig(baseDir);
71+
const environmentIdPath = serverConfig.environmentIdPath;
72+
yield* fileSystem.makeDirectory(nodePath.dirname(environmentIdPath), { recursive: true });
73+
yield* fileSystem.writeFileString(environmentIdPath, "persisted-environment-id\n");
74+
const writeAttempts: string[] = [];
75+
const failingFileSystemLayer = FileSystem.layerNoop({
76+
exists: (path) => Effect.succeed(path === environmentIdPath),
77+
readFileString: (path) =>
78+
path === environmentIdPath
79+
? Effect.fail(
80+
PlatformError.systemError({
81+
_tag: "PermissionDenied",
82+
module: "FileSystem",
83+
method: "readFileString",
84+
description: "permission denied",
85+
pathOrDescriptor: path,
86+
}),
87+
)
88+
: Effect.fail(
89+
PlatformError.systemError({
90+
_tag: "NotFound",
91+
module: "FileSystem",
92+
method: "readFileString",
93+
description: "not found",
94+
pathOrDescriptor: path,
95+
}),
96+
),
97+
writeFileString: (path) => {
98+
writeAttempts.push(path);
99+
return Effect.void;
100+
},
101+
});
102+
103+
const exit = yield* Effect.gen(function* () {
104+
const serverEnvironment = yield* ServerEnvironment;
105+
return yield* serverEnvironment.getDescriptor;
106+
}).pipe(
107+
Effect.provide(
108+
ServerEnvironmentLive.pipe(
109+
Layer.provide(
110+
Layer.merge(Layer.succeed(ServerConfig, serverConfig), failingFileSystemLayer),
111+
),
112+
),
113+
),
114+
Effect.exit,
115+
);
116+
117+
expect(Exit.isFailure(exit)).toBe(true);
118+
expect(writeAttempts).toEqual([]);
119+
expect(yield* fileSystem.readFileString(environmentIdPath)).toBe(
120+
"persisted-environment-id\n",
121+
);
122+
}),
123+
);
124+
});
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
import { EnvironmentId, type ExecutionEnvironmentDescriptor } from "@t3tools/contracts";
2+
import { Effect, FileSystem, Layer, Path, Random } from "effect";
3+
4+
import { ServerConfig } from "../../config.ts";
5+
import { ServerEnvironment, type ServerEnvironmentShape } from "../Services/ServerEnvironment.ts";
6+
import { version } from "../../../package.json" with { type: "json" };
7+
8+
function platformOs(): ExecutionEnvironmentDescriptor["platform"]["os"] {
9+
switch (process.platform) {
10+
case "darwin":
11+
return "darwin";
12+
case "linux":
13+
return "linux";
14+
case "win32":
15+
return "windows";
16+
default:
17+
return "unknown";
18+
}
19+
}
20+
21+
function platformArch(): ExecutionEnvironmentDescriptor["platform"]["arch"] {
22+
switch (process.arch) {
23+
case "arm64":
24+
return "arm64";
25+
case "x64":
26+
return "x64";
27+
default:
28+
return "other";
29+
}
30+
}
31+
32+
export const makeServerEnvironment = Effect.fn("makeServerEnvironment")(function* () {
33+
const fileSystem = yield* FileSystem.FileSystem;
34+
const path = yield* Path.Path;
35+
const serverConfig = yield* ServerConfig;
36+
37+
const readPersistedEnvironmentId = Effect.gen(function* () {
38+
const exists = yield* fileSystem
39+
.exists(serverConfig.environmentIdPath)
40+
.pipe(Effect.orElseSucceed(() => false));
41+
if (!exists) {
42+
return null;
43+
}
44+
45+
const raw = yield* fileSystem
46+
.readFileString(serverConfig.environmentIdPath)
47+
.pipe(Effect.map((value) => value.trim()));
48+
49+
return raw.length > 0 ? raw : null;
50+
});
51+
52+
const persistEnvironmentId = (value: string) =>
53+
fileSystem.writeFileString(serverConfig.environmentIdPath, `${value}\n`);
54+
55+
const environmentIdRaw = yield* Effect.gen(function* () {
56+
const persisted = yield* readPersistedEnvironmentId;
57+
if (persisted) {
58+
return persisted;
59+
}
60+
61+
const generated = yield* Random.nextUUIDv4;
62+
yield* persistEnvironmentId(generated);
63+
return generated;
64+
});
65+
66+
const environmentId = EnvironmentId.makeUnsafe(environmentIdRaw);
67+
const cwdBaseName = path.basename(serverConfig.cwd).trim();
68+
const label =
69+
serverConfig.mode === "desktop"
70+
? "Local environment"
71+
: cwdBaseName.length > 0
72+
? cwdBaseName
73+
: "T3 environment";
74+
75+
const descriptor: ExecutionEnvironmentDescriptor = {
76+
environmentId,
77+
label,
78+
platform: {
79+
os: platformOs(),
80+
arch: platformArch(),
81+
},
82+
serverVersion: version,
83+
capabilities: {
84+
repositoryIdentity: true,
85+
},
86+
};
87+
88+
return {
89+
getEnvironmentId: Effect.succeed(environmentId),
90+
getDescriptor: Effect.succeed(descriptor),
91+
} satisfies ServerEnvironmentShape;
92+
});
93+
94+
export const ServerEnvironmentLive = Layer.effect(ServerEnvironment, makeServerEnvironment());
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import type { EnvironmentId, ExecutionEnvironmentDescriptor } from "@t3tools/contracts";
2+
import { ServiceMap } from "effect";
3+
import type { Effect } from "effect";
4+
5+
export interface ServerEnvironmentShape {
6+
readonly getEnvironmentId: Effect.Effect<EnvironmentId>;
7+
readonly getDescriptor: Effect.Effect<ExecutionEnvironmentDescriptor>;
8+
}
9+
10+
export class ServerEnvironment extends ServiceMap.Service<
11+
ServerEnvironment,
12+
ServerEnvironmentShape
13+
>()("t3/environment/Services/ServerEnvironment") {}

apps/server/src/git/Layers/GitManager.test.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -854,7 +854,7 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => {
854854
"pr list --head jasonLaster:statemachine --state all --limit 20 --json number,title,url,baseRefName,headRefName,state,mergedAt,updatedAt,isCrossRepository,headRepository,headRepositoryOwner",
855855
);
856856
}),
857-
12_000,
857+
20_000,
858858
);
859859

860860
it.effect(
@@ -962,7 +962,7 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => {
962962
),
963963
).toBe(false);
964964
}),
965-
12_000,
965+
20_000,
966966
);
967967

968968
it.effect("status returns merged PR state when latest PR was merged", () =>
@@ -1685,7 +1685,7 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => {
16851685
false,
16861686
);
16871687
}),
1688-
12_000,
1688+
20_000,
16891689
);
16901690

16911691
it.effect(

apps/server/src/orchestration/Layers/CheckpointReactor.test.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import { afterEach, describe, expect, it, vi } from "vitest";
2020
import { CheckpointStoreLive } from "../../checkpointing/Layers/CheckpointStore.ts";
2121
import { CheckpointStore } from "../../checkpointing/Services/CheckpointStore.ts";
2222
import { GitCoreLive } from "../../git/Layers/GitCore.ts";
23+
import { RepositoryIdentityResolverLive } from "../../project/Layers/RepositoryIdentityResolver.ts";
2324
import { CheckpointReactorLive } from "./CheckpointReactor.ts";
2425
import { OrchestrationEngineLive } from "./OrchestrationEngine.ts";
2526
import { OrchestrationProjectionPipelineLive } from "./ProjectionPipeline.ts";
@@ -256,6 +257,7 @@ describe("CheckpointReactor", () => {
256257
Layer.provide(OrchestrationProjectionPipelineLive),
257258
Layer.provide(OrchestrationEventStoreLive),
258259
Layer.provide(OrchestrationCommandReceiptRepositoryLive),
260+
Layer.provide(RepositoryIdentityResolverLive),
259261
Layer.provide(SqlitePersistenceMemory),
260262
);
261263

0 commit comments

Comments
 (0)